d2dea38b01f53025f6cf8914c9d9d63a0fe36221
- Changed purchase flow to use generateIdempotencyKey() which uses secure window.crypto.getRandomValues with a math fallback, avoiding secure-context blocks on HTTP - Log actual error to console in both Buy and Redeem catch blocks
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,
/notificationspage, 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,calculateAgeinlib/utils/format.ts - Referral code registration: new users can enter a 12-character referral code during registration; relationship recorded in
user_referralstable - Admin schedule page: Google Calendar-style week view at
/admin/schedulewith 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
/todayafter login instead of the home page - Patch test duration on services:
patch_test_duration_hoursexposed on Service type; creating a service with duration > 0 auto-creates a patch test record created_by_nameon 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_startrecords - 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
ImageVariantcomponent 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 withfull_avif_url,full_webp_url,full_jpg_url,full_jxl_url,thumb_avif_url,thumb_webp_url,thumb_jpg_urlcolumns. 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,
flytransitions 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: Replacednew Date()withnew SvelteDate()in reactive contexts (admin schedule, account page booking filter) for proper Svelte 5 reactivity. $derivedfor computed values:totalPagesin notifications page now uses$derivedinstead 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/pathsresolve: All internal navigation hrefs now useresolve()from$app/pathsfor proper SPA routing.- Tailwind fuchsia ring safelist: Added
ring-fuchsia-*pattern to Tailwind safelist for loyalty stamp theming. - Vite WASM support:
vite.config.tsconfigured to include.wasmfiles 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 formatDateISOutility: newformatDateISO(date)function inlib/utils/format.tsreturning YYYY-MM-DD format for API calls, later extracted to shared utilities- JWT revocation with JTI: every JWT carries a unique
jticlaim (UUID v4), in-memory revoked JTI tracking with 5-minute cleanup ticker,POST /api/logoutendpoint revokes current token, refresh handler revokes old JTI before issuing replacement - Portfolio image deletion fix:
extractKeycorrectly extracts full S3 key path from URLs instead of just the filename, preventing orphaned files in storage - Notes validation: all
Notes *stringfields across booking structs validated withmax=1000000tag (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]booltomap[string]time.Timewith 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/imagesroute, distinct fromuploadBodyLimit(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 intofinancial_aggregates(monthly totals by payment method/type, no PII) and deleted. Triggered lazily onGET /api/availabilityalongside 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
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
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
# 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/.
Languages
Go
64.6%
JavaScript
16.5%
Svelte
14.8%
TypeScript
1.9%
PLpgSQL
1.2%
Other
0.9%