# Crussell Nail salon booking platform — Go 1.25 backend + SvelteKit 5 frontend + Docker. Built for a UK sole-trader nail artist. ## Features - **Three booking flows**: self-service (customer), walk-in (admin), call-in (admin) - **Slot reservations**: temporary holds prevent double-booking (4 TTL types) - **Guest accounts**: disposable accounts for one-off bookings, GDPR-compliant anonymization - **Service eligibility**: age requirements + patch test validation - **Deposit system**: 3-strike rule for late cancellations, 24-hour threshold - **Square payments**: in-person Terminal + online Web Payments SDK (dev mock + prod stub, build-tagged) - **Payment methods**: Card (Square Terminal/online), cash, gift card — with saved cards, refunds, and webhook handling - **Payment types**: deposit, full, partial, balance, tip — admin can override service prices during payment - **Scheduling**: default hours, holiday overrides, time blockers - **Loyalty & discounts**: stamp-based loyalty, time-based and milestone campaigns - **Portfolio gallery**: S3/R2 storage with tag/category filtering - **CardDAV sync**: profile photos synced to SabreDAV contacts - **Admin notifications**: priority-sorted queue with bell icon, `/notifications` page, acknowledge flow - **User notification preferences**: per-channel toggles (email, SMS, browser) in account settings - **Enriched edit requests**: side-by-side original vs proposed booking snapshots (time, services, prices, durations, user details) for admin review - **Admin reschedule modal**: full reschedule UI on Today page with real-time available slot lookup, conflict detection, and one-click confirmation - **Time blockers UI**: admin can create/manage one-off and recurring time blockers directly from the Admin dashboard, with overlap detection against existing bookings - **Today page enhancements**: interactive daily calendar grid with visual time blockers, today stats summary, responsive layout improvements - **Auto-select availability**: all booking flows (self-service, admin create, edit request) automatically select the first available date when data loads - **Shared time slot utilities**: extracted common time slot generation, lunch protection, and formatting logic into a reusable module - **Shared formatting utilities**: `formatDuration`, `formatDateTime`, `formatDate`, `formatTime`, `calculateAge` in `lib/utils/format.ts` - **Referral code registration**: new users can enter a 12-character referral code during registration; relationship recorded in `user_referrals` table - **Admin schedule page**: Google Calendar-style week view at `/admin/schedule` with drag-scroll, booking details modal, and working hours overlays - **BookingFlow welcome step**: unauthenticated users see a welcome card (Step 0) encouraging login before guest checkout, with clear messaging about lost loyalty/discount benefits - **Admin login redirect**: admins are redirected to `/today` after login instead of the home page - **Patch test duration on services**: `patch_test_duration_hours` exposed on Service type; creating a service with duration > 0 auto-creates a patch test record - **`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 - **`formatDateISO` utility**: new `formatDateISO(date)` function in `lib/utils/format.ts` returning YYYY-MM-DD format for API calls, later extracted to shared utilities - **JWT revocation with JTI**: every JWT carries a unique `jti` claim (UUID v4), in-memory revoked JTI tracking with 5-minute cleanup ticker, `POST /api/logout` endpoint revokes current token, refresh handler revokes old JTI before issuing replacement - **Portfolio image deletion fix**: `extractKey` correctly extracts full S3 key path from URLs instead of just the filename, preventing orphaned files in storage - **Notes validation**: all `Notes *string` fields across booking structs validated with `max=1000000` tag (13 fields across 4 files) - **CharCounter component**: reusable grapheme counter using `Intl.Segmenter`, shows counter only above 750K graphemes, color-coded (green <800K, yellow 800K-950K, red >950K), integrated into 6 booking/admin components - **loginInProgress rate limiting**: switched from `map[string]bool` to `map[string]time.Time` with 30-second staleness check, 20-entry cap (returns 429 when full), ticker goroutine cleans up stuck entries - **Profile picture upload limit**: 15MB client-side check before crop dialog in account page - **Portfolio image upload backend limit**: separate `portfolioBodyLimit` (20MB) applied to `/images` route, distinct from `uploadBodyLimit` (15MB) for profile pictures - **GDPR compliance system**: Full Article 15 Subject Access Request via `/gdpr` — async Go endpoint (`GET /api/user/gdpr-export`) with 12h in-memory cache and background generation, 16-section SQL export (`export_all_user_data()`) covering profile, bookings (with override pricing), payments, refunds, saved cards, social logins, loyalty redemptions, booking discounts, edit requests, affiliate payouts, verification codes, forgiven no-shows, patch tests, referrals, notification preferences. Frontend: skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download. Conditional rendering for empty sections, VAT breakdowns (hidden until non-zero), deposits required (hidden when 0). Account deletion (`DELETE /api/user/account`) extended with external system scrubbing — S3 profile picture deletion, Square saved card deletion — before SQL-level anonymization. `anonymize_user()` SQL function extended with child table PII scrubbing (social logins, saved cards soft-delete with PCI clearance, verification code expiry, time blocker reservation scrubbing, edit request notes nulling, notification preference deletion). `AnonymizeStaleGuestAccounts()` extended with additional field scrubbing (profile_pic_url, referral_code, notes, data_retention_consent). 25 tests covering all backend additions. - **Financial data retention & aggregation**: Granular payment/refund records retained for `MAX(created_at + 7 years, user_anonymized_at + 1 year)` (HMRC + Limitation Act compliance). Expired records are aggregated into `financial_aggregates` (monthly totals by payment method/type, no PII) and deleted. Triggered lazily on `GET /api/availability` alongside existing cleanup functions. Idempotent — safe to run repeatedly. 8 tests covering retention edge cases, aggregation correctness, refund handling, and idempotency. ## Project Structure ``` Crussell/ ├─ backend/ # Go 1.25 + chi router API │ ├─ handlers/ # API handlers (auth, bookings, payments, scheduling, etc.) │ │ ├─ payments/ # Square payment handlers (terminal, online, refunds, tips) │ │ └─ webhooks/ # Square webhook handler │ ├─ internal/ # Internal packages │ │ └─ square/ # Square client (dev mock + prod stub, build-tagged) │ └─ testutils/ # Test helpers (fixtures, testdb, JWT) ├─ frontend/ # SvelteKit 5 SPA (static build) │ └─ src/lib/components/ │ ├─ payments/ # PaymentModal (admin), UserPaymentModal (user) │ ├─ booking/ # BookingFlow (5-step wizard), DatePicker, TimeSlotPicker, TimeSlotList, SelectedTimeSummary │ ├─ admin/ # BookingCreateModal, RescheduleModal, TimeBlockers, WeeklySchedule, HolidayHours, etc. │ ├─ today/ # TodayCalendar (interactive grid), CurrentAppointment, PendingApprovals, TodayStats │ └─ account/ # EditRequestModal, UserBookingModal ├─ frontend/src/lib/utils/ # Shared utilities (timeSlots.ts, format.ts) ├─ sabredav/ # PHP + Composer for DAV ├─ nginx/ # Nginx reverse-proxy for HTTP & HTTPS ├─ init-scripts/ # PostgreSQL init SQL ├─ compose.yml # Docker-Compose definition ├─ local-dev-2.sh # Dev helper using tmux (seeds DB with test data) └─ README.md ``` ## Prerequisites | Tool | Version | |------|---------| | Docker & Docker Compose | >= 20.10 | | Go | >= 1.22 | | Node | >= 18 (npm) | | tmux | >= 3.0 | ## Getting Started ### Docker Compose ```bash cp .env.example .env # Edit .env — set POSTGRES_*, JWT_SECRET_KEY docker compose up --build -d ``` | Service | URL | |---------|-----| | Frontend | http://localhost | | API | http://localhost/api | | SabreDAV | http://localhost/dav | ### Local Development ```bash chmod +x local-dev-2.sh ./local-dev-2.sh ``` Launches a tmux session (`crussell-dev`) with 4 panes: psql console, Go dev server, Svelte dev server, and Rustfs logs. Seeds the database with 20 users, 12 services, 43 bookings, guest accounts, time blockers, and exceptional hours. Default login (all password: `password`): - Admin: `admin@example.com` - User: `user@example.com` ## Building & Testing ```bash # Backend cd backend && go build -o bin/backend ./main.go # Frontend cd frontend && npm ci && npm run build # Tests (584/587 passing, 3 skipped) cd backend && go test -tags "test,dev" ./... ``` ## Full Documentation For detailed user journeys, admin workflows, architecture, API reference, and database schema, see [obsidian/Crussell/](obsidian/Crussell/).