Files
budget2.0/TODO.md
T
2026-09-02 15:53:03 +02:00

9.1 KiB

TODO

Work queued for whoever picks this up next, human or agent. Ordered. Each item says enough to act on without re-deriving the reasoning.

Current state: merged to main. 419 tests passing, bunx tsc --noEmit clean, bun run build succeeds on both the live-fetch and offline paths. Sections 2 and 4 are DONE. Section 3 (redesign) is 1 of 4 sub-projects in. Section 6 (currencies) is DONE. Every screen has now actually been looked at — see section 5.

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.


1. Time-versioned allocation rules — DONE, all six tasks

Spec: docs/superpowers/specs/2026-09-02-versioned-allocation-rules-design.md Plan: docs/superpowers/plans/2026-09-02-versioned-allocation-rules.md Ledger + extracted briefs: .superpowers/sdd/2026-09-02-versioned-allocation-rules/

Task What Commit
1 Post.rules: RuleVersion[] replaces standingRule 68712e7
2 v1 -> v2 migration 15dcf93, 1214d2b
3 setRuleFrom / removeRuleFrom actions de128e3
4 JSON import validation for the rule series 1cb91d3
5 Rule-history editor in Settings e3023d0
6 "Change from here" control in the month view f94e458

Tasks 3-6 were executed by hand rather than dispatched. Every deviation from the briefs is recorded in the ledger's task log with its reasoning; four of them fixed real defects in the brief code (JSX that would not compile, a component that pre-filled the previous post's rule, a number field that could not be cleared, and missing validation that let a NaN allocation into the fold). Worth a read before reviewing the diff.

Not yet done for this feature: nobody has independently reviewed tasks 3-6, and no browser has seen the two new editing surfaces.

2. Baked FX rates — DONE (b4e1933)

So a fresh install can convert a EUR purchase on day one instead of erroring. Built as designed, all three build paths verified (live fetch, unreachable endpoint, SKIP_FX_FETCH=1), and confirmed in the real UI: Settings shows USD 6.449532 / EUR 7.474959 as manual · 2026-09-01 on a fresh dataset. The reasoning below is kept because it is the reasoning, not a plan.

  • build.ts fetches DKK/USD/EUR at build time and embeds them via define; on any network failure it falls back to committed constants rather than failing the build. Fetched rates get source: "api" with the build date; the fallback constants get source: "manual", so Settings never claims a hardcoded number came from an API.
  • They seed a new dataset only. They must NOT act as a fallback whenever a rate is missing — that would undo a deliberate decision: clearing a rate removes the row so MissingRateError surfaces, instead of silently converting money at a number the user never chose.
  • Domain purity forces the shape: createSeedDataset takes the rates as an argument; the env read lives in a new src/store/bakedRates.ts, in the store layer, using the try/catch accessor pattern (see CLAUDE.md gotchas).

3. Visual redesign — sub-project 1 of 4 DONE

The owner's words: "right now it looks a bit boring, simple and ugly. We want sleek, modern, and nice to use." Then, on scoping: both themes with a toggle, a full rethink of every screen, purchases compact and grouped by date, ship a webfont — and "override everything you want for this step."

Spec: docs/superpowers/specs/2026-09-02-design-foundation-design.md

The diagnosis, which should shape the rest: a design system already existed and was never wired up. src/index.css defined the paper/ink/teal palette with a contract document behind it, while the app painted shadcn's default white and near-black. The tokens reached the screen in two places only. Doing the foundation last is how that happened; do not repeat it.

# Sub-project State
1 Design foundation — two-theme tokens, theme toggle, self-hosted Inter + JetBrains Mono, one Section/Stat card, table furniture DONE (bb3c661)
2 Month view date-grouped purchases DONE (a159189); income/totals block and the carry meter's presentation still open
3 Year, Summary, Post detail on the new foundation but not rethought. The 12-month matrix only fits by scrolling — decide whether compact number formatting replaces that
4 Dialogs and fast entry untouched. Native <select> elements are still browser-default, and up/down/archive are bare text buttons

Watch for: the brand teal is --primary, never --accent — shadcn's --accent is a hover background whose paired foreground must move with it. src/cssPairs.test.ts enforces the pairing, and src/ui/cssTokens.test.ts fails if a component names a token index.css does not define. That second guard exists because renaming --rule silently made the carry meter paint nothing: an undefined custom property inside linear-gradient() invalidates the whole declaration without warning.

4. Smaller items — ALL DONE

All six, b9be882 / 71e8fc9 / f50c65b. Three were worse than recorded:

  • confirmImport now guards its write, and a source-level check covers the whole UI (b9be882).
  • The PNG icons were generatable here — Chrome is on PATH and rasterizes the SVG exactly. scripts/make-icons.ts produces them; they are committed, precached, and pixel-verified against the source (f50c65b).
  • Split values now round by what they MEAN — money in "fixed" mode, a percentage in "percent" mode. Narrower than the note claimed: round2 was always right for Rule.percent, which is not money (b9be882).
  • EMPTY_FIGURES is frozen (b9be882).
  • The ODS export was not "fine at personal-budget scale": 30 posts over 10 years took 7983 ms synchronously on the main thread. Now 135 ms, with byte-identical output (71e8fc9).
  • The dev manifest did not merely hardcode scope: "/" — it was never served at all; the catch-all answered with the HTML shell. One definition now feeds the build and the dev server (b9be882).

6. User-defined currencies, and amounts that carry their currency — DONE

Both cdece63. Currency is an open string code and Dataset.currencies is the authority on validity, symbols and decimal places; the purchase total field reads "30$" as USD 30.

The part worth knowing before touching money again: roundMoney(amount, digits) takes a digit count, and its ~29 call sites each resolve digits from the dataset with digitsFor. The argument is required deliberately — every currency the app shipped with uses two decimals, so a default of 2 would leave a missed call site passing every test and silently wrong for the first zero-decimal currency added. src/domain/currencyDigits.test.ts is the only test that can tell a real implementation from one that assumes 2: it drives a JPY-style currency through rounding, both FX directions, both split modes, plan division, the fold and the month view.

Left undone, deliberately:

  • The income field and the split editor's fixed-amount fields do not parse a currency yet — only the purchase total does. Same helper (parseMoneyInput), same three-line pattern; the income field is arguably wrong to make multi-currency at all, since a month has one income.
  • No currency picker convenience. Adding one means typing the code, name, symbol and decimals by hand. A bundled ISO 4217 list would be nicer, and is pure data.
  • Symbol collisions resolve to the first currency defined with that symbol. Fine for one "$" currency; if the owner adds two, typing the code disambiguates and nothing warns them. Tested, documented, not solved.

5. Still needs a human

scripts/screenshot.ts drives headless Chrome, so the list here is much shorter than it was. Seen and confirmed working: the carry meter (it renders, and shows red where a post is overspent), the rule-history editor in Settings, the "change from here" control, the baked FX rates arriving in a fresh dataset, and every route at desktop and mobile widths.

Genuinely outstanding:

  • How it looks and feels. A screenshot proves a layout is not broken. It cannot tell you the design is good, and the container's fonts are not the owner's fonts.
  • Offline behaviour and the install prompt. The service worker itself is no longer unverified: it demonstrably installs, activates, claims the page, precaches all 11 assets, and applies an update cleanly (e6da85e) — before that fix it had never installed once, in any environment, because cache.addAll rejects on a duplicate request. What is still unverified is a real offline load with the network actually cut, and Chrome's install prompt appearing.
  • the generated .ods opening in a real spreadsheet (validated structurally: mimetype first and stored, well-formed XML, numeric cells).
  • keyboard flow in fast entry — Tab traversal, Backspace-to-remove, and focus placement after a row auto-appends. Drivable over CDP in principle; not yet done.
  • GitHub Actions has never been executed.