quartermaster/README.md

170 lines
5.5 KiB
Markdown
Raw Normal View History

2026-04-17 10:57:47 -06:00
# quartermaster
Household budget tracker. FastAPI + HTMX frontend, SQLite backend.
## Pages
* `/` budget configuration. One section per category (Incomes, Fixed Amount
Bills, Debt Minimums, Primary Debt Target, Food and Essentials,
Subscriptions, Other). Every section accepts name + amount entries and
shows a running total. The Primary Debt Target is a pointer to a Debt
Minimums row.
* `/month/YYYY-MM` monthly view. Snapshots the budget at creation time and
tracks an `applied` amount per entry alongside the planned amount. Each
row is annotated when its name or planned value has been edited away
from the snapshot, or when the row was added after creation. Per-month
debt target is independent of the budget's target after snapshot.
Navigate between months with the prev / next buttons or the dropdown
picker. A "This month" link on `/` jumps to the current `YYYY-MM`; if it
has not been created yet, you land on the create flow.
## Requirements
* Python 3.12+
* [uv](https://github.com/astral-sh/uv) for dependency management
## Setup
```sh
uv sync
uv run alembic upgrade head
```
The SQLite file lives at `./quartermaster.db` by default. Override with the
`QUARTERMASTER_DB_URL` environment variable (any SQLAlchemy URL).
## Run
```sh
uv run uvicorn quartermaster.main:app \
--log-config src/quartermaster/logconfig.json \
--reload
```
Open http://127.0.0.1:8000.
## Logs
Logs are JSON on stdout. Each line has `level` and `event` as queryable
Loki labels (indexed by Promtail on the deploy host), plus arbitrary
extra fields in the JSON body.
Example LogQL queries in Grafana Explore (Loki data source):
```
{container="quartermaster"} | json
{container="quartermaster", event="http_request", status=~"5.."}
{container="quartermaster", event="month_closed"} | json | line_format "{{.year_month}} {{.message}}"
```
HTTP access logs appear as `event="http_request"` with `method`, `path`,
`status`, and `client_ip` extras. Application events (`month_created`,
`month_closed`, `template_entry_updated`, `posting_added`,
`posting_deleted`) fire at the matching mutation sites.
## Docker
A `Dockerfile` at the repo root produces a self-contained image that runs
`alembic upgrade head` (with the pre-upgrade backup hook) then
`uvicorn quartermaster.main:app` as a non-root user (`uid:gid 1000:1000`).
The image `EXPOSE`s port 8000 and declares a `HEALTHCHECK` against
`/healthz`.
Build and smoke-run locally against a tempfile database:
```sh
docker build -t quartermaster:dev .
mkdir -p /tmp/qm-data
docker run --rm -p 8000:8000 \
-e QUARTERMASTER_DB_URL=sqlite:////data/qm.db \
-v /tmp/qm-data:/data \
quartermaster:dev
```
Then `curl http://127.0.0.1:8000/healthz` should return
`{"status":"ok"}` with JSON access logs on the container's stdout.
In production on home-ctr-onyx the bind mount is `/mnt/quartermaster/`
and `QUARTERMASTER_DB_URL` points at the DB inside it; see the
compose file for the full wiring.
## Tests
```sh
uv run pytest
```
Tests run against an in-memory SQLite database; no migration step needed.
## Backups
The SQLite data file is precious and gitignored. Before any schema
change or destructive operation, back it up:
```sh
./scripts/backup-db.sh <reason>
```
Backups land next to the database in `./backups/` by default
(`QUARTERMASTER_BACKUP_DIR=/some/path` to override) as
`quartermaster-YYYYMMDD-HHMMSS-{slug}.db` using SQLite's online backup
API (safe even while the app is writing). Alembic invokes the script
automatically before every migration via `alembic/env.py`; retention
is forever for now. To restore, stop the app, copy the chosen backup
over `quartermaster.db`, and restart.
## Project Layout
```
src/quartermaster/
main.py FastAPI app factory
routes.py Budget configuration HTTP handlers
routes_month.py Monthly view HTTP handlers
service.py Budget queries, totals, target logic
month_service.py Snapshot, deviation, per-month CRUD
models.py SQLAlchemy models and Section enum
db.py Engine, session, PRAGMA foreign_keys=ON
config.py DB URL resolution
templates/ Jinja2 templates (base, index, month, partials)
static/ CSS
alembic/ Migrations
tests/ pytest suite
```
## Deferred
A transaction log that rolls up into `applied` on a per-entry per-month
basis is deferred. Once implemented it may replace the hand-edited applied
field.
## Deploy (home-ctr-onyx)
`compose.yml` at the repo root describes the `quartermaster-web`
service. The platform team has already provisioned `/mnt/quartermaster/`
(owned `1000:1000`), the `proxy-net` Docker network, and the
`quartermaster-basicauth@file` + `quartermaster-ratelimit@file` Traefik
middlewares on home-ctr-onyx. See the [platform contract
wiki page](https://forgejo.labbity.unbiasedgeek.com/homelab/homelab-IaC/wiki/PlatformContractQuartermaster)
for the canonical list.
Roll out a new build:
```sh
docker compose pull
docker compose up -d
```
`compose.yml` references the image as
`…/quartermaster:${QUARTERMASTER_TAG:-latest}`. The deploy workflow
writes `QUARTERMASTER_TAG=<git-sha>` to an `.env` file next to
`compose.yml` on the host, so each deploy pins a specific SHA without
editing the checked-in compose file.
**Four-slash gotcha.** `QUARTERMASTER_DB_URL` is
`sqlite:////data/quartermaster.db` — four slashes for an absolute
path. Three would resolve relative to the working directory, and the
SQLite file would NOT land on the bind mount (on next restart the
database would be empty).