diff --git a/HISTORY.md b/HISTORY.md index 1cfcad9..3e3538f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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 diff --git a/docs/QA_PLAN.md b/docs/QA_PLAN.md index e6917ac..d60eaee 100644 --- a/docs/QA_PLAN.md +++ b/docs/QA_PLAN.md @@ -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 @@ -105,28 +109,28 @@ 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 | -| B2 | Tracker (core) | `/` buckets, pay/skip/notes/overrides, balance cards, overdue, ledger, drift | seeded + adversarial | βœ… | 0 / 0 | -| B3 | Bills & schedules | `/bills` CRUD, custom schedules, reorder, merchant rules, historical import | adversarial | βœ… | 0 / 0 | -| B4 | Subscriptions & Categories | `/subscriptions`, catalog, `/categories`, groups, reorder | seeded | βœ… | 0 / 0 | -| B5 | Reporting reconciliation | `/summary`, `/calendar`, `/analytics`, `/health` cross-check totals | seeded + large + **live SimpleFIN DB** | βœ… | 0 / 4 | -| B6 | Spending | `/spending` YNAB view, averages, cover-overspending, safe-to-spend | seeded + edge months | βœ… | 0 / 1 | -| B7 | Debt planning (math) | `/snowball`, `/payoff` APR/amortization vs hand-calc | edge (APR=0, $0 debt) | βœ… | 0 / 2 | -| B8 | Banking & bank sync | `/bank-transactions`, SimpleFIN sync, matching, merchant/store, advisory filter | seeded txns + **live SimpleFIN sync** | βœ… | 0 / 0 | -| B9 | Data lifecycle | `/data` import (XLSX/CSV/SQLite), export, ICS feed, backups round-trip | empty + seeded | βœ… | 0 / 1 | -| B10 | Notifications & workers | email + ntfy/Gotify/Discord/Telegram, reminders, cron workers | seeded | βœ… | 0 / 1 | -| B11 | Admin panel | users, login mode, auth methods, backups, cleanup, status, onboarding | admin | βœ… | 0 / 0 | -| B12 | Settings, Profile & global UI | `/settings`, `/profile`, static pages, command palette, sidebar/nav | any | βœ… | 0 / 0 | -| B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | βœ… | 0 / 1 | -| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | βœ… | 0 / 4 | -| B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | βœ… | 0 / 0 | -| B16 | Migrations, secrets & deploy | migration idempotency/rollback/fresh==migrated, encryption-key lifecycle, `docker-entrypoint` (perms/first-run/migrate), update-check phone-home | scratch + docker | βœ… | 0 / 1 | -| B17 | **Code health & consolidation** (IMP) | duplication/DRY, dead code, overlapping modules to merge, oversized files to split, one canonical path per concern | whole repo | ⬜ | 0 / 0 | -| B18 | **UX & information architecture** (IMP) | core-flow friction, empty/loading/error states, feedback/affordances, nav/menu discoverability, surfacing actions into sensible menus | any | ⬜ | 0 / 0 | +| # | 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 | +| B2 | Tracker (core) | `/` buckets, pay/skip/notes/overrides, balance cards, overdue, ledger, drift | seeded + adversarial | βœ… | 0 / 0 | +| B3 | Bills & schedules | `/bills` CRUD, custom schedules, reorder, merchant rules, historical import | adversarial | βœ… | 0 / 0 | +| B4 | Subscriptions & Categories | `/subscriptions`, catalog, `/categories`, groups, reorder | seeded | βœ… | 0 / 0 | +| B5 | Reporting reconciliation | `/summary`, `/calendar`, `/analytics`, `/health` cross-check totals | seeded + large + **live SimpleFIN DB** | βœ… | 0 / 4 | +| B6 | Spending | `/spending` YNAB view, averages, cover-overspending, safe-to-spend | seeded + edge months | βœ… | 0 / 1 | +| B7 | Debt planning (math) | `/snowball`, `/payoff` APR/amortization vs hand-calc | edge (APR=0, $0 debt) | βœ… | 0 / 2 | +| B8 | Banking & bank sync | `/bank-transactions`, SimpleFIN sync, matching, merchant/store, advisory filter | seeded txns + **live SimpleFIN sync** | βœ… | 0 / 0 | +| B9 | Data lifecycle | `/data` import (XLSX/CSV/SQLite), export, ICS feed, backups round-trip | empty + seeded | βœ… | 0 / 1 | +| B10 | Notifications & workers | email + ntfy/Gotify/Discord/Telegram, reminders, cron workers | seeded | βœ… | 0 / 1 | +| B11 | Admin panel | users, login mode, auth methods, backups, cleanup, status, onboarding | admin | βœ… | 0 / 0 | +| B12 | Settings, Profile & global UI | `/settings`, `/profile`, static pages, command palette, sidebar/nav | any | βœ… | 0 / 0 | +| B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | βœ… | 0 / 1 | +| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | βœ… | 0 / 4 | +| B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | βœ… | 0 / 0 | +| B16 | Migrations, secrets & deploy | migration idempotency/rollback/fresh==migrated, encryption-key lifecycle, `docker-entrypoint` (perms/first-run/migrate), update-check phone-home | scratch + docker | βœ… | 0 / 1 | +| B17 | **Code health & consolidation** (IMP) | duplication/DRY, dead code, overlapping modules to merge, oversized files to split, one canonical path per concern | whole repo | ⬜ | 0 / 0 | +| B18 | **UX & information architecture** (IMP) | core-flow friction, empty/loading/error states, feedback/affordances, nav/menu discoverability, surfacing actions into sensible menus | any | ⬜ | 0 / 0 | > After B15, if any batch is πŸ” or has open S1/S2, loop back. Then start a new > cycle from B0 against the next build/version. @@ -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 @@ -152,13 +156,15 @@ One row per full QA cycle (Phase 1 find β†’ Phase 2 fix β†’ … β†’ Phase 5 re-r 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.** | +| 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 (B0–B2); **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) @@ -175,9 +181,10 @@ fixing. Keep only **Open / Fixing / Fixed** rows here. Once a finding is **Severity:** S1 Critical Β· S2 Major Β· S3 Minor Β· S4 Cosmetic Β· IMP Improvement (see [Appendix A](#appendix-a--severity-definitions)). **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)_ | | | | | | +| ID | Sev | Area (`file:line`) | Summary | Status | Notes / repro | +| ------------------------------------------------------------------------------------------------------------------------------ | --- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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): @@ -214,14 +221,18 @@ 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 | +| 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). --- @@ -257,12 +269,12 @@ 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` | -| Docker | `docker-compose up` | per compose config | +| 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` | +| Docker | `docker-compose up` | per compose config | - 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. @@ -273,18 +285,19 @@ 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 | -| Role | `user`, `admin`, default admin (first-run) | -| Auth mode | Multi-user, single-user | -| Density | Normal + compact desktop | -| Network | Online, Slow 3G, offline (PWA shell) | -| Data state | Empty, seeded demo, large/stress, adversarial | +| 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 | +| Role | `user`, `admin`, default admin (first-run) | +| Auth mode | Multi-user, single-user | +| Density | Normal + compact desktop | +| Network | Online, Slow 3G, offline (PWA shell) | +| 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). @@ -292,12 +305,12 @@ 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 | +| 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.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,72 +362,83 @@ 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 "