docs(architecture): add routes_health, logging_config, /healthz to module + route maps

claude-code 2026-04-19 12:32:09 -06:00
parent 8f3bd78a14
commit dd3ffd3453

@ -2,25 +2,30 @@
Small FastAPI app rendering Jinja2 templates with HTMX-driven partial Small FastAPI app rendering Jinja2 templates with HTMX-driven partial
updates. SQLite is the only storage. SQLAlchemy 2.x is the ORM; Alembic updates. SQLite is the only storage. SQLAlchemy 2.x is the ORM; Alembic
manages schema. `uv` manages Python deps. manages schema. `uv` manages Python deps. Structured JSON logging to
stdout is the runtime-observability contract; see
[Operations](Operations).
## Modules ## Modules
``` ```
src/quartermaster/ src/quartermaster/
main.py FastAPI app factory main.py FastAPI app factory
routes.py Budget configuration HTTP handlers routes.py Budget configuration HTTP handlers
routes_month.py Monthly view HTTP handlers routes_month.py Monthly view HTTP handlers
service.py Budget queries, totals, target, zero-amount, group views routes_health.py GET /healthz (unauthenticated, separate router)
month_service.py Month snapshot, deviation, per-month CRUD, lifecycle, group views service.py Budget queries, totals, target, zero-amount, group views
groups.py Group enum, labels, section->group mapping, default open month_service.py Month snapshot, deviation, per-month CRUD, lifecycle, group views
models.py SQLAlchemy models, Section enum, MonthState enum groups.py Group enum, labels, section->group mapping, default open
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
templates/ Jinja2 templates (base, index, month, partials) config.py DB URL resolution
logging_config.py Loads LOG_CONFIG from logconfig.json; AccessLogFilter
logconfig.json dictConfig source-of-truth (tests + uvicorn both consume)
templates/ Jinja2 templates (base, index, month, partials)
static/ static/
app.css Full stylesheet (Barlow Condensed + ledger layout) app.css Full stylesheet (Barlow Condensed + ledger layout)
brand/ Logo assets (full, mark-wide, shield-wide) brand/ Logo assets (full, mark-wide, shield-wide)
``` ```
## Sections and groups ## Sections and groups
@ -50,7 +55,7 @@ entry
id, section, name, amount, notes (nullable, up to 1024 chars), id, section, name, amount, notes (nullable, up to 1024 chars),
created_at, updated_at created_at, updated_at
debt_target (singleton, id = 1) debt_target (singleton, id = 1)
id, debt_minimum_id FK entry(id) ON DELETE SET NULL, updated_at id, debt_minimum_id FK entry(id) ON DELETE SET NULL, updated_at
``` ```
@ -79,7 +84,7 @@ posting
description NULL, payee NULL, description NULL, payee NULL,
created_at, updated_at created_at, updated_at
month_debt_target (singleton per month) month_debt_target (singleton per month)
month_id PK FK month(id) ON DELETE CASCADE, month_id PK FK month(id) ON DELETE CASCADE,
month_entry_id FK month_entry(id) ON DELETE SET NULL, month_entry_id FK month_entry(id) ON DELETE SET NULL,
updated_at updated_at
@ -192,28 +197,28 @@ Colour tone:
### Budget (`src/quartermaster/routes.py`) ### Budget (`src/quartermaster/routes.py`)
``` ```
GET / index page GET / index page
POST /sections/{section}/entries add entry POST /sections/{section}/entries add entry
DELETE /entries/{entry_id} remove entry DELETE /entries/{entry_id} remove entry
POST /entries/{entry_id}/notes update entry notes POST /entries/{entry_id}/save update entry (name, amount, notes)
POST /debt-target set / clear debt target POST /debt-target set / clear debt target
``` ```
### Month (`src/quartermaster/routes_month.py`) ### Month (`src/quartermaster/routes_month.py`)
``` ```
GET /month/{year_month} view page or create-flow GET /month/{year_month} view page or create-flow
POST /month/{year_month}/create snapshot the budget (lands in Planning) POST /month/{year_month}/create snapshot the budget (lands in Planning)
POST /month/{year_month}/activate Planning -> Active POST /month/{year_month}/activate Planning -> Active
POST /month/{year_month}/close Active -> Closed (requires applied zero = 0) POST /month/{year_month}/close Active -> Closed (requires applied zero = 0)
POST /month/{year_month}/reopen Closed -> Active POST /month/{year_month}/reopen Closed -> Active
POST /month/{year_month}/sections/{section}/entries add month entry POST /month/{year_month}/sections/{section}/entries add month entry
POST /month/{year_month}/entries/{entry_id} update (name / planned / notes) POST /month/{year_month}/entries/{entry_id} update (name / planned / notes)
DELETE /month/{year_month}/entries/{entry_id} remove month entry DELETE /month/{year_month}/entries/{entry_id} remove month entry
POST /month/{year_month}/entries/{entry_id}/postings add a transaction POST /month/{year_month}/entries/{entry_id}/postings add a transaction
POST /month/{year_month}/postings/{posting_id} update a transaction POST /month/{year_month}/postings/{posting_id} update a transaction
DELETE /month/{year_month}/postings/{posting_id} delete a transaction DELETE /month/{year_month}/postings/{posting_id} delete a transaction
POST /month/{year_month}/target set / clear per-month target POST /month/{year_month}/target set / clear per-month target
``` ```
`update_month_entry` no longer accepts an `applied` field. Applied `update_month_entry` no longer accepts an `applied` field. Applied
@ -229,6 +234,18 @@ All mutation routes return the section partial plus OOB swaps for:
OOB is used so section-level changes keep the page-level summary widgets OOB is used so section-level changes keep the page-level summary widgets
accurate without a reload. accurate without a reload.
### Health (`src/quartermaster/routes_health.py`)
```
GET /healthz liveness + DB reachability (unauthenticated)
```
Separate router, zero app-level dependencies. Success: 200
`{"status":"ok"}`. Failure: 503
`{"status":"error","detail":"<exception-class-name>"}` — class name
only, no message or traceback leaked. A failed probe emits a
structured warning log with `event=healthz_failed`, `error_class=<cls>`.
## HTMX conventions ## HTMX conventions
* Month entry rows carry inline inputs for `name` and `planned` plus a * Month entry rows carry inline inputs for `name` and `planned` plus a
@ -252,6 +269,23 @@ accurate without a reload.
and the server returns 204 + `HX-Redirect: /month/{year_month}` so the and the server returns 204 + `HX-Redirect: /month/{year_month}` so the
page re-renders cleanly in the new state. page re-renders cleanly in the new state.
## Observability
See [Operations](Operations) for the Logs and Health sections. Brief
summary:
* `src/quartermaster/logconfig.json` is the single source of truth for
the `logging.config.dictConfig`. Loaded into `LOG_CONFIG` in
`logging_config.py` at import time; same file read by uvicorn CLI
via `--log-config`.
* Formatter: `pythonjsonlogger.json.JsonFormatter` with `rename_fields`
mapping `asctime/levelname/name``timestamp/level/logger`.
* `AccessLogFilter` in `logging_config.py` enriches uvicorn access
records with `event="http_request"`, `method`, `path`, `status`,
`client_ip`.
* Five seed app events at the main mutation sites plus one
`healthz_failed` event at the probe.
## Visual identity ## Visual identity
* **Typography**: Barlow Condensed (300-800 + italic) imported from * **Typography**: Barlow Condensed (300-800 + italic) imported from
@ -276,7 +310,7 @@ accurate without a reload.
5-column summary grid (caret / name / planned / applied / actions). 5-column summary grid (caret / name / planned / applied / actions).
The 2px progress bar rides the summary's bottom border. The 2px progress bar rides the summary's bottom border.
* Budget entry rows use `table.entries` with a 4-column grid. * Budget entry rows use `table.entries` with a 4-column grid.
* `.target-section` with a burgundy left bar and `↳` margin glyph * `.target-section` with a burgundy left bar and margin glyph
* `.state-badge` tracked-caps label with bullet separators * `.state-badge` tracked-caps label with bullet separators
## Testing ## Testing
@ -286,4 +320,11 @@ accurate without a reload.
share the same in-memory engine across threads via `StaticPool`. share the same in-memory engine across threads via `StaticPool`.
* Backup script tests shell out and assert on filenames and sqlite * Backup script tests shell out and assert on filenames and sqlite
round-trips. round-trips.
* Full suite runs in under 4 seconds. * Logging tests instantiate the JSON formatter from `LOG_CONFIG`
directly, push a record through a `StringIO` handler, assert on the
parsed output. `AccessLogFilter` is tested with a synthetic uvicorn
`LogRecord`. Seed events are tested via `caplog`. The dictConfig
smoke test saves / restores logger state in a try/finally so it
cannot leak `propagate=False` to subsequent tests.
* `/healthz` is tested with and without a failing session.
* Full suite (148 tests) runs in under 6 seconds.