Files
lisilou-portfolio/CLAUDE.md
T
jhodgkin 952303d05c
Deploy to Dev / Deploy & Smoke Test (push) Successful in 20s
Document current state: backlog from overnight session, CLAUDE.md refresh
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:41:30 -06:00

7.8 KiB
Raw Blame History

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

# 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 — Trunk-Based Development

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 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, needs Authentik-side setup), 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.

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