Typed MerchantRule/Conflict/Suggestion + generic useDebounce; billId widened to
Id (number|string) to match the api layer; asserted billId at the BillModal call
site (rendered only for existing bills). Dropped unused RuleChip billId prop.
typecheck 0.
Also removed a stale PayoffChart.jsx duplicate (its .tsx was already committed).
Branded Dollars on income transactions/bank-tracking balances. typecheck 0.
The XLSX importer (workbook parse → per-row decision tree → bulk actions →
bill-history grouped import). Typed PreviewRow/Decision/BillGroup/ImportDetail
and the merged BillImportResult accumulator; fixed Date subtraction in the
duplicate-date sort to use getTime(). Dropped two unused imports (CountPill/fmt)
the original .jsx never used. Trimmed unused eslint-disable directives.
This completes the entire client/components/data/ directory (14 files) → .tsx.
typecheck 0, lint 0 errors, build green.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
The notifier used a hard-coded 3-day early reminder and never read
reminder_days_before, so the modal's 'Reminder Days' control was a no-op.
The early reminder now fires at the bill's own lead (>= 2 days so it never
collides with the 1-day/same-day reminders); email subject+body say 'due in
N days'. Lead-time selection extracted to a pure exported reminderTypeFor()
for unit testing. The Reminder Days control now shows for every bill and a
non-subscription save no longer clobbers the column to 3.
Test: tests/notificationLeadTime.test.js
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sync button and merchant-rule historical import both CREATE payments but
only reloaded linked transactions, so the modal's Payment History stayed stale
and the Tracker row behind the modal didn't update (kept showing due/overdue)
until close+reopen. Both now await Promise.all([loadPayments(),
loadLinkedTransactions()]) then onSave?.(), matching the unmatch handlers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New services/userDataService.js eraseUserData() permanently wipes a user's
financial + imported data in one transaction (child → parent order for FK
safety): bills (+ cascading payments/monthly_bill_state/bill_history_ranges),
transactions/accounts/data_sources, categories/groups, templates, snowball,
spending rules/budgets, merchant rules, imports, and per-user hint tables. It
PRESERVES the account, sessions, 2FA/WebAuthn, login history and preferences —
this resets your data, not your account — then re-seeds default categories and
writes an audit row to import_history.
- POST /api/user/erase-data — rate-limited (demoDataLimiter), requires a
type-to-confirm token ("ERASE"), structured errors.
- UI: EraseDataSection danger-zone card (Export & backups pane) — red-accented,
"download a backup first" nudge, type-to-confirm AlertDialog, toasts; on
success DataPage reloads all state.
Tests: tests/eraseUserData.test.js — wipes user A only, preserves user B +
account + session, re-seeds categories, audited. Server 139 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GET /api/export now accepts a date range (?from=&to= on paid_date) in addition
to ?year=, for CSV or JSON; filename derived from the range. Validates the
range (both bounds, from<=to).
- New GET /api/export/user-json — full portable JSON of the user's data, reusing
the same getUserExportData assembly as the SQLite/Excel exports (money via
fromCents).
- UI (DownloadMyDataSection): a JSON export card plus a "Payments export" with
From/To dates and a CSV/JSON toggle; shared blob-download helper; toasts and
client-side range validation.
Tests: tests/exportRicher.test.js (JSON assembly in dollars, year vs range
filtering, CSV filename, bad-range rejection). Server 134 pass; build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New services/ofxImportService.js parses OFX 1.x (SGML, unclosed leaf tags),
OFX 2.x (XML) and QFX (+ Intuit tags ignored) into the same normalized shape the
CSV path produces, then writes through the SAME shared primitives (session table,
(user_id, data_source_id, provider_transaction_id) dedupe, import_history) — now
exported from csvTransactionImportService (additive; CSV tests still pass).
- Routes POST /api/import/ofx/{preview,commit} mirror the CSV two-step (raw
upload → structured commit; no column mapping since OFX is structured).
- UI: ImportOfxSection (upload → preview list → import) in the Import pane;
amounts shown via formatCentsUSD; toasts on preview/commit/malformed.
- Gap handling: signed TRNAMT → signed cents; DTPOSTED → YYYY-MM-DD; FITID →
stable provider id (hash fallback); non-OFX / empty files rejected clearly.
Tests: tests/ofxImportService.test.js (SGML + XML/QFX parse, entity decode,
signed cents, preview→commit, re-import dedupe, import_history). Server 129 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Transactions nav shows a live "N to review" badge (unmatched count from the
bank-ledger summary, limit:1 so it's cheap; refreshes on sync/import).
- Bank sync nav shows a green/amber/grey health dot (connected / needs-attention
/ off), mirroring the hero tone.
- Connection hero connected line now shows the transaction count at a glance
("SimpleFIN · 1,159 transactions · synced 2h ago · syncs automatically").
- Command palette gains Data section deep-links (Bank sync / Transactions /
Import / Export) via ?section=.
- Count/stat fetch is non-blocking (.catch → 0), never blocks the page.
Build clean; client suite 46 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the Data page shell into a settings-style two-pane layout (sticky goal
-nav on desktop, segmented on mobile) with:
- ConnectionHero — 5 states so a network blip is never mistaken for "not
connected" and a server without SimpleFIN never shows a dead Connect button
(loading / disabled / error+retry / not-connected / connected±needs-attention);
Sync-now handles partial errors, 429, and failure with toasts.
- DataNav — <nav> landmark, aria-current, keyboard, responsive.
- ?section= deep-linking via useSearchParams (URL source of truth → localStorage
→ default; migrates the old 3-tab key), so refresh/back-button work.
- Goal-based regroup into 4 panes with plain-language titles/subtitles/icons
passed via cardProps (every section component reused unchanged).
- Lazy panes: ImportSpreadsheet/ImportMyData code-split (own chunks) + only the
active pane mounts; framer-motion cross-fade (reduced-motion aware);
focus-to-heading on switch.
- Repoint BankTransactions "Open Data" → ?section=bank-sync; add /data to the
authed axe sweep.
Build clean (heavy panes split into their own chunks); client suite 46 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the tiny grey uppercase section titles with a modern header: optional
leading icon in a soft chip, sentence-case high-contrast title, calm subtitle,
a right-aligned rotating chevron, and optional statusDot/badge slots. API is
unchanged (title/subtitle/collapsible/summary/storageKey/actions preserved) so
no section internals change — purely the shared card chrome for the Data page.
Build clean; client suite 46 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>