docs(architecture): add routes_health, logging_config, /healthz to module + route maps
parent
8f3bd78a14
commit
dd3ffd3453
1 changed files with 75 additions and 34 deletions
109
Architecture.md
109
Architecture.md
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue