3 DevelopmentGuide
claude-code edited this page 2026-04-19 12:32:57 -06:00

Development Guide

Prerequisites

  • Python 3.12+
  • uv for dependency management
  • SQLite via Python's sqlite3 module (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:

  1. Delete the remote branch via the API.
  2. git checkout main && git pull --ff-only.
  3. git branch -d <branch> locally.
  4. Close the tracking issue manually via the API (the Closes #N 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

  1. Edit src/quartermaster/models.py.
  2. uv run alembic revision --autogenerate -m "short description". The backup hook runs before alembic touches the DB.
  3. Inspect the generated migration under alembic/versions/ and edit if needed (autogenerate misses server defaults, check constraints, and index renames).
  4. uv run alembic upgrade head. The backup hook runs again.
  5. 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:

  1. Start the server against a throwaway DB.
  2. Curl the affected endpoints and check for the expected classes and text in the response bodies.
  3. Open the page in a browser and exercise the feature.
  4. Claude cannot run a real browser from the CLI; the human eyeball is 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.

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.