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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
ESLint surfaced 6 errors, incl. real bugs invisible before:
- ImportTransactionCsvSection called importErrorState() without importing it —
its own error handler would throw ReferenceError on a failed CSV import.
- client/components/MobileTrackerRow.jsx was a dead duplicate (unused; the live
one is tracker/MobileTrackerRow.jsx) with undefined-setter refs → deleted.
- StatusPage dbOk had a dead '?? true' (constant boolean LHS) — restored the
intended default-true-when-unknown.
- MobileBillRow redundant !! in a ternary condition.
Lint is now 0 errors; wired 'npm run lint' into 'npm run ci'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was no linting at all — nothing enforced rules-of-hooks (conditional-hook
crashes) or exhaustive-deps (stale closures) across 125 client components/pages.
Added an ESLint flat config (eslint.config.mjs) scoped to client/, an 'npm run
lint' script, and the devDeps. First run: 0 rules-of-hooks violations (good),
6 errors + 13 exhaustive-deps warnings to work through next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>