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) <noreply@anthropic.com>
This commit is contained in:
1 parent
61e7093c36
commit
0fcfdd27cd
7 files changed
+110
-12
No files matched your search
+13
-1
@@ -18,7 +18,7 @@ record.
|
||||
no household mode. → [`PRODUCT.md`](PRODUCT.md)
|
||||
- **Schema migrations are mandatory, not optional.** `schemaVersion` plus
|
||||
ordered migration functions applied on load, because IndexedDB holds the
|
||||
user's only copy of their financial data. Now at version 3.
|
||||
user's only copy of their financial data. Now at version 4.
|
||||
→ [`specs/2026-09-01-budget-app-design.md`](specs/2026-09-01-budget-app-design.md)
|
||||
- **A destructive write downloads a backup first.** Import and reset both export
|
||||
before replacing, and abort if the export fails. → commit `ac3d18a`
|
||||
@@ -129,6 +129,18 @@ record.
|
||||
chose. On a network failure the build falls back to committed constants marked
|
||||
`source: "manual"`, so Settings never claims a hardcoded number came from an
|
||||
API. → commit `b4e1933`
|
||||
- **The rate service must be a host that answers directly, not one that
|
||||
redirects.** `api.frankfurter.app` began 301-ing to `api.frankfurter.dev/v1`,
|
||||
and a redirect response has to carry `Access-Control-Allow-Origin` itself for
|
||||
a browser to follow it cross-origin — that 301 carries none, so "Fetch rates
|
||||
now" died with only a CORS error to go on. The build-time fetch never noticed,
|
||||
because a server-side fetch follows redirects freely. → commit history, and
|
||||
`src/store/fxApi.ts`
|
||||
- **The v3 -> v4 migration DROPS the stale URL rather than rewriting it** to the
|
||||
new endpoint, so a dataset that stored it 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 themselves is left strictly alone.
|
||||
→ `src/store/migrations.ts`
|
||||
- **ODS export is generated client-side with `fflate`.** No server exists to
|
||||
generate it. → [`specs/2026-09-01-budget-app-design.md`](specs/2026-09-01-budget-app-design.md)
|
||||
- **Base path is a build-time variable, read through a `try/catch` accessor.**
|
||||
|
||||
+5
-4
@@ -8,13 +8,14 @@ decisions behind it are in `docs/specs/` — one spec per sub-project,
|
||||
each recording the alternatives that were rejected. Rules that outlived their
|
||||
task moved to `AGENTS.md`.
|
||||
|
||||
**Current state:** merged to `main`. 452 tests passing,
|
||||
**Current state:** merged to `main`. 457 tests passing,
|
||||
`bunx tsc --noEmit` clean, `bun run build` succeeds on both the live-fetch and
|
||||
offline paths.
|
||||
|
||||
**Schema is at version 3.** v1 -> v2 made allocation rules a dated series;
|
||||
v2 -> v3 moved currencies into the dataset. Both migrations are
|
||||
behaviour-preserving by construction and tested as such.
|
||||
**Schema is at version 4.** v1 -> v2 made allocation rules a dated series;
|
||||
v2 -> v3 moved currencies into the dataset; v3 -> v4 drops a stored rate-service
|
||||
URL that can no longer work. All three are behaviour-preserving by construction
|
||||
— not one changes a stored figure — and are tested as such.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user