Commit Graph

439 Commits

Author SHA1 Message Date
null 2cac46fda2 fix(ts): scan .ts/.tsx in Tailwind content globs (B2 follow-up)
Root cause of the B2 e2e regression: tailwind's `content` was
`./client/**/*.{js,jsx}`, so once a component became .tsx, Tailwind stopped
scanning it and never generated CSS for any class used *only* there. The
release-notes dialog broke because its `left-[50%] top-[50%]` centering classes
(unique to the now-.tsx dialog) produced no CSS — the modal rendered off-screen
(transform applied, but `left/top` were 0/2359px), covering the tracker so the
auth-setup probe couldn't reach "Add Bill".

Fix: include `ts,tsx` in the content globs. Verified via computed style — the
dialog is centered again (left:640px top:360px, on-screen) and the 17/17 e2e
probe passes. This should have shipped with the TS foundation (T1); it silently
affected every .tsx conversion (they only looked fine because their classes also
appeared in some .jsx file).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 23:11:36 -05:00
null 1f18a720c4 refactor(ts): convert all UI primitives + ThemeContext to TSX (B2)
Foundational for phase B: the ui/* primitives were untyped .jsx, and TS infers
React-19 ref-as-prop components as *requiring* a ref, so every consumer errored.
Converted all 22 ui primitives (button, input, card, dialog, select, table,
dropdown-menu, tabs, badge, checkbox, switch, tooltip, separator, label,
Skeleton, collapsible, alert-dialog, sonner, save-status, theme-toggle,
input-dialog, confirm-dialog) using ComponentProps<'el'> / ComponentProps<typeof
Radix.X> + VariantProps for cva components — refs now optional, event params
typed. Also converted ThemeContext.tsx (createContext(null) made useContext's
post-guard return `never`, so setTheme was uncallable — fixed with a typed
context value) and three tracker cells (EditableCell/NotesCell/
LowerThisMonthButton) that consume the branded TrackerRow.

Added a shared errMessage(unknown) helper to utils (strict catch → unknown);
usePaymentActions reuses it. TrackerRow gains monthly_notes + amount_suggestion
(real server fields).

typecheck 0, lint 0 errors (42 warns), build green, 48 client tests. NOTE: the
e2e auth-setup probe is currently red on an unrelated, pre-existing
release-notes-modal dismissal race (reproduces on the last known-good commit;
the a11y snapshot shows these converted primitives rendering correctly) —
investigating separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:57:22 -05:00
null dc675fbecd refactor(ts): convert tracker leaf components to TSX (B1)
First .tsx conversions — the tracker leaf components, where the branded Dollars
now flows into the UI: StatusBadge (status: TrackerStatus), FilterChip,
PaymentProgress (paymentSummary's Dollars → fmt type-checks), SummaryCards
(SummaryCard value: Dollars, CardDef/CardType/TrendInfo typed). Props interfaces
added; no behavior change. Confirms the .tsx pattern: a typed parent importing
untyped .jsx children is fine (children accept any props — no forced cascade).

typecheck 0, lint 0 errors, build green, 48 client tests, 17/17 e2e probe (home
page renders).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:31:35 -05:00
null 4af738f947 feat(ts): type the shared Category API response (A4)
Adds Category (+ CategoryBillSummary) to @/types and wires categories/
createCategory/updateCategory. Category has no money field of its own (budgets
live on the spending endpoints); the nested per-bill total_paid is a raw SQL sum,
so it's deliberately left unbranded. With categories now typed, useSpending
Categories drops its `any` cast and dead d.categories branch.

Category, Bill, Payment, and the Tracker envelope are the cross-cutting types
used by many components — worth defining upfront. The remaining single-consumer
page responses (summary, analytics, spending, snowball, subscriptions, bank
ledger) are typed alongside their page conversions in phase B, where the exact
fields consumed are visible (more accurate than a speculative interface).

typecheck 0, build green, 48 client tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:27:08 -05:00
null 337ad95a3e feat(ts): type Bill + Payment API responses with branded Dollars (A2-A3)
Adds Bill and reuses Payment domain types (money → Dollars, mirroring
serializeBill/serializePayment which spread the DB row and convert the money
columns). Wired: bills/allBills/deletedBills → Bill[], bill/createBill/updateBill
→ Bill; quickPay/createPayment/updatePayment/restorePayment → Payment. Replaced
api.ts's minimal PaymentRecord with the richer @/types Payment (usePaymentActions
still resolves payment.id). typecheck 0, build green, 48 client tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:24:35 -05:00
null 3c7a55c3a1 feat(ts): type the Tracker API response with branded Dollars (A1)
New client/types.ts domain-types module. Types the full /tracker response —
TrackerResponse { summary, bank_tracking, cashflow, rows } — with every money
field branded `Dollars` (the server serializes cents→dollars, verified against
statusService.buildTrackerRow + trackerService.getTracker + buildBankTracking +
buildSafeToSpend). api.tracker() now returns Promise<TrackerResponse>.

TrackerRow + TrackerStatus move to @/types (canonical) and are re-exported from
@/lib/trackerUtils for compat; trackerUtils' row money fields upgrade from plain
`number` to branded `Dollars` (rowThreshold -> Dollars, paymentSummary re-brands
its computed amounts via asDollars so callers can fmt() them directly). This is
the first endpoint where the money brand flows end-to-end: fetch layer -> row
helpers, and (in phase B) into the components that format them.

Nested payloads no typed consumer reads yet (summary.trend, autopay_suggestion/
_stats, sparkline) are left `unknown`, to refine when consumed. asDollars is an
identity at runtime, so behavior is unchanged: typecheck 0, lint 0 errors, build
green, 48 client tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:21:45 -05:00
null d02d3c9c72 refactor(ts): convert React Query hooks to TypeScript (TS10)
hooks/useQueries.js -> .ts. Typed params and QueryParams-typed query builders
(QueryParams now exported from api.ts). Query result data stays unknown until
endpoint response shapes are typed.

TypeScript caught dead config: three hooks passed `cacheTime` to useQuery, which
React Query v5 renamed to `gcTime` and silently ignores — so those caches were
garbage-collected at the 5-min default instead of the intended 30 min / 2 hr.
Renamed to gcTime so the intended retention takes effect (memory-retention only;
no fetch or correctness change).

Verified: typecheck 0, lint 0 errors (47 warns), build green, 48 client tests,
17/17 e2e probe (every page renders).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:13:05 -05:00
null 9b805e60b2 refactor(ts): convert the API client to TypeScript (TS9)
client/api.js -> api.ts. Types the fetch infrastructure: generic _fetch<T>
and get/post/put/patch/del<T> helpers, an ApiError interface (status/code/
details/data), a QueryParams type for the query-string builder, and File-typed
upload helpers. Endpoint response shapes are typed incrementally — most methods
default to Promise<unknown>; the ones consumed by typed .ts files get real
shapes (quickPay -> PaymentRecord, togglePaid -> TogglePaidResult, settings ->
settings map).

Typing togglePaid's result surfaced a latent narrowing bug in usePaymentActions:
the un-pay Undo closure read result.paymentId (number|undefined) inside a
deferred async callback where TS re-widens the if-narrowed value — fixed by
capturing the id in a const before the closure.

The 16 files importing '@/api.js' with an explicit extension were normalized to
'@/api' so Vite/TS resolve the .ts.

Verified: typecheck 0, lint 0 errors (47 warns), build green, 48 client tests,
and 17/17 e2e probe (every page renders, all API paths respond) — the central
fetch-module rename is runtime-safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:09:04 -05:00
null 8c30a7ab09 refactor(ts): convert small hooks + version to TypeScript (TS8)
- useAutoSave.ts — generic useAutoSave<T>; AutoSaveStatus union; timer ref
  typed ReturnType<typeof setTimeout>; pending ref T|null narrows on != null.
- useSearchPanelPreference.ts — typed [boolean, setter] tuple return.
- version.ts — ReleaseNotes/ReleaseHighlight shapes; the Vite-injected
  __APP_VERSION__ global declared inline (declare const).

No behavior change. useAutoSave.test.jsx imports the .ts transparently and
passes. typecheck 0, lint 0 errors (47 warns), build green, 48 client tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:01:30 -05:00
null 3c51464bec refactor(ts): convert pure lib utilities to TypeScript (TS7)
Five self-contained leaf modules, no behavior change:
- reorder.ts — generic moveInArray<T>, reorderPayload, movedItemId
- billingSchedule.ts — a Schedule union + typed normalize/label helpers
- billDrafts.ts — SourceBill/Template/Category shapes for makeBillDraft
- trackerTableColumns.ts — typed column parse/normalize
- cashflowUtils.ts — typed SVG step-path timeline geometry (Safe-to-Spend)

noUncheckedIndexedAccess handled via in-range `!` (guarded lengths) and
destructuring defaults. Their .test.js suites import the .ts transparently via
Vite/vitest and still pass. typecheck 0, lint 0 errors (47 warns), build green,
48 client tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:59:16 -05:00
null 7c6be66794 refactor(ts): convert client/lib/trackerUtils to TypeScript (TS6)
The tracker row/status/sort helpers are now typed. Introduces a TrackerRow
domain interface (the fields these helpers read) and a TrackerStatus union
(paid|autodraft|upcoming|due_soon|late|missed|skipped). Row money fields stay
plain `number` for now — branding them Dollars is a later increment, done when
the tracker API response itself is typed so the brand flows in from the source.

Strict-mode handling: STATUS_SORT_ORDER is Record<TrackerStatus,number> (no
undefined on lookup); moveInArray guards the noUncheckedIndexedAccess `T |
undefined` from splice; the sort comparator relies on aliased-condition
narrowing to reach string|number in each branch. typecheck 0 errors, lint 0
errors (47 warnings unchanged), build green, 48 client tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:53:45 -05:00
null 7bb68db442 refactor(ts): convert usePaymentActions to TypeScript (TS5)
The shared quick-pay / toggle-paid mutation hooks are now typed: PayableRow for
the row refs, typed mutation payloads, and Dollars for the money amounts (so the
branded type flows through — a caller must supply branded dollars). Strict catch
clauses narrowed via an errMessage(unknown) helper. typecheck + lint (react-hooks
now enforced on this .ts) + build + 48 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:47:12 -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 b221e02d85 refactor(ts): convert client/lib/utils to TypeScript (TS4)
The highest-traffic leaf module (cn, fmt, date/byte/uptime formatters,
categoryColor) is now strict .ts. fmt inherits formatUSD's branded-dollars input
via Parameters<typeof formatUSD>; noUncheckedIndexedAccess handled (destructuring
defaults, in-range assertion). .jsx callers unaffected (Vite resolves the .ts).
typecheck + build + 48 client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:42:46 -05:00
null 7d9bf12bdc docs(history): TypeScript foundation + branded money types (T1-T3)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:37:21 -05:00
null 255036afc2 feat(ts): branded Cents/Dollars money types — first TS conversion (T2)
Converted client/lib/money.js -> money.ts with branded types:
  type Cents = number & { __unit: 'cents' }
  type Dollars = number & { __unit: 'dollars' }
plus asCents/asDollars/centsToDollars/dollarsToCents. The formatters now require
the correct branded unit (a bare number won't do), so a typed caller physically
cannot format cents as dollars (the 100×-too-big bug) or vice-versa. Existing
.jsx callers are unaffected (checkJs off) — gradual adoption.

money.type-test.ts is a compile-time guard (never imported/bundled): its
@ts-expect-error lines assert each unit mixup is a real error, so typecheck
fails loudly if the branding ever regresses. Verified: removing a guard makes
tsc error 'Argument of type 1234 is not assignable to DollarsInput'.

typecheck + build (with React Compiler) + 48 client tests all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:36:24 -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 a0a8579a08 refactor(tracker): shared useTogglePaid mutation hook — finish Tracker mutations (F1)
Toggle paid/unpaid was duplicated across the desktop and mobile rows (optimistic
flip + Undo/paid toasts + refresh). Extracted into a shared useTogglePaid()
React Query mutation hook: the caller keeps the instant local optimistic flip,
the hook rolls it back on error and invalidates the tracker/badge caches on
settle. isPending replaces the desktop row's local loading state (badge spinner
+ un-pay confirm dialog). With useQuickPay (P3), both core Tracker write actions
are now idiomatic useMutation hooks living in one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:18:18 -05:00
null c86acd7d75 docs(history): React Query deeper adoption — error handling, prefetch, useMutation (P1-P3)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:05:31 -05:00
null ad9fcbd56f refactor(tracker): shared useQuickPay mutation hook (P3)
Quick-pay was duplicated verbatim in TrackerRow and MobileTrackerRow (create
payment + Undo toast + refresh, with a manual busy flag on mobile). Extracted
into a shared useQuickPay() React Query mutation hook: isPending comes for free
(replaces the quickPaySaving state), the tracker/badge caches invalidate on
settle, and the flow lives in one place. Behavior identical.

(togglePaid keeps its local optimistic flip as-is; other mutations can adopt the
same useMutation pattern incrementally.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:04:43 -05:00
null e941f05cd6 feat(tracker): prefetch adjacent month on nav hover for instant switching (P2)
usePrefetchTracker() warms the ['tracker', y, m] cache when the user hovers/
focuses the prev/next month buttons, so clicking is instant (no round-trip).
No-op if already cached and fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:58:23 -05:00
null 2793927a5c feat(query): global background-refetch error toast (P1)
Added a QueryCache onError handler to the QueryClient: pages already render an
inline error on initial load, so this only toasts when a *background refetch*
fails while stale data is on screen (which would otherwise be silent). Restores
the load-error feedback dropped during the R5 migration, centrally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:56:31 -05:00
null 1fe71a7d5e docs(history): React Query migration of all 7 manual-fetch pages (R5)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:26:32 -05:00
null 6802a66e82 refactor(bank): migrate BankTransactionsPage ledger to React Query (R5.7)
The paginated/filtered ledger (the race-prone data) moves to a useBankLedger
query keyed on account/flow/page/query/sort — React Query handles caching,
dedup, cancellation and out-of-order responses, replacing the manual request-id
guard. Optimistic categorize routes through a setLedger setQueryData wrapper;
loadLedger is the query's refetch (mutations + Refresh); the refresh button uses
isFetching. Mount-once categories/bills stay local loads. This completes R5 —
all 7 manual-fetch pages are on React Query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:25:47 -05:00
null 2e8bc07552 refactor(spending): migrate SpendingPage to React Query (R5.6)
useSpendingSummary + useSpendingTransactions (paginated via a page-keyed query
with keepPreviousData) + useSpendingCategories + useCategoryGroups. Pagination
is now setTxPage (query refetches on the new key); a same-page loadTransactions
call invalidates to force a refresh. The editable budgets map seeds from the
summary via an effect; optimistic budget/summary and categorize edits route
through setQueryData wrappers; the R3 sequence guards are removed (React Query
handles races). load* calls became invalidate wrappers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:22:13 -05:00
null 1cb0325560 refactor(snowball): migrate SnowballPage to React Query (R5.5)
useSnowball + useSnowballSettings + useSnowballActivePlan + useSnowballPlans
(+ shared useCategories). The settings-derived form fields (extra payment,
ramsey mode, ready flags) are seeded via a settings-synced effect; the many
optimistic list/plan edits route through queryClient.setQueryData wrappers;
load() is an invalidate wrapper. The debounced live-projection stays a client
computation (not page data). Removed the now-dead loadPlans (hooks auto-fetch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:18:14 -05:00
null a37697d492 refactor(summary): migrate SummaryPage to React Query (R5.4)
useSummary(year, month) with keepPreviousData for smooth month nav. The editable
form fields (starting amounts, income) that loadSummary used to seed inline are
now seeded from the query result via a data-synced effect; refetchOnWindowFocus
is off so a background refetch can't reset a mid-edit. loadSummary is now an
invalidate wrapper (retry + post-mutation reconciliation), and the optimistic
expenses reorder writes through setQueryData.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:14:31 -05:00
null a0e4f87fe1 refactor(subscriptions): migrate SubscriptionsPage to React Query (R5.3)
useSubscriptions + useSubscriptionRecommendations (+ shared useBills/
useCategories). Optimistic updates (toggle, reorder, dismiss recommendation)
and their await-load() reconciliation preserved by routing setData/setBills/
setRecommendations through queryClient.setQueryData and load()/loadRecommendations
through invalidateQueries. The redundant mount-load effect was removed (hooks
fetch on mount). useOptimistic layer unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:12:02 -05:00
null 9cb254ea13 refactor(bills): migrate BillsPage to React Query (R5.2)
Reuses the shared useBills/useCategories caches (+ new useBillTemplates/
useDeletedBills), so bill mutations here now also refresh the Tracker/badge
live via the shared ['bills'] key. Optimistic list edits (delete, reorder)
write through queryClient.setQueryData; post-mutation load() calls became a
refresh() that invalidates the 4 page queries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:08:28 -05:00
null bb024ce161 refactor(analytics): migrate AnalyticsPage to React Query (R5.1)
Replaced the manual useState(data/loading/error) + load useCallback + useEffect
(and the R3 request-seq guard) with a useAnalyticsSummary(params) query hook.
React Query now handles caching, dedup, cancellation, and out-of-order responses
via the params-encoded key; keepPreviousData keeps the last result visible while
a new month/filter loads. Refresh -> refetch; the redundant page-load error toast
is dropped in favor of the existing inline error state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:04:14 -05:00
null 65c61d71fa docs(history): React correctness audit + ESLint enforcement (R1-R4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:58:54 -05:00
null 1387f7c2d7 fix(client): guard param-driven data loaders against out-of-order responses (R3)
The month/filter-driven loaders on Analytics, Summary, and Spending (x2)
fetched + setState with no race guard, so a slow response for old params could
overwrite fresher data (or setState after unmount) on rapid month/category nav.
Added the request-sequence guard already used by the Bank ledger (newest
request wins; stale ignored). Bills/Subscriptions load once ([] deps) so they
weren't at risk; BankTransactions already had the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:57:04 -05:00
null 02395b9ad4 refactor(client): remove orphaned dead logic flagged by ESLint (R2c)
- MobileBillRow: an autopayClass useMemo computed a badge class that was never
  rendered (dead since a refactor) -> removed.
- CategoriesPage: onRowKeyDown handler was never wired (keyboard toggle is now
  the dedicated chevron button, per the QA-B14-02 a11y fix) -> removed.
- BillModal: unused isDebtCategory derived flag -> removed.
Remaining lint warnings are unused imports / HMR / vestigial destructures
(non-correctness, non-blocking, now tracked by eslint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:51:21 -05:00
null b8d394061b fix(client): resolve all 13 exhaustive-deps warnings (R2b)
- BankSyncSection: handleBtSave read btLateGraceDays but it was missing from the
  useCallback deps -> saving could persist a STALE late-grace value (real bug).
- ProfilePage: EditProfile sync effect now depends on [profile].
- Stabilized identity of derived arrays/objects feeding useMemo deps (they were
  recreated every render, defeating memoization): TrackerPage filters + rows,
  HealthPage bills, BankTransactionsPage transactions -> wrapped in useMemo.
- BillModal + TransactionMatchingSection: intentional id/filter-scoped effects
  documented with a targeted eslint-disable + reason.
- Removed 2 stale eslint-disable directives (confirm-dialog, useAuth).

exhaustive-deps + rules-of-hooks now both 0. Build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:47:14 -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 38417ad8da docs(history): BM2 bill-modal decompose summary (v0.41.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:25:15 -05:00
null 20b46c81df refactor(bill-modal): extract TemplateSection (BM2, 7/n)
The save-as-template toggle + name input move to their own presentational
component. Behavior-preserving — BillModal 1105 -> 1090 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:23:57 -05:00
null ad68d965b6 refactor(bill-modal): extract LinkedTransactionsSection (BM2, 6/n)
Bank-matching rules (+ Sync action) and the linked-transaction list (+ Unmatch)
move to LinkedTransactionsSection. The big inline sync onClick is lifted to a
named parent handler (handleSyncBillPayments) sharing a refreshAfterImport helper
with handleRulesChanged. Dropped now-unused imports (BillMerchantRules, Link2,
Link2Off, RefreshCw, transactionDate, fmtTransactionAmount). Behavior-preserving
— BillModal 1193 -> 1105 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:22:58 -05:00
null 301bb152ef refactor(bill-modal): extract UnmatchDialogs + shared transactionDisplay (BM2, 5/n)
The three unmatch flows (choice dialog, single-unmatch confirm, bulk review with
optional merchant-rule removal) move to UnmatchDialogs; the transaction display/
matching helpers (transactionTitle/Date, fmtTransactionAmount, isSimilarPayee)
move to a shared transactionDisplay.js used by both the parent and the dialogs.
Dropped now-unused imports (Layers, Checkbox, formatCentsUSD). Behavior-
preserving — BillModal 1432 -> 1193 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:20:02 -05:00
null c61e3d84a5 refactor(bill-modal): extract PaymentFormFields (BM2, 4/n)
The add/edit manual-payment form (+ its PAYMENT_METHODS list) moves to its own
presentational component; the parent keeps the form state + submit handler.
Behavior-preserving — BillModal 1502 -> 1432 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:16:10 -05:00
null afba78e86b refactor(bill-modal): extract PaymentHistoryList (BM2, 3/n)
The payment-history list (rows with source badges + edit/remove for manual
payments) and its 4 display helpers (isTransactionLinkedPayment,
isHistoryOnlyPayment, paymentSourceLabel/Tone) move to their own presentational
component; the parent keeps payment state + add/edit/delete handlers and passes
them as props. Behavior-preserving — BillModal 1603 -> 1502 lines; build +
client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:11:05 -05:00
null 9d670985fe refactor(bill-modal): extract AutopayTrustIndicator (BM2, 2/n)
The edit-mode autopay trust panel (12-mo success rate, mark-verified, staleness/
failure warnings) moves to its own presentational component. Behavior-preserving
— BillModal 1637 -> 1603 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:07:35 -05:00
null 1018c55bb3 refactor(bill-modal): extract DebtDetailsSection (BM2, 1/n)
First BillModal decompose step: the collapsible Debt/Snowball fields (interest
rate, current balance, minimum payment, snowball visibility) move to
client/components/bill-modal/DebtDetailsSection.jsx as a presentational
component; state stays in the parent (the save action reads it). Behavior-
preserving — BillModal 1723 -> 1637 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:06:12 -05:00
null 2b710c459b feat(tracker): summary-card redesign — de-dup Paid, surface Remaining + progress (T3)
The summary row showed 'Paid' twice (this month + last month), indistinguishable,
while the actionable remaining only appeared in the CashFlow card below. Now:
- previous-month becomes a muted 'Last month: $X' hint under Total Paid (not a
  second green box);
- a Remaining card (existing unused blue type) surfaces summary.remaining when
  there's no bank hero (bank mode already shows projected remaining on the hero);
- a compact '$X of $Y paid · N bills left' progress line under the cards.
Respects tracker_show_summary_cards. Visual baseline for / may need a refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:49:35 -05:00
null 4a76eb9b92 feat(tracker): optimistic pay/skip + toast.promise on syncs (M1/M2)
M1: marking a bill paid/unpaid flips the row instantly via local optimistic
state (cleared when fresh data arrives, rolled back on error) on both desktop
and mobile rows, instead of waiting for the server round-trip.

M2: bank sync (Tracker) and the bill-modal Sync use sonner toast.promise —
one toast transitioning loading -> done -> error, replacing the manual
spinner-flag + separate success/error toasts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:47:32 -05:00
null 55b515c401 feat(tracker): Pay-all-due per bucket + reversible quick-pay with specific toasts (T4)
- Each bucket header gains a 'Pay all due (N)' action: one bulkPay for every
  unpaid gated bill in the bucket, behind a confirm (count + total) and a single
  Undo toast that deletes the created payments.
- Quick-pay and mark-paid now show a specific toast ('Rent - $1,200 paid');
  quick-pay gained an Undo action to match un-pay.
- Per-bill snooze already existed in the Overdue Command Center.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:43:01 -05:00
null 995f635d35 refactor(tracker): consolidate isPaidStatus + rowOutstanding + toast gap (T5)
Added a single isPaidStatus(status) (+ PAID_STATUSES) to statusService and a
matching client helper in trackerUtils, routing the unambiguous settled-status
checks through it (trackerService, StatusBadge, CalendarPage, rowIsPaid). The
intentionally paid-only counts stay distinct. Replaced two inline
Math.max(r.balance||0,0) with rowOutstanding, and gave the Tracker settings
load a quiet toast instead of a silent swallow. Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:36:30 -05:00
null d92cc38116 fix(bill-modal): correctness + toast fallbacks + validator consolidation (BM1)
- handleBlur now takes the field value explicitly (was positional guessing that
  fell through to interestRate for unmapped fields).
- Three copy-pasted money validators -> one shared validateNonNegativeMoney in
  client/lib/money.js; expected-amount copy 'positive' -> 'non-negative' (0 ok).
- Removed the save action's duplicate due-day/interest-rate re-validation
  (validateForm already covers it); kept the parses.
- Added err.message fallbacks to save/deactivate/verify-autopay toasts.
- Save toasts now name the bill.

Test: client/lib/money.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:32:25 -05:00