b4e193376166b942d3b9dd47d38a3295bb2d4954
TODO section 2, designed and approved, now built. A brand-new dataset arrives with DKK/USD/EUR rates instead of erroring on the first EUR purchase. build.ts fetches at build time and embeds the result via `define`. A network failure does NOT fail the build — it bakes committed constants instead, and an incomplete response is treated as a failure too, since one unconvertible currency is the problem this solves. Fetched rates carry source "api" and the BUILD date, which is why parseFxResponse now takes an explicit updatedAt; the constants carry source "manual" and their own date, so Settings never claims a hardcoded number came from a rate service. SKIP_FX_FETCH=1 and FX_API_URL are there for offline, CI and reproducible builds. They seed a NEW dataset and nothing else. They are NOT a fallback for a missing rate: clearing a rate removes its row so MissingRateError surfaces, rather than silently converting money at a number the user never chose. A test asserts a cleared rate still throws, and loading an existing dataset never touches its rates. Domain purity forced the shape. createSeedDataset takes the rates as an argument (and copies the rows, so shared constants cannot be edited out from under the caller); the env read lives in the new src/store/bakedRates.ts behind the try/catch accessor, because an unset BUN_PUBLIC_* leaves a bare process.env in the browser bundle; and createSnapshotStore takes seed rates as a parameter so it stays free of the ambient environment, with src/store/index.ts wiring it. Verified all three build paths: live fetch bakes api-sourced rates with today's date, an unreachable endpoint and SKIP_FX_FETCH both bake manual constants and still build, and the payload is present in the bundle in each case. Verified at runtime that a fresh install converts EUR 100 -> 747.50 DKK and USD 100 -> 644.95 DKK with the variable both set and unset. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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 --hot src/index.ts # dev server with hot reload
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. Import replaces everything and downloads a backup of your current data first.
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.
- The install prompt is incomplete.
manifest.webmanifestcurrently only ships an SVG icon. Chrome requires PNG icons at 192×192 and 512×512 to offer the "Install app" prompt; until those are added, the app works fine as a regular tab but won't be installable on Chrome/Chromium.
Documentation
- Design:
docs/superpowers/specs/2026-09-01-budget-app-design.md - Implementation plan:
docs/superpowers/plans/2026-09-01-budget-app.md
Languages
TypeScript
98.3%
CSS
1.4%
HTML
0.2%