Lucas Winther 1281362388 fix(recurring): never offer a coming-up row that can't be confirmed coherently
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.
2026-09-04 09:15:11 +02:00

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.md and GEMINI.md point 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.
S
Description
No description provided
Readme
1.7 MiB
Languages
TypeScript 98.3%
CSS 1.4%
HTML 0.2%