Development Guide
Prerequisites
- Python 3.12+
- uv for dependency management
- SQLite via Python's
sqlite3module (no external binary required)
Setup
git clone ssh://git@forgejo.labbity.unbiasedgeek.com:2222/archeious/quartermaster.git
cd quartermaster
uv sync
uv run alembic upgrade head
The migration creates ./quartermaster.db on first run. Override with
QUARTERMASTER_DB_URL=sqlite:///path/to/other.db if you want the data
file elsewhere.
Run the app
uv run uvicorn quartermaster.main:app \
--log-config src/quartermaster/logconfig.json \
--reload
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
uv run pytest
Tests use an in-memory SQLite engine; no separate migration step needed. Full suite runs in under 6 seconds.
Developing against a throwaway DB
Do not smoke-test or experiment against the live ./quartermaster.db.
See the DB safety rule in Operations for the full
reasoning. For any dev churn that mutates data (seeding fake entries,
exercising the HTMX flow, generating new alembic migrations), use a
throwaway path:
export QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db
export QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups
uv run alembic upgrade head
uv run uvicorn quartermaster.main:app --reload
# when done
unset QUARTERMASTER_DB_URL QUARTERMASTER_BACKUP_DIR
rm -rf /tmp/qm-dev.db /tmp/qm-dev-backups
Project layout
src/quartermaster/
main.py FastAPI app factory
routes.py Budget configuration handlers
routes_month.py Monthly view handlers + lifecycle transitions
routes_health.py GET /healthz (separate router, unauthenticated)
service.py Budget service: entries, target, zero-amount, groups
month_service.py Month service: snapshot, deviation, lifecycle, groups, postings
groups.py Group enum, labels, section->group mapping
models.py SQLAlchemy models, Section enum, MonthState enum
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/
base.html Barlow Condensed imports + favicon
index.html Budget config page
month.html Month view page
month_create.html Create-flow for a missing month
partials/
section.html Budget section card
target_card.html Budget debt target card
budget_zero.html Budget Zero Amount widget (with logo)
budget_group_totals.html OOB swap for all four budget group totals
month_section.html Month section card with inline HTMX edits
month_target.html Month debt target card
month_zero.html Month Zero Amount widget (with logo)
month_group_totals.html OOB swap for all four month group totals
month_nav.html prev / title / next / state / lifecycle button
static/
app.css Full stylesheet (Barlow Condensed + ledger layout)
brand/ Optimised logo assets
alembic/
env.py Includes backup-db.sh hook before migrations
versions/ Migration files
scripts/
backup-db.sh sqlite3.Connection.backup wrapper
tests/
test_service.py Budget service
test_routes.py Budget routes
test_month_service.py Month snapshot + CRUD
test_month_routes.py Month routes
test_zero_amount.py Zero-amount math + widgets
test_groups.py Group mapping + subtotals + default-open
test_notes.py Notes field + snapshot copy
test_month_lifecycle.py Transitions + close gate + edit locking
test_postings.py Posting CRUD + derived applied
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/
superpowers/ Specs and plans (per-session design notes)
wiki/ Cloned wiki (gitignored)
mockups/ Design history + mockup HTML
Conventions
Issue-driven work
Every change ties to a Forgejo issue. Branch naming:
feat/<issue>-<slug> new feature
fix/<issue>-<slug> bug fix
refactor/<issue>-<slug> internal change
chore/<issue>-<slug> tooling, deps, docs
Atomic commits
Each commit is one logical change. Typical PRs in this repo have 3-6
commits grouped by layer (data model, service, routes, templates,
tests, docs), or more for TDD-driven feature work where each task
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
PRs merge via the Forgejo API (or the web UI merge button), NOT
git merge locally. After the merge call:
- Delete the remote branch via the API.
git checkout main && git pull --ff-only.git branch -d <branch>locally.- Close the tracking issue manually via the API (the
Closes #Nkeyword 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
- Edit
src/quartermaster/models.py. uv run alembic revision --autogenerate -m "short description". The backup hook runs before alembic touches the DB.- Inspect the generated migration under
alembic/versions/and edit if needed (autogenerate misses server defaults, check constraints, and index renames). uv run alembic upgrade head. The backup hook runs again.- Write tests, update templates and routes.
UI verification
Type checks and pytest do not verify UI behaviour. For any change that affects the HTML, also:
- Start the server against a throwaway DB.
- Curl the affected endpoints and check for the expected classes and text in the response bodies.
- Open the page in a browser and exercise the feature.
- Claude cannot run a real browser from the CLI; the human eyeball is the final check before merge.
Adding a structured log event
- 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. - Use
logger.info(or.warning/.error) withextra={"event": "...", ...additional_fields}. Theeventname should be a concise snake_case verb_noun. - Place the log after the commit / refresh in service-layer functions,
before
return, so it only fires on durable success. - Write a test using
caplogthat asserts a record with the expectedeventattribute is emitted. Seetests/test_logging.pyfor the pattern. - Document the new event in the table in Operations.
Design workflow
UI experiments live under docs/mockups/ as self-contained HTML files
(typography + stylesheet inline, real shared logo assets). Iterate
there first, then port the shipped direction into
src/quartermaster/static/app.css and the Jinja templates.
Typography discipline: stick with Barlow Condensed for the family; use weight, size, letter-spacing, and italic for hierarchy. Reserve Fraunces / editorial serifs for future documentation or design explorations; they are not installed for the app.