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