docs: refresh wiki for groups, sinking funds, notes, lifecycle, UI redesign
parent
390b9ffe56
commit
fe7edfe973
5 changed files with 307 additions and 124 deletions
179
Architecture.md
179
Architecture.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Small FastAPI app rendering Jinja2 templates with HTMX-driven partial
|
||||
updates. SQLite is the only storage. SQLAlchemy 2.x is the ORM; Alembic
|
||||
manages schema.
|
||||
manages schema. `uv` manages Python deps.
|
||||
|
||||
## Modules
|
||||
|
||||
|
|
@ -11,33 +11,46 @@ 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, zero-amount
|
||||
month_service.py Snapshot, deviation, per-month CRUD, month zero
|
||||
models.py SQLAlchemy models and Section enum
|
||||
service.py Budget queries, totals, target, zero-amount, group views
|
||||
month_service.py Month snapshot, deviation, per-month CRUD, lifecycle, group views
|
||||
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
|
||||
config.py DB URL resolution
|
||||
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):
|
||||
income, fixed_bill, debt_minimum, food, subscription, other
|
||||
Section enum (budget + month)
|
||||
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
|
||||
selects one of the Debt Minimums rows as the current focus. Excluded
|
||||
from the zero-amount math since it has no total of its own.
|
||||
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. It is excluded
|
||||
from zero-amount math since it has no total of its own (its amount is
|
||||
already counted inside Debt Minimums).
|
||||
|
||||
## Budget data model
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
|
|
@ -48,24 +61,27 @@ the entry itself.
|
|||
|
||||
```
|
||||
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
|
||||
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,
|
||||
source_entry_id FK entry(id) ON DELETE SET NULL,
|
||||
created_at, updated_at
|
||||
|
||||
month_debt_target (singleton per month)
|
||||
month_debt_target (singleton per month)
|
||||
month_id PK FK month(id) ON DELETE CASCADE,
|
||||
month_entry_id FK month_entry(id) ON DELETE SET NULL,
|
||||
updated_at
|
||||
```
|
||||
|
||||
Creating a month copies every budget entry into `month_entry` and
|
||||
preserves `origin_name` / `origin_planned`. `source_entry_id` links
|
||||
back to the budget but is informational only; the snapshot is
|
||||
preserves `origin_name`, `origin_planned`, and `notes`. `source_entry_id`
|
||||
links back to the budget but is informational only; the snapshot is
|
||||
self-contained so the month keeps working even after the referenced
|
||||
budget row is deleted.
|
||||
|
||||
|
|
@ -73,69 +89,139 @@ budget row is deleted.
|
|||
|
||||
Computed from `(name, planned)` vs `(origin_name, origin_planned)`:
|
||||
|
||||
* `unchanged` — both match. No tag.
|
||||
* `edited` — name or planned differs. Orange tint + "modified" tag.
|
||||
* `new_in_month` — no origin (row added after snapshot). Blue tint +
|
||||
"new this month" tag.
|
||||
* `unchanged` — both match. No tag, progress bar renders in the section
|
||||
baseline colour.
|
||||
* `edited` — name or planned differs. "Modified" tag in ochre; progress
|
||||
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
|
||||
|
||||
Copied from the budget's current debt target at creation time, resolved
|
||||
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
|
||||
|
||||
`sum(income.amount) - sum(non_income.amount)` with
|
||||
`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.
|
||||
On both pages:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
* zero → green (`tone-zero`)
|
||||
* positive → amber (`tone-positive`): unassigned income
|
||||
* negative → red (`tone-negative`): over-budget
|
||||
* `tone-zero` → sage (balanced)
|
||||
* `tone-positive` → ochre (unassigned, under-budget)
|
||||
* `tone-negative` → burgundy (over-budget)
|
||||
|
||||
## Routes
|
||||
|
||||
### Budget (`src/quartermaster/routes.py`)
|
||||
|
||||
```
|
||||
GET / index page
|
||||
POST /sections/{section}/entries add entry
|
||||
GET / index page
|
||||
POST /sections/{section}/entries add entry
|
||||
DELETE /entries/{entry_id} remove entry
|
||||
POST /debt-target set / clear debt target
|
||||
POST /entries/{entry_id}/notes update entry notes
|
||||
POST /debt-target set / clear debt target
|
||||
```
|
||||
|
||||
### Month (`src/quartermaster/routes_month.py`)
|
||||
|
||||
```
|
||||
GET /month/{year_month} view or create-flow page
|
||||
POST /month/{year_month}/create snapshot the budget
|
||||
POST /month/{year_month}/sections/{section}/entries add month entry
|
||||
POST /month/{year_month}/entries/{entry_id} update (name / planned / applied)
|
||||
DELETE /month/{year_month}/entries/{entry_id} remove month entry
|
||||
POST /month/{year_month}/target set / clear per-month target
|
||||
GET /month/{year_month} view page or create-flow
|
||||
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}/entries/{entry_id} update (name / planned / applied / notes)
|
||||
DELETE /month/{year_month}/entries/{entry_id} remove month entry
|
||||
POST /month/{year_month}/target set / clear per-month target
|
||||
```
|
||||
|
||||
All mutation routes return the section partial plus OOB swaps for:
|
||||
|
||||
* The debt target card when a Debt Minimums row was added, edited, or
|
||||
deleted
|
||||
* The Zero Amount widget on every entry change
|
||||
* The debt target card when a Debt Minimums row was added / edited / deleted
|
||||
* The Zero Amount widget
|
||||
* All four group totals (`#group-total-{group}`)
|
||||
|
||||
OOB swap is cheaper than re-rendering the full page and keeps totals
|
||||
in sync without a reload.
|
||||
OOB is used so section-level changes keep the page-level summary widgets
|
||||
accurate without a reload.
|
||||
|
||||
## HTMX conventions
|
||||
|
||||
* Each month entry row carries three inline inputs (`name`, `planned`,
|
||||
`applied`) with `hx-post` and `hx-trigger="change"`. Each input
|
||||
sends only its own field; the server accepts any subset.
|
||||
* Month entry rows carry three inline inputs (`name`, `planned`,
|
||||
`applied`) plus a notes input, each with `hx-post` and
|
||||
`hx-trigger="change"`. Each input sends only its own field; the server
|
||||
accepts any subset.
|
||||
* Section partials swap via `hx-target="#section-{section}"` +
|
||||
`hx-swap="outerHTML"`.
|
||||
* 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.
|
||||
* 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
|
||||
|
||||
|
|
@ -144,3 +230,4 @@ in sync without a reload.
|
|||
share the same in-memory engine across threads via `StaticPool`.
|
||||
* Backup script tests shell out and assert on filenames and sqlite
|
||||
round-trips.
|
||||
* Full suite runs in under 4 seconds.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
|
||||
* Python 3.12+
|
||||
* [uv](https://github.com/astral-sh/uv) for dependency management
|
||||
* SQLite (the library, not the CLI; the backup script uses Python's
|
||||
sqlite3 module so no `sqlite3` binary is required)
|
||||
* SQLite via Python's `sqlite3` module (no external binary required)
|
||||
|
||||
## Setup
|
||||
|
||||
|
|
@ -35,22 +34,25 @@ uv run pytest
|
|||
```
|
||||
|
||||
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
|
||||
|
||||
Dev churn that needs a clean schema (autogenerating a new migration,
|
||||
loading a scratch dataset) should target a throwaway path, never
|
||||
`./quartermaster.db`. See [Operations](Operations) for why.
|
||||
**Do not smoke-test or experiment against the live `./quartermaster.db`.**
|
||||
See the DB safety rule in [Operations](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:
|
||||
|
||||
```sh
|
||||
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \
|
||||
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
||||
uv run alembic upgrade head
|
||||
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
|
||||
|
||||
QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \
|
||||
QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
||||
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
|
||||
|
|
@ -59,38 +61,50 @@ QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \
|
|||
src/quartermaster/
|
||||
main.py FastAPI app factory
|
||||
routes.py Budget configuration handlers
|
||||
routes_month.py Monthly view handlers
|
||||
service.py Budget service: entries, target, zero-amount
|
||||
month_service.py Month service: snapshot, deviation, zero-amount
|
||||
models.py SQLAlchemy models, Section enum
|
||||
routes_month.py Monthly view handlers + lifecycle transitions
|
||||
service.py Budget service: entries, target, zero-amount, groups
|
||||
month_service.py Month service: snapshot, deviation, lifecycle, groups
|
||||
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
|
||||
templates/
|
||||
base.html
|
||||
base.html Barlow Condensed imports + favicon
|
||||
index.html Budget config page
|
||||
month.html Month view page
|
||||
month_create.html Create-flow for missing month
|
||||
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
|
||||
month_section.html Month section card
|
||||
month_target.html Month debt target card
|
||||
month_zero.html Month Zero Amount widget (Planned/Applied)
|
||||
month_nav.html Prev / next / picker / back-to-config
|
||||
static/app.css
|
||||
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
|
||||
test_routes.py
|
||||
test_month_service.py
|
||||
test_month_routes.py
|
||||
test_zero_amount.py
|
||||
test_backup_script.py
|
||||
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_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
|
||||
|
|
@ -108,24 +122,23 @@ chore/<issue>-<slug> tooling, deps, docs
|
|||
|
||||
### Atomic commits
|
||||
|
||||
Each commit is one logical change. Typical PRs in this repo have 3-5
|
||||
commits grouped by layer (data model, service, routes + templates,
|
||||
tests, docs). Commit message subject under 72 chars, body under 80,
|
||||
trailers include `Refs #<issue>` and the `Co-Authored-By` line for
|
||||
Claude sessions.
|
||||
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). Commit subjects under 72 chars, body under 80, trailers
|
||||
include `Refs #<issue>` and the Claude co-author line.
|
||||
|
||||
### Merge flow
|
||||
|
||||
PRs merge via the Forgejo API (or web UI merge button), NOT `git merge`
|
||||
locally. After the merge call:
|
||||
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.
|
||||
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).
|
||||
|
||||
## Adding a schema change
|
||||
### Adding a schema change
|
||||
|
||||
1. Edit `src/quartermaster/models.py`.
|
||||
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,
|
||||
and index renames).
|
||||
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
|
||||
affects the HTML, also:
|
||||
|
|
@ -144,5 +157,18 @@ 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. If practical, open the page in a browser and exercise the feature.
|
||||
Claude cannot do this step from the CLI; ask the human.
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
* [Architecture](Architecture) — data model, routes, snapshot semantics,
|
||||
zero-amount calculation
|
||||
* [DevelopmentGuide](DevelopmentGuide) — setup, run, test, project
|
||||
layout, conventions
|
||||
* [Architecture](Architecture) — data model, route map, snapshot
|
||||
semantics, lifecycle states, deviation rules, zero-amount calculation,
|
||||
visual identity
|
||||
* [DevelopmentGuide](DevelopmentGuide) — prerequisites, setup, run, test,
|
||||
project layout, conventions, throwaway-DB pattern
|
||||
* [Operations](Operations) — backup script, restore, DB safety rule,
|
||||
alembic hook, throwaway-DB pattern
|
||||
* [Roadmap](Roadmap) — shipped and deferred work
|
||||
alembic hook
|
||||
* [Roadmap](Roadmap) — shipped features and what's deferred
|
||||
|
||||
## At a glance
|
||||
|
||||
* `/` — the budget configuration (the plan). One card per section with
|
||||
name + amount entries. Primary Debt Target points at a Debt Minimums
|
||||
row.
|
||||
* `/month/YYYY-MM` — a snapshot of the budget for a specific month with
|
||||
an `applied` column per entry. Deviations from the snapshot are
|
||||
visibly tagged.
|
||||
* Top of every page: a Zero Amount header. Green when every dollar is
|
||||
assigned, amber when there is unassigned income, red when over-budget.
|
||||
* `/` — the budget configuration (the plan). Sections organised into four
|
||||
collapsible groups: **Income**, **Committed** (fixed bills + debt
|
||||
minimums + Primary Debt Target), **Savings** (sinking funds), and
|
||||
**Flexible** (food, subscriptions, other).
|
||||
* `/month/YYYY-MM` — a snapshot of the budget for a specific calendar
|
||||
month. Adds an `applied` column per entry to track actuals. Rows carry
|
||||
deviation tags when edited or added post-snapshot.
|
||||
* **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
|
||||
|
||||
Shipped: initial scaffold, monthly snapshot view with deviation tags,
|
||||
database backup script with alembic auto-hook, zero-amount header. See
|
||||
the [Roadmap](Roadmap) for what is next.
|
||||
Shipped: initial scaffold, monthly snapshot with deviation tags, database
|
||||
backup script with alembic auto-hook, zero-amount header, section groups
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
||||
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
|
||||
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 |
|
||||
| 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 |
|
||||
| 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;
|
||||
file one when the time comes.
|
||||
Things we have explicitly called out as future work. File an issue when
|
||||
the time comes.
|
||||
|
||||
### 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
|
||||
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
|
||||
month's planned values. Useful for categories where underspend rolls
|
||||
forward (e.g. Food) and debatable for others (e.g. Rent). Likely a
|
||||
per-section policy.
|
||||
Closed months currently carry a muted state badge and disabled inputs.
|
||||
A deeper treatment would desaturate the page palette, add a subtle
|
||||
"Closed" watermark, and visually retire the closed month from the
|
||||
editing canvas.
|
||||
|
||||
### Copy-forward month
|
||||
|
||||
A "new month" that snapshots a previous month rather than the budget
|
||||
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
|
||||
over time. Would use the existing `month_entry` data, no new schema.
|
||||
A view that shows how zero-amount, applied, and category totals have
|
||||
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
|
||||
|
||||
|
|
@ -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
|
||||
SSO-aware reverse proxy.
|
||||
|
||||
### Styling pass
|
||||
### localStorage persistence of collapsed groups
|
||||
|
||||
Current CSS is functional but minimal. A typography and spacing pass
|
||||
would not change semantics but would improve the "looks like a
|
||||
spreadsheet" feel.
|
||||
Today the open/closed state of section groups resets on every page
|
||||
load. A tiny `localStorage` hook would remember which groups a given
|
||||
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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue