quartermaster/CLAUDE.md
Jeff Smith 546c0800cc docs: CLAUDE.md — Session 3 entry, live-in-prod state, current commit
Phase moves to "live in production"; last-commit pointer and open-issues
list reflect the deploy pipeline and proxy-headers fix. Session 3
summary added; Session 1 rotates off to the wiki (per the "most recent
3" rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:35:54 -06:00

6.1 KiB

Quartermaster (repo instructions)

Database safety rule

The SQLite database at ./quartermaster.db (or whatever path QUARTERMASTER_DB_URL points at) holds real user data. It is .gitignored, so losing it means the data is gone.

Before any operation that interacts with the database at the schema or destructive level, run:

./scripts/backup-db.sh <reason>

That includes:

  • rm, mv, truncate, or otherwise replace quartermaster.db
  • Ad-hoc sqlite3 sessions that might DROP, DELETE, or UPDATE
  • Any script that does data migration or cleanup outside the running application

Alembic runs scripts/backup-db.sh alembic automatically on every alembic upgrade, alembic downgrade, or alembic revision --autogenerate via the hook in alembic/env.py, so you do not need to call it manually for ordinary schema work. The hook is defense in depth, not a substitute for thinking.

Routine writes through the running web application are NOT covered by this rule. Those are normal application behaviour, not "interactions" in the sense meant here.

Where backups go

Default: <dir-of-db-file>/backups/quartermaster-YYYYMMDD-HHMMSS-{slug}.db. For the standard ./quartermaster.db that resolves to ./backups/. Override with QUARTERMASTER_BACKUP_DIR=/some/path.

The backups/ directory is .gitignored. Retention is forever: backups are small. Prune manually if you need to.

Restoring

A backup is a complete SQLite file. To restore, stop the app, replace quartermaster.db with the chosen backup (cp backups/... quartermaster.db), then restart.

Current Project State

  • Phase: live in production on home-ctr-onyx at https://quartermaster.unbiasedgeek.com/. Every merge to main rolls out automatically via .forgejo/workflows/deploy.yml.
  • Last worked on: 2026-04-19
  • Last commit on main: ee6eaae — fix(docker): enable uvicorn proxy-headers so url_for works behind Traefik
  • Open PRs: none
  • Open issues: #23 MCP proposal; #31 small cleanups (non-blocking polish); #26 and #27 are landed but weren't closed when their work merged — safe to close.
  • Test count: 148 / 148 passing
  • Migrations: 5 applied; latest cc60e7f73a1c
  • Blocking issues: none

After pulling new work, always:

uv run alembic upgrade head

The backup hook fires before any migration, so this is safe against the live DB.

Session Log

Most recent 3 sessions (full history in the wiki).

Session 3 — 2026-04-19

Deploy-pipeline arc: four PRs (#32 Dockerfile, #33 compose.yml, #34 Forgejo Actions workflow, #35 post-deploy proxy-headers fix) took Quartermaster from "deploy prep merged" to live on https://quartermaster.unbiasedgeek.com/. All three dependency-chained issues (#28/#29/#30) closed.

Key decision, locked mid-flight: the deploy workflow has no SSH step. Initial draft used an SSH-from-runner-to-host pattern with DEPLOY_SSH_KEY + DEPLOY_KNOWN_HOSTS secrets. Jeff pushed back — the homelab runner lives on home-ctr-onyx itself with the host's Docker socket mounted, so docker compose pull && up -d runs directly against the same daemon that hosts production. Dropped two secrets and the private-key risk surface. Remaining Actions secrets: REGISTRY_TOKEN (archeious PAT, read:package + write:package, minted via the Forgejo API using the admin password exposed by homelab-IaC/bin/load-ops-secrets) and QUARTERMASTER_SMOKE_PASSWORD (plaintext basic-auth for the post-deploy /healthz probe).

Other design points: image tag parameterised via QUARTERMASTER_TAG (workflow writes a per-deploy .env), COMPOSE_PROJECT_NAME=quartermaster pinned so the runner's ephemeral workspace path doesn't confuse compose, smoke step does curl -u admin:… https://quartermaster.unbiasedgeek.com/healthz to catch TLS + routing + basic-auth regressions in one probe.

One post-deploy bug: the first rolled image rendered unstyled because uvicorn was started without --proxy-headers, so Starlette ignored X-Forwarded-Proto from Traefik and url_for() generated http://<internal>/static/… hrefs, which browsers blocked as mixed content on the https:// page. Reproduced locally by curling the pre-fix image with Traefik-style headers; added --proxy-headers --forwarded-allow-ips='*' to docker/entrypoint.sh in #35. Safe to trust all forwarded IPs because compose.yml publishes no host port — only Traefik on proxy-net can reach port 8000.

Full retro: Session3.

Session 2 — 2026-04-19

Platform contract intake (#25) filled out and accepted; platform team provisioned DNS (quartermaster.unbiasedgeek.com), Traefik middlewares (basic-auth + rate-limit), the /mnt/quartermaster/ bind mount, and basic-auth creds. Two issues landed on main (#26 /healthz, #27 structured JSON logs) via the superpowers brainstorm → spec → plan → subagent-driven-TDD workflow. Thirteen commits on a single branch (feat/platform-deploy-prep), rebased onto origin/main so history stays linear despite the unrelated MCP-doc PR that landed alongside.

Key decisions: single-source-of-truth logconfig.json (not a Python dict + YAML shim, which would need pyyaml and introduce drift); five seed app events (month_created, month_closed, template_entry_updated, posting_added, posting_deleted) placed after commit + refresh so they only fire on durable success; /healthz on a dedicated router as a file boundary, with auth living in Traefik per the platform contract.

Two real misses caught by code review mid-flight: the autouse-fixture workaround for Task 3's dictConfig state leak was wrong (fix: make the contaminating test save/restore state itself); the third LogQL example in README used {{.path}} on month_closed records that carry year_month, not path. Both landed as fix commits.

Deploy-pipeline work queued as #28 (Dockerfile), #29 (compose.yml), #30 (Forgejo Actions), dependency-chained. Polish umbrella at #31.

Full retro: Session2.