Lucas WintherandClaude Opus 5 0fcfdd27cd fix(fx): the rate service moved, and a browser cannot follow its redirect
"Fetch rates now" failed on the deployed site with nothing but a CORS
error. api.frankfurter.app now answers every request with a 301 to
api.frankfurter.dev/v1, and a redirect response must itself carry
Access-Control-Allow-Origin for a browser to follow it cross-origin. That
301 carries no CORS headers at all, so the request died before the
redirect could be taken.

Measured rather than guessed, with an Origin header set:

  api.frankfurter.app  ->  301, no access-control-* headers
  api.frankfurter.dev  ->  200, access-control-allow-origin: *

The response shape is unchanged, so parseFxResponse needed no edit.

Nothing was wrong at build time, which is why this went unnoticed: a
server-side fetch follows the 301 freely, so the baked rates were always
correct. Only the browser path was broken.

Two changes, because a new default alone fixes only some datasets:

- DEFAULT_FX_API_URL points at .dev/v1, with a test that fails if the
  redirecting host ever comes back.
- A 3 -> 4 migration drops settings.fxApiUrl when it holds exactly the
  dead default, which is the case for anyone who edited that field. It is
  DROPPED rather than rewritten, so the dataset follows whatever the
  current default is and the next move of the service needs no second
  migration for the same user; a URL the owner chose is left strictly
  alone. No stored figure changes, so it cannot alter a number in the
  budget.

Verified by clicking the button in a real browser: "Updated 2 rate(s)",
USD 6.456195 and EUR 7.474959 both stored with source "api" — and the USD
figure differs from the baked constant, so it is a genuine live fetch.

Two migration tests asserted the chain ends at 3; they now assert
SCHEMA_VERSION, since what they mean is "all the way through".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-02 22:20:43 +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%