Commit Graph

296 Commits

Author SHA1 Message Date
null 0cc5cbd957 refactor(ts): convert BankSyncSection to TypeScript
SimpleFIN connect/sync/backfill/accounts + bank-budget-tracking + auto-categorize.
Branded Cents on account/tx amounts, Dollars on financial-account balances.
Dropped unused sourceId/bills props on AccountRow. typecheck 0, build green.
2026-07-04 21:46:55 -05:00
null 5a2e37fd61 refactor(ts): convert data/ import + matching sections to TypeScript
ImportMyData (SQLite), ImportTransactionCsv, TransactionMatching sections.
Branded BankTransaction/Bill/Category flow through the matching workbench;
relaxed BillModal initialBill to Partial<Bill> (it's a pre-filled draft).

TypeScript caught a real bug: ImportTransactionCsvSection used <CountPill> 6×
but never imported it — a ReferenceError that crashed the CSV preview/results
views. Added the missing import.

typecheck 0, build green.
2026-07-04 21:42:18 -05:00
null dfedb75e6d refactor(ts): convert data/ shared + small sections to TypeScript
dataShared (SectionCard/CountPill/importErrorState), DataNav, ConnectionHero,
DownloadMyData/Erase/SeedDemo/ImportHistory/AutoMatchReview/ImportOfx sections.
Branded Cents on OFX preview txns, Dollars on auto-match amounts; SectionCardProps
exported for section cardProps passthrough. typecheck 0, build green.
2026-07-04 21:33:25 -05:00
null a8496a9c64 refactor(ts): convert admin/ cards to TypeScript (AuthMethods, BankSync, Backup, ...)
Completes the entire client/components/admin/ directory conversion to .tsx:
OidcForm/AuthConfigData/OidcTest types for the OIDC config form; Backup/
BackupSettings types for the backup manager. errMessage() on all strict
catch(unknown) sites. typecheck 0, lint 0 errors, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 21:26:22 -05:00
null c9b5f93569 refactor(ts): convert LoginModeCard + EmailNotifCard admin cards to TSX (B18)
LoginModeCard (AuthModeConfig; AdminUser[]) and EmailNotifCard (EmailConfig +
keyed set(k,v)). typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 21:19:39 -05:00
null a9a58d8aa2 refactor(ts): convert CleanupPanel + UsersTable admin cards to TSX (B17)
CleanupPanel (CleanupForm/CleanupStatus/CleanupResult; keyed toggle array typed
[keyof CleanupForm,string][]). UsersTable (AdminUser type exported + reused by
AdminPage's users state; ResetForm; typed row handlers). typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 21:17:16 -05:00
null ea531f06c7 refactor(ts): convert OnboardingWizard to TSX (B16b)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:45:53 -05:00
null 7f4ef20b7c refactor(ts): convert admin shared helpers + AddUserCard; drop dead PrivacyAdminCard (B16)
adminShared.tsx (SectionHeading/FieldRow/Toggle/formatDateTime/BackupTypeBadge —
shared by the admin cards) and AddUserCard.tsx. Deleted PrivacyAdminCard.jsx:
TypeScript surfaced that it's imported nowhere AND calls non-existent
api.privacySettings/setPrivacySettings (the geolocation toggle actually lives on
/profile/settings) — dead code with broken calls, so removed rather than
converted. typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:44:40 -05:00
null 9ad8cc7e8a refactor(ts): convert SearchFilterPanel, RecentlyDeletedBillsDialog, BillRulesManager to TSX (B15)
SearchFilterPanel (presentational props), RecentlyDeletedBillsDialog (Bill[],
days_left cast, formatUSD on Dollars), BillRulesManager (MerchantRule/RuleGroup,
reduce<Record<number,RuleGroup>> with `?? (acc[key]=…)` to satisfy
noUncheckedIndexedAccess). typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:42:24 -05:00
null 6a9b8b3cb8 refactor(ts): convert transactions/ + snowball/ dirs to TSX (B14)
transactions/: CategoryPicker (Category[], Node-cast for outside-click),
MatchBillDialog (BankTransaction/Bill, advisory_filter cast). snowball/:
PayoffChart (TrackPoint chart geometry, asDollars for formatUSD), PlanHistory
Panel + PlanStatusBanner (SnowballPlan/ActivePlan types with Dollars-branded debt
balances; asDollars at fmt boundaries, ?? guards for optional-money comparisons).
typecheck 0, lint 0 errors, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:39:30 -05:00
null f34f03af51 refactor(ts): convert content pages to TSX (B13)
PrivacyPage (PrivacyDoc/PrivacySection + Record-typed iconByTitle), NotFoundPage
(typed GlitchDigit rAF loop, HTMLSpanElement ref), AboutPage (UpdateStatus/
AboutInfo + UpdateBadge), ReleaseNotesPage (ReleaseHistoryData, typed markdown
line parser). typecheck 0, lint 0 errors (35 warns), build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:32:06 -05:00
null 5e803f4547 refactor(ts): convert AdminPage + SubscriptionCatalogPage to TSX (B12)
First two pages — confirms the page pattern (self-contained, untyped .jsx
feature-children accept any props). AdminPage: User/boolean|null/unknown[] state
+ API-response casts. SubscriptionCatalogPage: consumes the now-typed BillModal
(Bill/Category), casts the partial fallback bill. typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:14:30 -05:00
null 73be95d12d refactor(ts): convert BillModal to TSX (B11) — bill-modal feature done
The app's largest, most-central component (1092 ln, the add/edit form reached
from 9 entry points). Typed ~40 useState vars (Bill/Payment/BankTransaction/
BulkUnmatchState/Date), the useActionState save (isNew narrows `bill` non-null
via the aliased `!bill` const), FormErrors = Record<string,string>, and every
API response cast (billPayments/billTransactions/syncBillSimplefinPayments/
verifyAutopay/billMerchantRules). Its now-typed sub-components type-check the
props BillModal passes them. cycle_day coerced to string for Radix Select;
Bill index-sig fields (autopay_verified_at, source_bill_id) cast at use.

client/components/bill-modal/ + BillModal are 100% .tsx. typecheck 0, lint 0
errors (35 warns), build green, 48 tests, 17/17 e2e probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:10:48 -05:00
null 31ff6adbd9 refactor(ts): convert bill-modal sub-components to TSX (B10)
The 7 BillModal sub-components + transactionDisplay helper: TemplateSection,
AutopayTrustIndicator, PaymentFormFields, PaymentHistoryList, DebtDetailsSection,
LinkedTransactionsSection, UnmatchDialogs. New @/types.BankTransaction — raw bank
amounts stay in CENTS on the wire, so branded `Cents` (formatCentsUSD, not
formatUSD), the complement to the Dollars brand. BulkUnmatchState typed with
null-safe setState callbacks. typecheck 0, lint 0 errors, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:03:09 -05:00
null 8a4b51915f refactor(ts): convert SummaryCard + ReleaseNotesDialog to TSX (B9)
SummaryCard (top-level; CardType/CardDef, value: Dollars) and ReleaseNotesDialog
(the version-ack modal we hardened; typed useAuth, HTMLElement-cast for
activeElement focus). typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:54:40 -05:00
null 8937dc5e25 refactor(ts): convert app-wide common components to TSX (B8)
PageLoader, PageTransition, StatusBadge (top-level), MarkdownText (typed
renderInlineMarkdown, isValidElement for keys), and ErrorBoundary (typed class
component: ErrorBoundaryProps/State, getDerivedStateFromError, ErrorInfo;
generic withErrorBoundary<P> HOC). typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:52:47 -05:00
null 609de2a946 refactor(ts): convert the app shell to TSX (B7)
App.tsx (routing + QueryClient), main.tsx (entry; index.html updated to
main.tsx), useAuth.tsx (typed AuthContextValue/User + MeResponse; uses the safe
useContext()||defaults pattern so no `never` trap), and the layout components
(Layout, Sidebar, NavPill, BrandBlock — NavItem type, LucideIcon-typed nav
config, casts for the untyped overdue/simplefin API responses). TS caught a dead
prop: App passed mainContentId to TrackerPage, which takes no params — removed.

typecheck 0, lint 0 errors (39 warns), build green, 17/17 e2e probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:49:57 -05:00
null c2097efc49 refactor(ts): convert TrackerRow + TrackerBucket to TSX (B6) — tracker/ dir done
The two largest tracker components. TrackerRow (726 ln): typed threshold/
optimistic-actual as branded Dollars, amount_suggestion/autopay_stats now real
@/types (AmountSuggestion/AutopayStats) with null-safe `stats?.` access,
querySelectorAll<HTMLElement> for keyboard nav, Date.now()-getTime() arithmetic,
drag-handler casts for the motion.tr TableRow. TrackerBucket: typed
dragPropsFor/moveControlsFor returns (DragProps/MoveControls), bulkPay result
cast, asDollars at every total's fmt boundary.

client/components/tracker/ is now 100% .tsx (18 components). typecheck 0, lint 0
errors (40 warns), build green, 48 client tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:42:26 -05:00
null f4f1dadaab refactor(ts): convert MobileTrackerRow to TSX (B5)
The mobile tracker row (consumes branded TrackerRow + shared payment hooks).
Supporting: TrackerRow.sparkline typed number[]|null; ROW_STATUS_CLS typed
Record<string,string> (it has no `skipped` key, so indexing by TrackerStatus
needed a string index). DragProps/MoveControls interfaces exported. Notable: the
HTML5 drag handlers conflict with framer-motion's own onDrag* gesture types on
motion.div — cast via ComponentProps<typeof motion.div>; quick-pay/remaining
amounts need asDollars at the boundary. typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:36:01 -05:00
null ee31513dc9 refactor(ts): convert Drift + Overdue panels to TSX (B4)
DriftInsightPanel (new @/types DriftBill — its own row shape, money in dollars)
and OverdueCommandCenter (TrackerRow[]). Note: TS flags `Date - Date`
arithmetic → use .getTime(); the overdue/net-delta totals need asDollars at the
fmt boundary. typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:32:39 -05:00
null c1ec0a4f2e refactor(ts): convert tracker dialogs + cards to TSX (B3)
Six tracker components consuming the branded domain types:
AutopaySuggestionActions, CashFlowCard (typed TrackerCashflow + TimelineChart/
UpcomingList), MonthlyStateDialog, PaymentModal, PaymentLedgerDialog,
StartingAmountsEditDialog. New @/types: AutopaySuggestion, TimelineBill; Payment
gains method/autopay_failure. Branded money paid off again — fmt(Math.abs(safe))
and the starting-amounts arithmetic correctly required asDollars wraps at the
format boundary. typecheck 0, lint 0 errors (41 warns), build green, 48 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:29:42 -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 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 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 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 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 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 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 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