docs(dev): update Run block, project layout, conventions for logging + /healthz

claude-code 2026-04-19 12:32:57 -06:00
parent dd3ffd3453
commit cbd2e11a64

@ -22,11 +22,21 @@ file elsewhere.
## Run the app ## Run the app
```sh ```sh
uv run uvicorn quartermaster.main:app --reload uv run uvicorn quartermaster.main:app \
--log-config src/quartermaster/logconfig.json \
--reload
``` ```
Open http://127.0.0.1:8000. Open http://127.0.0.1:8000.
The `--log-config` flag emits structured JSON to stdout (Loki-ready
when deployed). Omit it if you'd rather read logs in uvicorn's default
human-readable format during local dev; production must use the config.
Health probe (unauthenticated): `curl http://127.0.0.1:8000/healthz`
returns `{"status":"ok"}` on a healthy DB, 503 with the exception
class name in `detail` otherwise.
## Run tests ## Run tests
```sh ```sh
@ -34,7 +44,7 @@ uv run pytest
``` ```
Tests use an in-memory SQLite engine; no separate migration step Tests use an in-memory SQLite engine; no separate migration step
needed. Full suite runs in under 4 seconds. needed. Full suite runs in under 6 seconds.
## Developing against a throwaway DB ## Developing against a throwaway DB
@ -59,30 +69,33 @@ rm -rf /tmp/qm-dev.db /tmp/qm-dev-backups
``` ```
src/quartermaster/ src/quartermaster/
main.py FastAPI app factory main.py FastAPI app factory
routes.py Budget configuration handlers routes.py Budget configuration handlers
routes_month.py Monthly view handlers + lifecycle transitions routes_month.py Monthly view handlers + lifecycle transitions
service.py Budget service: entries, target, zero-amount, groups routes_health.py GET /healthz (separate router, unauthenticated)
month_service.py Month service: snapshot, deviation, lifecycle, groups service.py Budget service: entries, target, zero-amount, groups
groups.py Group enum, labels, section->group mapping month_service.py Month service: snapshot, deviation, lifecycle, groups, postings
models.py SQLAlchemy models, Section enum, MonthState enum groups.py Group enum, labels, section->group mapping
db.py Engine, session, PRAGMA foreign_keys=ON models.py SQLAlchemy models, Section enum, MonthState enum
config.py DB URL resolution db.py Engine, session, PRAGMA foreign_keys=ON
config.py DB URL resolution
logging_config.py Loads LOG_CONFIG from logconfig.json; AccessLogFilter
logconfig.json dictConfig source-of-truth for JSON stdout logs
templates/ templates/
base.html Barlow Condensed imports + favicon base.html Barlow Condensed imports + favicon
index.html Budget config page index.html Budget config page
month.html Month view page month.html Month view page
month_create.html Create-flow for a missing month month_create.html Create-flow for a missing month
partials/ partials/
section.html Budget section card section.html Budget section card
target_card.html Budget debt target card target_card.html Budget debt target card
budget_zero.html Budget Zero Amount widget (with logo) budget_zero.html Budget Zero Amount widget (with logo)
budget_group_totals.html OOB swap for all four budget group totals budget_group_totals.html OOB swap for all four budget group totals
month_section.html Month section card with inline HTMX edits month_section.html Month section card with inline HTMX edits
month_target.html Month debt target card month_target.html Month debt target card
month_zero.html Month Zero Amount widget (with logo) month_zero.html Month Zero Amount widget (with logo)
month_group_totals.html OOB swap for all four month group totals month_group_totals.html OOB swap for all four month group totals
month_nav.html prev / title / next / state / lifecycle button month_nav.html prev / title / next / state / lifecycle button
static/ static/
app.css Full stylesheet (Barlow Condensed + ledger layout) app.css Full stylesheet (Barlow Condensed + ledger layout)
brand/ Optimised logo assets brand/ Optimised logo assets
@ -92,19 +105,24 @@ alembic/
scripts/ scripts/
backup-db.sh sqlite3.Connection.backup wrapper backup-db.sh sqlite3.Connection.backup wrapper
tests/ tests/
test_service.py Budget service test_service.py Budget service
test_routes.py Budget routes test_routes.py Budget routes
test_month_service.py Month snapshot + CRUD test_month_service.py Month snapshot + CRUD
test_month_routes.py Month routes test_month_routes.py Month routes
test_zero_amount.py Zero-amount math + widgets test_zero_amount.py Zero-amount math + widgets
test_groups.py Group mapping + subtotals + default-open test_groups.py Group mapping + subtotals + default-open
test_notes.py Notes field + snapshot copy test_notes.py Notes field + snapshot copy
test_month_lifecycle.py Transitions + close gate + edit locking test_month_lifecycle.py Transitions + close gate + edit locking
test_backup_script.py Backup script shell behaviour test_postings.py Posting CRUD + derived applied
CLAUDE.md DB safety rule, repo-level instructions test_template_edit_isolation.py Template edit doesn't leak into snapshots
test_backup_script.py Backup script shell behaviour
test_logging.py JSON formatter, access filter, dictConfig, seed events
test_health.py /healthz 200 success + 503 on DB failure
CLAUDE.md DB safety rule, repo-level instructions
docs/ docs/
wiki/ Cloned wiki (gitignored) superpowers/ Specs and plans (per-session design notes)
mockups/ Design history + mockup HTML wiki/ Cloned wiki (gitignored)
mockups/ Design history + mockup HTML
``` ```
## Conventions ## Conventions
@ -124,8 +142,10 @@ chore/<issue>-<slug> tooling, deps, docs
Each commit is one logical change. Typical PRs in this repo have 3-6 Each commit is one logical change. Typical PRs in this repo have 3-6
commits grouped by layer (data model, service, routes, templates, commits grouped by layer (data model, service, routes, templates,
tests, docs). Commit subjects under 72 chars, body under 80, trailers tests, docs), or more for TDD-driven feature work where each task
include `Refs #<issue>` and the Claude co-author line. gets its own failing-test / implementation / commit triple. Commit
subjects use conventional-commits prefixes (`feat:`, `fix:`, `chore:`,
`test:`, `docs:`, `refactor:`), under 72 chars. Body under 80.
### Merge flow ### Merge flow
@ -138,6 +158,9 @@ PRs merge via the Forgejo API (or the web UI merge button), NOT
4. Close the tracking issue manually via the API (the `Closes #N` 4. Close the tracking issue manually via the API (the `Closes #N`
keyword is unreliable on this Forgejo instance). keyword is unreliable on this Forgejo instance).
Exception: purely local direct-to-main merges (rare) rebase onto
origin/main first to keep linear history.
### Adding a schema change ### Adding a schema change
1. Edit `src/quartermaster/models.py`. 1. Edit `src/quartermaster/models.py`.
@ -161,6 +184,21 @@ affects the HTML, also:
4. Claude cannot run a real browser from the CLI; the human eyeball is 4. Claude cannot run a real browser from the CLI; the human eyeball is
the final check before merge. the final check before merge.
### Adding a structured log event
1. Pick a namespaced module logger if one doesn't exist in the file
you're touching: `logger = logging.getLogger("quartermaster.<module>")`
placed after all imports.
2. Use `logger.info` (or `.warning`/`.error`) with `extra={"event": "...",
...additional_fields}`. The `event` name should be a concise
snake_case verb_noun.
3. Place the log after the commit / refresh in service-layer functions,
before `return`, so it only fires on durable success.
4. Write a test using `caplog` that asserts a record with the expected
`event` attribute is emitted. See `tests/test_logging.py` for the
pattern.
5. Document the new event in the table in [Operations](Operations).
### Design workflow ### Design workflow
UI experiments live under `docs/mockups/` as self-contained HTML files UI experiments live under `docs/mockups/` as self-contained HTML files