chore: QA plan recon update, dep bumps, export/server/vite tweaks (post-v0.41.0)
This commit is contained in:
parent
4294a1da8c
commit
b08c054f6a
397
docs/QA_PLAN.md
397
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 "<Route" client/App.tsx` — every path present here must appear in a batch playbook and Appendix C. *(The whole client is now TypeScript — `find client -name '*.jsx'` returns 0 — so recon greps target `.tsx`/`.ts`; `npm run typecheck` is a guard alongside build/lint/tests.)*
|
||||
|
||||
- [ ] **Client routes** — `grep -nE "<Route" client/App.tsx` — every path present here must appear in a batch playbook and Appendix C. _(The whole client is now TypeScript — `find client -name '*.jsx'` returns 0 — so recon greps target `.tsx`/`.ts`; `npm run typecheck` is a guard alongside build/lint/tests.)_
|
||||
- [ ] **Pages** — `ls client/pages/` — every page has an owning batch.
|
||||
- [ ] **Sidebar / nav entries** — `grep -nE "to:|label:|Only" client/components/layout/Sidebar.tsx` — new nav links (incl. conditional ones like `simplefinOnly`) are covered.
|
||||
- [ ] **API route mounts** — `grep -nE "app.use\('/api" server.js` — every mounted route group is in B13's list and mapped in Appendix C.
|
||||
- [ ] **API route mounts** — `grep -nE "require\('\./routes/" server.cts` (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.
|
||||
- [ ] **Middleware & workers** — `ls middleware/ workers/` (+ `services/*Worker*`, `*Scheduler*`) — each is covered (csrf/rateLimiter/securityHeaders/requireAuth → B13; dailyWorker/bankSyncWorker/backupScheduler → B10).
|
||||
- [ ] **Migrations & deploy** — new `db/database.js` migrations, `Dockerfile`/`docker-entrypoint.sh` changes, and `encryptionService`/`updateCheckService` behavior are covered by [B16](#b16--migrations-secrets--deployment).
|
||||
- [ ] **Interactive-control census (makes "every button tested" *provable*)** — for each page, enumerate every button, link, toggle/switch, checkbox, select, text/number/date/file input, tab, menu, and filter control, and record it in a per-page control checklist (template: [Appendix E](#appendix-e--per-page-control-census)). A control that isn't on a checklist hasn't been tested — the census is the completeness guarantee the batch playbooks alone don't give you. Quick starting inventory: `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages client/components` and `grep -rn "onClick=" client/pages/<Page>.tsx`.
|
||||
- [ ] **Migrations & deploy** — new migrations (`db/database.cts` + `db/migrations/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).
|
||||
|
||||
**Update the plan (do this now, not later)** — for anything the recon surfaced that isn't already covered:
|
||||
|
||||
- [ ] Add it to the relevant batch playbook (or create a new batch and a row in the [§1 table](#1-batch-plan--progress-tracker)).
|
||||
- [ ] Add/adjust its entry in [Appendix C](#appendix-c--page--route--api-quick-map).
|
||||
- [ ] Note the plan update in the [Cycle Log](#11-qa-cycle-log) row for this cycle.
|
||||
- [ ] If a whole surface is *missing* from the product that the plan expected (page removed/renamed), reconcile the plan too — don't test ghosts.
|
||||
- [ ] If a whole surface is _missing_ from the product that the plan expected (page removed/renamed), reconcile the plan too — don't test ghosts.
|
||||
|
||||
### B-UI — Design-system primitives
|
||||
|
||||
**Test each shared control once, thoroughly, in isolation — a bug here breaks every page at once.** Drive them wherever they're already mounted (or a scratch page); run each against the [per-control state matrix](#6-cross-cutting-checks-every-page) × light/dark × keyboard-only. One finding row per primitive.
|
||||
|
||||
| Primitive (`client/components/ui/`) | Must verify |
|
||||
|---|---|
|
||||
| `button.tsx` | every variant (default/destructive/outline/ghost/link) + size; **disabled truly blocks click**; loading state; focus ring; Enter/Space activate |
|
||||
| `input.tsx` | text/number/password/date/search/file types; placeholder; disabled/read-only; error styling; paste/autofill; number-input rules above |
|
||||
| `select.tsx` (Radix) | opens by mouse **and** keyboard; type-ahead; long lists scroll; onChange fires in **Firefox+Safari**; disabled options; value persists; Esc closes |
|
||||
| `checkbox.tsx` / `switch.tsx` | toggles by click **and** Space; indeterminate (if used); disabled; label click toggles; controlled value round-trips |
|
||||
| Primitive (`client/components/ui/`) | Must verify |
|
||||
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `button.tsx` | every variant (default/destructive/outline/ghost/link) + size; **disabled truly blocks click**; loading state; focus ring; Enter/Space activate |
|
||||
| `input.tsx` | text/number/password/date/search/file types; placeholder; disabled/read-only; error styling; paste/autofill; number-input rules above |
|
||||
| `select.tsx` (Radix) | opens by mouse **and** keyboard; type-ahead; long lists scroll; onChange fires in **Firefox+Safari**; disabled options; value persists; Esc closes |
|
||||
| `checkbox.tsx` / `switch.tsx` | toggles by click **and** Space; indeterminate (if used); disabled; label click toggles; controlled value round-trips |
|
||||
| `dialog.tsx` / `alert-dialog.tsx` / `confirm-dialog.tsx` / `input-dialog.tsx` | open/close; **focus trap + restore**; Esc closes; overlay click behaves; **Cancel actually cancels (no side effect)**; Confirm fires once; scroll-lock releases |
|
||||
| `dropdown-menu.tsx` | keyboard arrow nav; Esc; submenu; disabled items; click-outside closes; no clipping at viewport edge |
|
||||
| `tabs.tsx` | arrow-key nav; active state; content swaps; deep-link/refresh keeps tab (if applicable) |
|
||||
| `tooltip.tsx` | hover **and** keyboard-focus show it; dismiss on blur; touch behavior; not a11y-only info trap |
|
||||
| `table.tsx` | header/zebra/hover; horizontal scroll on narrow viewport (no page h-scroll); empty state |
|
||||
| `collapsible.tsx` | expand/collapse animation; state persists; keyboard operable |
|
||||
| `sonner.tsx` (toast) | success/error/loading; **stack + dismiss**; auto-dismiss timing; doesn't cover primary actions; announced to SR |
|
||||
| `save-status.tsx` | idle/saving/saved/error transitions reflect real autosave (`useAutoSave.test.ts`) |
|
||||
| `Skeleton.tsx` | matches final layout (no jump); no infinite skeleton on error |
|
||||
| `badge.tsx` / `card.tsx` / `separator.tsx` / `label.tsx` | contrast in dark mode; label `htmlFor` focuses its control; no overflow on long text |
|
||||
| `theme-toggle.tsx` | light↔dark↔system; applied **before first paint** (no flash); persists across reload |
|
||||
| `dropdown-menu.tsx` | keyboard arrow nav; Esc; submenu; disabled items; click-outside closes; no clipping at viewport edge |
|
||||
| `tabs.tsx` | arrow-key nav; active state; content swaps; deep-link/refresh keeps tab (if applicable) |
|
||||
| `tooltip.tsx` | hover **and** keyboard-focus show it; dismiss on blur; touch behavior; not a11y-only info trap |
|
||||
| `table.tsx` | header/zebra/hover; horizontal scroll on narrow viewport (no page h-scroll); empty state |
|
||||
| `collapsible.tsx` | expand/collapse animation; state persists; keyboard operable |
|
||||
| `sonner.tsx` (toast) | success/error/loading; **stack + dismiss**; auto-dismiss timing; doesn't cover primary actions; announced to SR |
|
||||
| `save-status.tsx` | idle/saving/saved/error transitions reflect real autosave (`useAutoSave.test.ts`) |
|
||||
| `Skeleton.tsx` | matches final layout (no jump); no infinite skeleton on error |
|
||||
| `badge.tsx` / `card.tsx` / `separator.tsx` / `label.tsx` | contrast in dark mode; label `htmlFor` focuses its control; no overflow on long text |
|
||||
| `theme-toggle.tsx` | light↔dark↔system; applied **before first paint** (no flash); persists across reload |
|
||||
|
||||
- [ ] Every primitive above passes its row in light **and** dark, keyboard-only, at mobile width.
|
||||
- [ ] Axe scan (see B14) on a page densely using primitives → zero critical violations.
|
||||
|
||||
### B1 — Auth & authorization
|
||||
|
||||
- [ ] **Password:** valid login → correct landing (Tracker for `user`, `/admin` for default admin); wrong password → clear error, no user-enumeration timing/message difference; logout clears session; expired session redirects and preserves `state.from`; session persists across refresh.
|
||||
- [ ] **Rate limiting:** repeated failed logins throttled (`loginLimiter`/`loginUsernameLimiter`), clear message, resets.
|
||||
- [ ] **TOTP:** enroll (QR + secret), code accepted, backup codes work once, login prompts for TOTP, wrong code rejected+throttled, disable requires re-auth.
|
||||
- [ ] **WebAuthn:** register/login/remove passkey in Chrome, Firefox, Safari; password fallback works.
|
||||
- [ ] **WebAuthn:** ⚠ **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.
|
||||
- [ ] **OIDC/Authentik:** SSO flow creates/links account; admin config errors surface cleanly; `oidcLimiter` throttles.
|
||||
- [ ] **Roles/guards:** `user` blocked from `/admin*`, `/status` (redirect) and admin APIs (403); default admin forced to `/admin`; single-user bypass correct but admin surfaces still protected; unauth API → 401.
|
||||
- [ ] **Data isolation (critical):** user A cannot read/modify user B's bills, payments, transactions, categories, snowball plans — test by ID enumeration on the API.
|
||||
- [ ] **CSRF:** state-changing request without a valid token → rejected.
|
||||
|
||||
### B2 — Tracker (`/`)
|
||||
|
||||
- [ ] Month nav (prev/next/jump), current month highlighted, data reloads per month.
|
||||
- [ ] Bills land in correct `1–14` / `15–31` bucket by due date; pin-due sorting works.
|
||||
- [ ] Quick pay marks paid + updates balance cards/progress; undo works; no double-count.
|
||||
|
|
@ -428,6 +455,7 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
|
|||
- [ ] Editable cells autosave; Esc cancels; invalid input handled. Mobile rows equal desktop actions. Compact mode intact.
|
||||
|
||||
### B3 — Bills (`/bills`)
|
||||
|
||||
- [ ] Create with all fields (name, amount, due date, category, schedule, account, autopay, active range).
|
||||
- [ ] Edit propagates to Tracker/Summary/Calendar/Analytics; delete confirms + handles orphan payments/history.
|
||||
- [ ] Custom schedules (weekly/biweekly/monthly/quarterly/annual/custom): next-due & occurrences correct across month/year boundaries.
|
||||
|
|
@ -436,44 +464,52 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
|
|||
- [ ] BillModal open/close, validation, cancel discards unsaved changes.
|
||||
|
||||
### B4 — Subscriptions & Categories
|
||||
|
||||
- [ ] Subscriptions: add/edit/delete, active/cancelled, renewal & annual→monthly normalization; totals feed Tracker/Summary/Analytics.
|
||||
- [ ] Catalog: browse/search, add-from-catalog pre-fills.
|
||||
- [ ] Categories: create/edit/delete (in-use handled: reassign/prevent); groups create/assign/reorder (`categoryGroups`/`categoryReorder` tests); colors/icons consistent on Tracker/Spending/Analytics.
|
||||
|
||||
### B5 — Reporting reconciliation
|
||||
|
||||
- [ ] Summary totals (paid/unpaid/overdue/remaining) reconcile with Tracker for the same month; income breakdown modal matches.
|
||||
- [ ] Calendar plots bills/payments on correct days (**timezone**: a bill due on the 1st must not render on the 31st); day totals correct.
|
||||
- [ ] Analytics charts render with data AND empty (no broken SVG/`NaN` axes); period selectors update all charts; figures reconcile with Summary/Tracker; large dataset perf OK.
|
||||
- [ ] Health indicators compute from real data, no crash on empty; recommendations sane.
|
||||
|
||||
### B6 — Spending (`/spending`)
|
||||
|
||||
- [ ] Category-group view assigned/spent/available math correct; 3-month averages correct.
|
||||
- [ ] Cover-overspending reallocates funds correctly and is reversible.
|
||||
- [ ] Safe-to-spend matches Tracker (`safeToSpend.test.js`); month nav; empty/partial months handled.
|
||||
|
||||
### B7 — Debt planning (`/snowball`, `/payoff`)
|
||||
|
||||
- [ ] Add debts (balance/APR/min); snowball vs avalanche ordering correct.
|
||||
- [ ] Projection + amortization vs a **hand-calculated** example; APR=0 and already-paid debts correct.
|
||||
- [ ] Extra-payment/budget updates payoff date + total interest; chart renders; plan history saves/restores; status banner accurate.
|
||||
- [ ] Edge: single debt, many debts, `$0` debt, negative/absurd inputs rejected.
|
||||
|
||||
### B8 — Banking (`/bank-transactions`)
|
||||
|
||||
- [ ] Ledger loads/virtualizes/filters (date/account/amount/merchant/status).
|
||||
- [ ] Transaction matching (match/unmatch), auto-match review approve/reject, no double-match (`transactionMatchService.test.js`).
|
||||
- [ ] Merchant/store matching rules + confidence/duplicates; advisory non-bill filter flags/hides with override.
|
||||
- [ ] Matched payments reflect on Tracker/ledger without double-counting; category picker persists.
|
||||
|
||||
### B9 — Data lifecycle (`/data`)
|
||||
|
||||
- [ ] Imports: spreadsheet (XLSX/CSV) map/preview/commit, malformed rejected, dup/partial handled; transaction CSV (`csvTransactionImportService.test.js`) dedupe + parsing; SQLite user import version-checked + confirms overwrite; seed demo data safe; import history lists + rollback.
|
||||
- [ ] Exports: download SQLite **round-trips** (export → fresh account → import → matches); Excel export opens uncorrupted; ICS calendar feed valid in a client AND properly **token-gated** (route mounts before auth — verify not open).
|
||||
- [ ] Backups: manual + scheduled restorable on a scratch instance; permissions not world-readable; old backups pruned (`backupAndCleanup.test.js`).
|
||||
|
||||
### B10 — Notifications & workers
|
||||
|
||||
- [ ] Each channel (email/SMTP, ntfy, Gotify, Discord, Telegram): test message delivers; bad token/URL → clear error, logged, no secret leak.
|
||||
- [ ] Reminders fire at configured lead time for upcoming/overdue; no duplicates; paid/skipped excluded; respects per-user prefs.
|
||||
- [ ] Workers: `dailyWorker`, `bankSyncWorker` (interval + guardrails), `backupScheduler` run on schedule; errors caught/logged, don't crash server, next run unblocked.
|
||||
|
||||
### B11 — Admin panel (`/admin`)
|
||||
|
||||
- [ ] Onboarding wizard completes without a broken state.
|
||||
- [ ] Users table: add/edit-role/reset-pw/disable/delete; **cannot remove the last admin**.
|
||||
- [ ] Login mode switch single↔multi verified live, no lockout; auth-methods enable/disable + bad config surfaced.
|
||||
|
|
@ -482,23 +518,29 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
|
|||
- [ ] Privacy admin edits reflect on public `/privacy`; system status metrics/versions/jobs accurate (`statusService.test.js`); admin actions rate-limited + audited (`auditService` — spot-check log).
|
||||
|
||||
### B12 — Settings, Profile & global UI
|
||||
|
||||
- [ ] Settings: theme (light/dark/system) persists; notification prefs save + reflect in B10; display/density/period/search-panel prefs persist; invalid rejected.
|
||||
- [ ] Profile: change password (current required, invalidates sessions), manage 2FA/passkeys, sessions revoke (`profileRoute.test.js`).
|
||||
- [ ] Static: About (public + admin, version shown), Privacy, Release Notes (dialog once per `user`, dismiss persists), Roadmap (admin), NotFound friendly + way home.
|
||||
- [ ] Global: command palette (`Ctrl+K`) search/keyboard/Esc, hidden for default admin; sidebar collapse/expand + mobile overlay (check overflow issue in `docs/UI_IMPROVEMENTS.md`); toasts stack/dismiss; page transitions no flash/double-fetch; theme applied before first paint.
|
||||
|
||||
### B13 — API / backend direct
|
||||
|
||||
Route groups: `auth`, `auth/oidc`, `admin`, `tracker`, `bills`, `subscriptions`, `payments`, `data-sources`, `transactions`, `matches`, `categories`, `settings`, `user`, `calendar`, `summary`, `monthly-starting-amounts`, `analytics`, `spending`, `snowball`, `notifications`, `status`, `about`, `about-admin`, `privacy`, `version`, `profile`, `export`, `import`/`imports`.
|
||||
|
||||
- [ ] Auth: unauth → 401, wrong role → 403, right role → 200.
|
||||
- [ ] CSRF: state-changing without valid token rejected; with token succeeds (`middleware/csrf.js`).
|
||||
- [ ] Validation: bad/missing body → structured 4xx (`middleware/errorFormatter.js`, `utils/apiError.js`), never a raw 500 stack.
|
||||
- [ ] CSRF: state-changing without valid token rejected; with token succeeds (`middleware/csrf.cts`).
|
||||
- [ ] Validation: bad/missing body → structured 4xx (`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).
|
||||
- [ ] IDOR/isolation: other user's resource by id → 403/404, no leak.
|
||||
- [ ] Rate limits: login/admin/export/import/OIDC limiters trigger + reset (`middleware/rateLimiter.js`).
|
||||
- [ ] Rate limits: login/admin/export/import/OIDC limiters trigger + reset (`middleware/rateLimiter.cts`).
|
||||
- [ ] **Orphan-endpoint sweep (added 2026-07-10):** 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.
|
||||
- [ ] Money in **integer cents** end-to-end (per `docs/cents-migration-plan.md`); API and DB agree; no float drift.
|
||||
- [ ] Idempotency: repeated create doesn't duplicate; concurrent edits resolve sanely.
|
||||
- [ ] Consistent error JSON + correct status codes; security headers present (`middleware/securityHeaders.js`); public routes (`about`/`privacy`/`version`/calendar feed) leak nothing sensitive.
|
||||
- [ ] Consistent error JSON + correct status codes; security headers present (`middleware/securityHeaders.cts`); public routes (`about`/`privacy`/`version`/calendar feed) leak nothing sensitive.
|
||||
|
||||
### B14 — Non-functional
|
||||
|
||||
- [ ] **a11y (manual):** keyboard-only reach/operate every control, visible focus, skip-link works; screen-reader labels/roles (Radix `aria-*`); WCAG-AA contrast light+dark; modals trap+restore focus, Esc closes; errors announced not color-only.
|
||||
- [ ] **a11y (automated):** run **axe-core** on every page (`@axe-core/playwright`, or `jest-axe` for component-level) — **zero critical/serious** violations; triage moderate. Wire it into the E2E suite so it re-runs every cycle, not just once.
|
||||
- [ ] **Visual regression:** capture a baseline screenshot per page × {desktop, mobile} × {light, dark} (Playwright `toHaveScreenshot`); diff against baseline each cycle. Every non-trivial pixel diff is either an intended change (update the baseline in the same commit) or a finding — never ignore it. This is what makes "every page looks right" repeatable instead of eyeballed.
|
||||
|
|
@ -510,7 +552,9 @@ Route groups: `auth`, `auth/oidc`, `admin`, `tracker`, `bills`, `subscriptions`,
|
|||
- [ ] **Timezone/locale:** non-UTC tz + DST boundary — due dates and calendar stay correct.
|
||||
|
||||
### B15 — Regression & sign-off
|
||||
|
||||
Run on the **production build** (`npm start`), not dev:
|
||||
|
||||
- [ ] `npm run ci` green. Log in as `user` and `admin`.
|
||||
- [ ] `npm run test:e2e` green (Playwright smoke + axe + visual-regression baselines match, §4.4).
|
||||
- [ ] Tracker: create bill → quick-pay → skip another → add note; reflected on Summary/Calendar/Analytics.
|
||||
|
|
@ -522,102 +566,112 @@ Run on the **production build** (`npm start`), not dev:
|
|||
- [ ] Confirm [exit criteria](#appendix-b--exit--sign-off-criteria).
|
||||
|
||||
### B16 — Migrations, secrets & deployment
|
||||
|
||||
Added Cycle 1 (previously uncovered). These run on every boot / container start and
|
||||
touch money columns and at-rest secrets — a bug here corrupts data or leaks/breaks
|
||||
secrets silently.
|
||||
|
||||
**Migrations** (`db/database.js` migration system, `scripts/migrate-db.js`, `schema_migrations`, `rollbackMigration`)
|
||||
**Migrations** (`db/database.cts` + `db/migrations/*.cts`, `scripts/migrate-db.js`, `schema_migrations`, `rollbackMigration`)
|
||||
|
||||
- [ ] **Idempotent:** boot twice on the same DB → second run applies nothing ("Skipping already applied"), no errors, no duplicate rows/columns.
|
||||
- [ ] **Fresh == migrated:** a brand-new DB (schema.sql + all migrations) has the same schema as a DB migrated up from an old version — same tables/columns/indexes, money columns are **integer cents**.
|
||||
- [ ] **Rollback:** `rollbackMigration` on the latest migration reverts cleanly and re-applying works; partial/failed migration leaves the DB consistent (transactions per migration).
|
||||
- [ ] **Money conversions correct:** v1.03 (dollars→cents) and v1.04 (template JSON) convert exact values, no ×100 drift, run once only.
|
||||
- [ ] Migrating a large/real DB doesn't lose or duplicate bills/payments/categories.
|
||||
|
||||
**Encryption-key lifecycle** (`services/encryptionService.js`, `TOKEN_ENCRYPTION_KEY`, HKDF v1/v2)
|
||||
**Encryption-key lifecycle** (`services/encryptionService.cts`, `TOKEN_ENCRYPTION_KEY`, HKDF v1/v2)
|
||||
|
||||
- [ ] **Key present:** secrets (SMTP pw, OIDC secret, push tokens, login IP/UA) encrypt at rest and decrypt correctly.
|
||||
- [ ] **Key missing:** app boots; secret features degrade gracefully (no crash); confirm secrets are **not** silently stored/served in plaintext.
|
||||
- [ ] **Key rotated/wrong:** old ciphertext fails to decrypt **gracefully** (no crash, no stack leak); `safeDecrypt` fallback path is sane; re-encryption migrations (v0.77–0.79) behave.
|
||||
- [ ] Encryption key is never committed, logged, or returned in any API response.
|
||||
|
||||
**Container / deploy** (`Dockerfile`, `docker-compose.yml`, `docker-entrypoint.sh`, `deploy.sh`)
|
||||
|
||||
- [ ] Image **builds**; container **starts**; app reachable; `/api/version` responds.
|
||||
- [ ] Entrypoint: creates `DATA_DIR`/`DB_DIR`/`BACKUP_DIR`, sets **`chmod 700`** (not world-readable), `chown`s to the non-root `bill` user, runs migrations when `RUN_DB_MIGRATIONS=true`.
|
||||
- [ ] Data **persists** across container restart (mounted volume); DB not re-created.
|
||||
- [ ] Runs as **non-root**; secrets come from env, not baked into the image.
|
||||
|
||||
**Update check / phone-home** (`services/updateCheckService.js`)
|
||||
**Update check / phone-home** (`services/updateCheckService.cts`)
|
||||
|
||||
- [ ] Confirm the external request to `REPO_API_URL` (default `dream.scheller.ltd`) is **disclosed** (privacy page) and **opt-out-able**; it must send no user data, only fetch the latest release; failure/offline degrades silently.
|
||||
|
||||
**Rate-limiter completeness** (`middleware/rateLimiter.js`) — beyond B13's list
|
||||
**Rate-limiter completeness** (`middleware/rateLimiter.cts`) — beyond B13's list
|
||||
|
||||
- [ ] `backupOperationLimiter` throttles admin backup/restore/cleanup; `skipRateLimitIfNoUsers` only relaxes limits on a genuinely empty instance (first-run), never afterward.
|
||||
|
||||
### B17 — Code health & consolidation (IMP)
|
||||
|
||||
An **improvement** batch: hunt for ways to make the codebase smaller, clearer, and more
|
||||
consistent — *without changing behavior*. Every candidate is logged as an `IMP-CODE-*`
|
||||
consistent — _without changing behavior_. Every candidate is logged as an `IMP-CODE-*`
|
||||
row in §2.1 with a concrete proposal; nothing is refactored silently. Consolidation
|
||||
lands only when it's behavior-preserving **and** covered by existing or added tests.
|
||||
|
||||
- [ ] **Duplication / DRY:** find logic copy-pasted across services/routes/components and
|
||||
propose a shared helper. Known hot spots: money formatting/rounding (`utils/money.js`
|
||||
vs inline), the `resolveDueDate` occurrence gate (must stay one implementation),
|
||||
error-response shaping (`utils/apiError.js` vs ad-hoc), React data-fetch patterns
|
||||
(repeated `useQuery` + toast + error handling → shared hooks).
|
||||
propose a shared helper. Known hot spots: money formatting/rounding (`utils/money.mts`
|
||||
vs inline), the `resolveDueDate` occurrence gate (must stay one implementation),
|
||||
error-response shaping (`utils/apiError.cts` vs ad-hoc), React data-fetch patterns
|
||||
(repeated `useQuery` + toast + error handling → shared hooks), **duplicate route
|
||||
handlers across files** (auth.cts vs admin.cts, QA-B17-01).
|
||||
- [ ] **Dead / unused code:** unused exports, unreachable branches, orphaned files,
|
||||
commented-out blocks, unused deps (`depcheck`), unused UI components/CSS, leftover
|
||||
scaffolding. Propose deletion (verify no dynamic/`require`-by-string use first).
|
||||
commented-out blocks, unused deps (`depcheck`), unused UI components/CSS, leftover
|
||||
scaffolding. Propose deletion (verify no dynamic/`require`-by-string use first).
|
||||
- [ ] **Overlapping modules:** services that do similar work and could merge or share a
|
||||
core — e.g. the matching family (`matchSuggestionService`, `transactionMatchService`,
|
||||
`merchantStoreMatchService`), the bank-sync family (`bankSyncService`,
|
||||
`bankSyncWorker`, `bankSyncConfigService`, `simplefinService`). Map responsibilities;
|
||||
propose a consolidation only where it removes real duplication, not just moves it.
|
||||
core — e.g. the matching family (`matchSuggestionService`, `transactionMatchService`,
|
||||
`merchantStoreMatchService`), the bank-sync family (`bankSyncService`,
|
||||
`bankSyncWorker`, `bankSyncConfigService`, `simplefinService`). Map responsibilities;
|
||||
propose a consolidation only where it removes real duplication, not just moves it.
|
||||
- [ ] **Oversized / low-cohesion files:** split by concern where it aids navigation
|
||||
(e.g. `db/database.js` is very large — migrations vs query helpers vs settings could
|
||||
be separate modules). Propose the seams; don't split for its own sake.
|
||||
(`db/database` was split in IMP-CODE-02; current largest: `SubscriptionsPage.tsx`
|
||||
2,371 ln and `TrackerPage.tsx` 1,943 ln — see IMP-CODE-05). Propose the seams;
|
||||
don't split for its own sake.
|
||||
- [ ] **One canonical path per concern:** cents handling, date/tz, CSRF, error shape,
|
||||
pagination — confirm there's a single blessed way and flag divergences.
|
||||
pagination — confirm there's a single blessed way and flag divergences.
|
||||
- [ ] **Consistency:** naming, file layout, async patterns, import ordering; a lint rule
|
||||
that would prevent a class of the bugs found in earlier batches is itself an IMP.
|
||||
that would prevent a class of the bugs found in earlier batches is itself an IMP.
|
||||
- [ ] **Test/infra dedupe:** repeated test setup → shared fixtures/helpers; flag coverage
|
||||
gaps a consolidation would risk.
|
||||
gaps a consolidation would risk.
|
||||
|
||||
### B18 — UX & information architecture / menus (IMP)
|
||||
|
||||
An **improvement** batch focused on the person using the app: is every feature
|
||||
*discoverable*, is every core flow *smooth*, and does every action live *where a user
|
||||
would look for it*? Candidates are logged as `IMP-UX-*` or `IMP-IA-*` in §2.1 with a
|
||||
_discoverable_, is every core flow _smooth_, and does every action live _where a user
|
||||
would look for it_? Candidates are logged as `IMP-UX-*` or `IMP-IA-*` in §2.1 with a
|
||||
concrete before/after proposal. Walk the app as a real user (both `user` and `admin`,
|
||||
desktop and mobile, light and dark), not just as a tester.
|
||||
|
||||
**Information architecture & menus**
|
||||
|
||||
- [ ] **Discoverability:** is any feature buried, orphaned, or reachable only by typing a
|
||||
URL? Everything should be reachable from the nav, a menu, or a clear in-page entry.
|
||||
URL? Everything should be reachable from the nav, a menu, or a clear in-page entry.
|
||||
- [ ] **Navigation structure:** sidebar/nav grouping is logical; related pages sit
|
||||
together; admin vs user separation is clear; active state + page titles are correct.
|
||||
together; admin vs user separation is clear; active state + page titles are correct.
|
||||
- [ ] **Menus where they belong:** actions that today are loose buttons or hidden should
|
||||
be grouped into sensible menus — overflow (`⋯`) menus on rows/cards, context menus,
|
||||
a consolidated **Settings** grouping, an account menu. Put related actions in one menu
|
||||
rather than scattering them. Flag anything that would be easier to find as a menu item.
|
||||
be grouped into sensible menus — overflow (`⋯`) menus on rows/cards, context menus,
|
||||
a consolidated **Settings** grouping, an account menu. Put related actions in one menu
|
||||
rather than scattering them. Flag anything that would be easier to find as a menu item.
|
||||
- [ ] **Command palette (`Ctrl+K`) coverage:** every page/primary action is reachable;
|
||||
no dead entries.
|
||||
no dead entries.
|
||||
- [ ] **Redundancy:** the same action offered in three places with different labels, or
|
||||
two pages that do nearly the same thing — propose consolidating.
|
||||
two pages that do nearly the same thing — propose consolidating.
|
||||
|
||||
**Experience quality**
|
||||
|
||||
- [ ] **Core-flow friction:** count the clicks for the top tasks (pay a bill, add a bill,
|
||||
connect SimpleFIN, run a sync, import data). Propose shortcuts where a step is wasted.
|
||||
connect SimpleFIN, run a sync, import data). Propose shortcuts where a step is wasted.
|
||||
- [ ] **States:** every list/page has a clear **empty state with a next-step CTA**, a
|
||||
**loading** state (skeleton/spinner, no layout jump), and a **recoverable error**
|
||||
state — no dead ends, no silent failures.
|
||||
**loading** state (skeleton/spinner, no layout jump), and a **recoverable error**
|
||||
state — no dead ends, no silent failures.
|
||||
- [ ] **Feedback & safety:** state changes confirm (toast); destructive actions confirm
|
||||
and, where feasible, offer undo (bills already soft-delete — surface a restore path);
|
||||
long actions show progress.
|
||||
and, where feasible, offer undo (bills already soft-delete — surface a restore path);
|
||||
long actions show progress.
|
||||
- [ ] **Consistency:** primary-action placement, button hierarchy, iconography,
|
||||
terminology, and confirmation patterns are consistent across pages.
|
||||
terminology, and confirmation patterns are consistent across pages.
|
||||
- [ ] **Mobile parity:** every action available on desktop is reachable on mobile; touch
|
||||
targets adequate; menus/overflow work on touch.
|
||||
targets adequate; menus/overflow work on touch.
|
||||
- [ ] **Onboarding:** first-run and empty-account guidance explains the next step; advanced
|
||||
features (SimpleFIN, debt planning, backups) have a short in-context explanation.
|
||||
features (SimpleFIN, debt planning, backups) have a short in-context explanation.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -625,17 +679,18 @@ desktop and mobile, light and dark), not just as a tester.
|
|||
|
||||
### Appendix A — Severity definitions
|
||||
|
||||
| Level | Definition |
|
||||
|-------|------------|
|
||||
| **S1 – Critical** | Data loss/corruption, security hole, crash/blank page, wrong money math, cannot log in/save. |
|
||||
| **S2 – Major** | Feature broken/unusable, wrong results, broken navigation, unhandled error. |
|
||||
| **S3 – Minor** | Works but wrong edge behavior, confusing UX, missing validation message. |
|
||||
| **S4 – Cosmetic** | Visual/copy/alignment/dark-mode-contrast, non-blocking. |
|
||||
| **IMP – Improvement** | Not a bug; enhancement or polish idea. |
|
||||
| Level | Definition |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| **S1 – Critical** | Data loss/corruption, security hole, crash/blank page, wrong money math, cannot log in/save. |
|
||||
| **S2 – Major** | Feature broken/unusable, wrong results, broken navigation, unhandled error. |
|
||||
| **S3 – Minor** | Works but wrong edge behavior, confusing UX, missing validation message. |
|
||||
| **S4 – Cosmetic** | Visual/copy/alignment/dark-mode-contrast, non-blocking. |
|
||||
| **IMP – Improvement** | Not a bug; enhancement or polish idea. |
|
||||
|
||||
### Appendix B — Exit / sign-off criteria
|
||||
|
||||
A cycle is release-ready when: **(Cycle 1 — all met ✅)**
|
||||
|
||||
- [x] All batches B0–B15 ✅ (Chromium desktop + mobile via the E2E projects; light + dark, `user` + `admin` exercised). Cross-browser Firefox/Safari carried to Cycle 2.
|
||||
- [x] B15 smoke green on the **production build** (`npm run smoke:prod`).
|
||||
- [x] **Zero open S1/S2** in the Findings Log; S3/S4/IMP all fixed & archived.
|
||||
|
|
@ -647,38 +702,46 @@ A cycle is release-ready when: **(Cycle 1 — all met ✅)**
|
|||
|
||||
### Appendix C — Page ↔ route ↔ API quick map
|
||||
|
||||
| Page | Route | Primary API |
|
||||
|------|-------|-------------|
|
||||
| Tracker | `/` | `/api/tracker`, `/api/bills`, `/api/payments`, `/api/monthly-starting-amounts` |
|
||||
| Calendar | `/calendar` | `/api/calendar` |
|
||||
| Summary | `/summary` | `/api/summary` |
|
||||
| Bills | `/bills` | `/api/bills`, `/api/categories`, `/api/matches` |
|
||||
| Subscriptions / Catalog | `/subscriptions`, `/subscriptions/catalog` | `/api/subscriptions` |
|
||||
| Categories | `/categories` | `/api/categories` |
|
||||
| Health | `/health` | `/api/analytics`, `/api/summary` |
|
||||
| Analytics | `/analytics` | `/api/analytics` |
|
||||
| Spending | `/spending` | `/api/spending` |
|
||||
| Banking | `/bank-transactions` | `/api/transactions`, `/api/matches`, `/api/data-sources` |
|
||||
| Snowball / Payoff | `/snowball`, `/payoff` | `/api/snowball` |
|
||||
| Settings | `/settings` | `/api/settings`, `/api/notifications` |
|
||||
| Profile | `/profile` | `/api/profile`, `/api/user` |
|
||||
| Data | `/data` | `/api/import`, `/api/export`, `/api/data-sources` |
|
||||
| Admin | `/admin`, `/admin/status` | `/api/admin`, `/api/status`, `/api/about-admin` |
|
||||
| About / Privacy / Release Notes / Roadmap | `/about`, `/privacy`, `/release-notes`, `/roadmap` | `/api/about`, `/api/privacy`, `/api/version` |
|
||||
| Page | Route | Primary API |
|
||||
| ------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| Tracker | `/` | `/api/tracker`, `/api/bills`, `/api/payments`, `/api/monthly-starting-amounts` |
|
||||
| Calendar | `/calendar` | `/api/calendar` |
|
||||
| Summary | `/summary` | `/api/summary` |
|
||||
| Bills | `/bills` | `/api/bills`, `/api/categories`, `/api/matches` |
|
||||
| Subscriptions / Catalog | `/subscriptions`, `/subscriptions/catalog` | `/api/subscriptions` |
|
||||
| Categories | `/categories` | `/api/categories` |
|
||||
| Health | `/health` | `/api/analytics`, `/api/summary` |
|
||||
| Analytics | `/analytics` | `/api/analytics` |
|
||||
| Spending | `/spending` | `/api/spending` |
|
||||
| Banking | `/bank-transactions` | `/api/transactions`, `/api/matches`, `/api/data-sources` |
|
||||
| Snowball / Payoff | `/snowball`, `/payoff` | `/api/snowball` |
|
||||
| Settings | `/settings` | `/api/settings`, `/api/notifications` |
|
||||
| Profile | `/profile` | `/api/profile`, `/api/user` |
|
||||
| Data | `/data` | `/api/import`, `/api/export`, `/api/data-sources` |
|
||||
| Admin | `/admin`, `/admin/status` (`/status` → redirect), `/admin/about`, `/admin/roadmap` | `/api/admin`, `/api/status`, `/api/about-admin` |
|
||||
| About / Privacy / Release Notes / Roadmap | `/about`, `/privacy`, `/release-notes`, `/roadmap` (admin-gated) | `/api/about`, `/api/privacy`, `/api/version` |
|
||||
| Calendar feed (no page — external ICS consumers) | — | `/api/calendar/feed.ics?token=…` (token-gated, mounted before auth) |
|
||||
|
||||
### Appendix D — Reference docs
|
||||
|
||||
`SECURITY_AUDIT.md` (security depth) · `docs/UI_IMPROVEMENTS.md` (known UI issues) · `docs/cents-migration-plan.md` (money-as-cents) · `docs/SIMPLEFIN_CONSUMER_GUARDRAILS.md` (sync limits) · `docs/CSRF-SPA-Setup.md`, `docs/RATE_LIMITING_ENHANCEMENT.md` (security middleware) · `REVIEW.md`, `DEVELOPMENT_LOG.md`, `roadmap.md`, `FUTURE.md` (context/known gaps) · `HISTORY.md` (changelog / fix archive) · `playwright.config.js` + `e2e/` (automated E2E/visual/a11y harness, §4.4).
|
||||
|
||||
### Appendix E — Per-page control census
|
||||
|
||||
The completeness ledger behind "every button, textbox, slider is right." Fill one table **per page** during [B0](#b0--baseline-tooling--coverage-recon) and check every control off during that page's batch. A control not listed here is a control not tested. Build the starting list with `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages/<Page>.tsx` + `grep -n "onClick=\|<Button\|<Select\|<Switch\|<Checkbox" client/pages/<Page>.tsx`.
|
||||
The completeness ledger behind "every button, textbox, slider is right." 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`.
|
||||
|
||||
**Template** (copy per page):
|
||||
|
||||
| Control | Type | Expected action | States checked (default/focus/disabled/error/loading) | Keyboard | Result |
|
||||
|---------|------|-----------------|-------------------------------------------------------|----------|--------|
|
||||
| *e.g.* Quick-pay button | button | marks bill paid, updates balance cards, undo available | default ✓ · disabled-while-saving ✓ | Enter ✓ | ✅ / finding id |
|
||||
| *e.g.* Amount input | number | per-month override, cents only, no wheel-scroll change | default ✓ · error-on-letters ✓ | Tab/Esc ✓ | ✅ / finding id |
|
||||
| Control | Type | Expected action | States checked (default/focus/disabled/error/loading) | Keyboard | Result |
|
||||
| ----------------------- | ------ | ------------------------------------------------------ | ----------------------------------------------------- | --------- | --------------- |
|
||||
| _e.g._ Quick-pay button | button | marks bill paid, updates balance cards, undo available | default ✓ · disabled-while-saving ✓ | Enter ✓ | ✅ / finding id |
|
||||
| _e.g._ Amount input | number | per-month override, cents only, no wheel-scroll change | default ✓ · error-on-letters ✓ | Tab/Esc ✓ | ✅ / finding id |
|
||||
|
||||
**Pages to census** (from `client/pages/`, keep in sync with [Appendix C](#appendix-c--page--route--api-quick-map)): Tracker, Calendar, Summary, Bills, Subscriptions, SubscriptionCatalog, Categories, Health, Analytics, Spending, Snowball, Payoff, BankTransactions, Data, Settings, Profile, Admin, Status, About, Privacy, ReleaseNotes, Roadmap, Login, NotFound — plus the shared **Sidebar/command-palette/header** chrome once.
|
||||
</content>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -76,11 +76,12 @@
|
|||
"@axe-core/playwright": "^4.10.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"concurrently": "^10.0.3",
|
||||
|
|
@ -97,7 +98,7 @@
|
|||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^8.1.4",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -342,11 +342,19 @@ function buildUserDbExportFile(userId) {
|
|||
|
||||
router.get('/user-db', (req: Req, res: Res) => {
|
||||
const file = buildUserDbExportFile(req.user.id);
|
||||
res.download(file, 'bill-tracker-user-export.sqlite', () => {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
} catch {}
|
||||
});
|
||||
// root-option form: send >= 2026-07 rejects bare absolute paths (4a dep sweep)
|
||||
res.download(
|
||||
path.basename(file),
|
||||
'bill-tracker-user-export.sqlite',
|
||||
{
|
||||
root: path.dirname(file),
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
} catch {}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Full portable JSON export of the user's data (same assembly as SQLite/Excel).
|
||||
|
|
|
|||
|
|
@ -273,7 +273,8 @@ const { getCsrfToken } = require('./middleware/csrf.cts');
|
|||
app.get('/{*splat}', (req, res) => {
|
||||
// Set CSRF cookie if not present (needed for SPA to read token)
|
||||
getCsrfToken(req, res);
|
||||
res.sendFile(path.join(DIST, 'index.html'));
|
||||
// root-option form: send >= 2026-07 rejects bare absolute paths (4a dep sweep)
|
||||
res.sendFile('index.html', { root: DIST });
|
||||
});
|
||||
|
||||
// ── Global error handler ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -103,18 +103,15 @@ export default defineConfig({
|
|||
output: {
|
||||
// QA-B0-01: split the shared vendor code out of the ~659 kB index chunk
|
||||
// so large libs load/cache independently and the main bundle shrinks.
|
||||
manualChunks: {
|
||||
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
|
||||
'vendor-radix': [
|
||||
'@radix-ui/react-dialog',
|
||||
'@radix-ui/react-select',
|
||||
'@radix-ui/react-dropdown-menu',
|
||||
'@radix-ui/react-tabs',
|
||||
'@radix-ui/react-tooltip',
|
||||
'@radix-ui/react-alert-dialog',
|
||||
],
|
||||
'vendor-motion': ['framer-motion'],
|
||||
'vendor-query': ['@tanstack/react-query', '@tanstack/react-virtual'],
|
||||
// Function form: vite 8 (rolldown core) dropped the object form.
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) return undefined;
|
||||
if (/node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/.test(id))
|
||||
return 'vendor-react';
|
||||
if (/node_modules[\\/]@radix-ui[\\/]/.test(id)) return 'vendor-radix';
|
||||
if (/node_modules[\\/]framer-motion[\\/]/.test(id)) return 'vendor-motion';
|
||||
if (/node_modules[\\/]@tanstack[\\/]/.test(id)) return 'vendor-query';
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue