- docs/DEPLOY_CHECKLIST.md: the prod-config items that only lived in
session notes are now in the repo — TOKEN_ENCRYPTION_KEY (v2:->e2:
migration), WEBAUTHN_RP_ID/_ORIGIN, simplefin_debug_logging off,
cdn.sheetjs.com egress, Node-version alignment, TS7 deferral
- renovate.json: weekly grouped non-majors, dashboard-approval majors,
xlsx (CDN-pinned) excluded, typescript capped <7 until
typescript-eslint supports it
- .forgejo/workflows/deps-audit.yml: weekly scheduled audit that FAILS
on high+ prod vulns + prints npm outdated — the durable QA-B0-02 fix
that works even before Renovate is enabled on the Forgejo instance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- delete root test-functional.js (imported uninstalled 'playwright' —
could never run) and run-functional-test.js (legacy pre-Playwright)
- delete postcss.config.js + autoprefixer/postcss devDeps (tailwind v4's
vite plugin owns the pipeline; the config had no plugins left)
- client/lib/money.ts: drop unused SUPPORTED_CURRENCIES; keep
dollarsToCents exported + @public-tagged (API symmetry with
centsToDollars — the sanctioned unit crossing)
- client/lib/billDrafts.ts: drop unused BillDraft type export
- move the two runtime seed JSONs (advisory filters, merchant-store
matches; 7 MB) out of docs/ into db/data/ next to their loader;
fresh-DB seed verified (5000+5000 rows); Dockerfile COPY . covers it
- knip.json: apply config hints, ignore the two CSS-side tailwind deps;
npx knip exits 0 — added to CI as a BLOCKING step so unused
files/exports/deps fail instead of rotting (this is how the WebAuthn
ghost feature survived four QA cycles)
NOT deleted: client/public/img/doingmypart.jpg — flagged unused by the
code grep but actually referenced from HISTORY.md markdown that the
release-notes renderer serves (user catch). Lesson encoded in the QA
plan: asset-reference sweeps must include rendered markdown content.
Full ci green (252 server / 52 client / build / knip 0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Official codemod (run in a clean worktree; docker-owned data/ blocks it
in-tree): tailwind.config.js inlined into client/index.css as CSS-first
@theme/@utility (config file deleted — v4 auto-detects content, which
also retires the .tsx-content-glob failure class from the TS migration),
33 template files' class renames applied, tailwindcss-animate loads via
the v4 @plugin bridge. Build runs through @tailwindcss/vite; postcss
config is now plugin-free. tailwind-merge -> 3 (v4 class model).
Visual-regression gate: only diffs were a ~2px global preflight shift +
the version string (baseline predated 0.41.1); re-baselined both login
screenshots in this commit after review. webauthn.probe confined to the
probe project (it enables WebAuthn for the seeded user, racing the
parallel UI projects' password logins).
e2e 27 passed, probe 18/18, PROD SMOKE PASS, typecheck/lint/tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- eslint: no-console error on all server dirs (sanctioned exceptions:
utils/logger.cts sink, utils/env.cts bootstrap, utils/apiError.cts
fallback); no-restricted-imports bans errorFormatter in routes/** so
hand-rolled standardizeError bodies can't return (transactions/import
are the two documented exception files)
- client/api.ts: 6 copy-pasted ApiError constructions collapsed into one
toApiError() helper
- CODE_STANDARDS.md: 'Error responses' and 'Logging' flipped target ->
ENFORCED, with the full documented-exceptions list and the client
error-shape/mutation-pattern conventions written down
Lint 0 errors, client 52/52, typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De-risks the flagship. The app is synchronous (better-sqlite3) but Kysely
executes async, so db/kysely.cts uses Kysely as a TYPE-SAFE BUILDER only:
build the query (columns + user_id/deleted_at scoping + types checked at
compile time), `.compile()`, then run via sync helpers (all/get/run) on the
shared connection. No async ripple, no change to the sync model.
- Schema typed for `categories` (extend as adopted).
- tests/kyselyPoc.test.js proves byte-identical rows vs the raw SQL and
that compiled SQL is parameterized (injection-safe).
- Pattern documented in CODE_STANDARDS; adopt for new/refactored queries;
it type-enforces user-scoping (the biggest data-isolation win) and
removes Row=any at the call site.
typecheck:server + check:server clean; 248/248 server tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
utils/logger.cts — console-compatible log.{debug,info,warn,error} with:
- level gating (LOG_LEVEL; debug in dev / info in prod), and
- REDACTION of secrets/PII (sensitive keys + bearer/JWT) so tokens,
passwords, and encrypted values never reach the logs (financial app).
Backend is abstracted (console now, swappable to pino later).
Swept all 317 server-runtime console.* across 35 files → log.* + inserted
the import at correct scope. tests/logger.test.js pins the redaction
contract (4 tests). CODE_STANDARDS updated; raw console.* is now banned in
server code.
Verified: check:server + typecheck:server + lint clean; 240/240 server
tests; client 50/50; build; e2e probe 17/17; boot smoke logs flow through
the logger and graceful shutdown intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The living rulebook (formatting, modules, API/wire + error contract,
security, logging, data integrity, testing) so conventions are codified
and persist. CONTRIBUTING points at it and the CI gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120-line brand guidelines document covering the v0.42.0 brand
system: naming ('Bill Tracker' with the space, not 'BillTracker'),
asset paths, lockup usage rules (light vs dark variants, mono
mark, social preview), the canonical source-of-truth in
client/lib/brand.ts, and the GitHub-Open-Graph card dimensions.
This is commit 4 (and the last) of the v0.42.0 follow-up batch.
Docs-only; no code change. Depends on 68e57cf (favicon + PWA).
Eight in-flight commits targeting v0.41.0 (Track A money integrity,
Track B reconciliation tests, Track C API response typing across
auth/snowball/subscriptions/spending/bank-ledger, Track D 500
handler code:'INTERNAL_ERROR') are not yet pushed. Documenting them
in the manual so it stays current with the code that will land next.
Section updates:
- Header: keep package version 0.40.0, note v0.41.0 is the in-flight
target; 'Last Updated' 2026-07-05.
- Notable changes: 4 new bullets summarizing Track A/B/C/D.
- API client (7): list Track C response typing and Track D error shape
with cross-references to the sections where each is detailed.
- Domain types (7): expand to include User, SnowballProjection,
SnowballSettings, Subscription*, Spending*, BankLedger*, CategoryGroup,
CopyBudgetsResponse. Note single-consumer non-money casts left in
place as a deliberate Track C stop.
- Auth state (7): User moved to @/types, re-exported from useAuth.
- client/lib/ (7): add paymentActions.ts (createPaymentOrConfirm).
- Response conventions (5): add 'INTERNAL_ERROR' for 500s; note
409 DUPLICATE_SUSPECTED on POST /payments.
- Payments (5.5): Track A notes — transactional balance mutations,
409 allow_duplicate flag, 409 DUPLICATE_SUSPECTED response shape,
new tests/paymentsRoute.test.js and tests/reconciliation.test.js.
- Verification checklist (11): list 8 unpushed Track commits with
one-line summaries and 19-file diff stat (813+/415-).
The living QA doc's copy-pasteable coverage-recon greps pointed at
client/App.jsx, Sidebar.jsx, <Page>.jsx and the B-UI primitive census
listed button.jsx/input.jsx/etc — all now .tsx, so the commands errored
('No such file'). Retargeted to .tsx (+ useAutoSave.test.ts) and noted
that recon greps target .tsx and 'npm run typecheck' is now a guard.
Behavior-only migration: client 42/42 + e2e probe 17/17 stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran syncDataSource (the Sync-button path) against the live SimpleFIN
connection off a working copy of the production DB: token decrypted via the
db-key fallback (no TOKEN_ENCRYPTION_KEY in prod env), bridge fetch OK,
18 accounts upserted, 145 fetched transactions skipped-not-duplicated,
0 new, 1159->1159 distinct. Dedup/upsert idempotency proven on real data.
Updated B8 data-state and the Cycle Log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand the QA plan beyond correctness to also hunt for improvements, per
three lenses woven into the execution model:
- Code health & consolidation (B17): duplication, dead code, overlapping
modules, oversized files, one canonical path per concern.
- UX (B18): core-flow friction, empty/loading/error states, feedback.
- Information architecture / menus (B18): discoverability, surfacing
actions into sensible menus, nav grouping.
Adds an Improvement Backlog (§2.1) for IMP proposals (separate from the bug
log; non-gating), detailed B17/B18 playbooks, and batch-table rows. Seeded
the backlog with 6 concrete, code-verified candidates (client money-format
consolidation, db/database.js split, match-service overlap, Data/menu IA,
recently-deleted restore view, empty/loading/error-state audit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found probing a copy of the live SimpleFIN DB: 3 transactions were
match_status='matched' with matched_bill_id=NULL. Bills are soft-deleted
(retained for recovery), then the retention GC hard-deletes them past the
30-day window. transactions.matched_bill_id is ON DELETE SET NULL, so the
purge nulled the pointer but left match_status='matched' — a limbo row
excluded from spending/analytics (match_status != 'matched') yet attributed
to no bill, silently dropping that spend.
pruneSoftDeletedFinancialRecords now releases those matches back to
'unmatched' in the same transaction and self-heals pre-existing orphans;
retention behaviour is unchanged. Verified on a live-DB copy (3→0 orphans,
0 transactions lost). Regression: 3 tests in backupAndCleanup.test.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- updateCheckService: gate the external request on `update_check_enabled`
(default on); when off, no network call, returns { disabled: true }
- aboutAdmin: GET/PUT /update-check-setting (admin-only) to toggle it
- StatusPage: a Switch on the admin System Status card to enable/disable
- privacy.js: state that an admin can disable it (was called "optional" with
no actual opt-out)
- tests/updateCheckOptOut.test.js: proves no external fetch when disabled
- docs: archive QA-B16-01, B16 ✅
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the quick B16 checks: encryption-key lifecycle safe (hasKey guard + v2
db-key fallback → graceful, no plaintext), migrations idempotent. Found: the
privacy policy calls the update/version check "optional" but there is no opt-out
setting, and it hits a hardcoded host on About/Status/version load. Logged S4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gap analysis of the codebase vs the plan surfaced surfaces with no QA home:
- DB migration system (idempotency/rollback/fresh==migrated, money conversions)
- encryption-key lifecycle (missing/rotated key → graceful degrade, no plaintext/leak)
- container deploy (docker-entrypoint: dir perms chmod 700, non-root, run migrations)
- update-check phone-home (external request → disclosed + opt-out)
- rate-limiter completeness (backupOperationLimiter, skipRateLimitIfNoUsers)
Added the B16 batch + playbook, and extended B0 recon to enumerate
middleware/workers/migrations/deploy so future cycles can't miss them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- notificationService buildEmailHtml: the message line interpolated bill.name
raw (`<strong>${bill.name}</strong> is due…`) while the detail table escaped
it; a `<img src=x onerror=…>` name landed unescaped in the email HTML. Now
escaped everywhere. (self-XSS — reminders go to the bill's owner — but a clear
inconsistent-escaping defect)
- expose buildEmailHtml via _email; add an escaping test across all 4 email types
- docs: archive QA-B14-04
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- notificationService: `module.exports._push = {...}` was set BEFORE the final
`module.exports = {...}`, which wiped it, so routes/notifications.js got
`_push || {}` → sendTestPush undefined → POST /api/notifications/test-push
always threw "Push service not initialised". Scheduled reminders were fine
(in-scope calls). Moved the _push assignment after the reassignment.
- add tests/notificationDelivery.test.js (7 tests: ntfy/gotify/discord payloads,
dispatch, error handling, unknown channel, no token leak in the body)
- docs: archive QA-B10-01
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- api.probe: assert a regular user is 403 on /api/admin/*, /api/status,
/api/about-admin (read + write) — B1/B11 authorization
- confirmed (static): settings PUT whitelists USER_SETTING_KEYS (no
mass-assignment), notifications route splits requireAdmin/requireUser
- docs: mark B10/B11/B12 probed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- scripts/prod-smoke.js + prod-smoke.sh: build, boot `node server.js` serving
dist/ against a scratch DB, and drive the real artifact with Playwright
(login + lazy routes) to confirm the vendor-chunk split loads in production
- npm run smoke:prod; passes green
- docs: B15 harness command + status
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- routes/summary buildBankTracking: fetch unpaid candidates and filter by
resolveDueDate in JS so annual / off-month quarterly bills don't inflate the
SimpleFIN "unpaid this month" metric (completes the occurrence-gating family)
- add tests/summaryBankTracking.test.js (isolated route test)
- docs: archive QA-B5-02; Active Findings Log now empty (0 open)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- analyticsService: only add a bill's expected_amount in months it actually
occurs (resolveDueDate), so annual / off-month quarterly bills no longer
inflate the expected-vs-actual line every month (QA-B5-03, same root as B5-01)
- add a Tracker<->Analytics reconciliation guard to e2e/api.probe.spec.js
- docs: archive QA-B5-03; cycle log
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- routes/summary: filter the expense list by resolveDueDate so annual and
off-month quarterly bills no longer inflate the monthly total / "monthly
result" — the Summary now agrees with the Tracker for the same month (QA-B5-01)
- add a Tracker<->Summary reconciliation guard in e2e/api.probe.spec.js
- docs: archive QA-B5-01; track QA-B5-02 (SimpleFIN unpaid_this_month residual)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- utils/money: toCents rounds off the shortest decimal string instead of
Math.round(n*100), so 1.005 -> 101 (not 100). Output is identical for all
integer / <=2-decimal / "$1,234.56" inputs, so no downstream change (QA-B7-01)
- add tests/money.test.js (9 tests; the money core previously had none)
- docs: archive QA-B7-01 to HISTORY v0.41.0; QA cycle 1 now 0 open findings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CategoriesPage: category rows are now a plain container with a dedicated
chevron toggle button, instead of role=button rows nesting action buttons
- PlanStatusBanner: split the collapsible header into a name/progress toggle,
sibling action buttons, and a chevron toggle (actions no longer nested in the
trigger button)
- add e2e/categories.spec.js expand regression; all 8 authed pages now pass axe
- docs: archive QA-B14-02 to HISTORY v0.41.0; QA plan status/cycle-log
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extracted known-service catalog to dedicated /subscriptions/catalog route
- Simplified main Subscriptions page to focus on tracked services + bank-backed recommendations
- Replaced inline Pause/Resume with Edit + MoreHorizontal dropdown on subscription rows
- Added 'Improve Matching' card linking to Service Catalog
- Vite proxy respects API_PORT env var for dev flexibility
- Added top_200_us_subscriptions_researched dataset
- Updated HISTORY.md with v0.35.0 changes