Lucas WintherandClaude Opus 5 f20a220dae Merge: recurring costs — bills that repeat, expected against actual
Rent every month, a subscription every 28 days, and a phone bill every 30 days
whose clock RESTARTS when its data cap is hit early. A recurring cost projects
EXPECTED charges forward; confirming one writes a real Purchase. The fold now
carries two balances per post per month: `remaining`, untouched by this feature,
and `projected`, which subtracts commitments that have not been paid yet.

The data-cap behaviour is not a feature of its own. Under `lastCharge`
anchoring the next occurrence is measured from the confirming purchase's OWN
date, so confirming a slot early rebases the series — and two-stage
confirmation already records that date. The domain never learns what a data cap
is. That collapse is most of why this design is small: three recurrence kinds
and one anchoring field, not a list of special cases.

Two consequences worth not re-litigating. `everyNMonths` carries no day,
because rent occurs IN September and IsoDate already permits "YYYY-MM" — which
keeps day arithmetic out of the commonest case. And `amount` is a single
mutable Money with no dated series, safe ONLY because confirmed purchases hold
what was actually paid; under projection-only it would destroy history.

Occurrences are computed, never stored. `upToMonth` comes from the caller, so
src/domain/ stays clock-free.

Schema 7. 651 tests, tsc clean, build green.

Four defects were found by review rather than by the suite, each reachable and
each invisible to a passing test:

- Creating a cost then switching its unit stored a month-only startDate on a
  day-granular kind, which threw out of the fold and took every money route
  down. Two clicks. The validators checked date SHAPE where the UI checked
  VALIDITY, so a calendar-impossible "2026-09-31" survived a second round.
- Confirming a future occurrence recorded today against a slot more than one
  cycle out, which walks the series backwards. Same crash, introduced by the
  change that closed a spec gap.
- Editing a confirmed purchase's date in the ordinary purchase dialog — an act
  with nothing to do with recurring costs — could do the same thing, and the
  dialog swallowed the refusal so a save appeared to silently do nothing.
- Import accepted a splitMode the fold would silently read as "fixed", turning
  intended percentages into absolute amounts.

AGENTS.md §3 held throughout: negatives, percentages past 100 and non-summing
splits are all still accepted, deliberately.

Design: docs/specs/2026-09-03-recurring-costs-design.md
Plan:   docs/plans/2026-09-03-recurring-costs.md

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-04 23:57:30 +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%