From b08c054f6a8f280d3d7eba3777e0dce4d7b29096 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 10 Jul 2026 17:47:50 -0500 Subject: [PATCH] chore: QA plan recon update, dep bumps, export/server/vite tweaks (post-v0.41.0) --- docs/QA_PLAN.md | 397 +++++++++++++---------- package-lock.json | 803 ++++++++-------------------------------------- package.json | 5 +- routes/export.cts | 18 +- server.cts | 3 +- vite.config.mjs | 21 +- 6 files changed, 393 insertions(+), 854 deletions(-) diff --git a/docs/QA_PLAN.md b/docs/QA_PLAN.md index e6917ac..8a2c85b 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 +**Version target:** v0.41.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/WebAuthn/OIDC, SMTP delivery, cross-browser, PWA-offline, load, container build) carried to Cycle 2 as non-blocking. +**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. + 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,14 @@ 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 | 0 (find-only) | 🔄 **Blind-spot recon pass** (deps/dead-code/coverage audit, no UI batches). Logged: high-vuln deps invisible to CI's critical-only gate (QA-B0-02) · package.json 0.40.0 vs HISTORY v0.41.0 drift (QA-B0-03) · WebAuthn ghost feature — backend + wrappers, zero UI (QA-B1-01) · 38 `@ts-nocheck` files incl. all 29 routes ⇒ vacuous typecheck (QA-B0-04) · 10 raw `err.message` 5xx leaks (QA-B13-02) · dead duplicate admin routes in auth.cts (QA-B17-01). IMP: dead files/exports (2.2 MB unused image, broken legacy test scripts, knip items) · oversized pages · runtime seed JSONs in `docs/` · automated control census. **Plan updated:** stale `.js`→`.cts` refs fixed; B0 gained dependency-freshness/dead-code/ts-nocheck/version-coherence recon steps; B13 gained error-shape + orphan-endpoint sweeps; Appendix C gained `/admin/about`, `/admin/roadmap`, `/status`-redirect + ICS-feed rows; Appendix E marked never-filled. Verified-clean along the way: no SQL-injection paths found (whitelists + params), no `dangerouslySetInnerHTML`/`as any` in client, calendar feed token-gated, no data/build artifacts tracked in git, all 29 route files mounted. | **Result key:** 🔄 in progress · 🔁 findings fixed, re-run required · ✅ clean (zero findings — QA complete) @@ -175,9 +180,14 @@ 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-02 | S2 | `package.json` deps | **Known high-severity vulns ship in prod deps and CI can't see them**: `nodemailer` <9 (raw-option file-read/SSRF, GHSA-p6gq-j5cr-w38f; fix = major bump to 9.x) and `xlsx` (prototype pollution GHSA-4r6h-8v6p-xvw6 + ReDoS GHSA-5pgg-2g8v-p4x9, **no registry fix**; it parses **user-uploaded** spreadsheets in `spreadsheetImportService`). CI audit gate is `--audit-level=critical` so high vulns pass silently. | 🔴 Open | `npm audit` (2026-07-10: 4 vulns — 3 high, 1 moderate). Fix: bump nodemailer→9 (check API changes at call sites in `notificationService`); for xlsx either move to the maintained SheetJS dist/`exceljs`, or document risk-acceptance + tighten import validation. Raise CI gate to `--audit-level=high` once clean. | +| 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. | **Finding template** (paste a new row above; keep the full write-up here until archived): @@ -214,14 +224,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 (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 | --- @@ -246,10 +260,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 +272,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 +288,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 +308,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 +365,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 "