From 127b69ffc28fb3de060ba2abf3cec558df2d3190 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 2 Jul 2026 20:47:50 -0500 Subject: [PATCH] chore(qa): vendor chunk splitting, remove unused markdown deps, remove dead totalInterestPaid (batch 0.41.0 QA cleanup) --- HISTORY.md | 6 + docs/QA_PLAN.md | 59 +- package-lock.json | 1511 +--------------------------------------- package.json | 3 - services/aprService.js | 25 - vite.config.mjs | 15 + 6 files changed, 31 insertions(+), 1588 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 5e8310d..f572320 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,12 @@ - **[UI] Negative amounts rendered as "$-50.00"** โ€” client `fmt()` (`client/lib/utils.js`) and server `formatUSD()` (`utils/money.js`) placed the minus sign after the currency symbol; now render the conventional "-$50.00". Test added in `client/lib/utils.test.js`. (was QA-B6-01) - **[a11y] Icon-only controls and chart SVGs had no accessible name** โ€” Radix Select filter/sort triggers (Tracker, Bills) and the Spending month-nav buttons rendered with no discernible text (screen readers announced a bare "button"); Analytics chart ``s had no name; a Snowball drag-handle `
` used a prohibited `aria-label`. Added `aria-label`s to the triggers/buttons and a `label` prop to the Analytics `SvgFrame`, and switched the drag-handle to `title`. Clears axe **critical `button-name`** and **serious `svg-img-alt` / `aria-prohibited-attr`** across those pages; guarded by `e2e/a11y.authed.spec.js`. (was QA-B14-01, part of QA-B14-02) +### ๐Ÿงน QA Cleanup + +- **[Build] Split the vendor bundle** โ€” the main `index` chunk was ~659 kB (over Vite's 500 kB warning). Added `build.rollupOptions.output.manualChunks` (`vite.config.mjs`) to split React, Radix, framer-motion, and TanStack into separately-cacheable vendor chunks; the index chunk dropped to ~334 kB and the warning is gone. (was QA-B0-01) +- **[Deps] Removed unused markdown libraries** โ€” `react-markdown`, `rehype-sanitize`, and `remark-gfm` were declared but never imported (client markdown is rendered by a custom `MarkdownText` component). Removed all three and corrected the security notes: the actual XSS defense is React auto-escaping + the restrictive custom renderer (https-only link hrefs, no `dangerouslySetInnerHTML` anywhere), not rehype-sanitize. (was QA-B14-03) +- **[Debt] Removed dead `totalInterestPaid`** โ€” unused outside its own export, and its unrounded accumulation diverged from `amortizationSchedule`'s per-month rounding. Removed from `services/aprService.js`. (was QA-B7-02) + ### โœจ Spending - **Category groups** โ€” Organize spending categories into named groups (e.g. "Bills", "Everyday", "Subscriptions"). New `category_groups` table with CRUD endpoints. Categories can be assigned to a group via the Spending page or API. Groups appear as collapsible headers in the category breakdown. diff --git a/docs/QA_PLAN.md b/docs/QA_PLAN.md index 3c8a339..d011a4f 100644 --- a/docs/QA_PLAN.md +++ b/docs/QA_PLAN.md @@ -86,7 +86,7 @@ before cross-cutting; regression last). Update **Status** and **Findings** every | # | 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 | ๐Ÿ”„ | 1 / 0 | +| 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 | @@ -94,14 +94,14 @@ before cross-cutting; regression last). Update **Status** and **Findings** every | B4 | Subscriptions & Categories | `/subscriptions`, catalog, `/categories`, groups, reorder | seeded | โฌœ | 0 / 0 | | B5 | Reporting reconciliation | `/summary`, `/calendar`, `/analytics`, `/health` cross-check totals | seeded + large | โฌœ | 0 / 0 | | 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) | ๐Ÿ”„ | 2 / 0 | +| B7 | Debt planning (math) | `/snowball`, `/payoff` APR/amortization vs hand-calc | edge (APR=0, $0 debt) | ๐Ÿ”„ | 1 / 1 | | B8 | Banking & bank sync | `/bank-transactions`, SimpleFIN sync, matching, merchant/store, advisory filter | seeded txns | โฌœ | 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 / 0 | | 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 | ๐Ÿ”„ | 2 / 1 | +| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | ๐Ÿ”„ | 1 / 2 | | B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | โฌœ | 0 / 0 | > After B15, if any batch is ๐Ÿ” or has open S1/S2, loop back. Then start a new @@ -115,7 +115,7 @@ until you get a clean cycle. | Cycle | Started | Build / commit | Findings logged | Fixed / archived | Result | |-------|---------|----------------|-----------------|------------------|--------| -| 1 | 2026-07-02 | `bdbf231` (dev) | 9 (find pass ongoing) | 4 (QA-B9-01, B13-01, B6-01, B14-01 โ†’ HISTORY v0.41.0; + a11y svg/aria parts of B14-02) | ๐Ÿ”„ in progress โ€” B0/B1/B4/B6/B7/B9/B13/B14 probed. Solid: auth-isolation, CSRF, payment/date validation, subscription+spending math, XSS. **Fixed & archived: seed 100ร— cents (S2), bill-amount validation, negative-money format, a11y button-name/svg/aria labels.** Open: 5 (B14-02 nested-interactive S3, B7-01 rounding S3, 3 IMP) | +| 1 | 2026-07-02 | `bdbf231` (dev) | 9 (find pass ongoing) | 7 โ†’ HISTORY v0.41.0 (B9-01, B13-01, B6-01, B14-01, B14-03, B0-01, B7-02; + a11y svg/aria of B14-02) | ๐Ÿ”„ in progress โ€” B0/B1/B3/B4/B6/B7/B8/B9/B13/B14 probed. Solid: auth-isolation, CSRF, payment/date validation, **recurrence (quarterly/annual gating, Feb-31 clamp, leap year โ€” all correct)**, **transaction matching/dedup guards**, subscription+spending math, XSS. **Fixed: seed 100ร— cents (S2), bill-amount validation, negative-money format, a11y labels, vendor-bundle split, unused-dep removal, dead-code removal.** Open: 2 (B7-01 rounding S3 [float-inherent], B14-02 nested-interactive S3 [architectural]) | **Result key:** ๐Ÿ”„ in progress ยท ๐Ÿ” findings fixed, re-run required ยท โœ… clean (zero findings โ€” QA complete) @@ -134,11 +134,8 @@ fixing. Keep only **Open / Fixing / Fixed** rows here. Once a finding is | ID | Sev | Area (`file:line`) | Summary | Status | Notes / repro | |----|-----|--------------------|---------|--------|---------------| -| QA-B7-01 | S3 | `utils/money.js:29` | `toCents` mis-rounds fractional cents: `toCents(1.005)` โ†’ 100 (`$1.00`) not 101 | ๐Ÿ”ด Open | see write-up | -| QA-B7-02 | IMP | `services/aprService.js:46` | `totalInterestPaid` is unused outside tests and its unrounded model diverges from `amortizationSchedule` | ๐Ÿ”ด Open | see write-up | -| QA-B0-01 | IMP | `vite build` / `client/index` | Main JS bundle 659 kB (203 kB gzip) โ€” over Vite's 500 kB warning | ๐Ÿ”ด Open | see write-up | +| QA-B7-01 | S3 | `utils/money.js:29` | `toCents` mis-rounds fractional cents: `toCents(1.005)` โ†’ 100 (`$1.00`) not 101 | ๐Ÿ”ด Open | see write-up (deferred โ€” float-inherent) | | QA-B14-02 | S3 | `/categories` (8), `/snowball` (1) | axe **serious** `nested-interactive`: draggable/expandable rows are `role=button` yet contain nested buttons | ๐Ÿ”ด Open | see write-up (deferred โ€” architectural) | -| QA-B14-03 | IMP | `package.json` / security docs | `react-markdown`/`rehype-sanitize`/`remark-gfm` unused; XSS docs credit sanitize that isn't wired | ๐Ÿ”ด Open | see write-up | **Finding template** (paste a new row above; keep the full write-up here until archived): @@ -178,31 +175,6 @@ Impact: bounded to sub-cent, and only when a 3+ decimal dollar value reaches the Fix (deferred): round on a string/scaled-integer basis, or add epsilon before round. ``` -``` -ID: QA-B7-02 -Severity: IMP (dead / latent-inconsistent code) -Area: services/aprService.js:46 (totalInterestPaid) -Finding: totalInterestPaid is referenced only by its own definition/export and - tests โ€” no route/client consumer. Its unrounded interest accumulation diverges - from amortizationSchedule's per-month rounded interest (measured up to $0.16 over - a 154-month term). If ever wired to a UI that also shows the schedule, the - headline total and the table would disagree. -Fix (deferred): remove it, or switch it to the per-month rounded model so it can't - diverge, and add a reconciliation test (sum of schedule interest == total). -``` - -``` -ID: QA-B0-01 -Severity: IMP (performance/build) -Area: vite build output โ€” dist/assets/index-*.js -Finding: main chunk 658.95 kB (203.72 kB gzip), over Vite's 500 kB warn threshold; - build prints the chunk-size advisory. Pages are already lazy/route-split, so the - index chunk is the shared vendor/core. -Fix (deferred): build.rollupOptions.output.manualChunks to split vendor (react, - radix, framer-motion, xlsx) or raise chunkSizeWarningLimit if accepted. Ties to - B14 performance (Slow-3G initial load). -``` - ``` ID: QA-B14-02 (partially fixed โ€” nested-interactive remains) Severity: S3 (accessibility โ€” WCAG 2 A/AA, axe "serious") @@ -223,25 +195,6 @@ Fix (deferred): don't make the whole row/trigger a button โ€” use a dedicated after. Guard: e2e/a11y.authed.spec.js (currently red on these two pages by design). ``` -``` -ID: QA-B14-03 -Severity: IMP (dependency hygiene + inaccurate security documentation) -Area: package.json (react-markdown, rehype-sanitize, remark-gfm) + SECURITY docs / - this plan's ยง6 and Appendix references to "markdown via rehype-sanitize". -Finding: those three deps are NOT imported anywhere in client/ (verified by grep). - Client markdown is rendered by a custom client/components/MarkdownText.jsx - (renderInlineMarkdown) โ€” bold/code/links only, link regex restricted to - https?:// hrefs, output is React-escaped, and there is NO dangerouslySetInnerHTML - anywhere in the client. So the real XSS defense is React escaping + the restrictive - custom renderer, not rehype-sanitize. -POSITIVE (confirmed, not a finding): XSS surface is safe โ€” stored