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:
@@ -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.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user