docs: refresh wiki for groups, sinking funds, notes, lifecycle, UI redesign

archeious 2026-04-17 17:03:12 -06:00
parent 390b9ffe56
commit fe7edfe973
5 changed files with 307 additions and 124 deletions

@ -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
uv run uvicorn quartermaster.main:app --reload
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \ # when done
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \ unset QUARTERMASTER_DB_URL QUARTERMASTER_BACKUP_DIR
uv run uvicorn quartermaster.main:app --reload 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

@ -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.

@ -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)