From 490274f466b2553c53a0c89bf0b7f94bce6fb420 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 4 Jun 2026 01:09:00 +0100 Subject: [PATCH] docs: update README and Obsidian documentation Document multi-format image pipeline, MapLibre GL map components, WASM encoder workers, loyalty stamp redesign, admin role restrictions, and Svelte 5 improvements across README, Overview, and Technical Manual. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- README.md | 11 +++ obsidian/Crussell/Overview.md | 11 +-- obsidian/Crussell/Technical Manual.md | 99 ++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 49a50e2..ba386a3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,17 @@ Nail salon booking platform — Go 1.25 backend + SvelteKit 5 frontend + Docker. - **`created_by_name` on bookings**: admin booking details now show who created the booking (admin name) - **Exceptional scheduling fix**: exceptional application queries expand start to Monday of the week, fixing single-day queries missing Monday-based `week_start` records - **Portfolio image upload limits**: 20MB file size limit with visual feedback (red borders, error text), upload button disabled for oversized files +- **Multi-format portfolio images**: Backend stores AVIF, WebP, JPEG, and optional JXL variants for both full-size and thumbnail images. Frontend `ImageVariant` component renders `` elements with format-aware fallback. WASM-based client-side encoders (`@jsquash/avif`, `@jsquash/webp`, `@jsquash/jpeg`, `@discourse/jxl`) run in Web Workers for parallel encoding. Database schema extended with `full_avif_url`, `full_webp_url`, `full_jpg_url`, `full_jxl_url`, `thumb_avif_url`, `thumb_webp_url`, `thumb_jpg_url` columns. Image validation supports AVIF and JXL magic bytes. Legacy single-URL images remain backward-compatible. +- **MapLibre GL map components**: Reusable Svelte map components (`Map`, `MapMarker`, `MapControls`, `MapPopup`, `MapRoute`, `MapClusterLayer`, `MapArc`) wrapping MapLibre GL JS. Features: light/dark theme auto-detection, controlled/uncontrolled viewport, zoom/compass/locate/fullscreen controls, draggable markers, popup/tooltip/label children. Used on contact page for salon location display. +- **NavBar accessibility improvements**: Hamburger menu animated with 4-line to X transform, keyboard-accessible backdrop overlay (Enter/Space to close), ARIA labels, `fly` transitions on notification badge. +- **Loyalty stamp redesign**: Fuchsia-themed stamp card with procedurally-generated flower-petal SVG stamps (unique shape per slot via sine-wave perturbation), star cutout mask, hover scale animation, gradient background. Admin users see simplified account view (no History/Referral/Danger Zone tabs). +- **Admin role restrictions**: Admin accounts hide History tab, Referral tab, Danger Zone (delete account), and loyalty stamp card on the account page. +- **Svelte 5 `SvelteDate`**: Replaced `new Date()` with `new SvelteDate()` in reactive contexts (admin schedule, account page booking filter) for proper Svelte 5 reactivity. +- **`$derived` for computed values**: `totalPages` in notifications page now uses `$derived` instead of runtime calculation. +- **Svelte 5 each block keys**: Added proper keys to `{#each}` blocks across multiple components (NavBar, PortfolioCarousel, TodayCalendar, BookingConfirmed, Demo, PayTip, Tip, Account, Schedule) to fix reordering bugs and improve render performance. +- **`$app/paths` resolve**: All internal navigation hrefs now use `resolve()` from `$app/paths` for proper SPA routing. +- **Tailwind fuchsia ring safelist**: Added `ring-fuchsia-*` pattern to Tailwind safelist for loyalty stamp theming. +- **Vite WASM support**: `vite.config.ts` configured to include `.wasm` files as assets and exclude JSquash/Discourse encoder packages from dependency optimization. - **NavBar enhancements**: admin links reordered (Today/Schedule promoted), unread notification badge on mobile burger icon, backdrop overlay + slide transition for mobile menu - **TodayCalendar improvements**: fetches working/available hours for full week range instead of single day, closing time indicator on timeline, skips lunch suggestion for short days (5h or less) - **Lunch protection refinement**: `shouldApplyLunchProtection()` skips lunch protection for days with 5 or fewer working hours diff --git a/obsidian/Crussell/Overview.md b/obsidian/Crussell/Overview.md index 9f24b94..1e3f5ab 100644 --- a/obsidian/Crussell/Overview.md +++ b/obsidian/Crussell/Overview.md @@ -18,7 +18,7 @@ The business operates exclusively in the UK (Europe/London timezone). Cloudflare | Layer | Technology | |-------|-----------| | **Backend** | Go 1.25, chi router, PostgreSQL driver (pgx) | -| **Frontend** | SvelteKit 5 (static adapter SPA), Svelte 5 runes ($state, $bindable, $effect), Tailwind CSS, shadcn-svelte, bits-ui | +| **Frontend** | SvelteKit 5 (static adapter SPA), Svelte 5 runes ($state, $bindable, $effect, $derived, SvelteDate), Tailwind CSS, shadcn-svelte, bits-ui, MapLibre GL JS | | **Database** | PostgreSQL 17 with custom ID generation, partial indexes, GDPR functions | | **DAV** | SabreDAV (PHP 8.2-FPM) for CardDAV/CalDAV contact and calendar sync | | **Storage** | S3/R2 abstraction — RustFS in dev, Cloudflare R2 in prod (build tags) | @@ -115,10 +115,12 @@ flowchart TD ### Customer Features - Profile management with profile picture upload (cropper, separate S3 bucket) -- Loyalty stamps display +- Loyalty stamps display (redesigned fuchsia-themed stamp card with procedural SVG flower-petal stamps) +- **Admin account restrictions**: Admin users see simplified account page — no History tab, no Referral tab, no Danger Zone (delete account), no loyalty stamp card - Booking history with cancel/reschedule - Calendar export (.ics download) -- Portfolio browsing with tag/category filtering +- Portfolio browsing with tag/category filtering (multi-format: AVIF/WebP/JPEG/JXL via `` element) +- **Contact page map**: Interactive MapLibre GL map showing salon location with marker, popup, and zoom controls - **Online payments**: pay deposits, pay early, partial payments, balance payments, tips on completed bookings - **Saved cards**: manage cards in Account → Cards tab — add/remove cards for faster checkout (soft-deleted on removal, 7-year retention) - **Tip page**: percentage-based tips (10%, 15%, 20%) or custom amount on completed bookings via `/pay-tip/[id]` @@ -180,8 +182,9 @@ flowchart TD ### Integrations - CardDAV: profile photos synced to SabreDAV contacts (vCard PHOTO field) - CalDAV: ready for calendar event sync -- S3/R2: portfolio image storage (RustFS dev, Cloudflare R2 prod) +- S3/R2: portfolio image storage (RustFS dev, Cloudflare R2 prod) — multi-format pipeline (AVIF, WebP, JPEG, optional JXL) with client-side WASM encoding - Square: payment processing — in-person Terminal + online Web Payments SDK. Dev mock (`//go:build dev`) simulates async checkout; prod stub (`//go:build !dev`) connects to live Square API. Build-tagged swap with no code changes. +- **MapLibre GL JS**: Interactive maps via reusable Svelte component library (`Map`, `MapMarker`, `MapControls`, etc.) --- diff --git a/obsidian/Crussell/Technical Manual.md b/obsidian/Crussell/Technical Manual.md index 58547e5..aea3af4 100644 --- a/obsidian/Crussell/Technical Manual.md +++ b/obsidian/Crussell/Technical Manual.md @@ -838,10 +838,107 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection - **Dev**: RustFS (local S3-compatible server) - **Prod**: Cloudflare R2 - **Buckets**: `crussell` (portfolio), `crussell-profile-pics` (profile pictures) -- **Image formats**: AVIF full-size (0.72 quality, 1500px max), WebP thumbnails (250x250) +- **Image formats**: Multi-format pipeline — AVIF, WebP, JPEG (required), optional JXL for full-size; AVIF, WebP, JPEG for thumbnails. Client-side WASM encoders run in Web Workers for parallel encoding before upload. - **Security**: EXIF/GPS metadata stripped on upload via `imaging` library - **extractKey fix**: Portfolio image delete now correctly extracts the full S3 key path (e.g., `portfolio/1234567890.jpg`) from URLs instead of just the filename. This prevents orphaned files in storage +#### Multi-Format Image Pipeline + +**Upload flow:** +1. Frontend crops/resizes the source image to canvas +2. Four Web Workers encode the ImageData in parallel: + - `avif-encoder.ts` → `@jsquash/avif` (quality 50) + - `webp-encoder.ts` → `@jsquash/webp` (quality 75) + - `jpeg-encoder.ts` → `@jsquash/jpeg` (quality 85) + - `jxl-encoder.ts` → `@discourse/jxl` (quality 75, effort 4) — optional, full-size only +3. WASM binaries (`avif_enc.wasm`, `webp_enc.wasm`, `mozjpeg_enc.wasm`, `jxl_enc.wasm`) loaded from `/static/` +4. Multipart form POST sends all format variants as separate fields: `file_full_avif`, `file_full_webp`, `file_full_jpg`, `file_full_jxl` (optional), `file_thumb_avif`, `file_thumb_webp`, `file_thumb_jpg` +5. Backend validates each file via `images.ValidateImageBytes()` (magic byte detection for JPEG, PNG, GIF, WebP, AVIF, JXL) +6. Each variant uploaded to S3 with format-appropriate content type +7. Database record stores all 8-9 URLs + +**Response format:** +```json +{ + "id": "...", + "url": "https://.../full.avif", + "thumbnail_url": "https://.../thumb.webp", + "full": { + "avif": "https://.../full.avif", + "webp": "https://.../full.webp", + "jpg": "https://.../full.jpg", + "jxl": "https://.../full.jxl" + }, + "thumb": { + "avif": "https://.../thumb.avif", + "webp": "https://.../thumb.webp", + "jpg": "https://.../thumb.jpg" + } +} +``` + +**`ImageVariant` component:** Renders `` element with `` tags for each format. Browser selects best supported format. JXL only included for full-size images. Fallback to JPEG. + +**Legacy compatibility:** Images uploaded before the multi-format change only have `url` and `thumbnail_url`. The `Image` struct populates `Full` and `Thumb` from the legacy columns when format-specific columns are NULL. + +**Database schema additions** (`images` table): +| Column | Type | Purpose | +|--------|------|---------| +| `full_avif_url` | text | Full-size AVIF URL | +| `full_webp_url` | text | Full-size WebP URL | +| `full_jpg_url` | text | Full-size JPEG URL | +| `full_jxl_url` | text | Full-size JXL URL (optional) | +| `thumb_avif_url` | text | Thumbnail AVIF URL | +| `thumb_webp_url` | text | Thumbnail WebP URL | +| `thumb_jpg_url` | text | Thumbnail JPEG URL | + +**Image validation** (`internal/images/validate.go`): +- Magic byte detection for: JPEG (`FF D8 FF`), PNG (`89 50 4E 47`), GIF (`47 49 46 38`), WebP (`RIFF....WEBP`), AVIF (`....ftypavif`/`ftypavis`), JXL (`....ftypjxl `) +- Minimum file size check (12 bytes) + +### MapLibre GL Map Components + +**Package**: `frontend/src/lib/components/ui/map/` + +A set of reusable Svelte components wrapping [MapLibre GL JS](https://maplibre.org/). + +| Component | Purpose | +|-----------|---------| +| `Map` | Core map container with theme detection, viewport control, style switching | +| `MapMarker` | Programmatic marker creation with drag support, event forwarding | +| `MarkerContent` | Custom HTML content inside a marker | +| `MarkerPopup` | Popup that appears on marker click | +| `MarkerTooltip` | Tooltip that appears on marker hover | +| `MarkerLabel` | Text label anchored to a marker | +| `MapControls` | Zoom, compass, locate, fullscreen controls | +| `MapPopup` | Standalone popup (not tied to marker) | +| `MapRoute` | Draw routes/paths on the map | +| `MapClusterLayer` | Point clustering for dense marker sets | +| `MapArc` | Animated arc lines between two points | + +**Key features:** +- **Theme auto-detection**: Reads `document.documentElement` class + `prefers-color-scheme` media query + Svelte store. Defaults: CartoDB Positron (light), Dark Matter (dark). +- **Controlled/uncontrolled viewport**: Pass `viewport` + `onviewportchange` for controlled mode, or let map manage its own state. +- **Context API**: `setContext("map", ...)` provides `getMap()`, `isLoaded()`, `isStyleReady()` to child components. +- **`useMap()` hook**: `frontend/src/lib/hooks/use-map.svelte.ts` — consumes map context for reactive `$derived` access to map instance and readiness state. + +**Dependencies**: `maplibre-gl` ^5.24.0, `@lucide/svelte` (for control icons). + +**Usage** (contact page): +```svelte + + + + + + + + + + + +``` + ### Square Payment Integration **Architecture:** `internal/square/` defines a `Client` interface with two implementations: