feat: add month breakdown and session backups
Continuous Integration / lint-and-test (push) Successful in 17s
Continuous Integration / docker-build-and-push (push) Successful in 35s

Allow recurring costs and confirmations to remain unassigned when created from a purchase without a post. Add browser backup retention and restore controls, with coverage and documentation updates.
This commit is contained in:
Lucas Winther
2026-10-01 04:01:49 +02:00
parent 019517e6e9
commit 7cccd7a41a
32 changed files with 640 additions and 41 deletions
+29 -5
View File
@@ -118,6 +118,28 @@ uses on a first run, so "reset" and "a brand-new browser" cannot drift apart.
Both are preceded in the UI by a JSON backup download that must succeed before
the destructive write is attempted.
## Session backups
On each page load, `src/frontend.tsx` waits for `store.load()`, then saves the
loaded dataset to a separate `backups` object store in the same IndexedDB
before rendering controls that can edit it. A failed backup does not prevent
access to the budget; the app shows an alert with the failure. The snapshot is
JSON, separate from the live `state` record, and is never read as live data.
Retention keeps the newest checkpoint from each of 7 UTC days, 8 Monday-based
UTC weeks and 12 UTC months. A checkpoint may cover multiple tiers. In
backup persistence, writing the new checkpoint and deleting checkpoints
outside that set share one IndexedDB transaction: a failed write cannot prune
older backups. Settings lists retained checkpoints for download and restore.
Restoring parses through the same JSON import boundary, downloads the current
active dataset first, then calls `store.replace()`; if the download fails, the
active data stays put. Reset replaces the active dataset but leaves session
backups available.
These backups use the same browser origin as the live dataset. They help with
accidental edits or resets, but do not survive loss of all browser storage.
Manual JSON export remains the portable backup.
## Session undo and redo
The snapshot store keeps up to 50 prior saved datasets in memory. A changed
@@ -175,11 +197,13 @@ test — and charges a fixed "2000 to Video Games" in full on every slice.
A purchase with no post has an empty `splits` array. Its dated amount counts in
monthly, yearly and summary Spent totals, but charges no envelope and changes no
rollover balance. Recurring bills still require a destination post. JSON import
and ODS export preserve these unassigned purchases. In the month Budget tree,
they appear in the permanent, derived Unassigned section for each month their
purchase or finance plan touches. Creating a post from one assigns its full
amount to the new post without changing the purchase total or schedule.
rollover balance. A recurring bill may also have empty splits: its occurrences
still appear in Expected and Bills due, but they move no post's projected
balance. Confirming one creates an unassigned purchase, which counts in Spent.
JSON import and ODS export preserve these unassigned purchases. In the month
Budget tree, purchases appear in the permanent, derived Unassigned section for
each month they touch. Creating a post from one assigns its full amount to the
new post without changing the purchase total or schedule.
A post with no applicable allocation rule has no overspend status, even if its
balance is negative. The balance remains visible and carries normally. An