docs(qa): archive the 2026-07-10 recon+fix cycle; release v0.42.0

QA_PLAN: findings archived (only QA-B0-04 stays open as the @ts-nocheck
burn-down program, 38 -> 29 with the B0 counter); IMP-CODE-04/-06/-07
flipped to Shipped (incl. the doingmypart.jpg correction — kept, it's
referenced from HISTORY.md markdown; lesson added to the B0 dead-code
recon); B0 gained the four standing recon steps (deps freshness,
dead-code w/ markdown-content rule, ts-nocheck counter, version
coherence); B1 WebAuthn now automated via virtual authenticator; B13
gained error-shape + orphan-endpoint sweeps; Appendix C completed
(/admin/about, /admin/roadmap, /status redirect, ICS feed row);
Appendix E marked AUTOMATED by the census project; Cycle Log rows
2·recon + 2·fix; stale server.js/.js refs -> .cts.

(Reconstructed: the original recon-day plan edits were uncommitted
working-tree changes lost to a parallel session checkout — written back
here in final post-fix state.)

HISTORY.md: v0.42.0 entry (dependency modernization, census, knip
blocking, ts-nocheck starter, deploy checklist); package.json 0.42.0
(versionSync guard green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-10 22:31:18 -05:00
parent 787d3cf694
commit 044bdbe1f1
3 changed files with 246 additions and 169 deletions

View File

@ -1,5 +1,22 @@
# Bill Tracker — Changelog
## v0.42.0
### 📦 Dependency modernization — every major current, zero known vulns
- **[Security] Both high-severity vulnerabilities cleared and CI can now see the next one.** `nodemailer` 8 → 9 (raw-option file-read/SSRF advisory; the vulnerable option was never used) and `xlsx` moved off the abandoned npm line to the official SheetJS dist 0.20.3 (prototype pollution + ReDoS, unfixable on npm; API-identical for our 8 call sites — note the lockfile now pins `cdn.sheetjs.com`, so builds need egress to it). The CI audit gate was raised from `--audit-level=critical` to **`high`** — the class of "known high vuln passes CI silently" (was QA-B0-02) is closed, and a new **weekly scheduled `deps-audit` workflow** fails red on any high+ prod vuln and prints `npm outdated`. `renovate.json` is in the repo ready for the Forgejo Renovate app. `npm audit --omit=dev`: **0 vulnerabilities**.
- **[Build] Vite 5 → 8 (rolldown)** — production build **6.5s → 0.65s**. Three breakages the prod-smoke gate caught and fixed: object-form `manualChunks` → function form; the newer `send` rejects bare absolute paths, so the SPA fallback `sendFile` and both download routes (admin backup, user-db export) use the `root`-option form; and rolldown minifies inline HTML scripts, so the pre-paint theme script moved to `client/public/theme-init.js`, keeping the CSP at `script-src 'self'` with no inline-hash maintenance.
- **[UI] Tailwind 3 → 4** via `@tailwindcss/vite`: config inlined into `client/index.css` as CSS-first `@theme` (config file deleted — v4 auto-detects content), 33 templates' class renames applied by the official codemod, `tailwind-merge` 3. Gated on the visual-regression baselines: the only pixel diffs were a ~2px preflight shift + the version string; re-baselined after review. `postcss.config.js` + `autoprefixer`/`postcss` deps removed (v4 owns the pipeline).
- **[Auth] openid-client 5 → 6**: the OIDC service's four library touchpoints rewritten to the functional API (`discovery`/`buildAuthorizationUrl`/`authorizationCodeGrant` with PKCE/state/nonce checks) — same validation surface, now enforced by the library's single grant call. OIDC smoke suite 44/44.
- **Also:** react-router-dom 7, eslint 10 stack (its new `no-useless-assignment` caught three real dead assignments), bcryptjs 3, sonner 2, lucide-react 1.x, typescript-eslint/vitest/Radix/react-query and the rest of the semver sweep. **Deferred with reason:** TypeScript 7 typechecks clean but `typescript-eslint` (peer `<6.1.0`) can't parse under it — pinned at 6.x, capped in renovate.json, revisit when support ships.
### 🧹 QA — durable guards from the blind-spot recon
- **[Tests] Automated per-page control census** (`e2e/control-census.spec.js`, own `census` project ordered before the mutating probe specs): every visible interactive control on all 15 authed pages is snapshotted as role + accessible name, so a new/removed/renamed control is a reviewable diff each cycle. Replaces QA_PLAN Appendix E's never-filled manual tables — the gap that let WebAuthn ship UI-less for four cycles. (was IMP-CODE-07)
- **[Code health] knip is clean and now a BLOCKING CI step.** Deleted the broken legacy root test harnesses (`test-functional.js` imported an uninstalled package), dropped dead exports, moved the two runtime seed JSONs (7 MB) out of `docs/` into `db/data/` next to their loader (fresh-DB seed verified, 5000+5000 rows). One near-miss worth recording: `client/public/img/doingmypart.jpg` greps as unused in _code_ but is referenced from HISTORY.md markdown served by the release-notes renderer — kept; dead-asset sweeps must include rendered markdown content. (was IMP-CODE-04/-06)
- **[Types] `@ts-nocheck` burn-down started: 38 → 29 files.** The nine smallest routes are now fully type-checked (fallout: four one-line fixes). The B0 recon counter ratchets the number down each cycle. (QA-B0-04, in progress)
- **[Ops] `docs/DEPLOY_CHECKLIST.md`** — the production-only config items (TOKEN_ENCRYPTION_KEY with its `v2:`→`e2:` migration, WEBAUTHN_RP_ID, simplefin_debug_logging off, SheetJS egress, Node alignment) now live in the repo instead of session notes.
## v0.41.1
### 🔐 Passkeys / security keys — WebAuthn finally reachable

View File

@ -1,14 +1,16 @@
# BillTracker — Master QA Plan (living document)
**Version target:** v0.41.x · **Executor:** Claude (active) · **Last updated:** 2026-07-05
**Cycle 1: COMPLETE ✅** — all 18 batches (B0→B16 + B-UI) run; **15 findings fixed**, verified & archived (3× S2); automated re-run of existing batches clean (0 new); the added **B16** (migrations/secrets/deploy) surfaced + fixed 1 (version-check opt-out). Guard suite green. External-infra items (live TOTP/WebAuthn/OIDC, SMTP delivery, cross-browser, PWA-offline, load, container build) carried to Cycle 2 as non-blocking.
**Version target:** v0.42.x · **Executor:** Claude (active) · **Last updated:** 2026-07-10
**Cycle 1: COMPLETE ✅** — all 18 batches (B0→B16 + B-UI) run; **15 findings fixed**, verified & archived (3× S2); automated re-run of existing batches clean (0 new); the added **B16** (migrations/secrets/deploy) surfaced + fixed 1 (version-check opt-out). Guard suite green. External-infra items (live TOTP/OIDC, SMTP delivery, cross-browser, PWA-offline, load, container build) carried to Cycle 2 as non-blocking.
**2026-07-10 blind-spot recon + fix pass: COMPLETE ✅** — a dedicated deps/dead-code/coverage audit logged 6 findings (1× S2) + 4 IMP; **all fixed, verified & archived** to `HISTORY.md` v0.41.1/v0.42.0 except the `@ts-nocheck` burn-down (QA-B0-04, a tracked program). Highlights: the WebAuthn ghost feature now **ships end to end** (passkey UI + CDP virtual-authenticator e2e — no longer "needs a human"); every route emits errors through the enforced throw-pattern; every dependency major is current (0 vulns, audit gate raised to high, weekly deps-audit workflow + renovate.json); knip is clean and blocking; Appendix E is automated (`census` e2e project). See Cycle Log rows 2·recon / 2·fix.
This is a **living, operational** QA document, not a static spec. Claude runs it,
in **batches**, actively hunting for bugs/errors/rough edges, **fixing** them, and
**archiving** each fixed finding to `HISTORY.md`. Update this document whenever a
better approach, a new risk area, or a missed surface is discovered.
> **The prime directive:** don't just confirm the happy path — try to *break*
> **The prime directive:** don't just confirm the happy path — try to _break_
> the product. Every batch should end with the tree green, the Findings Log
> up to date, and any fixes archived to `HISTORY.md`.
@ -30,7 +32,7 @@ better approach, a new risk area, or a missed surface is discovered.
## 0. Execution model — find, then fix, then repeat
**Separate finding from fixing.** During a QA pass we *hunt and log* — we do **not**
**Separate finding from fixing.** During a QA pass we _hunt and log_ — we do **not**
fix as we go (except show-stoppers, see below). Only after the whole plan has run
do we enter a dedicated **fix phase** and fix **every** logged finding. Then we run
the **entire** QA plan again from the top. Repeat until a full pass finds **zero**
@ -60,7 +62,7 @@ errors. Two nested loops:
mark batch status in §1 → next batch. (No fixing here.)
```
**Show-stopper exception.** A *show-stopper* is a finding that **blocks continued
**Show-stopper exception.** A _show-stopper_ is a finding that **blocks continued
QA** — the app won't boot, you can't log in, or a page crashes so hard you can't
test the rest of it. Only these get fixed immediately (mid-pass), because you
can't proceed otherwise. Log it, fix it, verify, and note it was a mid-pass fix;
@ -68,28 +70,30 @@ then continue the find pass. **Everything else is logged and left for Phase 2**
no matter how tempting or trivial.
**Discipline (for best results)**
- **Phase 1 is log-only.** Resist fixing. A clean, complete inventory of findings beats a scattered fix-as-you-go pass and produces better batching.
- Keep each find batch tight and focused — one batch per session — so probing stays thorough.
- **Phase 2 fixes everything**, not just S1/S2. Root-cause over surface patch; add/extend a test in `tests/` or `client/**/*.test.*` for every logic bug so it can't silently return.
- Never leave the repo red at the end of Phase 3 — `npm run ci` must be green before archiving.
- Touch product behavior? Run the `/verify` skill on the affected flow before archiving.
- **The exit is empirical:** you're done only when an entire find pass (B0→B15) turns up zero new findings — not when you *think* it's clean. Log the cycle result in the [Cycle Log](#11-qa-cycle-log) each time.
- **The exit is empirical:** you're done only when an entire find pass (B0→B15) turns up zero new findings — not when you _think_ it's clean. Log the cycle result in the [Cycle Log](#11-qa-cycle-log) each time.
- Improve THIS plan whenever a pass reveals a missed surface, a better repro, or a batch that should be reordered/split.
**Improvement lens (not just bug-hunting).** QA here is also about making the product
*better*, not only *correct*. On every batch, in addition to logging bugs, actively
_better_, not only _correct_. On every batch, in addition to logging bugs, actively
look through three improvement lenses and log what you find as **IMP** items in the
[Improvement Backlog (§2.1)](#21-improvement-backlog):
- **Code health & consolidation** — duplication to DRY up, dead code to delete,
overlapping modules to merge, oversized files to split, one canonical path per
concern. *Consolidate only where it genuinely reduces surface area and is
behavior-preserving.* (Dedicated pass: **B17**.)
concern. _Consolidate only where it genuinely reduces surface area and is
behavior-preserving._ (Dedicated pass: **B17**.)
- **User experience** — friction in core flows, unclear states (empty/loading/error),
weak feedback/affordances, inconsistent patterns, mobile parity. (Dedicated pass: **B18**.)
- **Information architecture / menus** — features that are buried or only reachable by
URL, actions that belong in a menu (nav, overflow, context, settings groupings), and
groupings that would make the app more discoverable. *Put things where a user would
look for them.* (Dedicated pass: **B18**.)
groupings that would make the app more discoverable. _Put things where a user would
look for them._ (Dedicated pass: **B18**.)
IMP items are **proposals**, not silent changes: log the candidate with a concrete
recommendation, agree the direction, then implement behind a test. They **don't block
@ -106,7 +110,7 @@ before cross-cutting; regression last). Update **Status** and **Findings** every
**Status key:** ⬜ Not started · 🔄 In progress · ✅ Done (green, findings archived) · 🔁 Needs recheck
| # | Batch | Primary surface | Data state | Status | Open / Fixed |
|---|-------|-----------------|-----------|--------|--------------|
| ---- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | ------ | ------------ |
| B0 | Baseline, tooling & **coverage recon** | `npm run ci`/`check`, app boots, console clean, **re-scan routes/pages/API vs plan & update it**, **control census** | any | ✅ | 0 / 1 |
| B-UI | **Design-system primitives** | each `client/components/ui/*` × state matrix (default/hover/focus/active/disabled/loading/error/read-only) × light/dark × keyboard | any | ✅ | 0 / 0 |
| B1 | Auth & authorization | login (pw/OIDC/TOTP/WebAuthn), roles, single-user, CSRF, data isolation | multi + single user | ✅ | 0 / 0 |
@ -141,10 +145,10 @@ human** and were **not** exercised — they are **non-blocking** for Cycle 1 sig
carried to Cycle 2:
- **B1** — live TOTP enrollment, WebAuthn/passkeys (browser/OS prompts), OIDC SSO round-trip. (Password login, roles, CSRF, data-isolation, admin authz **are** covered.)
- **B10** — real SMTP *delivery* (push delivery + email-HTML building/escaping **are** covered by `tests/notificationDelivery.test.js`).
- **B10** — real SMTP _delivery_ (push delivery + email-HTML building/escaping **are** covered by `tests/notificationDelivery.test.js`).
- **B11** — backup create/restore on a scratch instance (authorization + last-admin guards **are** covered).
- **B14** — Firefox/Safari cross-browser, PWA install/offline, and large/stress load+perf. (axe a11y on 8 pages, XSS/escaping, and prod-bundle perf **are** covered.)
- **B9** — spreadsheet/CSV import *from real files* end-to-end (money-unit handling + SQLite export→import round-trip **are** covered by tests).
- **B9** — spreadsheet/CSV import _from real files_ end-to-end (money-unit handling + SQLite export→import round-trip **are** covered by tests).
### 1.1 QA Cycle Log
@ -153,12 +157,14 @@ cycle is only "clean" when its **find pass logged zero findings**. Keep going
until you get a clean cycle.
| Cycle | Started | Build / commit | Findings logged | Fixed / archived | Result |
|-------|---------|----------------|-----------------|------------------|--------|
| ---------------- | ---------- | -------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | 2026-07-02 | `bdbf231`→`5ffe2db` (dev) | 14 | **14 → all fixed, verified & archived** (3× S2 incl. broken "Send test push", email XSS, reconciliation family, seed 100× cents) | 🔁 Phase 2 complete — 0 open. Every batch B0→B15 (+B-UI) run; 16 QA commits; guard suite green. |
| 1·re-run | 2026-07-02 | `5ffe2db` (dev) | **0 new** | — | ✅ **Automated re-run clean.** CI (server 109 + client 34, build), UI E2E 27, probe 16 (authz 403, Tracker↔Summary↔Analytics reconcile exactly, seed guard, a11y 8/8), prod-smoke PASS. **All 17 batches ✅ for automatable scope; external-infra residuals listed below are non-blocking and carried to Cycle 2.** |
| 1·simplefin-live | 2026-07-03 | `5ffe2db` (dev) vs prod DB | **1** (QA-B5-04) | **1 → fixed, verified & archived** | 🔁 Probed a **copy of the live SimpleFIN DB** (19 MB, v1.06: 3 users, 44 bills, 1,159 txns, 19 accounts, active SimpleFIN source). Integrity checks: dedup (1159/1159 distinct), money=integer cents, no double-match, pending have provider ids, no orphan-account txns — all pass **except** 3 matched txns with NULL bill → QA-B5-04 (retention GC + `ON DELETE SET NULL`). Fixed in `cleanupService`; healing verified on a DB copy (3→0, 0 txns lost). **Also ran a real end-to-end sync** (`syncDataSource`, the Sync-button path) against the live connection off a working copy: token decrypted via db-key fallback (no env key), bridge fetch OK (2.2s), 18 accounts upserted, 145 fetched txns **skipped not duplicated**, 0 new, 1159→1159 distinct — **dedup/upsert idempotency proven on the real connection.** |
| 1·data-overhaul | 2026-07-03 | `78ad63d`→`d53a64b` (dev) | **0 defects** (feature work) | — | ✅ **Data page overhaul, 7 batches, all green.** Two-pane goal layout + 5-state connection hero + `?section=` deep-linking + nav badges/health dots + lazy panes (B0B2); **new features** OFX/QFX import (B3), richer export JSON + date-range (B4), "Erase my data" danger zone (B5). Verified: server 139 tests, client 46, build; **axe on `/data` zero critical/serious** (added to `a11y.authed`); full probe suite 17/17. Section internals + the 7 SimpleFIN buttons untouched. |
| 2·recon | 2026-07-10 | `d9545d6` (dev) | **6** (1× S2, 4× S3, 1× S4) + 4 IMP | — (find-only) | 🔁 **Blind-spot recon** (deps/dead-code/coverage audit, no UI batches). Logged: high-vuln deps invisible to the critical-only CI gate (QA-B0-02) · package.json 0.40.0 vs HISTORY v0.41.0 drift (QA-B0-03) · **WebAuthn ghost feature** — full backend + api wrappers, zero UI, and a login fall-through that 500-locked any user who enabled it (QA-B1-01, S2) · 38 `@ts-nocheck` files ⇒ vacuous server typecheck (QA-B0-04) · raw `err.message` 5xx leaks (QA-B13-02) · dead duplicate admin routes incl. an unauthenticated `/api/auth/has-users` (QA-B17-01). Verified-clean along the way: SQL building (whitelists+params), no `dangerouslySetInnerHTML`/`as any` in client, ICS feed token-gated, git hygiene, all 29 route files mounted. |
| 2·fix | 2026-07-10 | `3c82c30`→HEAD (dev), tags `v0.41.1` | all 6 + 4 IMP fixed | **9 archived** (v0.41.1 + v0.42.0); QA-B0-04 continues as a program (38→29) | ✅ **Fix pass complete, ~30 commits.** Phases: bug fixes (lockout, 401-mid-session redirect, version sync + guard test, 20 err.message leak sites) → **standards unification: all 29 routes on the throw-pattern, eslint-enforced** (no-console + errorFormatter ban; exceptions documented in CODE_STANDARDS) → **passkey UI shipped** (+ virtual-authenticator e2e, WEBAUTHN_RP_ID deploy guard) → tag v0.41.1 → **full dependency modernization** (vite 8 · tailwind 4 · router 7 · openid-client 6 · eslint 10 · nodemailer 9 · SheetJS dist; 0 vulns; audit gate → high; prod-smoke caught & fixed 3 upgrade breakages: send absolute-path, CSP inline script, manualChunks; TS7 deferred on typescript-eslint peer cap) → dead-code sweep (knip clean + blocking; `doingmypart.jpg` kept after user catch — markdown-content lesson added to B0) → @ts-nocheck starter batch (38→29) → **automated control census** (own `census` project) → deploy checklist + renovate.json + weekly deps-audit workflow. Gates at close: ci green (252 server / 52 client), e2e 27, census+probe 33/33 ×3, OIDC smoke 44/44, PROD SMOKE PASS. |
**Result key:** 🔄 in progress · 🔁 findings fixed, re-run required · ✅ clean (zero findings — QA complete)
@ -176,8 +182,9 @@ fixing. Keep only **Open / Fixing / Fixed** rows here. Once a finding is
**Status:** 🔴 Open → 🟡 Fixing → 🟢 Fixed (verified, awaiting archive) → then remove on 📦 Archive.
| ID | Sev | Area (`file:line`) | Summary | Status | Notes / repro |
|----|-----|--------------------|---------|--------|---------------|
| _(none — all Cycle 1 findings fixed, verified & archived to `HISTORY.md` v0.41.0)_ | | | | | |
| ------------------------------------------------------------------------------------------------------------------------------ | --- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| QA-B0-04 | S3 | 29 server files (`grep -rl @ts-nocheck`) | **`@ts-nocheck` burn-down (program, in progress):** typecheck:server is vacuous over these files. Starter batch done (38 → **29**: the 9 smallest routes fully checked, `db53954`). The B0 recon counter must only go down. | 🟡 Fixing (program) | Remaining: 20 routes + server.cts + 4 db + 4 mega services. Convert opportunistically when touching a file; oidcService partially cleaned during the v6 rewrite. |
| _(all other 2026-07-10 recon findings fixed & archived — QA-B0-02 → v0.42.0, QA-B0-03/QA-B1-01/QA-B13-02/QA-B17-01 → v0.41.1)_ | | | | | |
**Finding template** (paste a new row above; keep the full write-up here until archived):
@ -215,13 +222,17 @@ graduate to `roadmap.md`/`FUTURE.md`.
**Status:** 🔵 Noted (proposal) → 🟡 Doing → then archive to `HISTORY.md` on implement.
| ID | Lens | Area (`file`/page) | Proposal (what & why) | Effort | Status |
|----|------|--------------------|-----------------------|--------|--------|
| ----------- | ---- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- |
| IMP-CODE-01 | Code | `client/lib/money.js` (+16 files) | ~~No shared client money formatter.~~ **Shipped `a15f00c`:** added `client/lib/money.js` (`formatUSD`/`formatUSDWhole`/`formatCentsUSD`); `lib/utils.fmt` delegates to it and 15 local formatters were removed. `null`/`NaN`/`-0` all handled. Test `client/lib/money.test.js`; full client suite + build green. | M | ✅ Shipped |
| IMP-CODE-02 | Code | `db/database.js` (4,174→1,297 ln) | ~~Oversized module.~~ **Shipped `7f2faea`,`026c6a5`,`12d9d4c`:** split into `subscriptionCatalogSeed.js` (data), `migrations/versionedMigrations.js` (~1,740 ln factory), `migrations/legacyReconcileMigrations.js` (~830 ln factory) — db + helpers injected, behavior byte-identical. Verified: suite 125, fresh DB 79 migrations idempotent, real prod DB no-op with data intact. Test `tests/migrationModules.test.js`. | L | ✅ Shipped |
| IMP-CODE-03 | Code | `services/transactionMatchState.js` | ~~Overlapping match logic.~~ **Shipped `fa24322`:** added canonical `markMatched`/`markUnmatched`/`markIgnored`; routed the 6 single-transaction transitions through it (guarded bulk sweeps keep their own queries by design). Test `tests/transactionMatchState.test.js`. | M | ✅ Shipped |
| IMP-IA-01 | IA | Sidebar · `/data` | ~~Central features under an overflow menu.~~ **Shipped `0b1c6a8`:** Data moved into the main app nav (desktop dropdown + mobile) alongside Bills/Categories/Spending; same default-admin gate preserved; removed the redundant account-dropdown entry. | S | ✅ Shipped |
| IMP-UX-01 | UX | Bills delete | ~~Retention isn't surfaced.~~ **Shipped `aace5a4`:** Bills shows a "Recently deleted (N)" button opening a restore dialog (amount, category, days-left); `GET /api/bills/deleted` (30-day window). Test `tests/billsDeletedRoute.test.js`. _Categories/payments could get the same treatment later._ | M | ✅ Shipped |
| IMP-UX-02 | UX | all list pages | **Audited — no gaps.** Checked all 11 main pages (Tracker/Bills/Subscriptions/Categories/Spending/Banking/Snowball/Payoff/Analytics/Summary/Calendar): each has a skeleton/`animate-pulse` **loading** state, an **empty** state, and a **rendered recoverable error** panel with a Retry/Try-again button (e.g. Summary 459-463, Calendar 899-903, BankTransactions 671-750, Payoff 276-302). No dead ends found; no change warranted. Keep as a standing B18/B14 check for new pages. | M | ✅ Audited |
| IMP-CODE-04 | Code | dead files/exports | ~~Dead-weight sweep.~~ **Shipped `a31a774`:** legacy root test harnesses deleted, unused exports dropped, knip config cleaned, **knip now clean and BLOCKING in CI**. ⚠ Correction (user catch): `client/public/img/doingmypart.jpg` greps as unused in code but is referenced from **HISTORY.md markdown** served by the release-notes renderer — kept. Lesson folded into the B0 dead-code recon step: sweeps must include rendered markdown content. | S | ✅ Shipped |
| IMP-CODE-05 | Code | `client/pages/SubscriptionsPage.tsx` (2,371 ln), `TrackerPage.tsx` (1,943 ln) | Largest two pages exceed the size BillModal was decomposed at (1,733). Same treatment: extract section components/hooks behind existing tests, behavior-preserving. SpendingPage (1,642) and BillsPage (1,586) next tier. | L | 🔵 Noted |
| IMP-CODE-06 | Code | `db/data/` | ~~Runtime seed JSONs lived in `docs/`.~~ **Shipped `a31a774`:** advisory-filter + merchant-store JSONs (7 MB) moved to `db/data/` next to their loader in `db/database.cts`; fresh-DB seed verified (5000+5000 rows). | S | ✅ Shipped |
| IMP-CODE-07 | Code | `e2e/control-census.spec.js` | ~~Appendix E never filled manually.~~ **Shipped `92439e7` + `64ab37a`:** automated control census — role + accessible name of every visible interactive control on all 15 authed pages, snapshot-diffed each run; own `census` Playwright project ordered before the mutating probe specs so baselines always see pristine seed data. | M | ✅ Shipped |
---
@ -246,10 +257,11 @@ or `### 🧹 QA` (polish/improvements) section, matching the existing changelog
```
**Rules**
- One bullet per finding; include the old `QA-B?-??` id in parentheses for traceability.
- If a fix added/changed a test, say which (`tests/…` or `client/…test.*`).
- Don't archive until the fix is verified (repro gone + `npm run ci` green).
- IMP items that were implemented are archived the same way; IMP items merely *noted* stay in the Findings Log (or graduate to `FUTURE.md`/`roadmap.md` if deferred).
- IMP items that were implemented are archived the same way; IMP items merely _noted_ stay in the Findings Log (or graduate to `FUTURE.md`/`roadmap.md` if deferred).
---
@ -258,7 +270,7 @@ or `### 🧹 QA` (polish/improvements) section, matching the existing changelog
### 4.1 Running the app
| Mode | Command | URL |
|------|---------|-----|
| -------------------------- | -------------------------------- | -------------------------------------------------- |
| Dev (API + UI, hot reload) | `npm run dev` | UI `http://localhost:5173` (proxies API → `:3000`) |
| API only | `npm run dev:api` | `http://localhost:3000` |
| Production build | `npm run build` then `npm start` | `http://localhost:3000` |
@ -274,7 +286,7 @@ or `### 🧹 QA` (polish/improvements) section, matching the existing changelog
Full functional pass across reasonable combinations; smoke (B15) across all.
| Dimension | Values |
|-----------|--------|
| ---------- | --------------------------------------------------------------- |
| Browser | Chrome/Chromium, Firefox, Safari (WebAuthn differs per browser) |
| Viewport | Desktop ≥1280, tablet ~768, mobile ~375 (iPhone SE), ~414 |
| Theme | Light, Dark, system-follow |
@ -285,6 +297,7 @@ Full functional pass across reasonable combinations; smoke (B15) across all.
| Data state | Empty, seeded demo, large/stress, adversarial |
### 4.3 Accounts to prepare
- `admin`, `user`, a **second** `user` (data-isolation), a single-user-mode instance (separate DB).
- Demo reference: `guest / guest123` (do not run destructive flows on any shared demo server).
@ -293,11 +306,11 @@ Full functional pass across reasonable combinations; smoke (B15) across all.
Manual passes prove a button works **once**; they don't stop it regressing next cycle. The Playwright suite is the regression net — it drives real clicks in a real browser, and it's where visual-regression, axe-a11y, and fault-injection (§B14) are wired so they re-run every cycle for free.
| Command | What it does |
|---------|--------------|
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run test:e2e` | run the E2E suite headless (boots the app via `webServer`) |
| `npm run test:e2e:ui` | Playwright UI mode — watch/debug interactively |
| `npm run test:e2e:update` | re-baseline visual-regression screenshots (review the diff before committing) |
| `npm run smoke:prod` | **B15 production-build smoke** — builds, boots `node server.js` (dist/), drives the real artifact so the split vendor chunks are validated at runtime |
| `npm run smoke:prod` | **B15 production-build smoke** — builds, boots `node server.cts` (serving dist/), drives the real artifact so the split vendor chunks are validated at runtime |
- **Setup (one-time):** `npm install` then `npx playwright install chromium`. Config: `playwright.config.js`; specs in `e2e/`.
- **Scope:** the suite is a **thin critical-path smoke**, not a replacement for the manual playbooks — it locks the happy paths (login → pay bill → skip → note → reconcile), the primitive state matrix, per-page axe scans, and page screenshots. Grow it whenever a manual pass finds a UI regression that a click-test could have caught.
@ -349,42 +362,51 @@ Run on **every** page during its batch — don't assume a shared component behav
Each batch below is the detailed script for the matching row in [§1](#1-batch-plan--progress-tracker). Apply [§6](#6-cross-cutting-checks-every-page) throughout.
### B0 — Baseline, tooling & coverage recon
**Run FIRST in every cycle.** This is where the plan re-syncs with reality — new
pages, routes, endpoints, or features added since the last cycle get discovered
and folded in **before** testing, so coverage never silently rots.
**Tooling baseline**
- [ ] `npm run ci` — record any failing server/client test or build error as a finding (S1/S2).
- [ ] `npm run check` — server syntax + build clean.
- [ ] App boots via `npm run dev` **and** production `npm start`; note startup warnings.
- [ ] Load the app; browser console + server logs clean on first load and first navigation.
- [ ] Confirm which auth mode / seed state the DB is in; snapshot a backup before proceeding.
**Coverage recon — enumerate the *actual* product and diff it against this plan.**
**Coverage recon — enumerate the _actual_ product and diff it against this plan.**
Run these, then compare the output to the batch playbooks (§7) and the [route map](#appendix-c--page--route--api-quick-map):
- [ ] **Client routes**`grep -nE "<Route" client/App.tsx` — every path present here must appear in a batch playbook and Appendix C. *(The whole client is now TypeScript — `find client -name '*.jsx'` returns 0 — so recon greps target `.tsx`/`.ts`; `npm run typecheck` is a guard alongside build/lint/tests.)*
- [ ] **Client routes**`grep -nE "<Route" client/App.tsx` — every path present here must appear in a batch playbook and Appendix C. _(The whole client is now TypeScript — `find client -name '*.jsx'` returns 0 — so recon greps target `.tsx`/`.ts`; `npm run typecheck` is a guard alongside build/lint/tests.)_
- [ ] **Pages**`ls client/pages/` — every page has an owning batch.
- [ ] **Sidebar / nav entries**`grep -nE "to:|label:|Only" client/components/layout/Sidebar.tsx` — new nav links (incl. conditional ones like `simplefinOnly`) are covered.
- [ ] **API route mounts**`grep -nE "app.use\('/api" server.js` — every mounted route group is in B13's list and mapped in Appendix C.
- [ ] **API route mounts**`grep -nE "require\('./routes/" server.cts` — every mounted route group is in B13's list and mapped in Appendix C.
- [ ] **Services & components**`ls services/` and `ls client/components/**/` — new service/component families have a home in a playbook.
- [ ] **UI primitives**`ls client/components/ui/` — every shared primitive is covered by the [B-UI](#b-ui--design-system-primitives) playbook; a new primitive gets a row there.
- [ ] **Middleware & workers**`ls middleware/ workers/` (+ `services/*Worker*`, `*Scheduler*`) — each is covered (csrf/rateLimiter/securityHeaders/requireAuth → B13; dailyWorker/bankSyncWorker/backupScheduler → B10).
- [ ] **Migrations & deploy** — new `db/database.js` migrations, `Dockerfile`/`docker-entrypoint.sh` changes, and `encryptionService`/`updateCheckService` behavior are covered by [B16](#b16--migrations-secrets--deployment).
- [ ] **Interactive-control census (makes "every button tested" *provable*)** — for each page, enumerate every button, link, toggle/switch, checkbox, select, text/number/date/file input, tab, menu, and filter control, and record it in a per-page control checklist (template: [Appendix E](#appendix-e--per-page-control-census)). A control that isn't on a checklist hasn't been tested — the census is the completeness guarantee the batch playbooks alone don't give you. Quick starting inventory: `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages client/components` and `grep -rn "onClick=" client/pages/<Page>.tsx`.
- [ ] **Migrations & deploy** — new migrations (`db/database.cts` + `db/migrations/*.cts`), `Dockerfile`/`docker-entrypoint.sh` changes, and `encryptionService`/`updateCheckService` behavior are covered by [B16](#b16--migrations-secrets--deployment).
- [ ] **Interactive-control census (makes "every button tested" _provable_)** — for each page, enumerate every button, link, toggle/switch, checkbox, select, text/number/date/file input, tab, menu, and filter control, and record it in a per-page control checklist (template: [Appendix E](#appendix-e--per-page-control-census)). A control that isn't on a checklist hasn't been tested — the census is the completeness guarantee the batch playbooks alone don't give you. Quick starting inventory: `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages client/components` and `grep -rn "onClick=" client/pages/<Page>.tsx`.
- [ ] **Feature flags / conditional surfaces** — search for `Only`, `enabled`, `featureFlag`, env gates that hide/show pages; ensure each state is tested.
- [ ] **What changed since last cycle** — skim `git log`/`HISTORY.md` since the previous cycle's commit (see [Cycle Log](#11-qa-cycle-log)) for new features/pages.
- [ ] **Dependency freshness (added 2026-07-10)** — run `npm outdated` and `npm audit` (full, not just the CI gate). Log every **high+ vulnerability** and every **major lag on a security-relevant dep** (auth, crypto, parsers of untrusted input) as a finding. Standing guards: weekly `deps-audit` workflow (fails on high+), `renovate.json`. Known cap: typescript <7 until typescript-eslint supports it.
- [ ] **Dead-code scan (added 2026-07-10)**`npx knip` is a **blocking CI step**; any new unused file/export/dep fails. Also grep for **API wrappers with no client callers** (how the WebAuthn ghost hid for 4 cycles) and remember: asset-reference sweeps must include **rendered markdown content** (HISTORY.md images are served by the release-notes renderer — the `doingmypart.jpg` near-miss).
- [ ] **Type-check debt census (added 2026-07-10)**`grep -rl "@ts-nocheck" routes services db workers middleware utils server.cts | wc -l` and record it in the Cycle Log. It must be **≤ last cycle's** (baseline after the starter batch: **29**; was 38 — see QA-B0-04).
- [ ] **Version coherence (added 2026-07-10)**`package.json` version == top `HISTORY.md` heading; `tests/versionSync.test.js` enforces it, this check is the reminder to bump both when releasing.
**Update the plan (do this now, not later)** — for anything the recon surfaced that isn't already covered:
- [ ] Add it to the relevant batch playbook (or create a new batch and a row in the [§1 table](#1-batch-plan--progress-tracker)).
- [ ] Add/adjust its entry in [Appendix C](#appendix-c--page--route--api-quick-map).
- [ ] Note the plan update in the [Cycle Log](#11-qa-cycle-log) row for this cycle.
- [ ] If a whole surface is *missing* from the product that the plan expected (page removed/renamed), reconcile the plan too — don't test ghosts.
- [ ] If a whole surface is _missing_ from the product that the plan expected (page removed/renamed), reconcile the plan too — don't test ghosts.
### B-UI — Design-system primitives
**Test each shared control once, thoroughly, in isolation — a bug here breaks every page at once.** Drive them wherever they're already mounted (or a scratch page); run each against the [per-control state matrix](#6-cross-cutting-checks-every-page) × light/dark × keyboard-only. One finding row per primitive.
| Primitive (`client/components/ui/`) | Must verify |
|---|---|
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `button.tsx` | every variant (default/destructive/outline/ghost/link) + size; **disabled truly blocks click**; loading state; focus ring; Enter/Space activate |
| `input.tsx` | text/number/password/date/search/file types; placeholder; disabled/read-only; error styling; paste/autofill; number-input rules above |
| `select.tsx` (Radix) | opens by mouse **and** keyboard; type-ahead; long lists scroll; onChange fires in **Firefox+Safari**; disabled options; value persists; Esc closes |
@ -405,16 +427,18 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
- [ ] Axe scan (see B14) on a page densely using primitives → zero critical violations.
### B1 — Auth & authorization
- [ ] **Password:** valid login → correct landing (Tracker for `user`, `/admin` for default admin); wrong password → clear error, no user-enumeration timing/message difference; logout clears session; expired session redirects and preserves `state.from`; session persists across refresh.
- [ ] **Rate limiting:** repeated failed logins throttled (`loginLimiter`/`loginUsernameLimiter`), clear message, resets.
- [ ] **TOTP:** enroll (QR + secret), code accepted, backup codes work once, login prompts for TOTP, wrong code rejected+throttled, disable requires re-auth.
- [ ] **WebAuthn:** register/login/remove passkey in Chrome, Firefox, Safari; password fallback works.
- [ ] **WebAuthn (UI shipped v0.41.1):** register/login/remove passkey — the happy path is **automated** via Playwright's CDP virtual authenticator (`e2e/webauthn.probe.spec.js`; retired the "needs a human" assumption). Manual residue for a live pass: Firefox/Safari ceremonies, a real hardware key, and the `WEBAUTHN_RP_ID` deploy check (see `docs/DEPLOY_CHECKLIST.md`).
- [ ] **OIDC/Authentik:** SSO flow creates/links account; admin config errors surface cleanly; `oidcLimiter` throttles.
- [ ] **Roles/guards:** `user` blocked from `/admin*`, `/status` (redirect) and admin APIs (403); default admin forced to `/admin`; single-user bypass correct but admin surfaces still protected; unauth API → 401.
- [ ] **Data isolation (critical):** user A cannot read/modify user B's bills, payments, transactions, categories, snowball plans — test by ID enumeration on the API.
- [ ] **CSRF:** state-changing request without a valid token → rejected.
### B2 — Tracker (`/`)
- [ ] Month nav (prev/next/jump), current month highlighted, data reloads per month.
- [ ] Bills land in correct `114` / `1531` bucket by due date; pin-due sorting works.
- [ ] Quick pay marks paid + updates balance cards/progress; undo works; no double-count.
@ -428,6 +452,7 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
- [ ] Editable cells autosave; Esc cancels; invalid input handled. Mobile rows equal desktop actions. Compact mode intact.
### B3 — Bills (`/bills`)
- [ ] Create with all fields (name, amount, due date, category, schedule, account, autopay, active range).
- [ ] Edit propagates to Tracker/Summary/Calendar/Analytics; delete confirms + handles orphan payments/history.
- [ ] Custom schedules (weekly/biweekly/monthly/quarterly/annual/custom): next-due & occurrences correct across month/year boundaries.
@ -436,44 +461,52 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
- [ ] BillModal open/close, validation, cancel discards unsaved changes.
### B4 — Subscriptions & Categories
- [ ] Subscriptions: add/edit/delete, active/cancelled, renewal & annual→monthly normalization; totals feed Tracker/Summary/Analytics.
- [ ] Catalog: browse/search, add-from-catalog pre-fills.
- [ ] Categories: create/edit/delete (in-use handled: reassign/prevent); groups create/assign/reorder (`categoryGroups`/`categoryReorder` tests); colors/icons consistent on Tracker/Spending/Analytics.
### B5 — Reporting reconciliation
- [ ] Summary totals (paid/unpaid/overdue/remaining) reconcile with Tracker for the same month; income breakdown modal matches.
- [ ] Calendar plots bills/payments on correct days (**timezone**: a bill due on the 1st must not render on the 31st); day totals correct.
- [ ] Analytics charts render with data AND empty (no broken SVG/`NaN` axes); period selectors update all charts; figures reconcile with Summary/Tracker; large dataset perf OK.
- [ ] Health indicators compute from real data, no crash on empty; recommendations sane.
### B6 — Spending (`/spending`)
- [ ] Category-group view assigned/spent/available math correct; 3-month averages correct.
- [ ] Cover-overspending reallocates funds correctly and is reversible.
- [ ] Safe-to-spend matches Tracker (`safeToSpend.test.js`); month nav; empty/partial months handled.
### B7 — Debt planning (`/snowball`, `/payoff`)
- [ ] Add debts (balance/APR/min); snowball vs avalanche ordering correct.
- [ ] Projection + amortization vs a **hand-calculated** example; APR=0 and already-paid debts correct.
- [ ] Extra-payment/budget updates payoff date + total interest; chart renders; plan history saves/restores; status banner accurate.
- [ ] Edge: single debt, many debts, `$0` debt, negative/absurd inputs rejected.
### B8 — Banking (`/bank-transactions`)
- [ ] Ledger loads/virtualizes/filters (date/account/amount/merchant/status).
- [ ] Transaction matching (match/unmatch), auto-match review approve/reject, no double-match (`transactionMatchService.test.js`).
- [ ] Merchant/store matching rules + confidence/duplicates; advisory non-bill filter flags/hides with override.
- [ ] Matched payments reflect on Tracker/ledger without double-counting; category picker persists.
### B9 — Data lifecycle (`/data`)
- [ ] Imports: spreadsheet (XLSX/CSV) map/preview/commit, malformed rejected, dup/partial handled; transaction CSV (`csvTransactionImportService.test.js`) dedupe + parsing; SQLite user import version-checked + confirms overwrite; seed demo data safe; import history lists + rollback.
- [ ] Exports: download SQLite **round-trips** (export → fresh account → import → matches); Excel export opens uncorrupted; ICS calendar feed valid in a client AND properly **token-gated** (route mounts before auth — verify not open).
- [ ] Backups: manual + scheduled restorable on a scratch instance; permissions not world-readable; old backups pruned (`backupAndCleanup.test.js`).
### B10 — Notifications & workers
- [ ] Each channel (email/SMTP, ntfy, Gotify, Discord, Telegram): test message delivers; bad token/URL → clear error, logged, no secret leak.
- [ ] Reminders fire at configured lead time for upcoming/overdue; no duplicates; paid/skipped excluded; respects per-user prefs.
- [ ] Workers: `dailyWorker`, `bankSyncWorker` (interval + guardrails), `backupScheduler` run on schedule; errors caught/logged, don't crash server, next run unblocked.
### B11 — Admin panel (`/admin`)
- [ ] Onboarding wizard completes without a broken state.
- [ ] Users table: add/edit-role/reset-pw/disable/delete; **cannot remove the last admin**.
- [ ] Login mode switch single↔multi verified live, no lockout; auth-methods enable/disable + bad config surfaced.
@ -482,23 +515,29 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
- [ ] Privacy admin edits reflect on public `/privacy`; system status metrics/versions/jobs accurate (`statusService.test.js`); admin actions rate-limited + audited (`auditService` — spot-check log).
### B12 — Settings, Profile & global UI
- [ ] Settings: theme (light/dark/system) persists; notification prefs save + reflect in B10; display/density/period/search-panel prefs persist; invalid rejected.
- [ ] Profile: change password (current required, invalidates sessions), manage 2FA/passkeys, sessions revoke (`profileRoute.test.js`).
- [ ] Static: About (public + admin, version shown), Privacy, Release Notes (dialog once per `user`, dismiss persists), Roadmap (admin), NotFound friendly + way home.
- [ ] Global: command palette (`Ctrl+K`) search/keyboard/Esc, hidden for default admin; sidebar collapse/expand + mobile overlay (check overflow issue in `docs/UI_IMPROVEMENTS.md`); toasts stack/dismiss; page transitions no flash/double-fetch; theme applied before first paint.
### B13 — API / backend direct
Route groups: `auth`, `auth/oidc`, `admin`, `tracker`, `bills`, `subscriptions`, `payments`, `data-sources`, `transactions`, `matches`, `categories`, `settings`, `user`, `calendar`, `summary`, `monthly-starting-amounts`, `analytics`, `spending`, `snowball`, `notifications`, `status`, `about`, `about-admin`, `privacy`, `version`, `profile`, `export`, `import`/`imports`.
- [ ] Auth: unauth → 401, wrong role → 403, right role → 200.
- [ ] CSRF: state-changing without valid token rejected; with token succeeds (`middleware/csrf.js`).
- [ ] Validation: bad/missing body → structured 4xx (`middleware/errorFormatter.js`, `utils/apiError.js`), never a raw 500 stack.
- [ ] CSRF: state-changing without valid token rejected; with token succeeds (`middleware/csrf.cts`).
- [ ] Validation: bad/missing body → structured 4xx (`utils/apiError.cts` throw-pattern — **enforced** by the eslint ratchet on `routes/**`; exceptions documented in `CODE_STANDARDS.md`), never a raw 500 stack.
- [ ] **Error-shape audit (added 2026-07-10):** no route returns raw `err.message` on a 5xx — thrown `ApiError` is the only whitelist (terminal handler masks everything else). 20 leak sites were fixed in the 2026-07 pass; the ratchet prevents regression.
- [ ] IDOR/isolation: other user's resource by id → 403/404, no leak.
- [ ] Rate limits: login/admin/export/import/OIDC limiters trigger + reset (`middleware/rateLimiter.js`).
- [ ] Rate limits: login/admin/export/import/OIDC limiters trigger + reset (`middleware/rateLimiter.cts`).
- [ ] **Orphan-endpoint sweep (added 2026-07-10):** every endpoint has a caller (client `api.ts`, worker, or documented external consumer like the ICS feed). No-caller endpoints are dead surface (found: auth.cts's duplicate admin block) — and note any less protected than their canonical twin.
- [ ] Money in **integer cents** end-to-end (per `docs/cents-migration-plan.md`); API and DB agree; no float drift.
- [ ] Idempotency: repeated create doesn't duplicate; concurrent edits resolve sanely.
- [ ] Consistent error JSON + correct status codes; security headers present (`middleware/securityHeaders.js`); public routes (`about`/`privacy`/`version`/calendar feed) leak nothing sensitive.
- [ ] Consistent error JSON + correct status codes; security headers present (`middleware/securityHeaders.cts`); public routes (`about`/`privacy`/`version`/calendar feed) leak nothing sensitive.
### B14 — Non-functional
- [ ] **a11y (manual):** keyboard-only reach/operate every control, visible focus, skip-link works; screen-reader labels/roles (Radix `aria-*`); WCAG-AA contrast light+dark; modals trap+restore focus, Esc closes; errors announced not color-only.
- [ ] **a11y (automated):** run **axe-core** on every page (`@axe-core/playwright`, or `jest-axe` for component-level) — **zero critical/serious** violations; triage moderate. Wire it into the E2E suite so it re-runs every cycle, not just once.
- [ ] **Visual regression:** capture a baseline screenshot per page × {desktop, mobile} × {light, dark} (Playwright `toHaveScreenshot`); diff against baseline each cycle. Every non-trivial pixel diff is either an intended change (update the baseline in the same commit) or a finding — never ignore it. This is what makes "every page looks right" repeatable instead of eyeballed.
@ -510,7 +549,9 @@ Route groups: `auth`, `auth/oidc`, `admin`, `tracker`, `bills`, `subscriptions`,
- [ ] **Timezone/locale:** non-UTC tz + DST boundary — due dates and calendar stay correct.
### B15 — Regression & sign-off
Run on the **production build** (`npm start`), not dev:
- [ ] `npm run ci` green. Log in as `user` and `admin`.
- [ ] `npm run test:e2e` green (Playwright smoke + axe + visual-regression baselines match, §4.4).
- [ ] Tracker: create bill → quick-pay → skip another → add note; reflected on Summary/Calendar/Analytics.
@ -522,46 +563,51 @@ Run on the **production build** (`npm start`), not dev:
- [ ] Confirm [exit criteria](#appendix-b--exit--sign-off-criteria).
### B16 — Migrations, secrets & deployment
Added Cycle 1 (previously uncovered). These run on every boot / container start and
touch money columns and at-rest secrets — a bug here corrupts data or leaks/breaks
secrets silently.
**Migrations** (`db/database.js` migration system, `scripts/migrate-db.js`, `schema_migrations`, `rollbackMigration`)
**Migrations** (`db/database.cts` + `db/migrations/*.cts`, `scripts/migrate-db.js`, `schema_migrations`, `rollbackMigration`)
- [ ] **Idempotent:** boot twice on the same DB → second run applies nothing ("Skipping already applied"), no errors, no duplicate rows/columns.
- [ ] **Fresh == migrated:** a brand-new DB (schema.sql + all migrations) has the same schema as a DB migrated up from an old version — same tables/columns/indexes, money columns are **integer cents**.
- [ ] **Rollback:** `rollbackMigration` on the latest migration reverts cleanly and re-applying works; partial/failed migration leaves the DB consistent (transactions per migration).
- [ ] **Money conversions correct:** v1.03 (dollars→cents) and v1.04 (template JSON) convert exact values, no ×100 drift, run once only.
- [ ] Migrating a large/real DB doesn't lose or duplicate bills/payments/categories.
**Encryption-key lifecycle** (`services/encryptionService.js`, `TOKEN_ENCRYPTION_KEY`, HKDF v1/v2)
**Encryption-key lifecycle** (`services/encryptionService.cts`, `TOKEN_ENCRYPTION_KEY`, HKDF v1/v2)
- [ ] **Key present:** secrets (SMTP pw, OIDC secret, push tokens, login IP/UA) encrypt at rest and decrypt correctly.
- [ ] **Key missing:** app boots; secret features degrade gracefully (no crash); confirm secrets are **not** silently stored/served in plaintext.
- [ ] **Key rotated/wrong:** old ciphertext fails to decrypt **gracefully** (no crash, no stack leak); `safeDecrypt` fallback path is sane; re-encryption migrations (v0.770.79) behave.
- [ ] Encryption key is never committed, logged, or returned in any API response.
**Container / deploy** (`Dockerfile`, `docker-compose.yml`, `docker-entrypoint.sh`, `deploy.sh`)
- [ ] Image **builds**; container **starts**; app reachable; `/api/version` responds.
- [ ] Entrypoint: creates `DATA_DIR`/`DB_DIR`/`BACKUP_DIR`, sets **`chmod 700`** (not world-readable), `chown`s to the non-root `bill` user, runs migrations when `RUN_DB_MIGRATIONS=true`.
- [ ] Data **persists** across container restart (mounted volume); DB not re-created.
- [ ] Runs as **non-root**; secrets come from env, not baked into the image.
**Update check / phone-home** (`services/updateCheckService.js`)
**Update check / phone-home** (`services/updateCheckService.cts`)
- [ ] Confirm the external request to `REPO_API_URL` (default `dream.scheller.ltd`) is **disclosed** (privacy page) and **opt-out-able**; it must send no user data, only fetch the latest release; failure/offline degrades silently.
**Rate-limiter completeness** (`middleware/rateLimiter.js`) — beyond B13's list
**Rate-limiter completeness** (`middleware/rateLimiter.cts`) — beyond B13's list
- [ ] `backupOperationLimiter` throttles admin backup/restore/cleanup; `skipRateLimitIfNoUsers` only relaxes limits on a genuinely empty instance (first-run), never afterward.
### B17 — Code health & consolidation (IMP)
An **improvement** batch: hunt for ways to make the codebase smaller, clearer, and more
consistent — *without changing behavior*. Every candidate is logged as an `IMP-CODE-*`
consistent — _without changing behavior_. Every candidate is logged as an `IMP-CODE-*`
row in §2.1 with a concrete proposal; nothing is refactored silently. Consolidation
lands only when it's behavior-preserving **and** covered by existing or added tests.
- [ ] **Duplication / DRY:** find logic copy-pasted across services/routes/components and
propose a shared helper. Known hot spots: money formatting/rounding (`utils/money.js`
vs inline), the `resolveDueDate` occurrence gate (must stay one implementation),
error-response shaping (`utils/apiError.js` vs ad-hoc), React data-fetch patterns
propose a shared helper. Known hot spots: money formatting/rounding (`utils/money.mts` vs inline), the `resolveDueDate` occurrence gate (must stay one implementation),
error-response shaping (`utils/apiError.cts` vs ad-hoc), React data-fetch patterns
(repeated `useQuery` + toast + error handling → shared hooks).
- [ ] **Dead / unused code:** unused exports, unreachable branches, orphaned files,
commented-out blocks, unused deps (`depcheck`), unused UI components/CSS, leftover
@ -584,12 +630,13 @@ lands only when it's behavior-preserving **and** covered by existing or added te
### B18 — UX & information architecture / menus (IMP)
An **improvement** batch focused on the person using the app: is every feature
*discoverable*, is every core flow *smooth*, and does every action live *where a user
would look for it*? Candidates are logged as `IMP-UX-*` or `IMP-IA-*` in §2.1 with a
_discoverable_, is every core flow _smooth_, and does every action live _where a user
would look for it_? Candidates are logged as `IMP-UX-*` or `IMP-IA-*` in §2.1 with a
concrete before/after proposal. Walk the app as a real user (both `user` and `admin`,
desktop and mobile, light and dark), not just as a tester.
**Information architecture & menus**
- [ ] **Discoverability:** is any feature buried, orphaned, or reachable only by typing a
URL? Everything should be reachable from the nav, a menu, or a clear in-page entry.
- [ ] **Navigation structure:** sidebar/nav grouping is logical; related pages sit
@ -604,6 +651,7 @@ desktop and mobile, light and dark), not just as a tester.
two pages that do nearly the same thing — propose consolidating.
**Experience quality**
- [ ] **Core-flow friction:** count the clicks for the top tasks (pay a bill, add a bill,
connect SimpleFIN, run a sync, import data). Propose shortcuts where a step is wasted.
- [ ] **States:** every list/page has a clear **empty state with a next-step CTA**, a
@ -626,7 +674,7 @@ desktop and mobile, light and dark), not just as a tester.
### Appendix A — Severity definitions
| Level | Definition |
|-------|------------|
| --------------------- | -------------------------------------------------------------------------------------------- |
| **S1 Critical** | Data loss/corruption, security hole, crash/blank page, wrong money math, cannot log in/save. |
| **S2 Major** | Feature broken/unusable, wrong results, broken navigation, unhandled error. |
| **S3 Minor** | Works but wrong edge behavior, confusing UX, missing validation message. |
@ -636,6 +684,7 @@ desktop and mobile, light and dark), not just as a tester.
### Appendix B — Exit / sign-off criteria
A cycle is release-ready when: **(Cycle 1 — all met ✅)**
- [x] All batches B0B15 ✅ (Chromium desktop + mobile via the E2E projects; light + dark, `user` + `admin` exercised). Cross-browser Firefox/Safari carried to Cycle 2.
- [x] B15 smoke green on the **production build** (`npm run smoke:prod`).
- [x] **Zero open S1/S2** in the Findings Log; S3/S4/IMP all fixed & archived.
@ -648,7 +697,7 @@ A cycle is release-ready when: **(Cycle 1 — all met ✅)**
### Appendix C — Page ↔ route ↔ API quick map
| Page | Route | Primary API |
|------|-------|-------------|
| ------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Tracker | `/` | `/api/tracker`, `/api/bills`, `/api/payments`, `/api/monthly-starting-amounts` |
| Calendar | `/calendar` | `/api/calendar` |
| Summary | `/summary` | `/api/summary` |
@ -663,22 +712,33 @@ A cycle is release-ready when: **(Cycle 1 — all met ✅)**
| Settings | `/settings` | `/api/settings`, `/api/notifications` |
| Profile | `/profile` | `/api/profile`, `/api/user` |
| Data | `/data` | `/api/import`, `/api/export`, `/api/data-sources` |
| Admin | `/admin`, `/admin/status` | `/api/admin`, `/api/status`, `/api/about-admin` |
| About / Privacy / Release Notes / Roadmap | `/about`, `/privacy`, `/release-notes`, `/roadmap` | `/api/about`, `/api/privacy`, `/api/version` |
| Admin | `/admin`, `/admin/status` (`/status` → redirect), `/admin/about`, `/admin/roadmap` | `/api/admin`, `/api/status`, `/api/about-admin` |
| About / Privacy / Release Notes / Roadmap | `/about`, `/privacy`, `/release-notes`, `/roadmap` (admin-gated) | `/api/about`, `/api/privacy`, `/api/version` |
| Calendar feed (no page — external ICS consumers) | — | `/api/calendar/feed.ics?token=…` (token-gated, mounted before auth) |
### Appendix D — Reference docs
`SECURITY_AUDIT.md` (security depth) · `docs/UI_IMPROVEMENTS.md` (known UI issues) · `docs/cents-migration-plan.md` (money-as-cents) · `docs/SIMPLEFIN_CONSUMER_GUARDRAILS.md` (sync limits) · `docs/CSRF-SPA-Setup.md`, `docs/RATE_LIMITING_ENHANCEMENT.md` (security middleware) · `REVIEW.md`, `DEVELOPMENT_LOG.md`, `roadmap.md`, `FUTURE.md` (context/known gaps) · `HISTORY.md` (changelog / fix archive) · `playwright.config.js` + `e2e/` (automated E2E/visual/a11y harness, §4.4).
### Appendix E — Per-page control census
The completeness ledger behind "every button, textbox, slider is right." Fill one table **per page** during [B0](#b0--baseline-tooling--coverage-recon) and check every control off during that page's batch. A control not listed here is a control not tested. Build the starting list with `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages/<Page>.tsx` + `grep -n "onClick=\|<Button\|<Select\|<Switch\|<Checkbox" client/pages/<Page>.tsx`.
The completeness ledger behind "every button, textbox, slider is right."
> ✅ **AUTOMATED (2026-07-10, IMP-CODE-07):** the manual tables below were never
> filled in — Cycle 1 signed off on playbooks alone. The census is now
> `e2e/control-census.spec.js` (own `census` Playwright project, runs before the
> mutating probe specs): every visible interactive control on all 15 authed
> pages, snapshotted as role + accessible name. A new/removed/renamed control is
> a reviewable snapshot diff each run; review the diff, then re-baseline
> intended changes in the same commit. The template below remains only for
> ad-hoc manual passes on new pages before they're added to the spec's PAGES list. Build the starting list with `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages/<Page>.tsx` + `grep -n "onClick=\|<Button\|<Select\|<Switch\|<Checkbox" client/pages/<Page>.tsx`.
**Template** (copy per page):
| Control | Type | Expected action | States checked (default/focus/disabled/error/loading) | Keyboard | Result |
|---------|------|-----------------|-------------------------------------------------------|----------|--------|
| *e.g.* Quick-pay button | button | marks bill paid, updates balance cards, undo available | default ✓ · disabled-while-saving ✓ | Enter ✓ | ✅ / finding id |
| *e.g.* Amount input | number | per-month override, cents only, no wheel-scroll change | default ✓ · error-on-letters ✓ | Tab/Esc ✓ | ✅ / finding id |
| ----------------------- | ------ | ------------------------------------------------------ | ----------------------------------------------------- | --------- | --------------- |
| _e.g._ Quick-pay button | button | marks bill paid, updates balance cards, undo available | default ✓ · disabled-while-saving ✓ | Enter ✓ | ✅ / finding id |
| _e.g._ Amount input | number | per-month override, cents only, no wheel-scroll change | default ✓ · error-on-letters ✓ | Tab/Esc ✓ | ✅ / finding id |
**Pages to census** (from `client/pages/`, keep in sync with [Appendix C](#appendix-c--page--route--api-quick-map)): Tracker, Calendar, Summary, Bills, Subscriptions, SubscriptionCatalog, Categories, Health, Analytics, Spending, Snowball, Payoff, BankTransactions, Data, Settings, Profile, Admin, Status, About, Privacy, ReleaseNotes, Roadmap, Login, NotFound — plus the shared **Sidebar/command-palette/header** chrome once.
</content>

View File

@ -1,6 +1,6 @@
{
"name": "bill-tracker",
"version": "0.41.1",
"version": "0.42.0",
"description": "Monthly bill tracking system",
"main": "server.cts",
"engines": {