Files
Crussell/obsidian/Crussell/Crussell Nails.md
T
2026-02-20 12:59:10 +00:00

24 KiB

Last Updated: February 2026 Status: Work in Progress


Project Checklist

Backend (Go / Chi / Postgres)

Authentication & Authorization

  • JWT authentication (login, refresh, role verification)
  • User registration with input validation
    • Names: 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot
    • Phone: UK format → E.164 (+44...)
    • Email: standard format
    • Age: Must be 16+ years
  • Password hashing with bcrypt
  • Middleware for auth/roles (mw.RequireAuth, mw.RequireAdmin)
  • DB connection pooling
  • Refresh token endpoint - Wired to POST /api/refresh-token, auto-refresh in frontend
  • Login rate limiting (1 attempt per 5 seconds)
  • Global rate limiting middleware (per-endpoint: 120/min public, 10/min register, 60/min filters, none admin)
  • Security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
  • Strict-Transport-Security (HSTS) - Tell browsers to only access via HTTPS, prevents downgrade attacks. Add after HTTPS is working in prod.
  • Referrer-Policy - Track referrer sources for analytics (social media tracking). Use strict-origin-when-cross-origin to send origin but not full URLs.
  • Rate limiter + Cloudflare - Currently doesn't read CF-Connecting-IP header, so behind Cloudflare all users share one rate limit bucket.
  • Account creation spam - Registration endpoint (10/min) could benefit from additional bot protection beyond rate limiting.
  • Input validation on all endpoints:
    • Registration: name (1-50), email (255), phone (20), password (72), age 16+
    • Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
    • Portfolio: tags/filters (256 char max), filter category validation, image ID pattern security

Booking System

  • /api/bookings - Full CRUD for authenticated users
  • /api/admin/bookings - List, search, create for user, progress, confirm, cancel
  • /api/admin/bookings/search - Search functionality
  • /api/admin/bookings/user/{user_id} - User-specific bookings
  • /api/admin/bookings/{id}/progress - Progress booking status
  • /api/admin/bookings/{id}/confirm - Confirm booking
  • /api/admin/bookings/{id}/cancel - Cancel booking
  • In-progress auto-infer - Status should auto-set based on time
  • Begin button on Today - Manual start for early arrivals (gray out if >3hrs away)

Admin Endpoints

  • /api/admin/services - Create, delete, list, toggle
  • /api/admin/users - List, view with booking history
  • /api/admin/today - Current/next appointment, today's appointments, pending approvals
  • /api/admin/notifications - GET/acknowledge endpoint wired, but:
    • Pending notification acknowledgment - Acknowledged on confirm/cancel (not deleted)
    • Cancelled booking notification - Created when cancelling non-pending bookings
    • Frontend UI to display notifications
    • Push mechanism (currently only pull-based)
    • User notifications (only admin notifications exist)
    • User notification preferences (DB table ready)

Scheduling System

  • /api/scheduling/default-hours - GET public, PUT admin
  • /api/scheduling/exceptional-groups - CRUD for holiday/special hours
  • /api/scheduling/working-hours - Merged default + exceptional hours
  • /api/scheduling/available-hours - Available slots accounting for bookings

User Endpoints

  • /api/user/profile - GET, PUT
  • /api/user/account - DELETE (GDPR compliant)
  • /api/user/loyalty - GET loyalty stamps
  • GDPR data export - export_all_user_data() exists but not wired to endpoint
  • Tax data export - Admin endpoint for tax-software-compatible format

Not Yet Wired

  • Social auth (handlers/auth/social.go exists, not imported)
  • Analytics (handlers/admin/analytics.go exists, not imported)
  • Portfolio/images - NOW WIRED: /api/portfolio/images, /api/portfolio/tags, /api/portfolio/filters, /api/portfolio/images/{id}
  • Guest user endpoint (/api/users/guest - needed for walk-in bookings)

Unit Tests & CI/CD

  • Unit tests
  • CI/CD pipeline

Frontend (SvelteKit / Tailwind / shadcn)

Core Pages

  • Home (/)
  • Prices (/prices)
  • Contact (/contact)
  • Book (/book) - Full wizard with service selection, date/time, customer details
  • Portfolio (/portfolio) - S3/R2 storage with tag filtering, category filters, pagination, ?img= featured image, admin upload
  • Today (/today) - Admin only, real-time schedule view
  • Account (/account)
  • Login (/login)
  • Manage (/manage)

Admin Dashboard (/admin)

  • Auth guard with role check
  • ImageUpload component
  • UsersCard + UserModal
  • BookingsCard + BookingModal
  • HolidayHours (exceptional hours management)
  • WeeklySchedule (default hours management)
  • ServicesManagement
  • BookingCreateModal (call-in/admin booking creation)
  • WalkInBooking + WalkInCreateModal
  • CallInBooking
  • ApprovalModal

Booking Flow

  • Service selection with pricing/duration
  • Calendar with availability detection
  • Time slot generation with gap logic
  • Customer details form (guest or authenticated)
  • Auth store with token refresh logic
  • Customer booking submit - submitBooking() only logs, needs POST /api/bookings
  • Payment integration (Square placeholder)

API Integration

  • Services fetch from /api/services
  • Working hours fetch from /api/scheduling/working-hours
  • Available hours fetch from /api/scheduling/available-hours
  • Admin bookings use /api/admin/bookings
  • Guest user creation (/api/users/guest not implemented)

Infrastructure

  • .env config
  • Docker Compose (postgres, backend, sabredav, nginx)
  • Static frontend build served via nginx
  • nginx reverse proxy - config under review
  • Monitoring/logging - no stack configured
  • CI/CD pipeline (Gitea) - not yet defined
  • Prometheus metrics integration

Integrations

  • CardDAV sync for contacts (SabreDAV)
  • CalDAV ready
  • Email/SMS reminders - not yet implemented
  • Square payment - placeholder only
  • S3/R2 image hosting - Rustfs for dev, Cloudflare R2 for prod via build tags

Current Architecture

Overview Diagram

flowchart TD
    User([Customer])
    Admin([Admin])

    subgraph Docker[Docker Compose Stack]
        subgraph NGINX[Nginx :80/:443]
            Static[Static Frontend Build]
            Proxy[API Proxy → Backend:8080]
            DAVProxy[DAV Proxy → SabreDAV]
        end

        subgraph Backend[Go + Chi :8080]
            Router[chi Router]
            Auth[JWT Middleware]
            Handlers[API Handlers]
        end

        subgraph Database[PostgreSQL :5432]
            DB[(Users / Bookings<br/>Payments / Services<br/>Scheduling)]
        end

        subgraph DAV[SabreDAV :9000]
            CardDAV[(vCard Contacts)]
            CalDAV[(Calendar Events)]
        end
    end

    subgraph External[External Services - TODO]
        Gmail[Gmail SMTP]
        SquareAPI[Square API]
        S3[S3/R2 Storage]
    end

    User -->|HTTPS| NGINX
    Admin -->|HTTPS| NGINX
    Static --> User
    Proxy --> Router
    Router --> Auth
    Auth --> Handlers
    Handlers --> DB
    Handlers --> DAV
    Handlers -.->|TODO| Gmail
    Handlers -.->|TODO| SquareAPI
    Handlers -.->|TODO| S3

API Reference

Public Endpoints

Method Path Description
GET /api/services List active services
POST /api/register Create new user account
POST /api/login Authenticate and receive JWT
GET /api/scheduling/default-hours Get weekly default hours
GET /api/scheduling/exceptional-groups List holiday/special hour groups
GET /api/scheduling/working-hours Get merged working hours for date range
GET /api/scheduling/available-hours Get available booking slots

Authenticated User Endpoints

Method Path Description
GET /api/user/profile Get current user profile
PUT /api/user/profile Update profile
DELETE /api/user/account Delete account (GDPR)
GET /api/user/loyalty Get loyalty stamp count
GET /api/bookings List user's bookings
POST /api/bookings Create booking
GET /api/bookings/{id} Get specific booking
PUT /api/bookings/{id} Update booking
DELETE /api/bookings/{id} Cancel booking

Admin Endpoints

Method Path Description
GET /api/admin/services List all services
POST /api/admin/services Create service
DELETE /api/admin/services/{id} Delete service
PUT /api/admin/services/{id}/toggle Toggle active status
GET /api/admin/bookings List all bookings
POST /api/admin/bookings Create booking for user
GET /api/admin/bookings/search Search bookings
GET /api/admin/bookings/user/{user_id} User's bookings
PUT /api/admin/bookings/{id}/progress Progress status
POST /api/admin/bookings/{id}/confirm Confirm booking
POST /api/admin/bookings/{id}/cancel Cancel booking
GET /api/admin/users List users
GET /api/admin/users/{id} Get user details
GET /api/admin/today/current-next Current and next appointment
GET /api/admin/today/appointments Today's appointments
GET /api/admin/today/pending-approvals Pending approval queue
GET /api/admin/notifications List notifications
POST /api/admin/notifications/{id}/acknowledge Acknowledge
PUT /api/scheduling/default-hours Update weekly hours
POST /api/scheduling/exceptional-groups Create exception group
DELETE /api/scheduling/exceptional-groups Delete exception group
PUT /api/scheduling/exceptional-applications Apply exceptions to dates

Portfolio Endpoints

Method Path Description
GET /api/portfolio/images List images with pagination, filter by tag/tags, filter by category:value
GET /api/portfolio/tags List all tags for autocomplete (public)
GET /api/portfolio/filters Get filter categories with counts. Supports tag filtering. Unselected categories show counts reduced by other filters.
GET /api/portfolio/images/{id} Get single image by UUID or timestamp (fallback to URL pattern match)
POST /api/portfolio/images Upload new image with thumbnail and tags (admin only)
DELETE /api/portfolio/images/{id} Delete image (admin only)

Portfolio Frontend Features:

  • /portfolio page with tag-based filtering and category filters
  • Category filters: ?filter[color]=red&filter[season]=summer
  • Tag search: ?tag=toby or ?tags=toby,summer
  • Featured image: ?img=<id|timestamp> - loads image directly, bypasses filters
  • Filter UI: scrollable dropdowns, keyboard navigation, mobile-optimized
  • Admin upload: ImageUpload component with live tag suggestions, keyboard nav, confirmation modal

Database Schema

Enums

account_role: unverified_email | verified_email | admin | guest | affiliate
account_type: email | google | microsoft | facebook | guest
booking_status: pending | confirmed | in_progress | completed | client_cancelled | we_cancelled | re-schedule | no_show
payment_type: deposit | full | tip | balance | partial
payment_method: online_square | in_person_card | cash | giftcard | discount
payment_status: pending | completed | failed | refunded
admin_notification_reason: pending_booking | cancelled_booking | rescheduled_booking | 1_week_no_pay | 1_month_no_pay | affiliate_claim

Suggested additional admin_notification_reason values:

Reason Purpose
payment_failed Payment processing failed
patch_test_due Customer needs patch test before appointment
first_time_customer New customer's first booking
inactive_customer Regular hasn't booked in X months - 5% discount (non stacking)
birthday_this_week Customer birthday - 5% discount (stacking)
schedule_conflict Potential double-booking detected - admin alert, 'second customer' alert

Core Tables

Table Purpose
users User accounts with profile data
user_social_logins Social auth provider links
services Service offerings
user_service_patch_tests Patch test tracking
bookings Appointment records
booking_services Services per booking
user_referrals Referral tracking
working_hours Default weekly schedule
exceptional_working_hours_groups Holiday/special hour groups
exceptional_working_hours Hours for exception groups
exceptional_group_applications Apply exceptions to date ranges
payments Payment transactions
business_settings Business configuration
admin_notifications Admin notification queue
images Portfolio gallery images with tags
tags Image tag autocomplete
user_notification_preferences User notification preferences (email/sms/push)

Key Functions

Function Purpose
generate_short_id() Generate 12-char IDs
anonymize_user() GDPR data removal
delete_guest_user() Clean up guest accounts
export_all_user_data() GDPR subject access
get_monthly_business_summary() Analytics
get_vat_return_data() VAT reporting
calculate_vat() VAT calculation
get_receipt_data() Receipt generation

Remaining Work

High Priority

Task Description Files Affected
Customer booking submit submitBooking() at line 600 only logs, needs POST /api/bookings frontend/src/lib/components/booking/BookingFlow.svelte
Remove console.logs Debug logs left in: BookingFlow.svelte:600, BookingCreateModal.svelte:224 Frontend components
Guest user endpoint Create /api/users/guest for walk-in bookings backend/handlers/user/ (new file)
In-progress auto-infer Auto-set in_progress status based on time Backend booking logic
Begin button (Today) Manual start for early arrivals, gray out if >3hrs away CurrentAppointment.svelte + backend
One-off custom services Admin creates custom service for single booking without adding to main list Backend + frontend booking modals
One-off exceptional hours Single-day exceptions (dentist, afternoon off) - not yearly/weekly Backend scheduling + frontend HolidayHours
Auto lunch protection Block bookings that remove lunch break (1h customer, 30min admin with warning) Backend available-hours logic
Walk-in slot blocking Properly block next available slot during walk-in intake WalkInCreateModal.svelte
Square payment integration Full Square SDK integration Backend payment handlers + frontend payment step
GDPR data export User button for "give me my data" using export_all_user_data() Backend endpoint + account page
Tax data export Admin button for tax-software-compatible format Backend endpoint + admin page

Medium Priority

Task Description
Notifications UI Frontend panel to display admin notifications
Notifications push Real-time notification mechanism (WebSocket/polling)
User notification preferences [x] DB table ready, waiting on user notification system
User notifications Notification system for regular users (booking confirmations, reminders)
Remove debug logs console.log in BookingFlow.svelte:600 and BookingCreateModal.svelte:224
Loyalty display component Show stamps in account/bookings
Email/SMS reminders Scheduled notification jobs
Prometheus metrics Monitoring integration

Low Priority

Task Description
Social auth Wire handlers/auth/social.go
Analytics Wire handlers/admin/analytics.go
nginx config review Finalize production config
CI/CD pipeline Gitea Actions workflow
And many more

Environment Variables

Variable Purpose Required
JWT_SECRET_KEY Secret for JWT signing Yes
DATABASE_URL PostgreSQL connection string Yes
POSTGRES_USER Database username Docker
POSTGRES_PASSWORD Database password Docker
POSTGRES_DB Database name Docker

Docker Compose

services:
  postgres:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init-scripts/init-script.sql:/docker-entrypoint-initdb.d/init-script.sql:ro

  backend:
    build: ./backend
    depends_on: [postgres]

  sabredav:
    image: php:8.2-fpm
    volumes:
      - ./sabredav:/var/www/dav
    depends_on: [postgres]

  nginx:
    image: nginx:stable
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./frontend/build:/usr/share/nginx/html
    depends_on: [backend, sabredav]

Frontend Component Structure

src/lib/components/
├── admin/
│   ├── ApprovalModal.svelte
│   ├── BookingCreateModal.svelte
│   ├── BookingModal.svelte
│   ├── BookingsCard.svelte
│   ├── CallInBooking.svelte
│   ├── HolidayHours.svelte
│   ├── ImageUpload.svelte
│   ├── ServicesManagement.svelte
│   ├── UserModal.svelte
│   ├── UsersCard.svelte
│   ├── WalkInBooking.svelte
│   └── WalkInCreateModal.svelte
├── booking/
│   ├── BookingActions.svelte
│   ├── BookingFlow.svelte
│   ├── BookingSummary.svelte
│   ├── DatePicker.svelte
│   ├── ServiceCard.svelte
│   ├── ServiceSelector.svelte
│   ├── StepIndicator.svelte
│   └── TimeSlotPicker.svelte
└── today/
    ├── CurrentAppointment.svelte
    ├── PendingApprovals.svelte
    └── TodayCalendar.svelte

Notes

  • Holiday hours ARE integrated into all 3 booking flows (customer, call-in, walk-in) via /api/scheduling/working-hours and /api/scheduling/available-hours
  • The backend merges default hours with applied exceptional hours automatically
  • Static frontend is built and served by nginx; API calls go directly to Go backend in production
  • Local dev uses SvelteKit's API proxy for CORS avoidance
  • GDPR functions exist in SQL (anonymize_user, export_all_user_data, delete_guest_user) but only DELETE /api/user/account is wired - need user data export and admin tax export endpoints
  • Debug console.logs in BookingFlow.svelte:600 and BookingCreateModal.svelte:224 should be removed before production
  • Notifications are pull-based only (no push/WebSocket). Admin endpoint exists but no frontend UI. No user-facing notification system yet.
  • Notification acknowledgment: When a booking is confirmed, any pending notification is acknowledged. When cancelled, pending is acknowledged and cancelled_booking notification is only created if the booking was not in pending status (e.g. was confirmed or in_progress).
  • User notification preferences: user_notification_preferences table exists with email/sms/push enabled flags, waiting on user notification system to be implemented.

Development Workflow

Quick Start

./local-dev-2.sh  # Creates tmux session 'crussell-dev'

Creates 3 panes:

  • Pane 0: psql interactive shell
  • Pane 1: Backend (go run -tags dev ./main.go)
  • Pane 2: Frontend (npm run dev -- --host)

Dev Build Tag

Backend uses -tags dev - check for dev-specific behavior in code with build constraints.

Admin User Setup

No API endpoint exists for role promotion. Admin users are created via direct SQL:

UPDATE users SET account_role = 'admin' WHERE email = 'admin@example.com';

Seed Data

Running local-dev-2.sh creates:

Resource Count Details
Users 18 1 admin, 17 regular users
Services 6 Classic Manicure, Gel Manicure (BIAB), Luxury Pedicure, Express Mani & Pedi, Gel Removal, Nail Art Add-on
Bookings 45 8 past, 3 today, 4 tomorrow, 30 future (spread over 15 days)
Confirmed ~50% Random selection of upcoming bookings auto-confirmed
Exceptional 2 November Break (closed), Christmas Holiday (reduced hours)

API JSON Examples

Create Booking:

{
  "start_time": "2025-01-15T10:00:00+00:00",
  "service_ids": ["abc123def456"],
  "notes": "Optional notes"
}

Create Service:

{
  "name": "Classic Manicure",
  "description": "Nail shaping, cuticle care, hand massage, and polish.",
  "price": 25.00,
  "duration_minutes": 45,
  "patch_test_duration_hours": 0,
  "minimum_age_required": 0
}

Confirm Booking:

POST /api/admin/bookings/{id}/confirm
Body: {"serviceOverrides": []}

Create Exceptional Group:

{
  "name": "Christmas Holiday Period",
  "description": "Reduced hours for Christmas and New Year",
  "hours": [
    {"weekday": 0, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false},
    {"weekday": 1, "startTime": "10:00:00", "endTime": "15:00:00", "isOpen": true}
  ],
  "weekStarts": ["2025-12-22", "2025-12-29"]
}

Time Format

All timestamps use ISO 8601 with timezone: YYYY-MM-DDTHH:MM:SS±HH:MM (e.g., 2025-01-15T10:00:00+00:00)

Timezone is always Europe/London (handles BST automatically).


Code Patterns

Transaction Pattern

Used throughout for atomic operations:

tx, err := db.DB.Begin(r.Context())
if err != nil { /* handle error */ }
defer tx.Rollback(r.Context())

// Use tx instead of db.DB for queries
err = tx.QueryRow(r.Context(), `INSERT INTO...`)

if err := tx.Commit(r.Context()); err != nil { /* handle error */ }

CardDAV Synchronization

  • On registration: Creates vCard in SabreDAV via dav.Service.CreateContact()
  • On profile update: Updates existing vCard via updateCardDAV() helper
  • Uses internal HTTP calls to DAV server

Role Change Detection

RefreshTokenHandler verifies user's current role hasn't changed since token was issued. If role changed, forces re-login with 401 Unauthorized.

Build Tags

  • db_dev.go - Used with -tags dev for local development (localhost connection)
  • db.go - Production build (uses env var for host)
  • internal/dav/service_dev.go / service_prod.go - Same pattern for DAV service