943fd9495f
Deploy to Dev / Deploy & Smoke Test (push) Successful in 23s
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.
227 lines
9.7 KiB
Markdown
227 lines
9.7 KiB
Markdown
# 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 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
|
||
|
||
### 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
|