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>
Build drops 6.5s -> 0.65s. Three breakages found by the prod-smoke gate,
all fixed:
- rolldown removed object-form manualChunks -> function form in
vite.config.mjs (same vendor split, all @radix/@tanstack grouped)
- the 4a 'send' bump rejects bare absolute paths: SPA fallback sendFile
and both download routes (admin backup, user-db export) now use the
root-option form
- rolldown minifies inline HTML scripts, breaking any CSP hash approach:
the pre-paint theme script moved to client/public/theme-init.js so
script-src 'self' holds with no inline hash to maintain
@testing-library/dom added (peer no longer auto-installed). Server 252,
client 52, probe 18/18, PROD SMOKE: PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eslint 10's recommended set adds no-useless-assignment; fixed the three
real dead initial assignments it caught (version.cts updatedAt,
simplefinService claimUrl/accessUrl). Lint 0 errors; ratchet rules from
the standards batch carry over unchanged. Server 252, client 52.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- nodemailer 8 -> 9 (GHSA-p6gq-j5cr-w38f, raw-option file-read/SSRF; we
never use the raw option — notificationDelivery tests 13/13)
- xlsx -> official SheetJS dist 0.20.3 (prototype pollution
GHSA-4r6h-8v6p-xvw6 + ReDoS GHSA-5pgg-2g8v-p4x9; npm registry line is
abandoned at 0.18.5 with no fix). API-identical for our 8 call sites;
import/export round-trip tests green. NOTE: the lockfile now pins
https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz — CI runners and
Docker builds need egress to cdn.sheetjs.com
- CI: npm audit --audit-level critical -> high (0 vulns as of this
commit; highs can no longer pass silently — QA-B0-02)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
package.json said 0.40.0 while HISTORY.md's shipped release was v0.41.0, so
/api/version, the release-notes badge (has_new_version), and the update
checker all reported the wrong version. tests/versionSync.test.js now fails
the suite whenever the two drift again.
(QA-B0-03)
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>
A financial app must prove its money math, not sample it. tests/
moneyProperties.test.js fuzzes utils/money.mts: toCents always integer;
dollars→cents→dollars == roundMoney (no drift); sumMoney is cent-exact;
integer cents round-trip; classic float-drift cases exact; invalid inputs
never yield a bogus number. 6 properties, all hold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pillar A enforcement so standards can't drift:
- .forgejo CI now runs format:check + lint (client+server) + typecheck
(client) + typecheck:server, in addition to the existing check:server /
tests / build. Previously lint and typecheck were NOT run in CI.
- lefthook pre-commit auto-formats staged files (prettier) + lints;
pre-push typechecks. Auto-installed via the `prepare` script.
- Knip config (advisory `npm run knip`) for dead-code/unused-export
review; the design-system ui/** is ignored to keep signal clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standardization foundation (Pillar A):
- Prettier (.prettierrc.json, .prettierignore) + `format`/`format:check`
scripts; style tuned to the house style (single quotes, semicolons,
2-space, printWidth 100).
- .editorconfig for editor-level consistency.
- Extend the flat ESLint config to lint server `.cts` (Node globals, TS
parser, "focused signal" rule set mirroring the client) so `eslint .`
now covers both halves; `lint` → `eslint .`.
- Wire `format:check` into the `ci` script.
`npm run lint` is green (0 errors; pre-existing intentional patterns kept
as advisory warnings to ratchet later).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the JS→TS server migration. server.js → server.cts (Node
type-stripping, no build step), and every launch/config reference now
points at it: package.json (main/dev:api/start/check:server) and the
Docker CMD.
scripts/seedDemoData.js is required at runtime by routes/user.cts, so it
crosses into the server runtime — migrated to .cts and its require of
db/database updated to the .cts path. The remaining scripts/*.js are
standalone dev/ops tooling (outside the runtime and the check:server
boundary); their stale extensionless requires of now-.cts modules were
repaired so they still run, and the PM2 ecosystem config entry point was
updated to server.cts.
Verified: 0 runtime .js remain in the server tree; typecheck:server and
check:server clean; 226/226 tests pass; `node server.cts` boots and
/api/health returns 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Dockerfile: npm ci (reproducible, lock-pinned, matches CI) instead of npm
install; add a HEALTHCHECK against the /api/health liveness route.
- package.json: pin engines node>=22.18.0 — the server require()s .mts/.cts and
relies on Node's default TS type-stripping (unflagged only on >=22.18), which
was previously undocumented on a floating base image.
- .dockerignore: exclude .env / .env.* so a stray env file can't be baked into
the image by COPY . .
- docker-compose: set TRUST_PROXY=true (behind the reverse proxy, so Secure
cookies + client IP for rate-limit/audit are correct); flip CSRF_HTTP_ONLY to
true (SPA reads the token from an endpoint, so no JS cookie access needed);
remove the active default INIT_ADMIN_PASS; document TOKEN_ENCRYPTION_KEY (set
it to move at-rest secrets off the DB-resident key); drop obsolete version key.
Not included: TOKEN_ENCRYPTION_KEY itself — that's a secret to generate and
inject into the deployment env (the app self-migrates to the env-key scheme on
next boot); documented in compose + .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete unreachable in-file dialogs: MonthlyStateDialog block (setShowMbs
never called) and inline PaymentModal in TrackerRow + MobileTrackerRow
(setEditPayment only ever null; editing works via PaymentLedgerDialog).
- Delete fully-unreferenced files: tracker/EditableCell.tsx,
ui/confirm-dialog.tsx, ui/separator.tsx (+ orphaned @radix-ui/react-separator).
- Simplify OverdueCommandCenter dead branch (early-returns null when empty).
- Correct stale roadmap/FUTURE docs: tracker keyboard nav is implemented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Establishes the server-TypeScript foundation and migrates utils/money →
utils/money.mts with branded Cents/Dollars (mirroring the client), so the
cents↔dollars boundary — the origin of the reconciliation bug class — is a
compile error for any typed server caller.
Approach (no build step): Node 25 runs .ts natively (type-stripping).
Migrated modules use the explicit-ESM .mts extension (the project is
"type":"commonjs", which disables .ts ESM auto-detection) and are required
from the CJS callers via Node's require(esm) — verified working. Infra:
tsconfig.server.json (allowJs + checkJs:false → incremental like the
client), npm run typecheck:server, check:server extended to .mts, wired
into ci. 28 require sites repointed to money.mts.
typecheck:server clean; full server suite 225 + e2e probe 17/17 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The maintained line, and it forwards rejected async handlers to the error
middleware natively — closing the Express-4 gap where a throw in an async
route became an unhandled rejection that hung the request (this is why no
throwaway asyncHandler wrapper was needed). Small surface: only three
bare-* routes needed the path-to-regexp-v8 named-wildcard form
(/api/*splat, /legacy/*splat, /{*splat}); no removed APIs in use, all
req.query uses are reads. Suite 225 + probe 17/17 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
As files convert to TypeScript they must keep the react-hooks enforcement.
Added a .ts/.tsx config block using the typescript-eslint parser with the same
rules-of-hooks/exhaustive-deps/react-refresh rules; swapped core no-undef +
no-unused-vars (type-blind) for TS-aware equivalents. Verified 'npm run lint'
traverses .ts and flags issues there; 0 errors on the existing .ts files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopting TypeScript incrementally (the move off plain JS). Upgraded jsconfig ->
tsconfig with strict + noUncheckedIndexedAccess, but allowJs + checkJs:false so
every existing .js/.jsx keeps resolving and running untouched — only .ts/.tsx
get type-checked. Added 'npm run typecheck' (tsc --noEmit) and wired it into
'npm run ci'. Installed typescript 6 + @types/react/react-dom 19. Client-scoped
(Vite resolves the '@/' paths); the CommonJS server is a separate TS story.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Added babel-plugin-react-compiler to the Vite React plugin. The compiler
auto-memoizes components/hooks at build time, so manual useMemo/useCallback
become largely unnecessary going forward (existing ones are left in place —
harmless, and the compiler adds the rest). Safe here because the codebase is
rules-of-hooks clean (enforced by eslint-plugin-react-hooks).
Validated: production build succeeds and the e2e probe passes 17/17 with the
compiler on — every authed page renders axe-clean and Tracker/Summary/Analytics
reconciliation holds. (Build time ~2.2s -> ~6.2s from the compiler pass; the
runtime win is automatic memoization.) The compiler ESLint rule needs
eslint-plugin-react-hooks v6 — a future upgrade.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ESLint surfaced 6 errors, incl. real bugs invisible before:
- ImportTransactionCsvSection called importErrorState() without importing it —
its own error handler would throw ReferenceError on a failed CSV import.
- client/components/MobileTrackerRow.jsx was a dead duplicate (unused; the live
one is tracker/MobileTrackerRow.jsx) with undefined-setter refs → deleted.
- StatusPage dbOk had a dead '?? true' (constant boolean LHS) — restored the
intended default-true-when-unknown.
- MobileBillRow redundant !! in a ternary condition.
Lint is now 0 errors; wired 'npm run lint' into 'npm run ci'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was no linting at all — nothing enforced rules-of-hooks (conditional-hook
crashes) or exhaustive-deps (stale closures) across 125 client components/pages.
Added an ESLint flat config (eslint.config.mjs) scoped to client/, an 'npm run
lint' script, and the devDeps. First run: 0 rules-of-hooks violations (good),
6 errors + 13 exhaustive-deps warnings to work through next.
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>
Replace all Save buttons on the Settings page with debounced auto-save:
- useAutoSave hook: debounce with latest-payload-wins, flush() for blur,
pending-edit flush on unmount, status machine (idle/saving/saved/error)
with saved fading back to idle. Covered by 6 Vitest tests (fake timers).
- SaveStatus pill (framer-motion) in the page header and notification card
headers — Saving…/Saved/Save failed.
- Timing per control: toggles/selects/channel ~150-400ms; typed inputs
(email, URLs, grace period, drift pct) 900ms + flush on blur.
- Push token never auto-saves mid-type: saves on blur only, so a partial
token can never overwrite a working one.
- Notification cards no longer refetch parent settings on save (would
clobber in-flight edits under auto-save).
- Decision: no undo toast — settings are non-destructive and instantly
re-editable; undo would add noise without safety.
- vitest include now picks up .jsx tests; jsdom + @testing-library/react
added as devDependencies.
- Copy pipeline-report.py from Pipeline project into scripts/
- Update TOOLS.md and MEMORY.md to reflect workflow consolidation
- (includes all uncommitted v0.36.0 changes from prior session)