- ThemeContext gains a 'system' theme that follows prefers-color-scheme and
updates live when the OS flips; exposes resolvedTheme (consumed by Sonner) and
keeps the <meta theme-color> in sync. Pre-bundle inline script in index.html
prevents a light/dark flash. Both theme selectors (Settings ThemeCards +
theme-toggle dropdown) gain the System option.
- New MotionPreferenceContext mounts framer-motion <MotionConfig>; a Reduce-motion
toggle (localStorage device pref) forces reducedMotion="always" (else "user",
which still respects the OS).
- Add an accessible SettingHelp tooltip (focusable button + aria-label) wrapped by
a page-level TooltipProvider; used for the System + Reduce-motion explanations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tier B moved the tracker's display settings onto a React Query cache
(staleTime 5m). SettingsPage saves via its own path and didn't touch that
cache, so toggling a region off (e.g. the Overdue Command Center) could leave
the tracker showing the stale value for up to 5 minutes. Invalidate the
['settings'] query after a successful save so returning to the tracker refetches
and the toggle takes effect immediately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- aria-label on the pencil "Edit bill" buttons (were title-only), matching the
autopay buttons' title+aria-label pattern.
- role="status" aria-live on the filter result count so filtering announces
"N of M shown"; a visually-hidden live region announces drag/keyboard reorder
("Moved <bill> to position N of M").
- Header "Keyboard shortcuts" dropdown documenting the (previously invisible)
j/k/arrows/Enter/p/Esc row shortcuts + ⌘K palette.
Deferred as low-value/optional: extra display toggles (C4) and a due-this-week
chip (C5) — avoided header/option clutter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add a leading selection checkbox to each desktop + mobile row, threaded from
the page through TrackerBucket via an optional RowSelection prop (renders only
when the page supplies a toggle handler).
- Page owns the selection Set; a sticky bottom action bar appears when ≥1 bill is
checked: "N selected · Pay · Skip · Clear".
- Pay selected reuses the bulkPay item shape + Undo from "Pay all due".
- Skip selected fans out saveBillMonthlyState (no bulk endpoint; skip has no money
effect) with Promise.allSettled, an aggregated "n skipped / m failed" toast, and
an Undo that un-skips. Selection clears on month change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Header "Export" menu: Export CSV (via the existing /api/export?from&to date
range for the viewed month) and Print (window.print()). Extract the shared
downloadFile blob helper to lib/download.ts (reused by DownloadMyDataSection).
- Add api.exportRangeUrl(from, to, fmt).
- Tracker-scoped @media print CSS: hide interactive chrome (.tracker-print-hide),
keep the month title + summary + buckets, avoid splitting a row across pages.
- Header "Calendar" button deep-links to /calendar?year&month; CalendarPage now
reads those params as its initial month (falls back to today).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- B1: replace the two raw useEffect/api fetches (simplefinStatus, settings) with
useSimplefinStatus() and useSettings() query hooks — cached/deduped like the
rest of the app. Tracker settings now derive from the ['settings'] cache
merged under module-level TRACKER_SETTING_DEFAULTS.
- B2: settings save is now useSaveSettings() — a useMutation with optimistic
cache update + rollback on error, replacing the hand-rolled optimistic/refetch.
- B3: useDeferredValue on the search term feeding filteredRows so typing stays
snappy on large months (matches BillsPage/SubscriptionsPage).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extract parseUtc (was copy-pasted in 4 files) and settingEnabled (3 copies,
one named settingsBool) into client/lib/utils.ts as the single source.
- Replace all local copies with the shared import.
- Add tracker_show_safe_to_spend to the tracker's local settings defaults so it
matches SettingsPage's list (behavior unchanged; aligns the two sources).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete unreachable in-file dialogs: MonthlyStateDialog block (setShowMbs
never called) and inline PaymentModal in TrackerRow + MobileTrackerRow
(setEditPayment only ever null; editing works via PaymentLedgerDialog).
- Delete fully-unreferenced files: tracker/EditableCell.tsx,
ui/confirm-dialog.tsx, ui/separator.tsx (+ orphaned @radix-ui/react-separator).
- Simplify OverdueCommandCenter dead branch (early-returns null when empty).
- Correct stale roadmap/FUTURE docs: tracker keyboard nav is implemented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pin-upcoming sort ranked on raw row.status, so a bill paid-by-threshold
but still carrying a stale 'late'/'missed' status was pinned to the top.
Rank on rowEffectiveStatus (same source the overdue filter uses) so it
sorts to the bottom instead.
- Filtering to zero results showed a blank content area (rows.length checks
the unfiltered list). Add a "No bills match your filters" state with a
Clear-filters action.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both pages filter the whole list on every keystroke. The input now updates
instantly while the (deferred) filter+render lags behind — React 19's
useDeferredValue, which the app wasn't using anywhere. Typing stays snappy
on large lists; zero behavior change otherwise. typecheck/lint/build clean,
probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parseJsonSafe/upload-body → a structured ResponseBody type (typed error
envelope + index sig) instead of any, no cast churn; declineRecommendation
→ Recommendation; snowballPlans endpoint typed so the hook drops its (d: any).
Annotated the Spending settings-load catch as best-effort (Finding #8) —
cosmetic options fall back to defaults, no mount-time nag. Client 0 any's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: _fetch had no timeout — a hung request (server accepted the
connection but never responds) spun the UI forever with no error. Now
wrapped in AbortSignal.timeout(120s, generous enough for the slowest
legit import/sync/backup on a LAN) and both timeout and network failure
map to a clean ApiError (REQUEST_TIMEOUT / NETWORK_ERROR) so they surface
as a normal error toast instead of a freeze or raw 'Failed to fetch'.
Server: the global error handler now returns next(err) when res.headersSent,
so an error after a partial response can't throw 'cannot set headers'.
(The async-handler-hang gap is folded into the Express 5 upgrade in Phase
2.2, which forwards async rejections natively — no throwaway wrapper.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Track C (auth): User lived in @/hooks/useAuth; moved it to @/types
(re-exported from useAuth for the 7 importers) and typed me/login/
totpChallenge/hasUsers, dropping those Admin/Login casts. Left the
single-consumer non-money casts (adminUsers, version/about/privacy/
health docs) and the optimistic-UI rewrite as deliberate low-value/
high-churn stops.
Track D: global 500 handler now emits code:'INTERNAL_ERROR' so an
unexpected error carries the same {error,code} field as structured 4xx.
typecheck/lint(0 err)/build/server-193 all green; probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted SnowballProjection (+details) and SnowballSettings into @/types;
wired snowball (Bill[]), snowballSettings/saveSnowballSettings, and
snowballProjection with get<T>/_fetch<T>. Removed the projection/settings/
bills casts. Plan-lifecycle SnowballPlan casts kept (component-owned type).
typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted SubscriptionsPage's Subscription/SubData/Recommendation/
CatalogMatch/SubTx (+ deps) into @/types and wired subscriptions/
recommendations/transaction-matches/create/match-bill with get<T>/
post<T>. Removed the (r: any) in useSubscriptionRecommendations, the 5
page casts, and an unused CatalogMatch import. typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted SpendingPage's local shapes into @/types (SpendingCategoryEntry,
SpendingSummary, SpendingTransaction(s), SpendingIncome*, SpendingRule,
CategoryGroup, CopyBudgetsResponse) and wired the /spending/* + category-
groups endpoints with get<T>/post<T>. Hooks return typed data; 6 casts
removed; 2 orphaned type aliases dropped. typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted BankTransactionsPage's local Tx/Account/Ledger/... interfaces
into @/types (BankLedgerTransaction extends BankTransaction so amounts
stay branded Cents) and wired bankTransactionsLedger/match/unmatch/
ignore/unignore/apply-merchant-match/auto-categorize with get<T>/post<T>.
useBankLedger data is now typed; all 9 as {...} casts in the page removed.
Dropped a dead Cents import. typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deliberate manual POST /payments had no dedupe guard (double-submit
made a second payment), but silently deduping it like the auto paths
would lose a legitimately-identical same-day payment — a money bug. So
the server returns 409 DUPLICATE_SUSPECTED (existing payment attached),
and a shared client helper (client/lib/paymentActions.ts, used by the
tracker inline cell, partial-payment dialog, and bill modal) turns it
into an 'add anyway?' toast that resends with allow_duplicate. Automated
one-click paths keep deduping silently (a repeat there is a retry).
Dropped a now-unused api import in PaymentLedgerDialog (Track E sweep).
Server suite 188 green; typecheck/lint/build clean; probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bill list/reorder/history-visibility page typed: Bill[] from useBills,
BillFilters/DisplayPrefs/ModalState/HistoryRange local types, drag +
move-controls typed via DragProps/MoveControls, makeBillDraft(...) as
Partial<Bill> at the modal call sites, setQueryData<Bill[]> for
optimistic list edits, errMessage(unknown) for strict catches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
YNAB-style spending view typed: CatEntry (category_id number|'other'|
null) with a RealCatEntry narrowing for the budget-indexed paths,
SpendingSummary/SpendingTx/IncomeTx/SpendingRule/CategoryGroup local
types, budgets as Record<string,number|null>, React Query
setQueryData<T> for optimistic budget/categorize edits, cast-at-boundary
for api.* results. Dropped a pre-existing dead 'total' state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cents-based bank ledger typed end-to-end: local Tx interface (amount:
Cents, index sig for BankTransaction compat), Ledger/Account/Summary
types, React Query setQueryData<Ledger> for optimistic row patches,
cast-at-boundary for api.* results, Promise.allSettled bulk handlers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tracker: URL-driven month/filters/sort, React Query data (TrackerResponse),
bank-projection banner, summary cards, safe-to-spend, overdue center, drift
insights, drag-reorder buckets, and all row/modal orchestration. TrackerSummary.trend
is typed unknown in @/types so it's cast at the TrendCard boundary.
ProfilePage (profile summary + login history + edit + notifications + push +
2FA + privacy) — exports asSettings/NotificationPreferences/PushNotifications
now typed, so SettingsPage picks up real types. Widened useAuth setUser to
Dispatch<SetStateAction<User|null|undefined>> to allow the updater form.
TS flagged a dead prop: onSaved was passed to NotificationPreferences (which only
takes settings) — removed it. Notification components imported from ProfilePage
(still .jsx) type as any until that page converts.
Annotated makeBillDraft's return as SourceBill (index-signatured) and exported it,
so its draft output stays typed; BillModal keeps initialBill: Partial<Bill> and
callers cast makeBillDraft(...) as Partial<Bill> at the boundary. typecheck 0.
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>