Data integrity (A):
- "Apply … to my budget" overwrote the bill's expected_amount (its real
recurring amount, used in budgets + payment recording) with the simulated
payoff payment. Replace with "Save APR to this bill" — persists interest_rate,
leaves expected_amount untouched. Fixes the corruption AND the interest-free
root cause (future runs use the saved APR). BillModal debt section also opens
when a bill has an interest_rate.
Error handling (B):
- Undo now has try/catch + error toast (was an unhandled rejection that left the
wrong value applied); Apply uses errMessage.
- loadData no longer snaps the selection back to the first bill after a
save/undo (was a stale-closure over selectedId); background refreshes are
"silent" so a transient failure no longer replaces the whole page.
Makes-sense (C):
- Distinguish "no APR" from a real 0% and show a notice (was silently interest-
free with no indication).
- Without a real minimum, show absolute Time-to-Payoff + Total Interest instead
of a broken "$0 savings" / "~580 mo sooner".
- Remove the arbitrary, mislabeled "Snowball plan" line (was extra applied only
to the alphabetically-first bill) from the page + chart.
- Detect the 600-month cap (incl. 0% tiny payment) as "won't pay off".
- Correct the "set a minimum on the Snowball page" guidance (no such editor).
Clarity (D):
- Plain-language toasts on every write; targeted HelpCircle tooltips (Interest/
Time Savings, Total Interest, Required Minimum, APR, Save-APR); a clearer page
description; states that explain why + what to do.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The user-facing pages pick up the new primitives: LoginPage (21
lines) gets the new form tone + the Bill Tracker wordmark on the
auth card; TrackerPage (109 lines) gets the new cashflow timeline
geometry, the new BrandGlyph on the bucket empty state, and the
new TonedCard on the summary stack; the rest are 1-9 line
import/header adjustments. App.tsx is the lazy-loaded import for
the new brand module on the about/privacy paths.
This is commit 6 of 9 in the v0.42.0 brand refresh. Depends on
b714715 (tracker cards).
- New POST /auth/logout-others keeps the current session and invalidates all
others (invalidateOtherSessions with the current session id), writes a
logout.others audit entry, and returns the count ended.
- api.logoutOthers(); a "Sign out of other devices" button in the Login History
dialog, guarded by an AlertDialog confirmation. On success: toast with the
count and refetch the history so it collapses to just this device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New per-user 'default_landing_page' setting (tracker default / calendar /
summary / spending), registered in the whitelist + defaults.
- Implemented as a post-login redirect (keeps /=Tracker so the Home link and
direct nav are unaffected). Precedence: admin → deep-link they were bounced
from (state.from) → default landing → /. Resolved by fetching settings in the
login success path, with a safe / fallback so a fetch failure never blocks login.
- Settings: Select in the Regional section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- statusService: resolveDueSoonDays (clamp 0–31, default 3) replaces the
hardcoded 3-day threshold in calculateStatus; threaded via options.dueSoonDays
through rowOptions in trackerService (×2) and routes/calendar.
- New per-user 'due_soon_days' setting registered in the whitelist + defaults.
- Settings: numeric "Due soon window" row next to Grace period, with min/max +
clamp guard. Tooltips added to Grace period, Due-soon, and Price-change
sensitivity explaining each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New per-user 'week_start' setting (0=Sun default / 1=Mon), registered in
USER_SETTING_KEYS + USER_SETTING_DEFAULTS.
- CalendarPage reads it and offsets the month grid: leading-blank count
(getDay()-weekStart+7)%7 and a rotated weekday header. Purely presentational.
- Settings: Sun/Mon Select in the new "Regional" section. Also aligns the
grace_period_days client default (3→5) with the server seed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Currency/Date-format dropdowns were decorative (nothing read them). Wire
them into the formatters, display-only (single active currency, no conversion):
- money.ts: activeCurrency/activeLocale read synchronously from localStorage at
load (correct first paint), setMoneyFormat() write-through; formatUSD/
formatUSDWhole/formatCentsUSD honor them. Default USD/en-US is byte-identical,
so money.test + the probe stay green.
- utils.ts: fmtDate honors an activeDateFormat (MM/DD/YYYY | DD/MM/YYYY |
YYYY-MM-DD) with setDateFormat() write-through.
- FormatSync (mounted in Layout) reconciles the server settings → localStorage/
module and re-renders once only on a genuine cross-device divergence.
- Settings selects write through on change, currency list expanded (AUD/CHF/JPY/
INR), "Regional" section, tooltips clarifying display-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
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>
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.
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>
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>
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>
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>