**2026-07-10 blind-spot recon (pre-Cycle 2):** a dedicated deps/dead-code/coverage audit logged **6 open findings** (1× S2 — high-severity `xlsx`/`nodemailer` vulns invisible to CI's critical-only audit gate; plus version drift, the WebAuthn ghost feature, vacuous `@ts-nocheck` typecheck coverage, `err.message` 5xx leaks, dead duplicate admin routes) and **4 IMP** rows — see §2/§2.1 and the Cycle Log. B0 gained four standing recon steps so these classes can't hide again.
- **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.
| 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.** |
| QA-B0-02 | S2 | `package.json` deps | **Known high-severity vulns ship in prod deps and CI can't see them**: `nodemailer`<9(raw-optionfile-read/SSRF,GHSA-p6gq-j5cr-w38f;fix =majorbumpto9.x)and`xlsx`(prototypepollutionGHSA-4r6h-8v6p-xvw6+ReDoSGHSA-5pgg-2g8v-p4x9,**no registry fix**;itparses**user-uploaded**spreadsheetsin`spreadsheetImportService`).CIauditgateis`--audit-level=critical`sohighvulnspasssilently.|🔴Open|`npm audit`(2026-07-10:4vulns—3high,1moderate).Fix:bumpnodemailer→9(checkAPIchangesatcallsitesin`notificationService`);forxlsxeithermovetothemaintainedSheetJSdist/`exceljs`,ordocumentrisk-acceptance+tightenimportvalidation.RaiseCIgateto`--audit-level=high`onceclean.|
| QA-B0-03 | S3 | `package.json:3` vs `HISTORY.md:1` | **Version drift**: `package.json` says `0.40.0` but `HISTORY.md`'s latest release is `v0.41.0`. `/api/version`, `/api/auth/me` (`has_new_version`/release-notes badge) and `updateCheckService` all read `package.json`, so the app under-reports its version and the release-notes "new version" logic keys off the wrong value. | 🔴 Open | `grep version package.json` vs `head HISTORY.md`. Fix: bump package.json to match the shipped release; add a check (test or CI step) that `package.json` version == top `HISTORY.md` heading. |
| QA-B1-01 | S2 | `routes/auth.cts:600-824` + `auth.cts:82-111`, `services/authService.cts:86-94`, `client/api.ts:203-210` | **WebAuthn is a ghost feature — and a live self-lockout hazard**: full server implementation (routes, service, DB tables) and client API wrappers exist, but **no UI component ever calls them** (commit `99abca9` shipped backend only). Worse: `authService.login` returns `{requires_webauthn, challenge_token, webauthn_options}` when `webauthn_enabled=1`, but **POST /login has no `requires_webauthn` branch** — it falls through to `result.user.id` (undefined) → TypeError → 500 "Login failed" on **every** login attempt. The enable endpoint is reachable today (any session + CSRF via curl), so a user can permanently brick their own login; recovery requires a DB edit. | 🔴 Open | `grep -rn webauthn client --include='*.tsx'` → 0 hits; `grep -n requires_webauthn routes/auth.cts` → 0 hits. Decide: build the Login/Profile passkey UI (client work mirrors `TotpSection`; `@simplewebauthn/browser` already installed; Playwright's CDP **virtual authenticator** makes it CI-testable — it does NOT need a human/external infra as Cycle 1 assumed), **or** remove/feature-flag the server surface + dep. **Either way, fix or guard the login fall-through first** — it's a lockout bug independent of the UI decision. |
| QA-B0-04 | S3 | 38 server files (`grep -rl @ts-nocheck`) | **Server typecheck gate is vacuous where it matters most**: all 29 `routes/*.cts` + `server.cts` + `db/database.cts` + both migration modules + 4 large services carry `@ts-nocheck`, so `npm run typecheck:server` green ≠ routes type-checked. The "TS migration complete" claim is about file extensions, not checking. | 🔴 Open | Burn-down: remove `@ts-nocheck` file-by-file (routes are thin and mostly typed already — start with small routes like `version`, `privacy`, `about`); track the count in B0 each cycle (**must only go down**). |
| QA-B13-02 | S3 | `routes/admin.cts:515,550,553` · `notifications.cts:82,190` · `spending.cts:85,198` · `transactions.cts:999` · `import.cts:45` | **Raw `err.message` returned to clients on 5xx** in 10 places — leaks internal error text and breaks the standardized error shape (`standardizeError`/`ApiError`). | 🔴 Open | `grep -rn "error: err.message" routes`. Fold into the route throw-pattern standardization (28 routes remaining); where the message is intentionally user-facing (e.g. SMTP test-send feedback) whitelist it explicitly. |
| QA-B17-01 | S4 | `routes/auth.cts:530-598` | **Dead duplicate admin routes**: auth.cts's "ADMIN ROUTES (MOUNTED AT /api/admin)" section duplicates `admin.cts` (`/has-users`, GET/POST `/users`) but auth.cts is only mounted at `/api/auth` — the client calls the `/api/admin/*` copies. Side effect: `/api/auth/has-users` is reachable **unauthenticated** (minor "instance has users" disclosure). | 🔴 Open | Delete the section (and the stale comment); verify nothing calls `/api/auth/has-users` / `/api/auth/users`; keep the admin.cts copies as canonical. |
| 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 (knip + manual, 2026-07-10) | **Delete dead weight**: `client/public/img/doingmypart.jpg` (2.2 MB, referenced nowhere, copied into every build) · root `test-functional.js` (imports bare `playwright`, not a dependency — broken) + `run-functional-test.js` (legacy pre-Playwright harness) · unused exports `dollarsToCents`/`SUPPORTED_CURRENCIES` (`client/lib/money.ts`) and type `BillDraft` (`client/lib/billDrafts.ts`) · `@simplewebauthn/browser` dep (unused until QA-B1-01 is resolved — remove or keep per that decision) · apply knip.json config hints (redundant entry patterns, `types/**` ignore). | S | 🔵 Noted |
| 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 | `docs/` runtime data | `advisory_non_bill_transaction_filters_us_ms_5000.json` (4.3 MB) + `merchant_store_match_us_nems_online_5k_v0_2.json` (3.1 MB) are **runtime seed data** loaded by `db/database.cts` but live in `docs/` next to actual documentation. Move to `db/data/` (or similar) so docs stays docs and the Docker COPY surface is explicit. | S | 🔵 Noted |
| IMP-CODE-07 | Code | `e2e/` + Appendix E | **Automate the control census**: Appendix E has never been filled in manually (see Cycle-Log 2·recon). Add an e2e spec that walks each page and snapshots the list of interactive elements (role/name via `getByRole` enumeration) — a new/removed/renamed control then shows up as a reviewable diff every cycle instead of relying on a hand-maintained table. | M | 🔵 Noted |
- 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).
- Backend: Node/Express on `PORT` (default `3000`). Frontend dev: Vite on `5173`.
- Data: SQLite at `db/bills.db` (WAL). **Back it up before destructive tests** (`backups/` or a manual copy). Prefer a scratch DB for B9/B11 restore tests.
- Configure a dedicated **test**`.env` from `.env.example`. Never point tests at production data or a live SimpleFIN account with real credentials.
- Test commands: `npm run ci` (check + all tests + build), `npm run check` (syntax + build), `npm run test` (server), `npm run test:client` (vitest).
### 4.2 Test matrix
Full functional pass across reasonable combinations; smoke (B15) across all.
-`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).
### 4.4 Automated E2E harness (Playwright)
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.
| `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.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.
- **Don't** point it at production data or a live SimpleFIN account — it runs against a scratch DB with seeded demo data.
---
## 5. Test data strategy
- **Empty:** brand-new account. Every page must render a sensible empty state — no crash, no `NaN`, no blank white screen.
- **Seeded:** use **Data → Seed Demo Data** for a realistic mid-size dataset.
- Debt: APR `0%`, very high APR, `$0` balance, absurd inputs.
- Non-UTC system timezone + a DST boundary date.
---
## 6. Cross-cutting checks (every page)
Run on **every** page during its batch — don't assume a shared component behaves the same everywhere.
**Navigation & routing** — reachable from nav and by direct URL (deep link) + after hard refresh · back/forward restores state, no stuck spinners · unknown sub-paths → `NotFoundPage` · active nav highlighted · `simplefinOnly` (Banking) gated · `Ctrl+K` palette finds & opens it.
**Buttons & interactions** — every button/link/icon/dropdown/tab/toggle/menu does something or is disabled with a reason · no dead controls · double-click doesn't duplicate records · **rapid repeated toggling** (spam a switch / pay-skip) resolves to one correct state, no stuck spinner · action started then **navigate away mid-flight** doesn't corrupt or throw · destructive actions confirm + cancel · primary action keyboard-reachable (Tab/Enter/Esc).
**Forms & validation** — required fields enforced · numeric/currency reject letters, handle 0/negative/decimal · errors don't wipe entered data · **paste** into every field (incl. `"$1,234.56"` into currency) · **browser/password-manager autofill** on login & forms · **IME/composition** (emoji, CJK) in text fields commits correctly · success shows toast (sonner) and the view updates without manual refresh (React Query invalidation).
**Number inputs (you have ~45 `type="number"` fields — the highest-risk control type)** — scroll-wheel over a focused field must **not** silently change the value · spinner up/down buttons step correctly and respect min/max · reject/`e`/`+`/exponent and multiple decimals · locale decimal comma vs dot · leading zeros · empty field ⇒ no `NaN` submitted · cents fields never accept >2 decimals.
**Per-control state matrix** — for each control on the page, verify every applicable state renders and behaves in **both light and dark**: default · hover · keyboard-focus (visible ring) · active/pressed · disabled (and truly non-interactive) · loading/in-flight · error/invalid · read-only · filled-to-overflow (1,000-char string / max-digit number wraps or truncates, no layout break).
> **Note — "sliders":** this app has **no `<input type=range>` sliders.** The `SlidersHorizontal` glyph is just the Bills **filter-panel** button; the closest real thing to a slider is a number stepper. Test those two surfaces where a slider would otherwise be expected.
**States** — loading skeleton/spinner, no layout jump · helpful empty state · error state (4xx/5xx/offline) recovers, `ErrorBoundary` shows a fallback not a white page.
**Visual & responsive** — correct at desktop/tablet/mobile, no overflow/h-scroll · dark mode contrast, no white flash · compact mode readable · long strings/big numbers wrap/truncate.
**Data integrity** — money 2-decimals, no float artifacts (`9.999999`) · dates in expected tz, period boundaries correct · values agree across pages (a bill total on Tracker == Summary == Analytics).
---
## 7. Batch playbooks (detailed checklists)
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.
- [ ]**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.)_
- [ ]**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 "require\('\./routes/" server.cts` (mounts are multi-line; grepping `app.use\('/api` alone misses ~half) — 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.
- [ ]**Migrations & deploy** — new migrations (`db/database.cts` + `db/migrations/versionedMigrations.cts`/`legacyReconcileMigrations.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 CI's `--audit-level=critical` gate). Log every **high+ vulnerability** and every **major-version lag on a security-relevant dep** (auth, crypto, parsers of untrusted input: `xlsx`, `nodemailer`, `openid-client`, `express`, `better-sqlite3`) as a finding. CI passing ≠ deps healthy.
- [ ]**Dead-code scan (added 2026-07-10)** — run `npx knip`; every new unused file/export/dependency is an IMP-CODE candidate. Also grep for **API wrappers with no client callers** (`client/api.ts` methods never referenced in `client/**/*.tsx`) — that's how the WebAuthn ghost feature (QA-B1-01) hid for 4 cycles: the server surface existed, tests touched it, but no user could ever reach it.
- [ ]**Type-check debt census (added 2026-07-10)** — `grep -rl "@ts-nocheck" routes services db workers middleware utils server.cts | wc -l` and record the count in the Cycle Log. It must be **≤ last cycle's** (baseline 2026-07-10: **38**, incl. all 29 routes — see QA-B0-04). A green `typecheck:server` means nothing for files on this list.
- [ ]**Version coherence (added 2026-07-10)** — `package.json` version == top `HISTORY.md` release heading (`/api/version`, the release-notes badge and `updateCheckService` all read package.json — see QA-B0-03).
**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.
- [ ]**WebAuthn:** ⚠ **currently untestable — no UI exists** (QA-B1-01: backend + `api.ts` wrappers only, no component calls them). Blocked until the passkey UI ships or the surface is removed. When it ships: register/login/remove passkey in Chrome, Firefox, Safari; password fallback works.
- [ ]**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.
- [ ] 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.
- [ ] 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).
- [ ] CSRF: state-changing without valid token rejected; with token succeeds (`middleware/csrf.cts`).
- [ ] Validation: bad/missing body → structured 4xx (`middleware/errorFormatter.cts`, `utils/apiError.cts`), never a raw 500 stack.
- [ ]**Error-shape audit (added 2026-07-10):** no route returns raw `err.message` on a 5xx — `grep -rn "err.message" routes` and verify each hit is either a whitelisted user-facing message or converted to `standardizeError`/`ApiError` (QA-B13-02 found 10 leaks).
- [ ]**Orphan-endpoint sweep (added 2026-07-10):** for each route file, confirm each endpoint has a caller (client `api.ts`, worker, or documented external consumer like the ICS feed). Endpoints with no caller are dead surface to delete (found: auth.cts's duplicate admin block, QA-B17-01) — and note any that are **less protected** than their canonical twin.
- [ ]**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.
- [ ]**Performance:** initial load + lazy route splitting OK on Slow 3G; large lists responsive; no memory leak over 10+ navigations; no duplicate/excess requests (React Query `staleTime`).
- [ ]**PWA/offline:** installs; manifest/icon correct; offline shell loads with graceful messaging; SW updates without stale-cache breakage.
- [ ]**Security spot-checks:** XSS in bill names/notes/category names/imported data escaped everywhere (defense = React auto-escaping + the restrictive custom `MarkdownText` renderer — https-only link hrefs, **no**`dangerouslySetInnerHTML` anywhere; NOT rehype-sanitize, which is unused, see QA-B14-03); no secrets (SimpleFIN token, SMTP creds, OIDC secret) in bundle/responses/logs; cookies `HttpOnly`/`Secure`/`SameSite`; `encryptionService` protects at-rest secrets, keys not committed. (Depth: `SECURITY_AUDIT.md`.)
- [ ]**Resilience:** kill API mid-session → recoverable errors, no data loss on next save; locked/corrupt SQLite surfaces clearly; SimpleFIN/SMTP/push down → graceful degrade; two-tab concurrent edits don't silently clobber.
- [ ]**Fault injection (systematic):** with a request-interception harness (Playwright `page.route`, or DevTools network overrides), force each page's API calls to **401 mid-session / 403 / 429 / 500 / network-timeout / malformed-JSON** and confirm the UI shows a recoverable error (toast or `ErrorBoundary` fallback), never a white screen, stuck spinner, or silent success. Do this per page, not once globally — each page handles failure differently.
- [ ]**Timezone/locale:** non-UTC tz + DST boundary — due dates and calendar stay correct.
- [ ]**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.
- [ ]**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.77–0.79) behave.
- [ ] Encryption key is never committed, logged, or returned in any API response.
- [ ] 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.
- [ ]`backupOperationLimiter` throttles admin backup/restore/cleanup; `skipRateLimitIfNoUsers` only relaxes limits on a genuinely empty instance (first-run), never afterward.
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.
> ⚠ **Status (2026-07-10): no census has ever been filled in** — Cycle 1 signed off on
> playbooks alone. Either fill these during Cycle 2's B0, or (preferred) implement the
> automated control-census e2e spec (IMP-CODE-07) and let the diff be the ledger.
> Density for planning: SpendingPage has 28 `onClick` handlers, Tracker 25,
> Subscriptions 23, Bills 22, Banking 18 — the manual version is a multi-day job. 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`.