Commit Graph

138 Commits

Author SHA1 Message Date
null a31a774f95 chore: dead-code sweep — knip now clean and blocking in CI (Phase 5)
- 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>
2026-07-10 18:15:04 -05:00
null 1627e75b29 chore(deps)!: openid-client 5 -> 6 (4j)
v6 replaced the Issuer/Client classes with a functional API. The four
touchpoints rewritten: discovery() + ClientSecretBasic/Post for the cached
configuration, serverMetadata() for the admin config test,
buildAuthorizationUrl() (returns URL -> .href), and authorizationCodeGrant()
with pkceCodeVerifier/expectedNonce/expectedState for the callback exchange —
same validation surface (JWKS signature, iss/aud/exp/nbf, PKCE, state, nonce),
now enforced by the library's single grant call.

scripts/test-oidc-smoke.js: 44/44. Server suite 252/252.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:05:57 -05:00
null 522eaaf101 chore(deps)!: tailwind 3 -> 4 via @tailwindcss/vite (4i)
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>
2026-07-10 18:03:15 -05:00
null d8365d0e22 chore(deps)!: vite 8 (rolldown) + @vitejs/plugin-react 6 (4h)
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>
2026-07-10 17:53:27 -05:00
null b76772b8eb chore(deps): eslint 10 stack — @eslint/js 10, react-hooks 7, react-refresh 0.5 (4f)
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>
2026-07-10 17:36:50 -05:00
null 8248575b1e chore(deps): low-risk majors — bcryptjs 3, sonner 2, lucide-react 1.x, concurrently 10 (4e)
bcryptjs 3 verifies existing $2a/$2b hashes (authService tests 7/7);
sonner 2 + lucide 1.x compile clean (typecheck + build); no icon renames
in our set. Server 252/252, client 52/52.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:35:39 -05:00
null af20d8de07 fix(deps)!: clear both high-severity vulns; CI audit gate raised to high
- 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>
2026-07-10 17:34:51 -05:00
null 55b2baee5f release: v0.41.1 — passkeys shipped + QA blind-spot fixes + standards unification
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:31:27 -05:00
null e73f51e91b fix(version): sync package.json to v0.41.0 + guard against future drift
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>
2026-07-10 16:30:41 -05:00
null 35a0e4262e feat(db): Kysely typed-SQL POC — sync builder, proven parity (Pillar E)
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>
2026-07-06 16:23:50 -05:00
null 0cad1626b1 test(money): property-based proof of cent-exact invariants (fast-check, Pillar F)
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>
2026-07-06 16:08:50 -05:00
null 5aaf292b2e ci(enforce): run the full gate in CI + add lefthook pre-commit + Knip
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>
2026-07-06 14:27:03 -05:00
null f65138a114 chore(tooling): add Prettier + editorconfig, extend ESLint to the server
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>
2026-07-06 14:23:53 -05:00
null 20d2e8965b refactor(server): migrate app entry point to TypeScript (.cts) — server migration 100%
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>
2026-07-06 11:41:56 -05:00
null 005b0b7b64 refactor(server): migrate 10 services to TypeScript (.cts)
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>
2026-07-06 10:35:29 -05:00
null f3f46a73f0 chore(deploy): harden Docker/compose from the deployment audit
- 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>
2026-07-05 18:54:50 -05:00
null 3973560e92 refactor(tracker): remove dead code and unreferenced files (plan A1)
- 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>
2026-07-05 14:35:13 -05:00
null 200c6373f5 feat(server-ts): migrate the money boundary to TypeScript on Node's native runtime (Phase 2)
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>
2026-07-05 13:44:04 -05:00
null 1d3b7435a0 feat(server): upgrade Express 4.18 → 5.2 (Phase 2)
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>
2026-07-05 13:25:00 -05:00
null 62a99fcaa4 build(lint): extend ESLint to .ts/.tsx via typescript-eslint
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>
2026-07-03 21:44:35 -05:00
null 907a407399 build(ts): TypeScript foundation — tsconfig + typecheck in CI (T1)
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>
2026-07-03 21:33:26 -05:00
null 47c031f1e4 build(react): enable React Compiler (React 19 auto-memoization) (F2)
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>
2026-07-03 21:20:26 -05:00
null 5e267e4fa7 fix(client): fix ESLint errors — real latent bugs (R2a)
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>
2026-07-03 19:42:52 -05:00
null c679022592 build(lint): add ESLint 9 + eslint-plugin-react-hooks/react-refresh (R1)
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>
2026-07-03 19:41:24 -05:00
null 3c1d000bab test(qa): production-build smoke (B15) — validates split chunks at runtime
- 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>
2026-07-02 21:45:25 -05:00
null 127b69ffc2 chore(qa): vendor chunk splitting, remove unused markdown deps, remove dead totalInterestPaid (batch 0.41.0 QA cleanup) 2026-07-02 20:47:50 -05:00
null 029c227685 fix(qa): seed demo data amounts, bill amount validation, negative USD format, a11y aria-labels, Playwright E2E setup (batch 0.41.0 QA) 2026-07-02 20:36:09 -05:00
null 35e5d185de feat(spending): category groups, YNAB-style spending page overhaul, 3-month averages, cover overspending (batch 0.41.0) 2026-06-14 19:21:34 -05:00
null d9a441dff6 feat(settings): auto-save preferences with live save status (batch 0.39.0)
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.
2026-06-12 02:08:42 -05:00
null dc49eb9633 feat(cashflow): safe-to-spend projection with timeline, vitest setup, package upgrades 2026-06-12 01:32:28 -05:00
null 4b74a456d9 chore(release): bump to v0.38.4, update HISTORY, remove Stage 2 from FUTURE 2026-06-11 20:14:02 -05:00
null ec7869abbc feat: framer-motion page transitions and UI polish 2026-06-07 15:14:09 -05:00
null 6d60eebe1a chore: dependency updates and UI fixes (batch) 2026-06-07 14:23:19 -05:00
null 4f5a3d0cff feat: bank sync section, data sources route, subscription page updates, package updates 2026-06-07 02:03:00 -05:00
null 7455dff5b8 feat: v0.37.0 — auto-learn merchant rules, ambiguous match protection, session hashing, geolocation opt-in 2026-06-06 18:30:21 -05:00
null 99abca9868 security: WebAuthn / FIDO2 hardware security key 2FA 2026-06-05 22:05:23 -05:00
null 653dd72e12 feat: TOTP 2FA for login & profile setup flow 2026-06-04 04:10:14 -05:00
null 36a65156e3 feat: merge pipeline workflow into bill-tracker (batch v0.36.0)
- 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)
2026-06-03 20:28:37 -05:00
null e4f1f58730 feat: Roadmap pulls from Forgejo issues (v0.35.1) 2026-05-31 19:37:01 -05:00
null 557378dab9 chore: bump to v0.35.0 2026-05-31 16:09:40 -05:00
null ab93c53c82 chore: bump to v0.36.0 2026-05-31 15:57:03 -05:00
null 67ce59db50 v0.35.0 2026-05-31 15:52:50 -05:00
null 31bafb0e55 0.34.3 2026-05-31 15:06:10 -05:00
null c6cd81e33a chore: bump to v0.34.2, subscription badge fix on Tracker rows 2026-05-30 21:52:02 -05:00
null 90cfed035b feat: Payoff Custom mode, Summary reordering, unifed billing schedule, SimpleFIN + backup fixes (batch v0.34.1.3) 2026-05-30 21:20:51 -05:00
null c23cae1107 feat: reordering across management pages (Bills, Subscriptions, Categories, Snowball) — batch v0.34.1.2 2026-05-30 20:04:50 -05:00
null 6edb23cd66 chore: bump to v0.34.1.1, Claude.ai catalog seed, subscription fixes 2026-05-30 17:57:34 -05:00
null 35d0cbf8be chore: reset tracked db file 2026-05-30 17:27:15 -05:00
null 8b0f33085c feat: Payoff Simulator page with live SVG chart, debt selector, and apply-to-budget (batch 0.34.3) 2026-05-30 15:22:16 -05:00
null 799189059b chore: roadmap audit v0.34.2 — remove completed FUTURE.md items, update partial statuses 2026-05-30 15:18:45 -05:00