Update CLAUDE.md to reflect current booking system architecture
Deploy to Dev / Deploy & Smoke Test (push) Successful in 17s
Deploy to Dev / Deploy & Smoke Test (push) Successful in 17s
Documents the two-service Docker Compose setup, booking wizard step status, all JS functions, location photo workflow, CI/CD pattern, and pending issues so the project can be resumed from any machine that clones the repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,71 +1,197 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
This file provides guidance to Claude Code when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
LisiLou Photography Portfolio is a lightweight, zero-framework photography portfolio website designed to integrate with Immich for image hosting and client gallery access. It's a single-page application with all code in one HTML file, using vanilla JavaScript and inline CSS.
|
||||
LisiLou Photography Portfolio — a photography booking and portfolio site for Elysse Hodgkin.
|
||||
Built as a zero-framework SPA with a Node.js/Express booking API, deployed on a homelab Proxmox cluster.
|
||||
|
||||
**Live dev URL:** http://192.168.1.192:8080 (CT114, lisilou-dev LXC)
|
||||
**Gitea repo:** https://git.jerodrigged.com/jhodgkin/lisilou-portfolio
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend:** Vanilla HTML5, CSS3, JavaScript (no framework)
|
||||
- **Server:** Nginx (Alpine-based)
|
||||
- **Deployment:** Docker + Docker Compose
|
||||
- **CI/CD:** Gitea Actions
|
||||
| Layer | Tech |
|
||||
|-------|------|
|
||||
| Frontend | Vanilla HTML5/CSS3/JS (no framework), single file |
|
||||
| Portfolio server | Nginx Alpine |
|
||||
| Booking API | Node.js 20 / Express / better-sqlite3 |
|
||||
| Deployment | Docker Compose (two services: `portfolio` + `api`) |
|
||||
| CI/CD | Gitea Actions → auto-deploys to CT114 on push to `main` |
|
||||
| Image hosting | Immich (immich.jerodrigged.com) |
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Start development server (serves at http://localhost:8080)
|
||||
# Start full stack locally (portfolio on :8080, API on :3001 internally)
|
||||
docker compose up -d
|
||||
|
||||
# Rebuild after code changes to src/index.html
|
||||
# Rebuild after changes to src/index.html or api/
|
||||
docker compose build && docker compose up -d
|
||||
|
||||
# View container logs
|
||||
docker logs lisilou-portfolio
|
||||
# API logs only
|
||||
docker logs lisilou-api
|
||||
|
||||
# Run smoke tests against dev LXC
|
||||
bash scripts/smoke-test.sh http://192.168.1.192:8080
|
||||
```
|
||||
|
||||
**Note:** Configuration (`config/`) and image (`public/images/`) changes are applied immediately without rebuild since they're mounted as volumes.
|
||||
**Volume-mounted (no rebuild needed):**
|
||||
- `config/` — site content and booking config
|
||||
- `public/images/` — portfolio and location photos
|
||||
- `api/data/` — SQLite database
|
||||
- `api/signed-contracts/` — completed contract PDFs
|
||||
|
||||
## Architecture
|
||||
|
||||
### Single-File SPA
|
||||
The entire application lives in `src/index.html` (~865 lines):
|
||||
- Lines 1-605: Inline CSS with CSS custom properties for theming
|
||||
- Lines 606-865: HTML structure and vanilla JavaScript
|
||||
### Two-Service Docker Compose
|
||||
|
||||
### Configuration-Driven Content
|
||||
All site content is loaded dynamically from `config/site.json` at runtime:
|
||||
- Site branding (title, tagline, logo)
|
||||
- Theme colors (applied to CSS variables)
|
||||
- Portfolio categories with Immich album links
|
||||
- Social media links
|
||||
- Client gallery access settings
|
||||
```
|
||||
Browser → Nginx (:8080)
|
||||
├── / → src/index.html (SPA)
|
||||
├── /config/ → config/ volume (5-min cache)
|
||||
├── /images/ → public/images/ volume (1-year cache)
|
||||
├── /api/ → proxy → api:3001 (Node.js)
|
||||
└── /health → 200 OK
|
||||
```
|
||||
|
||||
### Immich Integration
|
||||
The app serves as a gateway to Immich shared albums:
|
||||
- Portfolio categories link to Immich albums via `immichAlbumId`
|
||||
- Client gallery codes are Immich share IDs
|
||||
- Full URLs constructed as: `{baseUrl}{publicAlbumPrefix}{albumId}`
|
||||
### Single-File SPA (`src/index.html`, ~1800 lines)
|
||||
- Lines 1–900: Inline CSS with CSS custom properties for theming
|
||||
- Lines 900+: HTML structure and vanilla JavaScript
|
||||
- Booking modal is a 7-step wizard rendered entirely in this file
|
||||
|
||||
### Multi-Tenant Support
|
||||
Multiple photographers can be supported via `config/profiles.json`, mapping domains to different configuration files.
|
||||
### Configuration-Driven Content (`config/site.json`)
|
||||
|
||||
All content is loaded at runtime — no rebuild needed:
|
||||
|
||||
```
|
||||
site.json
|
||||
├── site — title, tagline, logo, favicon
|
||||
├── photographer — name, bio, profile image
|
||||
├── contact — email, phone
|
||||
├── social — instagram, facebook, etc.
|
||||
├── immich — baseUrl, publicAlbumPrefix
|
||||
├── portfolio — categories with immichAlbumId
|
||||
├── clientAccess — enable/disable client gallery login
|
||||
├── locations[] — location picker cards (see below)
|
||||
├── booking — sessionTypes[], pricing {mini, full}, venmoUsername
|
||||
└── theme — colors, fonts
|
||||
```
|
||||
|
||||
### Booking API (`api/`)
|
||||
|
||||
Express app at port 3001 with SQLite (`api/data/bookings.db`).
|
||||
|
||||
```
|
||||
api/
|
||||
├── server.js — Express app (routes, CORS, JSON middleware)
|
||||
├── db.js — SQLite init; creates bookings table on first run
|
||||
├── package.json
|
||||
├── package-lock.json ← committed; required for npm ci in Docker
|
||||
├── Dockerfile
|
||||
├── .env ← gitignored; copy from .env.example
|
||||
└── .env.example ← committed template
|
||||
```
|
||||
|
||||
**Bookings table columns:** `id`, `created_at`, `client_name`, `client_email`,
|
||||
`client_phone`, `client_sub` (OIDC), `session_date`, `session_type`,
|
||||
`session_length`, `location`, `contract_signed_at`, `contract_pdf_path`,
|
||||
`payment_status`, `payment_notified_at`, `status`, `notes`
|
||||
|
||||
**API env vars** (set in `api/.env` on the server, never committed):
|
||||
```
|
||||
PORT=3001
|
||||
CORS_ORIGIN=http://localhost:8080
|
||||
N8N_WEBHOOK_URL=
|
||||
GOOGLE_CALENDAR_ID=
|
||||
GOOGLE_SERVICE_ACCOUNT_JSON=
|
||||
SESSION_SECRET=
|
||||
OIDC_ISSUER=
|
||||
OIDC_CLIENT_ID=
|
||||
OIDC_CLIENT_SECRET=
|
||||
OIDC_REDIRECT_URI=
|
||||
```
|
||||
|
||||
### Location Photos
|
||||
|
||||
Drop images into `public/images/locations/<location-id>/` on the server.
|
||||
No rebuild needed — nginx serves the volume directly with 1-year caching.
|
||||
|
||||
```
|
||||
public/images/locations/
|
||||
├── river-bottoms/ hero.jpg, 1.jpg, 2.jpg
|
||||
├── city-park/ hero.jpg, 1.jpg, 2.jpg
|
||||
├── downtown/ hero.jpg, 1.jpg, 2.jpg
|
||||
└── studio/ hero.jpg, 1.jpg, 2.jpg
|
||||
```
|
||||
|
||||
Missing images fall back to a styled placeholder — safe to deploy before photos are ready.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.html` | Complete SPA (CSS + HTML + JS) |
|
||||
| `config/site.json` | Runtime configuration for all content |
|
||||
| `config/profiles.json` | Multi-tenant profile routing |
|
||||
| `nginx.conf` | Caching rules, security headers, SPA routing |
|
||||
| `Dockerfile` | Multi-stage build (Node Alpine → Nginx Alpine) |
|
||||
| `.gitea/workflows/deploy.yml` | CI/CD pipeline |
|
||||
| `src/index.html` | Complete SPA — CSS + HTML + booking wizard JS |
|
||||
| `config/site.json` | All runtime content: portfolio, booking, locations, theme |
|
||||
| `config/profiles.json` | Multi-tenant domain → config file routing |
|
||||
| `nginx.conf` | Proxy rules, caching, security headers, SPA fallback |
|
||||
| `Dockerfile` | Portfolio image (Node Alpine → Nginx Alpine, multi-stage) |
|
||||
| `api/Dockerfile` | API image (Node 20 Alpine + build tools for native deps) |
|
||||
| `docker-compose.yml` | Both services, volumes, healthchecks |
|
||||
| `.gitea/workflows/deploy.yml` | Push-to-main → SSH deploy → smoke tests |
|
||||
| `scripts/smoke-test.sh` | 6 curl assertions; exits non-zero on failure |
|
||||
|
||||
## Nginx Configuration Highlights
|
||||
## CI/CD — Trunk-Based Development
|
||||
|
||||
- **Caching:** Images = 1 year, CSS/JS = 1 month, Config = 5 minutes
|
||||
- **Health Check:** `/health` endpoint for container monitoring
|
||||
- **SPA Routing:** Falls back to `index.html` for all unmatched routes
|
||||
- **Security Headers:** X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy
|
||||
Every push to `main`:
|
||||
1. Gitea runner (CT117) SSHes into CT114 (dev LXC at 192.168.1.192)
|
||||
2. `git fetch origin && git reset --hard origin/main` — robust, never fails on drift
|
||||
3. `docker compose build && docker compose up -d`
|
||||
4. Health check loop (24 × 5s attempts)
|
||||
5. `bash scripts/smoke-test.sh` — 6 tests; pipeline fails if any fail
|
||||
|
||||
**Gitea secrets required:** `DEV_SSH_KEY`, `DEV_HOST`, `DEV_USER`
|
||||
|
||||
## Booking Wizard — Implementation Status
|
||||
|
||||
| Step | Feature | Issue | Status |
|
||||
|------|---------|-------|--------|
|
||||
| 1 | Date picker | #3 | Shell done; Google Calendar availability pending |
|
||||
| 2 | Session type | #4 | Done — config-driven from `sessionTypes[]` |
|
||||
| 3 | Session length | #4 | Done — mini/full with pricing from config |
|
||||
| 4 | Location picker | #5 | Done — photo cards with detail expand panel |
|
||||
| 5 | Contract e-signature | #6 | Shell placeholder; PDF.js + pdf-lib pending |
|
||||
| 6 | Venmo payment | #7 | Shell placeholder; deep link + QR pending |
|
||||
| 7 | Confirm & submit | — | Done — summary + POST /api/bookings |
|
||||
|
||||
### Booking JS Functions (in `src/index.html`)
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `openBooking()` | Reset state, render all cards, show modal |
|
||||
| `closeBooking()` | Hide modal, restore scroll |
|
||||
| `goToStep(n)` | Activate step panel, update step bar |
|
||||
| `renderSessionTypeCards()` | Builds step 2 from `siteConfig.booking.sessionTypes` |
|
||||
| `renderSessionLengthCards()` | Builds step 3 with pricing from `siteConfig.booking.pricing` |
|
||||
| `renderLocationCards()` | Builds step 4 from `siteConfig.locations` |
|
||||
| `selectOption(card, field)` | Generic card select; handles "other" field show/hide |
|
||||
| `selectLocation(id)` | Location card select + toggle detail panel |
|
||||
| `validateStep(step)` | Returns bool; validates "other" text on step 2 |
|
||||
| `populateSummary()` | Fills step 7 confirm rows from bookingState + config labels |
|
||||
| `submitBooking()` | POST /api/bookings; shows success state |
|
||||
|
||||
## Pending Issues
|
||||
|
||||
| # | Feature |
|
||||
|---|---------|
|
||||
| #3 | Google Calendar availability (real date picker with blocked dates) |
|
||||
| #6 | Contract e-signature (PDF.js viewer + HTML5 canvas signature pad) |
|
||||
| #7 | Venmo payment step (deep link + QR code) |
|
||||
| #8 | n8n email notifications on new booking |
|
||||
| #10 | Authentik OIDC (photographer admin + client self-registration) |
|
||||
| #11 | Management dashboard with payment status |
|
||||
| #12 | Authentik client self-registration enrollment flow |
|
||||
| #13 | Client portal /my-bookings |
|
||||
|
||||
Issues are tracked at: https://git.jerodrigged.com/jhodgkin/homelab/issues
|
||||
|
||||
Reference in New Issue
Block a user