docs: refresh wiki for groups, sinking funds, notes, lifecycle, UI redesign
parent
390b9ffe56
commit
fe7edfe973
5 changed files with 307 additions and 124 deletions
163
Architecture.md
163
Architecture.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
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.
|
manages schema. `uv` manages Python deps.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
|
@ -11,31 +11,44 @@ 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 logic, zero-amount
|
service.py Budget queries, totals, target, zero-amount, group views
|
||||||
month_service.py Snapshot, deviation, per-month CRUD, month zero
|
month_service.py Month snapshot, deviation, per-month CRUD, lifecycle, group views
|
||||||
models.py SQLAlchemy models and Section enum
|
groups.py Group enum, labels, section->group mapping, default open
|
||||||
|
models.py SQLAlchemy models, Section enum, MonthState enum
|
||||||
db.py Engine, session, PRAGMA foreign_keys=ON
|
db.py Engine, session, PRAGMA foreign_keys=ON
|
||||||
config.py DB URL resolution
|
config.py DB URL resolution
|
||||||
templates/ Jinja2 templates (base, index, month, partials)
|
templates/ Jinja2 templates (base, index, month, partials)
|
||||||
static/app.css All styling
|
static/
|
||||||
|
app.css Full stylesheet (Barlow Condensed + ledger layout)
|
||||||
|
brand/ Logo assets (full, mark-wide, shield-wide)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sections
|
## Sections and groups
|
||||||
|
|
||||||
```
|
```
|
||||||
Section enum (budget + month):
|
Section enum (budget + month)
|
||||||
income, fixed_bill, debt_minimum, food, subscription, other
|
income, fixed_bill, debt_minimum, food, subscription, other, sinking_fund
|
||||||
|
|
||||||
|
Group enum (display-layer)
|
||||||
|
income -> [income]
|
||||||
|
committed -> [fixed_bill, debt_minimum] + Primary Debt Target card
|
||||||
|
savings -> [sinking_fund]
|
||||||
|
flexible -> [food, subscription, other]
|
||||||
|
|
||||||
|
Default open: income=yes, committed=no, savings=no, flexible=yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Primary Debt Target is NOT a section. It is a singleton pointer that
|
The Primary Debt Target is NOT a section. It is a singleton pointer that
|
||||||
selects one of the Debt Minimums rows as the current focus. Excluded
|
selects one of the Debt Minimums rows as the current focus. It is excluded
|
||||||
from the zero-amount math since it has no total of its own.
|
from zero-amount math since it has no total of its own (its amount is
|
||||||
|
already counted inside Debt Minimums).
|
||||||
|
|
||||||
## Budget data model
|
## Budget data model
|
||||||
|
|
||||||
```
|
```
|
||||||
entry
|
entry
|
||||||
id, section, name, amount, created_at, updated_at
|
id, section, name, amount, notes (nullable, up to 1024 chars),
|
||||||
|
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
|
||||||
|
|
@ -48,11 +61,14 @@ the entry itself.
|
||||||
|
|
||||||
```
|
```
|
||||||
month
|
month
|
||||||
id, year_month UNIQUE, created_at
|
id, year_month UNIQUE ("YYYY-MM"),
|
||||||
|
state ('planning' | 'active' | 'closed', default 'planning'),
|
||||||
|
activated_at NULL, closed_at NULL,
|
||||||
|
created_at
|
||||||
|
|
||||||
month_entry
|
month_entry
|
||||||
id, month_id FK month(id) ON DELETE CASCADE,
|
id, month_id FK month(id) ON DELETE CASCADE,
|
||||||
section, name, planned, applied,
|
section, name, planned, applied, notes NULL,
|
||||||
origin_name NULL, origin_planned NULL,
|
origin_name NULL, origin_planned NULL,
|
||||||
source_entry_id FK entry(id) ON DELETE SET NULL,
|
source_entry_id FK entry(id) ON DELETE SET NULL,
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
|
|
@ -64,8 +80,8 @@ month_debt_target (singleton per month)
|
||||||
```
|
```
|
||||||
|
|
||||||
Creating a month copies every budget entry into `month_entry` and
|
Creating a month copies every budget entry into `month_entry` and
|
||||||
preserves `origin_name` / `origin_planned`. `source_entry_id` links
|
preserves `origin_name`, `origin_planned`, and `notes`. `source_entry_id`
|
||||||
back to the budget but is informational only; the snapshot is
|
links back to the budget but is informational only; the snapshot is
|
||||||
self-contained so the month keeps working even after the referenced
|
self-contained so the month keeps working even after the referenced
|
||||||
budget row is deleted.
|
budget row is deleted.
|
||||||
|
|
||||||
|
|
@ -73,28 +89,65 @@ budget row is deleted.
|
||||||
|
|
||||||
Computed from `(name, planned)` vs `(origin_name, origin_planned)`:
|
Computed from `(name, planned)` vs `(origin_name, origin_planned)`:
|
||||||
|
|
||||||
* `unchanged` — both match. No tag.
|
* `unchanged` — both match. No tag, progress bar renders in the section
|
||||||
* `edited` — name or planned differs. Orange tint + "modified" tag.
|
baseline colour.
|
||||||
* `new_in_month` — no origin (row added after snapshot). Blue tint +
|
* `edited` — name or planned differs. "Modified" tag in ochre; progress
|
||||||
"new this month" tag.
|
bar recoloured to indicate drift.
|
||||||
|
* `new_in_month` — no origin (row added after snapshot). "New this month"
|
||||||
|
tag in indigo; progress bar tinted indigo.
|
||||||
|
|
||||||
|
Notes changes do NOT flip the deviation state. Notes are annotation, not
|
||||||
|
plan drift.
|
||||||
|
|
||||||
### Per-month debt target
|
### Per-month debt target
|
||||||
|
|
||||||
Copied from the budget's current debt target at creation time, resolved
|
Copied from the budget's current debt target at creation time, resolved
|
||||||
through `source_entry_id`. Fully editable per month.
|
through `source_entry_id`. Fully editable per month.
|
||||||
|
|
||||||
|
## Month lifecycle
|
||||||
|
|
||||||
|
Three explicit states. Transitions happen via dedicated routes and are
|
||||||
|
validated on the server.
|
||||||
|
|
||||||
|
```
|
||||||
|
Planning -- POST /month/{ym}/activate --> Active
|
||||||
|
Active -- POST /month/{ym}/close --> Closed (applied zero must be $0.00)
|
||||||
|
Closed -- POST /month/{ym}/reopen --> Active
|
||||||
|
```
|
||||||
|
|
||||||
|
Each transition stamps or clears the relevant timestamp. The close gate
|
||||||
|
is authoritative on the server; the UI disables the Close button with a
|
||||||
|
tooltip when the balance is nonzero but would not be trusted alone.
|
||||||
|
|
||||||
|
`month_service.ensure_editable(month)` is the guard that every mutation
|
||||||
|
route calls. When the month is closed it raises `MonthLifecycleError`,
|
||||||
|
which the route layer translates to an HTTP 400. Inputs on a closed
|
||||||
|
month carry `disabled`; delete buttons, add forms, and the target form
|
||||||
|
are omitted entirely from rendered HTML.
|
||||||
|
|
||||||
|
Reopen does NOT reverse any sweep — nothing is ever swept automatically.
|
||||||
|
Reopening just sets the state back to Active and lets you edit again.
|
||||||
|
|
||||||
## Zero-amount calculation
|
## Zero-amount calculation
|
||||||
|
|
||||||
`sum(income.amount) - sum(non_income.amount)` with
|
On both pages:
|
||||||
`debt_target` excluded. On the budget page there is one value; on the
|
|
||||||
month page there are two (Planned and Applied), each computed over the
|
```
|
||||||
relevant column.
|
zero = sum(income.amount | .applied | .planned)
|
||||||
|
- sum(non_income.amount | .applied | .planned)
|
||||||
|
```
|
||||||
|
|
||||||
|
Debt Target is excluded from the calculation because it is a pointer,
|
||||||
|
not a totalable section.
|
||||||
|
|
||||||
|
On the budget page there is one value. On the month page there are two:
|
||||||
|
Planned (over the planned column) and Applied (over the applied column).
|
||||||
|
|
||||||
Colour tone:
|
Colour tone:
|
||||||
|
|
||||||
* zero → green (`tone-zero`)
|
* `tone-zero` → sage (balanced)
|
||||||
* positive → amber (`tone-positive`): unassigned income
|
* `tone-positive` → ochre (unassigned, under-budget)
|
||||||
* negative → red (`tone-negative`): over-budget
|
* `tone-negative` → burgundy (over-budget)
|
||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
|
|
@ -104,38 +157,71 @@ Colour tone:
|
||||||
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 /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 or create-flow page
|
GET /month/{year_month} view page or create-flow
|
||||||
POST /month/{year_month}/create snapshot the budget
|
POST /month/{year_month}/create snapshot the budget (lands in Planning)
|
||||||
|
POST /month/{year_month}/activate Planning -> Active
|
||||||
|
POST /month/{year_month}/close Active -> Closed (requires applied zero = 0)
|
||||||
|
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 / applied)
|
POST /month/{year_month}/entries/{entry_id} update (name / planned / applied / 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}/target set / clear per-month target
|
POST /month/{year_month}/target set / clear per-month target
|
||||||
```
|
```
|
||||||
|
|
||||||
All mutation routes return the section partial plus OOB swaps for:
|
All mutation routes return the section partial plus OOB swaps for:
|
||||||
|
|
||||||
* The debt target card when a Debt Minimums row was added, edited, or
|
* The debt target card when a Debt Minimums row was added / edited / deleted
|
||||||
deleted
|
* The Zero Amount widget
|
||||||
* The Zero Amount widget on every entry change
|
* All four group totals (`#group-total-{group}`)
|
||||||
|
|
||||||
OOB swap is cheaper than re-rendering the full page and keeps totals
|
OOB is used so section-level changes keep the page-level summary widgets
|
||||||
in sync without a reload.
|
accurate without a reload.
|
||||||
|
|
||||||
## HTMX conventions
|
## HTMX conventions
|
||||||
|
|
||||||
* Each month entry row carries three inline inputs (`name`, `planned`,
|
* Month entry rows carry three inline inputs (`name`, `planned`,
|
||||||
`applied`) with `hx-post` and `hx-trigger="change"`. Each input
|
`applied`) plus a notes input, each with `hx-post` and
|
||||||
sends only its own field; the server accepts any subset.
|
`hx-trigger="change"`. Each input sends only its own field; the server
|
||||||
|
accepts any subset.
|
||||||
* Section partials swap via `hx-target="#section-{section}"` +
|
* Section partials swap via `hx-target="#section-{section}"` +
|
||||||
`hx-swap="outerHTML"`.
|
`hx-swap="outerHTML"`.
|
||||||
* OOB swaps use `hx-swap-oob="outerHTML"` on the top-level element of
|
* OOB swaps use `hx-swap-oob="outerHTML"` on the top-level element of
|
||||||
the partial so the same id can be re-rendered anywhere in the page.
|
the partial so the same id can be re-rendered anywhere in the page.
|
||||||
|
* Transition buttons (activate, close, reopen) POST with `hx-swap="none"`
|
||||||
|
and the server returns 204 + `HX-Redirect: /month/{year_month}` so the
|
||||||
|
page re-renders cleanly in the new state.
|
||||||
|
|
||||||
|
## Visual identity
|
||||||
|
|
||||||
|
* **Typography**: Barlow Condensed (300-800 + italic) imported from
|
||||||
|
Google Fonts. Barlow proportional as a secondary pair. All numeric
|
||||||
|
columns carry `font-variant-numeric: tabular-nums` plus
|
||||||
|
`font-feature-settings: "lnum" 1, "tnum" 1`.
|
||||||
|
* **Palette** (CSS variables in `app.css`):
|
||||||
|
* `--paper #f5efe0` warm cream background with layered radial gradients
|
||||||
|
for depth
|
||||||
|
* `--ink #1a1814` warm near-black
|
||||||
|
* `--accent #732629` burgundy, sampled from the logo shield
|
||||||
|
* `--ochre #9c6b1a` under-budget / deviation
|
||||||
|
* `--sage #2d4a30` balanced / at-plan
|
||||||
|
* `--indigo #353a5e` new-in-month
|
||||||
|
* **Layout primitives**:
|
||||||
|
* `.zero-widget` grid at the top of each page; logo on the left, zero
|
||||||
|
in the centre, flanking labels
|
||||||
|
* `details.group` collapsible groups with a hairline CSS chevron that
|
||||||
|
rotates on `[open]`
|
||||||
|
* `.section` with a small-caps header and an inline subtotal
|
||||||
|
* `table.entries` with a 4-column grid (name / planned / applied /
|
||||||
|
actions) and a 2px progress bar on each row's bottom border
|
||||||
|
* `.target-section` with a burgundy left bar and `↳` margin glyph
|
||||||
|
* `.state-badge` tracked-caps label with bullet separators
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|
@ -144,3 +230,4 @@ in sync 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.
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
|
|
||||||
* Python 3.12+
|
* Python 3.12+
|
||||||
* [uv](https://github.com/astral-sh/uv) for dependency management
|
* [uv](https://github.com/astral-sh/uv) for dependency management
|
||||||
* SQLite (the library, not the CLI; the backup script uses Python's
|
* SQLite via Python's `sqlite3` module (no external binary required)
|
||||||
sqlite3 module so no `sqlite3` binary is required)
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -35,22 +34,25 @@ uv run pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests use an in-memory SQLite engine; no separate migration step
|
Tests use an in-memory SQLite engine; no separate migration step
|
||||||
needed.
|
needed. Full suite runs in under 4 seconds.
|
||||||
|
|
||||||
## Developing against a throwaway DB
|
## Developing against a throwaway DB
|
||||||
|
|
||||||
Dev churn that needs a clean schema (autogenerating a new migration,
|
**Do not smoke-test or experiment against the live `./quartermaster.db`.**
|
||||||
loading a scratch dataset) should target a throwaway path, never
|
See the DB safety rule in [Operations](Operations) for the full
|
||||||
`./quartermaster.db`. See [Operations](Operations) for why.
|
reasoning. For any dev churn that mutates data (seeding fake entries,
|
||||||
|
exercising the HTMX flow, generating new alembic migrations), use a
|
||||||
|
throwaway path:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \
|
export QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db
|
||||||
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
export QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups
|
||||||
uv run alembic upgrade head
|
uv run alembic upgrade head
|
||||||
|
|
||||||
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \
|
|
||||||
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
|
||||||
uv run uvicorn quartermaster.main:app --reload
|
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
|
## Project layout
|
||||||
|
|
@ -59,38 +61,50 @@ QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
||||||
src/quartermaster/
|
src/quartermaster/
|
||||||
main.py FastAPI app factory
|
main.py FastAPI app factory
|
||||||
routes.py Budget configuration handlers
|
routes.py Budget configuration handlers
|
||||||
routes_month.py Monthly view handlers
|
routes_month.py Monthly view handlers + lifecycle transitions
|
||||||
service.py Budget service: entries, target, zero-amount
|
service.py Budget service: entries, target, zero-amount, groups
|
||||||
month_service.py Month service: snapshot, deviation, zero-amount
|
month_service.py Month service: snapshot, deviation, lifecycle, groups
|
||||||
models.py SQLAlchemy models, Section enum
|
groups.py Group enum, labels, section->group mapping
|
||||||
|
models.py SQLAlchemy models, Section enum, MonthState enum
|
||||||
db.py Engine, session, PRAGMA foreign_keys=ON
|
db.py Engine, session, PRAGMA foreign_keys=ON
|
||||||
config.py DB URL resolution
|
config.py DB URL resolution
|
||||||
templates/
|
templates/
|
||||||
base.html
|
base.html Barlow Condensed imports + favicon
|
||||||
index.html Budget config page
|
index.html Budget config page
|
||||||
month.html Month view page
|
month.html Month view page
|
||||||
month_create.html Create-flow for missing month
|
month_create.html Create-flow for a missing month
|
||||||
partials/
|
partials/
|
||||||
section.html Budget section card
|
section.html Budget section card
|
||||||
target_card.html Budget debt target card
|
target_card.html Budget debt target card
|
||||||
budget_zero.html Budget Zero Amount widget
|
budget_zero.html Budget Zero Amount widget (with logo)
|
||||||
month_section.html Month section card
|
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_target.html Month debt target card
|
||||||
month_zero.html Month Zero Amount widget (Planned/Applied)
|
month_zero.html Month Zero Amount widget (with logo)
|
||||||
month_nav.html Prev / next / picker / back-to-config
|
month_group_totals.html OOB swap for all four month group totals
|
||||||
static/app.css
|
month_nav.html prev / title / next / state / lifecycle button
|
||||||
|
static/
|
||||||
|
app.css Full stylesheet (Barlow Condensed + ledger layout)
|
||||||
|
brand/ Optimised logo assets
|
||||||
alembic/
|
alembic/
|
||||||
env.py Includes backup-db.sh hook before migrations
|
env.py Includes backup-db.sh hook before migrations
|
||||||
versions/ Migration files
|
versions/ Migration files
|
||||||
scripts/
|
scripts/
|
||||||
backup-db.sh sqlite3.Connection.backup wrapper
|
backup-db.sh sqlite3.Connection.backup wrapper
|
||||||
tests/
|
tests/
|
||||||
test_service.py
|
test_service.py Budget service
|
||||||
test_routes.py
|
test_routes.py Budget routes
|
||||||
test_month_service.py
|
test_month_service.py Month snapshot + CRUD
|
||||||
test_month_routes.py
|
test_month_routes.py Month routes
|
||||||
test_zero_amount.py
|
test_zero_amount.py Zero-amount math + widgets
|
||||||
test_backup_script.py
|
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_backup_script.py Backup script shell behaviour
|
||||||
|
CLAUDE.md DB safety rule, repo-level instructions
|
||||||
|
docs/
|
||||||
|
wiki/ Cloned wiki (gitignored)
|
||||||
|
mockups/ Design history + mockup HTML
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
@ -108,24 +122,23 @@ chore/<issue>-<slug> tooling, deps, docs
|
||||||
|
|
||||||
### Atomic commits
|
### Atomic commits
|
||||||
|
|
||||||
Each commit is one logical change. Typical PRs in this repo have 3-5
|
Each commit is one logical change. Typical PRs in this repo have 3-6
|
||||||
commits grouped by layer (data model, service, routes + templates,
|
commits grouped by layer (data model, service, routes, templates,
|
||||||
tests, docs). Commit message subject under 72 chars, body under 80,
|
tests, docs). Commit subjects under 72 chars, body under 80, trailers
|
||||||
trailers include `Refs #<issue>` and the `Co-Authored-By` line for
|
include `Refs #<issue>` and the Claude co-author line.
|
||||||
Claude sessions.
|
|
||||||
|
|
||||||
### Merge flow
|
### Merge flow
|
||||||
|
|
||||||
PRs merge via the Forgejo API (or web UI merge button), NOT `git merge`
|
PRs merge via the Forgejo API (or the web UI merge button), NOT
|
||||||
locally. After the merge call:
|
`git merge` locally. After the merge call:
|
||||||
|
|
||||||
1. Delete the remote branch.
|
1. Delete the remote branch via the API.
|
||||||
2. `git checkout main && git pull --ff-only`.
|
2. `git checkout main && git pull --ff-only`.
|
||||||
3. `git branch -d <branch>` locally.
|
3. `git branch -d <branch>` locally.
|
||||||
4. Close the tracking issue manually via the API (the `Closes #N`
|
4. Close the tracking issue manually via the API (the `Closes #N`
|
||||||
keyword is unreliable on this Forgejo instance).
|
keyword is unreliable on this Forgejo instance).
|
||||||
|
|
||||||
## Adding a schema change
|
### Adding a schema change
|
||||||
|
|
||||||
1. Edit `src/quartermaster/models.py`.
|
1. Edit `src/quartermaster/models.py`.
|
||||||
2. `uv run alembic revision --autogenerate -m "short description"`.
|
2. `uv run alembic revision --autogenerate -m "short description"`.
|
||||||
|
|
@ -134,9 +147,9 @@ locally. After the merge call:
|
||||||
if needed (autogenerate misses server defaults, check constraints,
|
if needed (autogenerate misses server defaults, check constraints,
|
||||||
and index renames).
|
and index renames).
|
||||||
4. `uv run alembic upgrade head`. The backup hook runs again.
|
4. `uv run alembic upgrade head`. The backup hook runs again.
|
||||||
5. Write tests, update templates / routes.
|
5. Write tests, update templates and routes.
|
||||||
|
|
||||||
## UI verification
|
### UI verification
|
||||||
|
|
||||||
Type checks and pytest do not verify UI behaviour. For any change that
|
Type checks and pytest do not verify UI behaviour. For any change that
|
||||||
affects the HTML, also:
|
affects the HTML, also:
|
||||||
|
|
@ -144,5 +157,18 @@ affects the HTML, also:
|
||||||
1. Start the server against a throwaway DB.
|
1. Start the server against a throwaway DB.
|
||||||
2. Curl the affected endpoints and check for the expected classes and
|
2. Curl the affected endpoints and check for the expected classes and
|
||||||
text in the response bodies.
|
text in the response bodies.
|
||||||
3. If practical, open the page in a browser and exercise the feature.
|
3. Open the page in a browser and exercise the feature.
|
||||||
Claude cannot do this step from the CLI; ask the human.
|
4. Claude cannot run a real browser from the CLI; the human eyeball is
|
||||||
|
the final check before merge.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
|
||||||
69
Home.md
69
Home.md
|
|
@ -7,30 +7,65 @@ real spending on the applied side.
|
||||||
|
|
||||||
## Pages
|
## Pages
|
||||||
|
|
||||||
* [Architecture](Architecture) — data model, routes, snapshot semantics,
|
* [Architecture](Architecture) — data model, route map, snapshot
|
||||||
zero-amount calculation
|
semantics, lifecycle states, deviation rules, zero-amount calculation,
|
||||||
* [DevelopmentGuide](DevelopmentGuide) — setup, run, test, project
|
visual identity
|
||||||
layout, conventions
|
* [DevelopmentGuide](DevelopmentGuide) — prerequisites, setup, run, test,
|
||||||
|
project layout, conventions, throwaway-DB pattern
|
||||||
* [Operations](Operations) — backup script, restore, DB safety rule,
|
* [Operations](Operations) — backup script, restore, DB safety rule,
|
||||||
alembic hook, throwaway-DB pattern
|
alembic hook
|
||||||
* [Roadmap](Roadmap) — shipped and deferred work
|
* [Roadmap](Roadmap) — shipped features and what's deferred
|
||||||
|
|
||||||
## At a glance
|
## At a glance
|
||||||
|
|
||||||
* `/` — the budget configuration (the plan). One card per section with
|
* `/` — the budget configuration (the plan). Sections organised into four
|
||||||
name + amount entries. Primary Debt Target points at a Debt Minimums
|
collapsible groups: **Income**, **Committed** (fixed bills + debt
|
||||||
row.
|
minimums + Primary Debt Target), **Savings** (sinking funds), and
|
||||||
* `/month/YYYY-MM` — a snapshot of the budget for a specific month with
|
**Flexible** (food, subscriptions, other).
|
||||||
an `applied` column per entry. Deviations from the snapshot are
|
* `/month/YYYY-MM` — a snapshot of the budget for a specific calendar
|
||||||
visibly tagged.
|
month. Adds an `applied` column per entry to track actuals. Rows carry
|
||||||
* Top of every page: a Zero Amount header. Green when every dollar is
|
deviation tags when edited or added post-snapshot.
|
||||||
assigned, amber when there is unassigned income, red when over-budget.
|
* **Zero Amount header** on both pages. Green when every dollar is
|
||||||
|
assigned, amber when unassigned income remains, red when over-budget.
|
||||||
|
The logo anchors the left of the hero; Applied / Planned flank the big
|
||||||
|
number on month pages.
|
||||||
|
* **Per-entry notes** — free-text annotation on every row. Copied through
|
||||||
|
at snapshot time; editable inline.
|
||||||
|
|
||||||
|
## Month lifecycle
|
||||||
|
|
||||||
|
Each month moves through three explicit states:
|
||||||
|
|
||||||
|
| State | Editable? | Transition |
|
||||||
|
|---|---|---|
|
||||||
|
| Planning | yes | *Activate* → Active |
|
||||||
|
| Active | yes | *Close* → Closed (requires applied zero = $0.00) |
|
||||||
|
| Closed | no | *Reopen* → Active |
|
||||||
|
|
||||||
|
Nothing sweeps automatically. The Primary Debt Target is a hint about
|
||||||
|
where leftover applied dollars belong; filling it is the user's job.
|
||||||
|
|
||||||
|
## Visual identity
|
||||||
|
|
||||||
|
* **Typography**: Barlow Condensed throughout (weights 300-800 + italic),
|
||||||
|
Barlow proportional as a secondary pair. Tabular lining figures in
|
||||||
|
every numeric column.
|
||||||
|
* **Palette**: warm cream paper, warm near-black ink, burgundy accent
|
||||||
|
(`#732629`, sampled from the logo shield). Sage / ochre / indigo for
|
||||||
|
balance / under-spend / new-this-month states.
|
||||||
|
* **Layout**: ledger-style density. Entry rows carry a 2-pixel progress
|
||||||
|
bar along the bottom border that fills to the applied/planned ratio
|
||||||
|
and overshoots in burgundy when over-budget.
|
||||||
|
* **Brand**: the logo's shield encloses a Q, a $, and a `$0` — literally
|
||||||
|
the zero-based thesis. It sits in the left column of every zero hero.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Shipped: initial scaffold, monthly snapshot view with deviation tags,
|
Shipped: initial scaffold, monthly snapshot with deviation tags, database
|
||||||
database backup script with alembic auto-hook, zero-amount header. See
|
backup script with alembic auto-hook, zero-amount header, section groups
|
||||||
the [Roadmap](Roadmap) for what is next.
|
with collapsible headers, sinking funds section, per-entry notes, month
|
||||||
|
lifecycle with the balance gate, UI redesign in Barlow Condensed + logo.
|
||||||
|
See the [Roadmap](Roadmap) for what is next.
|
||||||
|
|
||||||
## Code
|
## Code
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,13 @@ You forgot `uv run alembic upgrade head` after cloning. The backup
|
||||||
hook will print "no database at ..., nothing to back up" the first
|
hook will print "no database at ..., nothing to back up" the first
|
||||||
time, which is expected.
|
time, which is expected.
|
||||||
|
|
||||||
|
### "no such column: month_entry.notes" (or similar)
|
||||||
|
|
||||||
|
You pulled code with a new column but did not apply the migration.
|
||||||
|
Run `uv run alembic upgrade head`. The backup hook backs up the live
|
||||||
|
DB first, then the migration adds the missing column. Common after
|
||||||
|
a pull that includes schema work (notes field, month lifecycle).
|
||||||
|
|
||||||
### Alembic reports a revision it cannot locate
|
### Alembic reports a revision it cannot locate
|
||||||
|
|
||||||
The DB is on a schema version whose migration file is not in the
|
The DB is on a schema version whose migration file is not in the
|
||||||
|
|
@ -115,3 +122,17 @@ or downgrade the DB to a known-good revision before continuing.
|
||||||
|
|
||||||
Intentional. The script is sqlite-specific. Switch the URL back to
|
Intentional. The script is sqlite-specific. Switch the URL back to
|
||||||
sqlite:///... or do the backup manually via your Postgres tooling.
|
sqlite:///... or do the backup manually via your Postgres tooling.
|
||||||
|
|
||||||
|
## Current schema
|
||||||
|
|
||||||
|
Applied migrations at time of writing:
|
||||||
|
|
||||||
|
| Revision | Description |
|
||||||
|
|---|---|
|
||||||
|
| `f1ccdc4bc1bf` | initial schema (`entry`, `debt_target`) |
|
||||||
|
| `03ebe3c07262` | add month snapshot tables (`month`, `month_entry`, `month_debt_target`) |
|
||||||
|
| `ec804bdf366d` | add `notes` column to `entry` and `month_entry` |
|
||||||
|
| `a4ec4f8f6e9f` | add month lifecycle columns (`state`, `activated_at`, `closed_at`) |
|
||||||
|
|
||||||
|
After pulling new code, `uv run alembic upgrade head` walks the chain
|
||||||
|
and the backup hook fires between each hop.
|
||||||
|
|
|
||||||
46
Roadmap.md
46
Roadmap.md
|
|
@ -8,11 +8,16 @@
|
||||||
| 3 | Monthly budget view with snapshot-from-config and applied tracking | 2026-04-17 |
|
| 3 | Monthly budget view with snapshot-from-config and applied tracking | 2026-04-17 |
|
||||||
| 5 | DB backup script invoked before every schema or destructive DB operation | 2026-04-17 |
|
| 5 | DB backup script invoked before every schema or destructive DB operation | 2026-04-17 |
|
||||||
| 7 | Zero Amount header at the top of budget and month pages | 2026-04-17 |
|
| 7 | Zero Amount header at the top of budget and month pages | 2026-04-17 |
|
||||||
|
| 9 | Gitignore the local wiki checkout at docs/wiki/ | 2026-04-17 |
|
||||||
|
| 11 | Section groups with collapsible headers + Sinking Funds section | 2026-04-17 |
|
||||||
|
| 13 | Notes field per entry | 2026-04-17 |
|
||||||
|
| 15 | Month lifecycle: Planning, Active, Closed with reconciliation gate | 2026-04-17 |
|
||||||
|
| 17 | UI redesign: condensed-sans ledger style with logo in the zero hero | 2026-04-17 |
|
||||||
|
|
||||||
## Deferred / on the horizon
|
## Deferred
|
||||||
|
|
||||||
Things we have explicitly called out as future work. Not yet issues;
|
Things we have explicitly called out as future work. File an issue when
|
||||||
file one when the time comes.
|
the time comes.
|
||||||
|
|
||||||
### Transaction log behind applied
|
### Transaction log behind applied
|
||||||
|
|
||||||
|
|
@ -22,23 +27,25 @@ Implies a new `month_transaction` table, a UI for entering
|
||||||
transactions, and a migration path that preserves existing applied
|
transactions, and a migration path that preserves existing applied
|
||||||
values as an opening balance.
|
values as an opening balance.
|
||||||
|
|
||||||
### Month close-out / carryover
|
### Closed-month "archived" visual treatment
|
||||||
|
|
||||||
When a month ends, allow rolling remaining amounts into the next
|
Closed months currently carry a muted state badge and disabled inputs.
|
||||||
month's planned values. Useful for categories where underspend rolls
|
A deeper treatment would desaturate the page palette, add a subtle
|
||||||
forward (e.g. Food) and debatable for others (e.g. Rent). Likely a
|
"Closed" watermark, and visually retire the closed month from the
|
||||||
per-section policy.
|
editing canvas.
|
||||||
|
|
||||||
### Copy-forward month
|
### Copy-forward month
|
||||||
|
|
||||||
A "new month" that snapshots a previous month rather than the budget
|
A "new month" that snapshots a previous month rather than the budget
|
||||||
config. Useful when reality has drifted far enough from the plan that
|
config. Useful when reality has drifted far enough from the plan that
|
||||||
last month is a better starting point.
|
last month is a better starting point. Would augment `create_month` to
|
||||||
|
accept a `from_month_id` parameter.
|
||||||
|
|
||||||
### Cross-month summary / charts
|
### Cross-month summary and charts
|
||||||
|
|
||||||
A view that shows how zero, applied, and category totals have moved
|
A view that shows how zero-amount, applied, and category totals have
|
||||||
over time. Would use the existing `month_entry` data, no new schema.
|
moved over time. Would use the existing `month_entry` data, no new
|
||||||
|
schema. First pass: a sparkline per category across the last N months.
|
||||||
|
|
||||||
### CSV import / export
|
### CSV import / export
|
||||||
|
|
||||||
|
|
@ -51,11 +58,18 @@ Single-user, local-only today. If the app ever runs on a LAN or is
|
||||||
exposed externally, add a simple password gate or run behind an
|
exposed externally, add a simple password gate or run behind an
|
||||||
SSO-aware reverse proxy.
|
SSO-aware reverse proxy.
|
||||||
|
|
||||||
### Styling pass
|
### localStorage persistence of collapsed groups
|
||||||
|
|
||||||
Current CSS is functional but minimal. A typography and spacing pass
|
Today the open/closed state of section groups resets on every page
|
||||||
would not change semantics but would improve the "looks like a
|
load. A tiny `localStorage` hook would remember which groups a given
|
||||||
spreadsheet" feel.
|
browser last had open.
|
||||||
|
|
||||||
|
### Budget entry name / amount edit
|
||||||
|
|
||||||
|
Budget entries currently require delete-and-recreate to change the
|
||||||
|
name or amount. Notes are editable inline. Extending the inline edit
|
||||||
|
pattern to name and amount on the budget page would mirror what the
|
||||||
|
month page already allows.
|
||||||
|
|
||||||
## Out of scope (probably forever)
|
## Out of scope (probably forever)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue