`npm audit fix`: react-router/react-router-dom 6.30.3 → 6.30.4 (moderate
open-redirect via protocol-relative URL), nodemailer 8.0.9 → 8.0.11.
Verified: server 232/232, client 50/50, build green.
Remaining audit findings are assessed non-actionable/mitigated (see
SECURITY.md): nodemailer `raw` send option is not used; xlsx is confined
to the import service with raw:false + size/content-type limits.
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>
The lockfile had drifted (written by local npm 11 / Node 25) into a form that
node:22-alpine's npm 10.9.8 reads as incomplete — `npm ci` failed with
"Missing: esbuild@0.28.1 from lock file", breaking the Docker image build
(Dockerfile now uses `npm ci`, not `npm install`). Regenerated the lock inside
node:22-alpine with its own npm so `npm ci` resolves cleanly (887 packages,
lockfileVersion 3). No dependency version changes — lockfile metadata only.
Verified: clean `npm ci` in node:22-alpine, full image build, container boots
migrations + workers, full QA (auth/CSRF/admin/security) and a live SimpleFIN
sync all pass.
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>
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>
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.
- 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