Commit graph

48 commits

Author SHA1 Message Date
archeious
517578f4f3 feat(db): add Posting model, derive MonthEntry.applied, seed opening balances
Posting is a child of MonthEntry with occurred_on, amount, optional
description and payee. Cascade delete so removing an entry wipes its
ledger. Ordered on load by occurred_on DESC for readable UIs.

MonthEntry.applied becomes a @property summing posting amounts. The
stored applied column is dropped in the same migration.

The migration walks existing month_entry rows: for every non-zero
applied value, it inserts one opening-balance posting on the month's
activated_at (or created_at) date with description "opening balance"
and amount equal to the existing applied. Empty applied values get
no opening posting. Closed months go through the same path; their
totals stay intact via that single seeded row.

Downgrade is symmetric: re-adds the column and populates from
SUM(postings).

Refs #19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:34:34 -06:00
52e3217aee UI redesign: condensed-sans ledger style with logo in the zero hero (#18) 2026-04-17 17:04:54 -06:00
archeious
761336c71d docs: keep design mockups alongside the shipped UI
Three iterations captured under docs/mockups/ with shared logo
assets:

* month-editorial-ledger.html — first pass, loose spacing, Fraunces
  serif throughout, no logo.
* month-editorial-ledger-dense.html — tightened spacing, logo
  integrated as masthead and behind the zero widget.
* month-condensed-sans.html — the shipped direction. Logo in the
  zero hero, Barlow Condensed everywhere, masthead dropped.

These give a paper trail for why the app looks the way it does and
a sandbox for future typographic experiments without touching the
live CSS.

Refs #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:58:22 -06:00
archeious
3fa5887d3f test: update selector expecting the retired zero-widget-pair class
The condensed-sans redesign unifies budget and month zero widgets
into a single class; the pair variant is distinguished by column
count, not class. Assertion now checks that the month page still
renders both Applied and Planned labels alongside the zero widget
id.

Refs #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:58:18 -06:00
archeious
eb689cd9a1 feat(ui): pretty_year_month helper, render month names in titles
pretty_year_month('2026-04') -> 'April 2026' for display. The
templates fall back to the raw year_month slug when the helper
returns nothing. Used by month.html and month_create.html to give
the nav a broadsheet feel; the URL routes still use the YYYY-MM
slug everywhere.

Refs #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:58:14 -06:00
archeious
59ccb4db3c feat(ui): page templates for the new layout, logo in the zero hero
Rebuild index.html, month.html, month_create.html, and every partial
against the new stylesheet:

* Drop the old Quartermaster / Household budget header block.
* Zero hero carries the logo on the left column with flanking
  Applied / Planned on the month page, and a Budget / Planning-for
  split on the budget page. Colophon tagline at the foot of every
  page.
* month_nav switches to a single horizontal line: prev / title /
  next / state badge / lifecycle button, with the month picker
  wrapping below when present.
* Entry rows render through the new table.entries grid (name,
  planned, applied, actions) with the notes row tucked underneath
  and hidden when empty via :has(input:placeholder-shown).
* Primary Debt Target card uses the target-section styling with the
  burgundy left bar.
* Back to Budget / Back to Configuration link justified right under
  each month nav.

Refs #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:58:11 -06:00
archeious
368a4d0741 feat(ui): condensed-sans ledger CSS and Barlow Condensed typography
Swap the old minimal stylesheet for a comprehensive condensed-sans
ledger system:

* Barlow Condensed everywhere (300-800 + italic), via Google Fonts;
  Barlow proportional as a secondary pair. No more system-ui stack.
* Warm cream paper background with layered radial gradients for
  depth. Ink is warm near-black; accent is the logo burgundy
  #732629 sampled directly from the shield.
* Tabular lining figures for every numeric column.
* <details> groups with a hairline chevron rotating on [open].
* Dense entry rows with a 2px progress bar riding the row's bottom
  border. State colours by deviation: sage at-plan, ochre under,
  accent over, indigo new-in-month.
* Primary Debt Target card with a burgundy left bar and margin
  glyph so it reads as a hint pointing inward.
* State badges inline as tracked-caps labels with bullet separators
  (planning indigo, active sage, closed muted italic).
* Favicon wired to the shield mark.

Refs #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:58:03 -06:00
archeious
4ba4a1ba18 chore(brand): add cleaned logo assets and gitignore source files
logo.png at the repo root is a working 4.8MB source; optimised
variants used by the running app live in src/quartermaster/static/brand/
(full mark, mark-wide without wordmark, shield-wide for favicon).
Background checkerboard was masked out of all three so they sit on
any page colour. Gitignore the root source and WSL Zone.Identifier
sidecars.

Refs #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:57:51 -06:00
961ea46669 Month lifecycle: Planning, Active, Closed with reconciliation gate (#16) 2026-04-17 13:04:59 -06:00
archeious
8fec2fdff7 test: cover lifecycle transitions, balance gate, and edit-locking
Service tests walk Planning -> Active -> Closed -> Active and
confirm rejects on out-of-order transitions. Close rejects when
applied zero is nonzero; succeeds when balanced; reopens cleanly.
Route tests confirm each endpoint's status codes, HX-Redirect
headers, and that the page renders the right badge and button per
state. Closed months reject every mutation with 400 and their
rendered HTML carries disabled inputs without add forms or delete
buttons.

Refs #15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:04:06 -06:00
archeious
1df3c1c218 feat(lifecycle): transition routes, state badge, and edit-locking UI
POST /month/{ym}/activate, /close, /reopen each return 204 with
HX-Redirect so the page re-renders in the new state. All existing
mutation routes now go through _require_editable_month, which 400s
on closed months.

Month nav grows a state badge and a context-appropriate lifecycle
button. Close is rendered with a disabled attribute and tooltip
when applied zero != 0. On closed months, name / planned / applied
/ notes inputs carry the disabled attribute; delete buttons, add
forms, and the target form are omitted entirely.

Refs #15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:04:03 -06:00
archeious
fa9a397d83 feat(lifecycle): activate, close, reopen transitions with validation
activate_month moves Planning to Active and stamps activated_at.
close_month moves Active to Closed only when applied zero equals
exactly $0.00; otherwise raises MonthLifecycleError with a message
naming the current balance. reopen_month moves Closed back to
Active and nulls closed_at. ensure_editable is the guard mutation
routes call before any write. No automatic sweep: filling the
target row is the user's job via editing applied amounts.

Refs #15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:03:57 -06:00
archeious
eb7e47bcbe feat(db): add MonthState enum and lifecycle columns
state defaults to 'planning' (server default plus SQLAlchemy default).
activated_at and closed_at are nullable timestamps that record when
the month crossed each boundary. Alembic batch_alter_table handles
the SQLite rewrite. MonthState is a Python string enum mapped to a
non-native VARCHAR(16).

Refs #15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:03:53 -06:00
d040b7b66c Notes field per entry (#14) 2026-04-17 12:54:24 -06:00
archeious
28d097cfbf test: cover notes add, update, clear, snapshot copy, and no-deviation
Service tests hit create, update, missing-id, blank-collapses-to-null,
and the snapshot copying behaviour. Route tests hit both pages'
create flows, the budget notes endpoint, and the month update route.
A dedicated assertion confirms that changing notes on a month entry
does not flip the deviation state: notes are free-form annotation,
not a signal of plan drift.

Refs #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:51:36 -06:00
archeious
0f2f549d85 feat(notes): render and wire notes inputs on budget and month pages
Each entry row gains a secondary notes row with an inline-editable
text input. Budget entries post to a new /entries/{id}/notes
endpoint; month entries reuse the existing update route. Add forms
gain an optional "notes (optional)" input that spans the form row.
Notes render muted with a dashed underline on hover to signal
editability without cluttering the layout. Changing notes on a
month row does not flip the deviation state since the financial
values are unchanged.

Refs #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:51:31 -06:00
archeious
034a8d65f5 feat(notes): service-layer support for notes on entry and month_entry
add_entry and add_month_entry accept an optional notes keyword. A
new set_entry_notes function updates a single budget entry's notes.
update_month_entry gains a notes parameter guarded by a sentinel so
callers can distinguish "do not touch notes" from "clear to NULL".
create_month copies entry.notes into each freshly snapshotted
month_entry. Blank / whitespace notes normalise to NULL.

Refs #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:51:25 -06:00
archeious
4d40843e24 feat(db): add nullable notes column to entry and month_entry
Free-text annotation up to 1024 chars. Nullable so existing rows need
no backfill; an empty or whitespace-only input will be normalised to
NULL in the service layer. Alembic batch_alter_table handles the
SQLite table rebuild automatically.

Refs #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:51:22 -06:00
1eecfc3ae8 Section groups with collapsible headers + Sinking Funds (#12) 2026-04-17 12:46:04 -06:00
archeious
0ba7a19972 test: cover group mapping, subtotals, default state, and OOB swaps
Every section maps to a group. Group order and defaults match the
spec. Budget and month subtotal calculations check out across
seeded entries. Pages render the expected details ids, income is
open by default, committed is closed. Mutations return OOB group
total spans. Sinking Funds section is visible on both pages.

Refs #11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:44:21 -06:00
archeious
4d9b64d760 feat(groups): render collapsible group details and OOB-swap subtotals
Budget and month pages now wrap sections in native <details> blocks
with summary rows showing the group name and subtotal. Income and
Flexible default open, Committed and Savings default closed so the
day-to-day editing targets are visible and the set-and-forget
commitments collapse out of the way. Primary Debt Target renders
inside the Committed group after Debt Minimums.

Every mutation appends a group-totals partial with OOB spans for
all four group subtotals so the header stays in sync without a
reload regardless of which section changed.

Refs #11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:44:18 -06:00
archeious
0c533d62ed feat(groups): group views with subtotals for budget and month
budget_group_views composes SectionViews into grouped dataclasses
with a combined total and the default open flag. month_group_views
does the same with planned and applied totals. Group order, labels,
and section-to-group mapping all come from the groups module.

Refs #11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:44:12 -06:00
archeious
032c35c75e feat(groups): add sinking_fund section and define four-group layout
Section enum gains sinking_fund with label "Sinking Funds". A new
groups module maps each section to one of Income, Committed, Savings,
Flexible and records the default open state per group. The existing
section column is a plain VARCHAR(32) with no CHECK, so no schema
migration is needed to accept the new value.

Refs #11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:44:09 -06:00
496f44cf8c Gitignore docs/wiki/ (#10) 2026-04-17 12:16:26 -06:00
archeious
671a7405cb chore: gitignore the local wiki checkout
docs/wiki/ is a checkout of quartermaster.wiki.git, not part of the
main repo. Ignoring it keeps git status clean and prevents accidental
cross-repo commits.

Refs #9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:14:52 -06:00
85efed8f2c Zero Amount header at the top of budget and month pages (#8) 2026-04-17 12:08:49 -06:00
archeious
ce7e0f2b3f test: cover zero-amount math and OOB rendering
Service tests hit empty, positive, negative, and exactly-zero cases,
verify debt_target is excluded from the calculation, and confirm
month_zero responds to applied updates. Route tests assert the zero
widget appears OOB on every mutation with the expected tone class.

Refs #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:06:45 -06:00
archeious
29c6594347 feat(zero): render Zero Amount header and OOB-swap on every mutation
Single large number on /, Planned + Applied pair on /month/YYYY-MM,
both colour-coded (green at zero, amber positive, red negative).
Every mutation route (add, delete, month entry update) appends the
zero widget to its response with hx-swap-oob so totals stay in sync
without a full page reload. An _append_oob helper replaces the ad-hoc
string concatenation the target card was already using.

Refs #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:06:42 -06:00
archeious
5a65b4c524 feat(zero): compute zero-amount for budget and month
budget_zero sums income minus every non-income section on the budget
config. month_zero returns both Planned and Applied versions over a
month's entries. zero_tone classifies a value as zero / positive /
negative so templates can pick a colour.

Refs #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:06:37 -06:00
38c8921885 Monthly budget view with snapshot and applied tracking (#4) 2026-04-17 11:59:08 -06:00
archeious
b2d16120d2 docs: document monthly view, updated layout, and deferred work
Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:57:30 -06:00
archeious
abdb68a29c test: cover month snapshot, deviation states, and per-month target
Service tests assert that create_month produces origin fields matching
the budget, that edits flip deviation_state to edited, that added rows
are new_in_month, and that a budget entry deleted after snapshot leaves
the month entry unchanged. Route tests exercise the create flow,
applied updates, name edits producing the modified tag, per-month
target isolation, and the malformed-year-month 404.

Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:57:30 -06:00
archeious
e7354ba8d6 feat(month): add routes, templates, and nav between budget and months
Non-existent months return a page with a single "Create this month"
button; create POSTs return HX-Redirect to the newly-created month.
Each entry row carries three inline HTMX-wired inputs (name, planned,
applied) that trigger on change, posting only the field that changed.
Edits swap the section partial so totals and deviation tags update
together. Deleting a debt minimum in a month also re-renders the
target card via OOB swap. The budget page grows a This-month link and
a month picker; each month page has prev / next / picker / back-to-
config controls.

Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:57:30 -06:00
archeious
ed038fd974 feat(month): add snapshot service with deviation tracking
create_month copies every budget entry into month_entry with
origin_name/origin_planned retained, resolves the budget's debt target
through source_entry_id to the corresponding MonthEntry, and is
idempotent. deviation_state classifies each row as unchanged, edited,
or new_in_month. Year-month handling (validation, shift across year
boundaries) lives here so the route layer stays thin.

Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:57:30 -06:00
archeious
58faa9dfe9 feat(db): add Month, MonthEntry, and MonthDebtTarget models with migration
A month is a snapshot of the budget. MonthEntry holds the copied planned
amount plus applied and origin_name/origin_planned so the UI can mark
edited rows. source_entry_id links back to the budget but is nullable
with ON DELETE SET NULL, so deleting a budget row after snapshot leaves
the month intact. MonthDebtTarget is one row per month via CASCADE from
month.

Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:57:30 -06:00
6986081ee4 DB backup script and alembic auto-backup hook (#6) 2026-04-17 11:57:19 -06:00
archeious
9f1dd7a914 test: cover backup-db.sh exit paths and slug sanitisation
Asserts soft exit when the source DB is missing, a successful backup
round-trips sqlite rows, a non-file sqlite URL is rejected, the reason
slug defaults to "manual", and a messy reason is sanitised to a safe
filename fragment.

Refs #5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:51:55 -06:00
archeious
9ee934629a docs: document DB safety rule in CLAUDE.md and README
CLAUDE.md states the durable rule: run scripts/backup-db.sh before
any schema change, data migration, or destructive DB operation. The
rule deliberately excludes routine app writes. README summarises
backup location, override env var, and restore procedure.

Refs #5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:51:52 -06:00
archeious
a09860dc61 feat(ops): alembic env hook invokes backup-db.sh before migrations
Runs at env module load so the backup fires ahead of offline and
online migration paths, as well as alembic current / revision. A
failing backup does not stop the migration: this is defense in depth,
not a hard prerequisite, and the common failure case is "DB does not
exist yet" on a fresh checkout.

Refs #5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:51:49 -06:00
archeious
a492294dd7 feat(ops): add backup-db.sh for safe sqlite snapshots
Uses sqlite3.Connection.backup for an online, WAL-safe copy. Resolves
the DB path from QUARTERMASTER_DB_URL or ./quartermaster.db, places
the snapshot in <db-dir>/backups (override with QUARTERMASTER_BACKUP_DIR),
timestamps the filename, and tags it with an optional reason slug.
Absent DB file is a soft exit so the script is safe to call from hooks.

Refs #5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:51:46 -06:00
7d205d2853 Initial scaffold: single-month budget MVP (#2) 2026-04-17 11:31:11 -06:00
archeious
c6cb037f4f feat(ui): stacked tabular layout, drop card look
Sections stack vertically under a fixed-width column. Each
section renders a table with two data columns (name, amount)
plus a delete action. The Primary Debt Target now sits between
Debt Minimums and Food and Essentials and uses a dashed rule to
distinguish it from sections that accept freeform entries.

Refs #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:21:48 -06:00
archeious
94e1571a3f docs: expand README with setup, run, tests, and layout
Refs #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:04:21 -06:00
archeious
3a17dee4ef test: cover CRUD, debt target selection, and ON DELETE SET NULL
Service-level and route-level coverage. Route tests share an
in-memory SQLite engine across threads via StaticPool and
override the get_session dependency.

Refs #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:04:18 -06:00
archeious
c52fa9c470 feat(web): add FastAPI routes, service layer, and HTMX templates
Index view renders one card per section plus the Primary Debt
Target card. Adding or deleting a Debt Minimums entry returns the
section partial plus an out-of-band swap for the target card so
the target dropdown stays in sync without a reload.

Refs #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:04:13 -06:00
archeious
0f5980bd94 feat(db): add Entry and DebtTarget models with initial migration
Entry stores one row per section entry (name, amount, timestamps).
DebtTarget is a singleton table (CHECK id = 1) with a nullable
foreign key to Entry using ON DELETE SET NULL so deleting the
referenced Debt Minimums row clears the pointer.

Refs #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:04:06 -06:00
archeious
cd196c0816 chore: init uv project with FastAPI, SQLAlchemy, Alembic
Refs #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:03:59 -06:00
833a91c2e8 Initial commit 2026-04-17 10:57:47 -06:00