feat: add month breakdown and session backups
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:
1 parent
019517e6e9
commit
7cccd7a41a
32 files changed
+640
-41
No files matched your search
+29
-5
@@ -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
|
||||
|
||||
@@ -20,6 +20,10 @@ record.
|
||||
ordered migration functions applied on load, because IndexedDB holds the
|
||||
user's only copy of their financial data. Now at version 11.
|
||||
→ [`specs/2026-09-01-budget-app-design.md`](specs/2026-09-01-budget-app-design.md)
|
||||
- **Session backups live in browser storage.** Each app start saves a checkpoint
|
||||
before editing is available; 7 daily, 8 weekly and 12 monthly checkpoints
|
||||
are retained. Chosen for automatic operation, with manual JSON export still
|
||||
needed for browser-storage loss. → [`ARCHITECTURE.md`](ARCHITECTURE.md)
|
||||
- **A destructive write downloads a backup first.** Import and reset both export
|
||||
before replacing, and abort if the export fails. → commit `ac3d18a`
|
||||
- **Reset restores first-run state, not an empty dataset.** The seed posts, the
|
||||
@@ -85,6 +89,10 @@ record.
|
||||
|
||||
## Recurring costs
|
||||
|
||||
- **A recurring cost may have no post.** It still appears as due, while no
|
||||
envelope projection moves; confirming it records an unassigned purchase.
|
||||
This lets the purchase dialog turn an unassigned charge into a bill without
|
||||
inventing a destination. → [`ARCHITECTURE.md`](ARCHITECTURE.md)
|
||||
- **Two-stage confirmation, over projection-only or generation-only.** A
|
||||
recurring cost projects expected occurrences that stay expected until the
|
||||
owner confirms one into an ordinary `Purchase`. Projection-only would charge
|
||||
|
||||
+8
-2
@@ -74,11 +74,17 @@ Confirmed and in place:
|
||||
- Multi-currency (DKK/USD/EUR) against a user-maintained exchange-rate table,
|
||||
with an optional on-demand fetch that fails soft and is never required.
|
||||
- JSON export/import and ODS (spreadsheet) export, both generated in the
|
||||
browser.
|
||||
browser. On each app start, a separate browser-stored session backup is made
|
||||
before editing is available; recent daily, weekly and monthly checkpoints can
|
||||
be downloaded or restored from Settings. These checkpoints share the browser
|
||||
origin with the live data, so JSON export is still needed for protection
|
||||
against losing browser storage.
|
||||
- Offline-capable installable PWA. Currently deploys as one static artifact to
|
||||
both GitHub Pages (under a subpath) and a self-hosted Docker image.
|
||||
- Recurring costs — a bill that repeats every N months, every N days, or
|
||||
every N weeks on a weekday, split across posts like any purchase. A cost is
|
||||
every N weeks on a weekday, optionally split across posts like any purchase.
|
||||
A cost may have no post: it remains expected without moving an envelope's
|
||||
projected balance, and confirming it records unassigned spending. A cost is
|
||||
anchored one of two ways: `calendar` holds a fixed grid of due dates, so
|
||||
paying two months at once still leaves nothing due until the one after; a
|
||||
cost anchored to its **last charge** has no grid at all and can be charged
|
||||
|
||||
+4
-14
@@ -9,9 +9,10 @@ each recording the alternatives that were rejected. Rules that outlived their
|
||||
task moved to `AGENTS.md`.
|
||||
|
||||
**Current state:** the anchored model is merged with budget folders and
|
||||
placement. 870 tests passing across 61 files. `bunx tsc --noEmit` is clean;
|
||||
`bun run --bun build` succeeds using the committed seed rates when live-fetch
|
||||
is unavailable. The runtime override bypasses a broken local Bun shim.
|
||||
placement. The month pie showcase, browser-stored session backups, and
|
||||
unassigned recurring costs are implemented. 881 tests pass across 64 files;
|
||||
`bunx tsc --noEmit` is clean, and `SKIP_FX_FETCH=1 bun run build` succeeds
|
||||
using the committed seed rates.
|
||||
|
||||
**Schema is at version 11.** v1 -> v2 made allocation rules a dated series;
|
||||
v2 -> v3 moved currencies into the dataset; v3 -> v4 drops a stored rate-service
|
||||
@@ -153,17 +154,6 @@ reintroduce the bug the anchored model exists to fix.
|
||||
The anchored model was merged on 2026-09-30, after main had gained budget
|
||||
folders, placement and nested bills. Everything below is known and unfinished.
|
||||
|
||||
### "+ New recurring cost…" is offered for a purchase with no post
|
||||
|
||||
`newCostOptionIsSelectable` in `src/ui/recurringForm.ts` checks only the date.
|
||||
A bill needs at least one destination post (`requireRecurringSplitDestinations`
|
||||
in `src/store/actions.ts`), so a purchase left unassigned (no splits) can pick
|
||||
the option, and Save is then refused with "A bill needs a destination post".
|
||||
The error surfaces and the dialog stays open, so nothing is lost — but it is a
|
||||
doomed save the dialog could have prevented. Disable the option, with the
|
||||
reason, while the draft has no splits, the same way a month-only date disables
|
||||
the day-granular units.
|
||||
|
||||
### A bill created from the purchase dialog has no folder choice
|
||||
|
||||
Settings' new-bill form picks a folder; the dialog's "+ New recurring cost…"
|
||||
|
||||
Reference in new issue
Block a user