Files
lisilou-portfolio/CLAUDE.md
T
jhodgkin 943fd9495f
Deploy to Dev / Deploy & Smoke Test (push) Successful in 23s
Update docs: Authentik OIDC setup is complete and verified
Reflects tonight's work: scripts/authentik-setup.sh actually run for
the first time (three bugs found/fixed), OIDC login and client
self-registration verified end-to-end on both dev and prod. Closes
the loop on issues #10/#12/#17.
2026-07-20 06:33:28 +00:00

227 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
## Project Overview
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)
**Production URL:** https://lisilou.jerodrigged.com (CT111 via Cloudflare tunnel → NPM CT102)
**Gitea repo:** https://git.jerodrigged.com/jhodgkin/lisilou-portfolio (deploys dev, CT114)
**GitHub mirror:** https://github.com/jhodgkin/lisilou-portfolio (deploys prod, CT111 — push here too or prod drifts!)
## Technology Stack
| 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 → CT114 (dev); GitHub Actions → CT111 (prod) on push to `main` |
| Image hosting | Immich (immich.jerodrigged.com) |
## Development Commands
```bash
# Start full stack locally (portfolio on :8080, API on :3001 internally)
docker compose up -d
# Rebuild after changes to src/index.html or api/
docker compose build && docker compose up -d
# API logs only
docker logs lisilou-api
# Run smoke tests against dev LXC
bash scripts/smoke-test.sh http://192.168.1.192:8080
```
**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
### Two-Service Docker Compose
```
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
```
### Single-File SPA (`src/index.html`, ~1800 lines)
- Lines 1900: 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
### Configuration-Driven Content (`config/site.json`)
All content is loaded at runtime — no rebuild needed:
```
site.json
├── site — title, tagline, logo, favicon, heroImage (optional hero photo)
├── 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 + 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 |
## CI/CD — Dev auto-deploys, prod is a deliberate promotion
Both pipelines run on Gitea Actions (git.jerodrigged.com). GitHub is not part
of the deploy story — `main` on the GitHub mirror is unused.
**Dev (every push to `main`)**`.gitea/workflows/deploy.yml`:
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` (tags images `lisilou-portfolio-web:local` / `lisilou-portfolio-api:local`)
4. If `REGISTRY_TOKEN` is set: tag+push both images to
`git.jerodrigged.com/jhodgkin/lisilou-portfolio-{web,api}:<short-sha>`
best-effort, a publish failure never blocks the dev deploy itself
5. `docker compose up -d` (runs the just-built local image)
6. Health check loop (24 × 5s attempts), then `bash scripts/smoke-test.sh`
**Prod (only on `git push origin main:prod`, or `<sha>:prod` to pin an older
commit)** — `.gitea/workflows/deploy-prod.yml`:
1. Gitea runner SSHes into CT111 (prod LXC, 192.168.1.246)
2. `git fetch origin && git reset --hard origin/prod`, then computes the same
short SHA dev used to tag its published image
3. `docker compose -f docker-compose.yml -f docker-compose.deploy.yml pull`
pulls that exact SHA-tagged image from the registry. **No `docker compose
build` ever runs here** — this is what guarantees prod runs the same
artifact dev already validated, not a fresh rebuild that could drift.
Pulling a SHA that dev never published fails loudly instead of silently
rebuilding.
4. `docker compose -f docker-compose.yml -f docker-compose.deploy.yml up -d`
5. Same health check + smoke test pattern, against CT111
`docker-compose.deploy.yml` is the override that swaps each service's `image:`
for the registry-tagged one; it's only ever used for the prod pull, never for
local dev (`docker compose up -d` alone still builds from source as before).
**Gitea secrets required:** `DEV_SSH_KEY`, `DEV_HOST`, `DEV_USER` (dev);
`PROD_SSH_KEY`, `PROD_HOST`, `PROD_USER` (prod); `REGISTRY_USER`,
`REGISTRY_TOKEN` (both pipelines — a Gitea access token scoped
`write:package,read:package`).
## Booking Wizard — Implementation Status
| Step | Feature | Issue | Status |
|------|---------|-------|--------|
| 1 | Date picker | #3 | Done — Google Calendar busy dates (graceful when unconfigured) |
| 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 | Done — PDF.js viewer + canvas pad; needs `api/contracts/model-release.pdf` on volume |
| 6 | Venmo payment | #7 | Done — deep link + server-generated QR |
| 7 | Confirm & submit | — | Done — summary + POST /api/bookings |
Also done: n8n webhooks (#8), admin dashboard at `/dashboard` (#11), Authentik OIDC
login (#10) and client self-registration enrollment (#12) - both live and verified
on dev and prod as of 2026-07-20, client portal at `/my-bookings` (#13).
Auth lives in `api/auth.js` (zero-dep OIDC + HMAC cookie sessions); admin routes
accept an OIDC admin session or the legacy `ADMIN_SECRET` bearer. One shared
Authentik provider/application serves both environments - see `scripts/authentik-setup.sh`.
### 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 Work
All original wizard issues (#3#13) are code-complete. Remaining items — mostly
infrastructure setup, secrets, and content — are catalogued in
`docs/BACKLOG-2026-07-16.md` and should be transferred to Gitea issues.
Issues are tracked at: https://git.jerodrigged.com/jhodgkin/homelab/issues