# Crussell - Beauty Salon Booking System Stack: Go (Chi) backend | SvelteKit 5 static frontend | PostgreSQL | SabreDAV | Docker ## Dev Startup ```bash ./local-dev-2.sh # Creates tmux session 'crussell-dev' with 3 panes: # Pane 0: psql interactive # Pane 1: backend (go run -tags dev ./main.go) # Pane 2: frontend (npm run dev -- --host) ``` ## Architecture ``` nginx:80/443 → static frontend build + /api/* → go:8080 → postgres:5432 + sabredav ``` Frontend is built static (no SSR). API calls go directly to Go backend in production. Local dev uses SvelteKit proxy for CORS. ## Project Structure ``` Crussell/ ├── backend/ │ ├── main.go # Router, all routes defined here │ ├── auth/jwt.go # JWT init, signing │ ├── auth/password.go # bcrypt hashing │ ├── mw/auth.go # RequireAuth, RequireAdmin middleware │ ├── db/db.go # Connection pooling │ ├── db/db_dev.go # Dev-specific DB config │ ├── handlers/ │ │ ├── auth/local.go # Login, register, refresh-token │ │ ├── auth/social.go # NOT WIRED │ │ ├── bookings/ # User + admin booking CRUD │ │ ├── admin/ # Users, analytics (analytics NOT WIRED) │ │ ├── scheduling/ # Default + exceptional hours │ │ ├── services/ # Service management │ │ ├── user/ # Profile, loyalty, account │ │ ├── notifications/ # NOT WIRED │ │ ├── portfolio/ # NOT WIRED │ │ └── today/ # Current/next appointments │ └── internal/dav/ # CardDAV/CalDAV client ├── frontend/ │ └── src/ │ ├── routes/ │ │ ├── admin/+page.svelte # Admin dashboard │ │ ├── today/+page.svelte # Today view │ │ ├── book/+page.svelte # Booking wizard │ │ ├── login/+page.svelte # Auth │ │ └── api/[...path]/+server.ts # Dev proxy only │ └── lib/ │ ├── components/ │ │ ├── admin/ # 13 components │ │ ├── booking/ # 8 components │ │ └── today/ # 3 components │ ├── stores/auth.svelte.ts # Auth state │ └── types/booking.ts # TS interfaces ├── init-scripts/init-script.sql # Full schema + functions ├── compose.yml # Docker stack ├── nginx/conf.d/ # nginx config ├── sabredav/ # DAV server └── local-dev-2.sh # Dev environment + seeding ``` ## Helpful Greps ```bash # Find all API routes grep -n "r\.\(Get\|Post\|Put\|Delete\|Route\)" backend/main.go # Find TODOs/FIXMEs in code grep -rn "TODO\|FIXME" backend/ frontend/src/ --include="*.go" --include="*.svelte" # Find unwired handlers (imported in main.go?) grep -n "import.*handlers" backend/main.go # Shows all three booking creation handlers: user self-booking, admin walk-in, admin call/message-in grep -rn "func.*Create.*Booking\|func.*WalkIn\|func.*Walk.*In\|POST.*booking" backend/handlers/ --include="*.go" # Shows all three frontend booking flows: customer BookingFlow, admin walk-in modal, admin booking modal grep -rln "BookingFlow\|WalkIn\|walk-in\|call.*in\|message.*in" frontend/src/lib/components/ --include="*.svelte" F # Find where each booking flow starts - API routes and page loads grep -rn "booking.*create\|/api/bookings\|booking/POST\|booking/new" backend/ frontend/ --include="*.go" --include="*.ts" # Find all booking status handling grep -rn "booking_status\|in_progress\|confirmed\|pending" backend/handlers/ # Find frontend API calls grep -rn "fetch.*\/api\/" frontend/src/ --include="*.svelte" --include="*.ts" # Find auth-protected routes grep -n "RequireAuth\|RequireAdmin" backend/main.go # Find Svelte 5 reactive state grep -n "\$state\|\$derived\|\$effect" frontend/src/ -r --include="*.svelte" # Find SQL function definitions grep -n "CREATE.*FUNCTION" init-scripts/init-script.sql # Find specific handler implementation grep -l "func.*Handler" backend/handlers/**/*.go # Find transaction patterns grep -rn "tx, err := db.DB.Begin" backend/handlers/ # Find refresh token handler (exists but not wired) grep -n "RefreshTokenHandler" backend/handlers/auth/local.go # Find notification handler (exists but not wired) grep -n "func.*Notification" backend/handlers/notifications/notifications.go # Find guest booking TODO in frontend grep -n "TODO.*guest\|guest.*TODO" frontend/src/lib/components/admin/WalkInCreateModal.svelte # Find console.logs to remove grep -rn "console\.log" frontend/src/ --include="*.svelte" | grep -v node_modules ``` ## Auth Flow JWT in localStorage → decoded for role/user_id → profile fetch from `/api/user/profile`. Refresh logic exists but endpoint not wired. Roles: `unverified_email | verified_email | admin | guest | affiliate` **Admin promotion**: Direct SQL only, no API endpoint: `UPDATE users SET account_role = 'admin' WHERE email = '...'` **Validation rules**: - Names: 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot only - Phone: UK format, converted to E.164 (+44...) - Email: standard format validation - Age: Must be 16+ years old - Login rate limit: 1 attempt per 5 seconds ## Booking Status Flow ``` pending → confirmed → in_progress → completed ↘ client_cancelled | we_cancelled | no_show | re-schedule ``` TODO: `in_progress` should auto-infer by time OR manual "Begin" button (gray if >3hrs away). ## Scheduling - `/api/scheduling/default-hours` - Weekly template - `/api/scheduling/exceptional-groups` - Recurring exceptions (holidays) - `/api/scheduling/working-hours` - Merged result (default + applied exceptions) - `/api/scheduling/available-hours` - Slots minus bookings All 3 booking flows (customer, call-in, walk-in) correctly use merged hours. ## Critical TODOs **HIGH:** - `BookingFlow.svelte:600` - `submitBooking()` logs only, needs `POST /api/bookings` - `BookingCreateModal.svelte:224` - Remove `console.log(users)` debug - `/api/users/guest` - Guest endpoint for walk-ins - One-off custom services (single booking, no list add) - One-off exceptional hours (single day, not recurring) - Auto lunch protection (block if removes 1h lunch, 30min admin with warning) - Walk-in slot blocking during intake - Square payment integration - GDPR export endpoint (`export_all_user_data()` SQL exists) - Tax data export (admin, software-compatible format) **MEDIUM:** - Notifications UI (frontend panel for admin notifications) - Notifications push (WebSocket/polling mechanism) - User notifications (booking confirmations, reminders for customers) - Refresh token endpoint (handler exists, not wired) - Loyalty display component - S3/R2 for portfolio images - Prometheus metrics - CI/CD (Gitea) **NOT WIRED:** - `handlers/auth/social.go` - `handlers/admin/analytics.go` - `handlers/portfolio/images.go` ## Dev Build Tag Backend uses `go run -tags dev ./main.go` - check for dev-specific behavior. ## API JSON Examples **Booking:** `{"start_time":"2025-01-15T10:00:00+00:00","service_ids":["abc123def456"],"notes":"optional"}` **Service:** `{"name":"Classic Manicure","description":"...","price":25.00,"duration_minutes":45,"patch_test_duration_hours":0,"minimum_age_required":0}` **Confirm booking:** `POST /api/admin/bookings/{id}/confirm` with body `{"serviceOverrides":[]}` **Exceptional group:** `{"name":"Holiday","description":"...","hours":[{"weekday":0,"startTime":"00:00:00","endTime":"00:00:00","isOpen":false},...],"weekStarts":["2025-12-22"]}` ## Database Enums ```sql account_role, account_type, booking_status, payment_type, payment_method, payment_status, admin_notification_reason ``` **Current `admin_notification_reason`**: `pending_booking | cancelled_booking | rescheduled_booking | 1_week_no_pay | 1_month_no_pay | affiliate_claim` **Suggested additions**: `no_show`, `payment_failed`, `patch_test_due`, `loyalty_milestone`, `first_time_customer`, `vip_booking`, `inactive_customer`, `birthday_this_week`, `special_request`, `schedule_conflict` ## Key SQL Functions `anonymize_user()`, `export_all_user_data()`, `delete_guest_user()`, `get_vat_return_data()`, `calculate_vat()`, `get_receipt_data()` ## Env Required `JWT_SECRET_KEY`, `DATABASE_URL`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` ## Conventions - IDs: 12-char generated strings (not UUIDs) - Timezone: UK local throughout (`TZ=Europe/London`) - Frontend: Svelte 5 runes (`$state`, `$derived`, `$effect`) - Auth header: `Authorization: Bearer ${token}` - All times in ISO format with timezone: `YYYY-MM-DDTHH:MM:SS±HH:MM` ## Seed Data (local-dev-2.sh) - 18 users (1 admin, 17 regular) - 6 services (manicure, gel, pedicure, express, removal, nail-art) - 45 bookings (8 past, 3 today, 4 tomorrow, 30 future spread over 15 days) - ~50% of upcoming bookings auto-confirmed - 2 exceptional groups (November Break, Christmas Holiday) ## Code Patterns **Transaction pattern** (used throughout): ```go tx, err := db.DB.Begin(r.Context()) if err != nil { ... } defer tx.Rollback(r.Context()) // ... queries using tx instead of db.DB ... if err := tx.Commit(r.Context()); err != nil { ... } ``` **CardDAV sync**: - On registration: creates vCard in SabreDAV - On profile update: updates existing vCard via `updateCardDAV()` helper - Uses internal HTTP calls to DAV server **Role change detection**: `RefreshTokenHandler` checks if role changed since token issued - forces re-login if so.