diff --git a/README.md b/README.md index 6915d59..10f95fc 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ your browser — there is no server and no account. ```bash bun install -bun --hot src/index.ts # dev server with hot reload +bun run dev # dev server with hot reload, on :3000 bun test # run the test suite bun run build # production build into dist/ ``` @@ -37,8 +37,12 @@ the source in the repository settings. 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. +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 @@ -46,12 +50,20 @@ your current data first. 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.webmanifest` currently 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. +- **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 -- Design: `docs/specs/2026-09-01-budget-app-design.md` -- Implementation plan: `docs/plans/2026-09-01-budget-app.md` +- `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/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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 58a02f0..784e266 100755 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -126,6 +126,10 @@ Adding a "missing" guard here is a regression, not a fix: ## Gotchas paid for with real bugs +`AGENTS.md` §2 carries the enforceable one-line form of these, with the test +that fails when each reappears. What follows is the *mechanism* behind them, +which is the part worth reading once. + - **Never read `event.target.value` inside a `mutate()` callback.** `mutate` defers behind the queue and a write; React resets a controlled input's DOM value long before the callback runs, so you read the *old* value and commit @@ -175,7 +179,11 @@ timing that happy-dom does not reproduce. ## Where the reasoning lives +- `AGENTS.md` — the rules, in enforceable one-line form, and which test fails + when one is broken. This file holds the reasoning; that one holds the rule. - `docs/PRODUCT.md` — durable product truth: who it is for, what must never break. -- `docs/TODO.md` — queued work, with the reasoning behind each decision already made. -- `docs/specs/` — design decisions and the alternatives rejected. +- `docs/TODO.md` — what is *still* queued, each item carrying the reasoning already + settled. Finished work is not kept there; it is in the commit messages. +- `docs/specs/` — design decisions and the alternatives rejected, one document + per piece of work. - `docs/plans/` — task-by-task implementation plans. diff --git a/docs/TODO.md b/docs/TODO.md index 50d3e94..16c836a 100755 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,13 +1,16 @@ # 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. +Work still queued, for whoever picks it up next, human or agent. Ordered. Each +item says enough to act on without re-deriving the reasoning. -**Current state:** merged to `main`. 443 tests passing, `bunx tsc --noEmit` -clean, `bun run build` succeeds on both the live-fetch and offline paths. -Sections 2, 4 and 7 are DONE. Section 3 (redesign) is 2 of 4 sub-projects in. -Section 6 (currencies) is DONE. Every screen has now actually been looked at — -see section 5. +Finished work is not listed here. It is in the commit messages, and the +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:** 443 tests passing, `bunx tsc --noEmit` clean, `bun run build` +succeeds on both the live-fetch and offline paths. The month view redesign and +the reset button are on branch `month-view-and-reset`, not yet merged to `main`. **Schema is at version 3.** v1 -> v2 made allocation rules a dated series; v2 -> v3 moved currencies into the dataset. Both migrations are @@ -15,79 +18,31 @@ behaviour-preserving by construction and tested as such. --- -## 1. ~~Time-versioned allocation rules~~ — DONE, all six tasks +## 1. Visual redesign, sub-project 3 — Year, Summary, Post detail -**Spec:** `docs/specs/2026-09-02-versioned-allocation-rules-design.md` -**Plan:** `docs/plans/2026-09-02-versioned-allocation-rules.md` -**Ledger + extracted briefs:** `.superpowers/sdd/2026-09-02-versioned-allocation-rules/` +The owner's brief for the whole redesign: *"right now it looks a bit boring, +simple and ugly. We want sleek, modern, and nice to use."* 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."* -| 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` | +Sub-projects 1 (design foundation, `bb3c661`) and 2 (month view, `7b71811`) are +done, each with a spec in `docs/specs/`. These three screens are on +the new foundation but have not been rethought. The open question is the +12-month matrix: it only fits by scrolling, and compact number formatting may be +what replaces that rather than a scroll container. -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. +**The diagnosis that shaped the decomposition, which still applies:** 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 — +extend the foundation rather than styling a screen locally. -Not yet done for this feature: nobody has independently reviewed tasks 3-6, and -no browser has seen the two new editing surfaces. +**Three things sub-project 2 settled that these screens should inherit rather +than re-decide** (spec: `2026-09-02-month-view-design.md`): -## 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 AGENTS.md gotchas). - -## 3. Visual redesign — sub-projects 1 and 2 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/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 | DONE. Date-grouped purchases (`a159189`), then the header, the post rows and the carry meter | -| 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 `` elements are still browser-default, and +`up` / `down` / `archive` in Settings are bare text buttons. Covers the purchase +dialog, the split editor, the plan editor and bulk entry. -## 4. ~~Smaller items~~ — ALL DONE +There is already a spec for fast-entry *behaviour* — +`2026-09-02-fast-entry-refinements-design.md`. Read it before restyling, so the +visual work does not fight it. -All six, `b9be882` / `71e8fc9` / `f50c65b`. Three were worse than recorded: +## 3. Currency work deliberately left undone -- `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: +All three were decided against at the time, not forgotten. Reopen only with a +reason. - **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. + currency** — 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 a currency 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. -## 7. ~~Reset to a fresh install~~ — DONE +## 4. Nobody has independently reviewed the rule-history editing surfaces -Settings > Your data > "Reset everything…". The owner asked for a reset that -includes the currencies and the seed categories, and chose fresh-install state -over an empty dataset: the three starter posts, the DKK/USD/EUR table, the -baked rates, this month at zero income, no purchases. +Tasks 3-6 of the versioned-allocation-rules work — `setRuleFrom` / +`removeRuleFrom`, the JSON import validation, the Settings rule-history editor, +and the month view's "change from here" — were executed by hand rather than +dispatched, and no second pair of eyes has been over the diff. -- `store.reset()` calls the same `createSeedDataset(currentMonth, seedRates)` - that `load()` uses on a first run, through the same write queue. Seeding - lives in the store, not the UI, so "reset" and "a brand-new browser" cannot - drift apart. -- The confirm panel reuses the import flow's shape and its **order of - operations**: the JSON backup downloads BEFORE the destructive write, and a - backup that fails aborts the reset. This is the user's only copy. -- Verified in Chrome by clicking through and reading IndexedDB back: 3 seed - posts, DKK/USD/EUR, both baked manual rates, 0 purchases, one month at zero - income. +Worth reading first: the task log in +`.superpowers/sdd/2026-09-02-versioned-allocation-rules/` records every deviation +from the original briefs 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. + +Both surfaces have since been seen working in a browser. What is missing is +review, not verification. ## 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. +`scripts/screenshot.ts` drives headless Chrome, so this list is much shorter +than it was. Already seen and confirmed working: the carry meter (including red +where a post is overspent), the rule-history editor, "change from here", the +baked FX rates arriving in a fresh dataset, the reset writing a fresh dataset to +IndexedDB, and every route at desktop and mobile widths in both themes. 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). -- **the pre-destructive backup actually landing as a file.** Both the import - and the reset flow download a JSON backup before replacing the dataset, and - the code path demonstrably runs (the reset proceeds only when it returns - success). Whether headless Chrome wrote the file is not observable here, so - nobody has yet confirmed the backup is openable — only that it was issued. -- **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. +- **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`). What is + still unverified is a real offline load with the network actually cut, and + Chrome's install prompt appearing. +- **The pre-destructive backup actually landing as a file.** Import and reset + both download a JSON backup before replacing the dataset, and only proceed + when that reports success — but headless Chrome's download is not observable + here, so the backup is *issued*, never confirmed openable. +- **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. diff --git a/scripts/screenshot.ts b/scripts/screenshot.ts index 0ba630a..775a643 100755 --- a/scripts/screenshot.ts +++ b/scripts/screenshot.ts @@ -7,11 +7,12 @@ * [--eval=|--eval-file=] [--dark] [--reload] * [--profile=] [--eval-after-file=] * - * Why this exists: docs/ARCHITECTURE.md and docs/TODO.md both say nothing here can run a - * browser, so appearance has never been verified. Chrome IS available, but its - * plain `--screenshot` flag fires on the load event — before this app's async - * IndexedDB read resolves, which captures an empty page. Driving CDP directly - * lets us wait, act, and read console errors. + * Why this exists: the docs used to say nothing here could run a browser, so + * appearance went unverified for a long time. Chrome IS available — but its + * plain `--screenshot` flag fires on the load event, before this app's async + * IndexedDB read resolves, so it captures an empty page, which is how that + * belief survived. Driving CDP directly lets us wait, act, and read console + * errors. * * This does NOT replace a human: it cannot judge feel, and it renders with one * font stack on one platform. It does prove a layout exists and is not broken.