Critical (X1): confirming a "Coming up" row records TODAY against a future slot, and occurrencesOf throws — permanently, since the purchase persists — if that pairing can't advance the series. Reproduced with the demo's own Car insurance (everyNDays 30, lastCharge, from 2026-08-14): viewing 2026-09 and confirming its coming-up row (slot 2026-10-13, recorded 2026-09-04) bricks every month route, yearView, summaryView and PostYearRoute. `expectedGroups` now omits a coming-up row outright (never disabled-with-an-explanation) unless `wouldAdvancePast` says recording it today would leave the walk able to advance past that slot. Important (X3): a cost with an outstanding occurrence in the displayed month or any earlier foldable month never gets a coming-up row either — that was the double-counting path into X1 (Car insurance listed under both "Expected" 2026-09-13 and "Coming up" 2026-10-13; confirming the later while the earlier stayed pending would double-count the bill). Settle the earlier bill first. Important (X2): extracted the this-month-vs-coming-up recorded-date decision into `recordedDateFor(group, today)`, previously inlined as `row(occurrence, null)` vs `row(occurrence, today)` with nothing testing that the two aren't swapped. Tested directly. Minor: the "Coming up" label is now an `<h3>` (matching MonthRoute's own group-heading convention) instead of a `<p>`, so assistive tech gets the section divider. New tests in ExpectedBand.test.ts cover all of the above, including a regression built from the exact reported shape: confirming everything expectedGroups offers as "coming up" and then folding must never throw. Pre-fix, that reproduction throws: Recurring cost "Car insurance" did not advance past 2026-10-13 (produced 2026-10-04). Purchase <id> claims that slot but is dated 2026-09-04, which does not move the series past 2026-10-13 — correct its date so the series advances.
Budget 2.0
A personal budgeting PWA. Envelope-style monthly budgets with rollover, split purchases, finance plans, multi-currency, and JSON/ODS export. All data lives in your browser — there is no server and no account.
Development
bun install
bun run dev # dev server with hot reload, on :3000
bun test # run the test suite
bun run build # production build into dist/
Self-hosting with Docker
docker build -t budget2 .
docker run --rm -p 3000:3000 budget2
To serve from a subpath, build and run with BUN_PUBLIC_BASE_PATH:
docker build -t budget2 --build-arg BUN_PUBLIC_BASE_PATH=/budget/ .
docker run --rm -p 3000:3000 -e BUN_PUBLIC_BASE_PATH=/budget/ budget2
GitHub Pages
Pushing to main builds and deploys to Pages automatically, using
/<repository-name>/ as the base path. Enable Pages with "GitHub Actions" as
the source in the repository settings.
Your data
Everything is stored in this browser's IndexedDB. Export regularly from Settings → Your data: the JSON export is both your backup and how you move your budget to another device.
Both destructive actions there — Import, and "Reset everything" — download a backup of your current data before replacing it, and abort if that download fails. Reset puts the app back to a brand-new install: the three starter posts, the DKK/USD/EUR currency table, this month at zero income, no purchases.
Known limitations
- Data is per-browser. There is no account and no sync. If you clear this browser's site data, or move to a different browser or device, your budget is gone unless you exported it first. The JSON export is not just a nice-to-have backup — it is the only way to move a budget between devices.
- Installing and offline use are unverified. The manifest ships the SVG plus 192×192 and 512×512 PNG icons, and the service worker demonstrably installs, activates and precaches every asset — but nobody has yet confirmed that Chrome offers its "Install app" prompt, or loaded the app with the network actually cut. It works as a regular tab regardless.
Documentation
docs/ARCHITECTURE.md— layers, data flow, and why the non-obvious choices are what they are.docs/PRODUCT.md— who this is for and what must never break.AGENTS.md— the rules for changing this codebase, and the test that catches each one when broken.CLAUDE.mdandGEMINI.mdpoint at it.docs/DECISIONS.md— an index of the choices that shaped the app, and what each was chosen over.docs/TODO.md— what is still queued.docs/specs/— one design document per piece of work, each recording the alternatives that were rejected.docs/plans/— task-by-task implementation plans.