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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-04 01:09:00 +01:00
co-authored by Sisyphus
parent 6c16d65476
commit 490274f466
3 changed files with 116 additions and 5 deletions
+11
View File
@@ -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 `<picture>` 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
+7 -4
View File
@@ -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 `<picture>` 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.)
---
+98 -1
View File
@@ -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 `<picture>` element with `<source>` 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
<Map theme="light" center={[-3.476, 56.078]} zoom={15}>
<MapMarker longitude={-3.476} latitude={56.078}>
<MarkerContent>
<!-- custom marker icon -->
</MarkerContent>
<MarkerPopup>
<!-- popup content -->
</MarkerPopup>
</MapMarker>
<MapControls position="bottom-right" showZoom />
</Map>
```
### Square Payment Integration
**Architecture:** `internal/square/` defines a `Client` interface with two implementations: