- delete root test-functional.js (imported uninstalled 'playwright' —
could never run) and run-functional-test.js (legacy pre-Playwright)
- delete postcss.config.js + autoprefixer/postcss devDeps (tailwind v4's
vite plugin owns the pipeline; the config had no plugins left)
- client/lib/money.ts: drop unused SUPPORTED_CURRENCIES; keep
dollarsToCents exported + @public-tagged (API symmetry with
centsToDollars — the sanctioned unit crossing)
- client/lib/billDrafts.ts: drop unused BillDraft type export
- move the two runtime seed JSONs (advisory filters, merchant-store
matches; 7 MB) out of docs/ into db/data/ next to their loader;
fresh-DB seed verified (5000+5000 rows); Dockerfile COPY . covers it
- knip.json: apply config hints, ignore the two CSS-side tailwind deps;
npx knip exits 0 — added to CI as a BLOCKING step so unused
files/exports/deps fail instead of rotting (this is how the WebAuthn
ghost feature survived four QA cycles)
NOT deleted: client/public/img/doingmypart.jpg — flagged unused by the
code grep but actually referenced from HISTORY.md markdown that the
release-notes renderer serves (user catch). Lesson encoded in the QA
plan: asset-reference sweeps must include rendered markdown content.
Full ci green (252 server / 52 client / build / knip 0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Official codemod (run in a clean worktree; docker-owned data/ blocks it
in-tree): tailwind.config.js inlined into client/index.css as CSS-first
@theme/@utility (config file deleted — v4 auto-detects content, which
also retires the .tsx-content-glob failure class from the TS migration),
33 template files' class renames applied, tailwindcss-animate loads via
the v4 @plugin bridge. Build runs through @tailwindcss/vite; postcss
config is now plugin-free. tailwind-merge -> 3 (v4 class model).
Visual-regression gate: only diffs were a ~2px global preflight shift +
the version string (baseline predated 0.41.1); re-baselined both login
screenshots in this commit after review. webauthn.probe confined to the
probe project (it enables WebAuthn for the seeded user, racing the
parallel UI projects' password logins).
e2e 27 passed, probe 18/18, PROD SMOKE PASS, typecheck/lint/tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build drops 6.5s -> 0.65s. Three breakages found by the prod-smoke gate,
all fixed:
- rolldown removed object-form manualChunks -> function form in
vite.config.mjs (same vendor split, all @radix/@tanstack grouped)
- the 4a 'send' bump rejects bare absolute paths: SPA fallback sendFile
and both download routes (admin backup, user-db export) now use the
root-option form
- rolldown minifies inline HTML scripts, breaking any CSP hash approach:
the pre-paint theme script moved to client/public/theme-init.js so
script-src 'self' holds with no inline hash to maintain
@testing-library/dom added (peer no longer auto-installed). Server 252,
client 52, probe 18/18, PROD SMOKE: PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backend existed since 99abca9 with no way to reach it (QA-B1-01).
Now wired:
- LoginPage: requires_webauthn second step mirrors TOTP — the key prompt
fires automatically after the password; cancel/retry + back-to-sign-in
- ProfilePage: PasskeySection (mirrors TotpSection) — enroll with a name,
list keys, password-gated per-key remove + remove-all
- webauthnService: WEBAUTHN_ORIGIN stays authoritative; otherwise accept
the browser's origin when its hostname equals the RP ID — fixes real
dev/e2e enrollment where the Vite UI port differs from the API port
(the browser already enforces RP ID ⊆ origin, so no widened trust)
- Deploy safety: WEBAUTHN_RP_ID documented in .env.example + a boot-time
prod warning in utils/env.cts (localhost-bound keys silently fail
behind a real domain)
- e2e/webauthn.probe.spec.js: CDP virtual authenticator drives the full
lifecycle (enroll -> key sign-in -> password-gated removal), retiring
Cycle 1's 'needs a human with a key' assumption. Probe suite 18/18.
Server 252/252, client 52/52, typecheck + build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- eslint: no-console error on all server dirs (sanctioned exceptions:
utils/logger.cts sink, utils/env.cts bootstrap, utils/apiError.cts
fallback); no-restricted-imports bans errorFormatter in routes/** so
hand-rolled standardizeError bodies can't return (transactions/import
are the two documented exception files)
- client/api.ts: 6 copy-pasted ApiError constructions collapsed into one
toApiError() helper
- CODE_STANDARDS.md: 'Error responses' and 'Logging' flipped target ->
ENFORCED, with the full documented-exceptions list and the client
error-shape/mutation-pattern conventions written down
Lint 0 errors, client 52/52, typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
useAuth only checked /auth/me on mount, so a session that expired later left
the app half-alive: mutations failed with raw 401 toasts and no way back to
login. api.ts now dispatches 'auth:expired' on any 401 outside /auth/*
(under /auth/* a 401 means wrong credential input — e.g. change-password —
and must not log the user out); AuthProvider listens and clears the user, so
the existing RequireAuth redirect kicks in preserving state.from.
Test: client/api.sessionExpiry.test.ts (both directions).
(QA follow-up 1e, error-handling audit)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BrandGlyph drew hardcoded monochrome shapes from an inline switch and
ignored the actual /brand/glyphs/*.svg files. Render the real files via a
robust <img> (fail-soft onError, intrinsic width/height so no layout
shift, alt for a11y parity) so every existing placement — PageHeader
(Tracker/About/Release), Command Palette, Release Notes, Onboarding,
Login — shows the full-color brand tiles. Deletes the ~85-line dead
BrandGlyphShape switch and the now-unused useId import.
Senior-review while-here:
- Guard test (client/lib/brand.test.ts): asserts every BrandGlyphName
asset — and every /brand asset in brand.ts — resolves to a real,
non-empty file on disk. Catches the "image added but path wrong /
renamed" regression TS can't see.
- Widen vitest include to {js,jsx,ts,tsx}: TypeScript client tests were
silently never run (client/hooks/useAutoSave.test.ts). Now 6 files / 50
tests run (was 4 / 42).
- Remove dead brand.ts entries: legacyLogo (pointed at the OLD pre-brand
/img/logo.png) and the unused ADMIN_GLYPH const.
- PageHeader glyph class inline-grid -> inline-block for the <img>.
Verified: typecheck + lint clean; test:client 50/50; build (glyphs land
in dist/brand/glyphs, served 200 as image/svg+xml); e2e probe 17/17;
login visual baseline unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo seeder created bills + categories only, so nearly every page
rendered empty; clear-demo had a data-loss bug and error-protection gaps.
Seed now populates every user-facing surface: bills with autopay states,
3-6 months of payment history (with drift + debt paydown), monthly
skips/overrides/snoozes/notes, a demo bank source + accounts + matched/
unmatched/subscription transactions, categorised spending + budgets +
rules, planning income/starting-amounts, category groups, an active
snowball plan (+ extra-payment), merchant rules, and a calendar feed —
so a new user can explore Tracker (incl. overdue/drift), Analytics,
Summary, Spending, Snowball, Payoff, Transactions, Matches, Subscriptions,
Banking and Calendar with realistic data.
Correct removal (the emphasis):
- Migration v1.07 adds an is_seeded marker to the 12 user-scoped,
non-cascading demo tables (bill-children cascade with their seeded bill).
- clearSeededDemoData() (new, in userDataService alongside eraseUserData)
removes ONLY seeded rows in one transaction, child->parent, and keeps a
seeded category if a user's own bill still references it.
- Fixes the category collision: seed marks ONLY categories it creates, so
clear no longer deletes the user's default categories / uncategorises
their real bills.
Error protection: idempotency guard on seeded (not all) bills -> 409
instead of a fake "created 0"; whole seed wrapped in one transaction
(atomic); rate-limit + audit + standardizeError on all three routes;
seeded-status reports payments/transactions counts; dead --force removed.
Client copy/counts corrected.
Verified: typecheck/check clean; server suite 232/232 (+6 new); client
typecheck/lint/build clean; e2e probe 17/17; on a COPY of the real prod
DB the v1.07 migration applies (integrity ok) and a seed->clear round-trip
on the data-rich user restores their real data byte-for-byte with no
orphans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Overdue Command Center and Price-Change (drift) panels reset to
expanded on every load — their collapse arrows used local useState. Now
the collapsed state is remembered per-user via two new settings
(tracker_overdue_collapsed / tracker_drift_collapsed), mirroring the
search panel's useSearchPanelPreference pattern. Each panel header also
gains an inline "Hide" control (EyeOff, like the search panel) that writes
the existing tracker_show_* setting and fires an Undo toast; hidden
sections come back from Settings → Tracker Layout.
- services/userSettings.js: allowlist + defaults for the two collapse keys
- TrackerPage: read collapse state, hide handlers with Undo, wire props
- OverdueCommandCenter/DriftInsightPanel: controlled collapse + header hide
Verified: typecheck 0, lint 0 errors, build, and a live settings round-trip
(collapse persists, hide writes, Undo restores, allowlist rejects junk keys).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The encryption key must never be retrievable through the app (that would undo
the whole point of TOKEN_ENCRYPTION_KEY — a stolen session/XSS could then read
the master key). Instead surface a non-reversible fingerprint so operators can:
- verify which key is currently active,
- confirm a running instance matches the key they backed up, and
- spot an accidental key change (which would make existing secrets unrecoverable).
- encryptionService.keyFingerprint(): domain-separated SHA-256 prefix of the
active key (env if set, else the stored DB key); never force-creates a key
(returns null when none exists), shares nothing with the cipher derivation.
- admin bank-sync-config exposes key_fingerprint alongside encryption_key_source.
- Admin Bank Sync card renders it with backup guidance.
- Test: stable per key, differs by key, not reversible / not the raw key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Brand.tsx (101 line churn) and brand.ts (47 line churn) now expose
the new SVG-first primitives:
- BrandMark: <img> with BRAND.assets.mark (vector, scales to any size).
- BrandWordmark: text fallback if the mark is unavailable.
- BrandLockup: the combined mark + wordmark for the layout chrome,
honours the light/dark variant via CSS variables.
- BrandGlyph: unchanged but re-uses the new accent tokens.
brand.ts adds the new asset-path constants (mark, markMono, lockup,
lockupLight, socialPreview) and a tokens helper so the lockup can
switch variants without prop drilling.
This is commit 2 of the v0.42.0 follow-up batch. Depends on
720d31b (SVG assets).
Adds the vector source-of-truth for the v0.42.0 brand refresh:
- lockup.svg / lockup-light.svg: the wordmark + mark combination,
light and light/dark variants
- mark.svg / mark-mono.svg: the standalone mark, colour and mono
- social-preview.svg: 1200x630 Open Graph / oEmbed card
These are the assets the new BrandMark / BrandWordmark / BrandLockup
components (commit 3 of this batch) consume, and the assets the
favicon / PWA manifest (commit 4) reference. They replace the
PNG-only brand directory committed in dfee7f9 with a vector
first-class set; PNG derivatives stay in the same dir for legacy
favicon / PWA paths.
This is the first commit in the v0.42.0 working-tree follow-up; the
previous v0.42.0 brand refresh landed as 9 commits (dfee7f9..3de91af)
in the prior round.
The v0.42.0 brand refresh renamed client copy but missed a few user-facing
server strings:
- routes/about.js: the /api/about `name` (rendered as the About page title) was
still 'BillTracker' → 'Bill Tracker'.
- userDbImportService.js: two import-error messages referenced 'BillTracker'.
- Remove the now-unused APP_NAME export from lib/version.ts (0 consumers;
lib/brand.ts BRAND.name is the canonical source).
Left intentionally as identifiers (not display copy): the updateCheckService
User-Agent 'BillTracker/<version>' and all repository URLs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top-level client/components/StatusBadge.tsx has no importers — the tracker's
components/tracker/StatusBadge.tsx (used by TrackerRow/MobileTrackerRow) is the
real one. Deferred from the previous cleanup because it had uncommitted brand-
refresh edits; those landed in b714715, and it's still dead, so it goes now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the repo-wide dead-code audit (High-confidence, re-verified against the live
tree; 0 references each):
- Delete client/components/SummaryCard.tsx — orphan superseded by the tracker's
components/tracker/SummaryCards.tsx (the one actually imported).
- Remove the unused withErrorBoundary HOC from ErrorBoundary.tsx (the
ErrorBoundary component itself stays; it's used app-wide).
- Remove 17 dead api.ts client wrappers with no caller (spendingBudgets,
upcomingBills, billAmortization, billMonthlyState, archiveBill,
deleteBillTemplate, updateSnowballPlan, aboutAdmin, exportUrl, notifMe,
saveNotifMe, profileExports, profileImportHistory, createManualTransaction,
updateTransaction, deleteTransaction, transactionMerchantMatch).
Deliberately left: StatusBadge.tsx orphan (has uncommitted parallel edits — will
remove once that lands) and the WebAuthn feature (a build-or-bin product decision,
not dead-code cleanup).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Small follow-ups the rest of the refresh depends on:
- client/lib/trackerUtils.ts: status-tone helper now returns the
'good' / 'warn' / 'danger' tokens the new TonedCard / ToneDot
consume, so the per-row badges stay in sync with the new design
tokens.
- client/lib/version.ts: release-notes shape accepts the optional
brand_image field that HISTORY.md now uses for the 'Doing my
part' release image.
This is commit 8 of 9 in the v0.42.0 brand refresh. Depends on
1807456 (admin + data + dialogs).
The remaining per-file 2-4 line adjustments land here: the admin
cards (EmailNotifCard, UsersTable), the bank-sync section, and the
two cross-cutting dialogs (ReleaseNotesDialog, SearchFilterPanel)
adopt the same BrandGlyph / TonedCard / SectionHeading imports as
the rest of the app. Each file is independently buildable on top of
the previous 6 commits.
This is commit 7 of 9 in the v0.42.0 brand refresh. Depends on
0eb024d (pages).
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).
CashFlowCard, OverdueCommandCenter, SummaryCards, TrackerBucket, and
the shared StatusBadge move to the new TonedCard / ToneDot / BrandGlyph
primitives. The biggest single change is SummaryCards (81 lines
churn) where the per-card tone tokens now defer to the design system.
This is commit 5 of 9 in the v0.42.0 brand refresh. Depends on
c2b6a7d (UI primitives).
The three shadcn primitives get the small adjustments the new
design-token set calls for — shadow, ring, and border tokens that
align with the index.css overhaul, and tone helpers that defer to
the tokens the primitives export. 20 lines total, no behaviour
change: same variants, same API, same default sizes.
This is commit 4 of 9 in the v0.42.0 brand refresh. Depends on
36834e6 (layout chrome + index.css tokens).
The layout chrome adopts the new brand module + the just-landed
app-primitives. CSS variables, the favicon, the page title, and the
vite PWA manifest all switch to the new brand assets:
- BrandBlock / NavPill / Sidebar / Layout use BrandMark / BrandWordmark
/ BRAND.name from @/lib/brand.
- ThemeContext exposes the system / dark / light options for the new
'theme = System' setting (commit 1 of 9 in the brand refresh set;
the actual settings UI lands in the per-page commit).
- client/index.css: 153 lines of churn — the design-token refresh
(accent palette, neutrals, surface tokens) consumed by every
downstream commit.
- index.html: title, favicon, apple-touch-icon, theme-color, manifest
href all point at client/public/brand/*.
- vite.config.mjs: VitePWA manifest icons + theme/background colors
follow the new brand.
This is commit 3 of 9 in the v0.42.0 brand refresh. Depends on
4898987 (app-primitives).
Adds the cross-cutting UI primitives the rest of the brand refresh
consumes. The 159-line file bundles the small, repeated bits every
page/dialog needs so the per-page commits can stay minimal:
- TonedCard: bordered card with neutral / good / warn / danger / info
variants (border + bg + text per tone), consumed by the
overdue/summary/bank-sync cards.
- ToneDot: small status dot for the new badges.
- EmptyState: lucide Inbox + heading + body used for filtered-empty
and zero-data states.
- SectionHeading / FieldRow: small typographic primitives for the
Settings / Profile / Admin pages.
- BrandGlyph (re-export of the Brand module) so consumers only need
to import from '@/components/ui/app-primitives'.
This is commit 2 of 9 in the v0.42.0 brand refresh. Depends on
dfee7f9 (brand module); consumed by the layout/UI/consumer commits.
Adds the canonical brand definition used by the v0.42.0 brand refresh:
- client/lib/brand.ts — single source of truth for name, tagline,
repository URL, asset paths, navigation glyphs, and accent palette
(BRAND_GLYPHS / accent tokens consumed by Brand.tsx + page chrome).
- client/components/brand/Brand.tsx — BrandMark (img with the
logo asset), BrandWordmark, BrandGlyph, and a small BrandStack
used by the layout chrome.
- client/public/brand/{logo,pwa-192,pwa-512,apple-touch-icon}.png —
replaces the inline img-cut references in README/HISTORY with a
canonical set; PWA manifest + favicon now point here.
This is commit 1 of 9 splitting the working-tree brand refresh into
per-layer commits. Each commit is independently buildable; the order
matches the dependency direction (lib → component → chrome → UI
primitives → page consumers → admin/dialog consumers → docs).
The daily reminder job ran at a hardcoded 6 AM. Make the hour configurable:
- dailyWorker: resolveReminderHour() reads the global 'reminder_hour' setting
(clamp 0–23, default 6) as the single source for both the cron expression and
the next-run display; the task is stored so rescheduleDailyWorker() can restart
it live (defensively wrapped so a bad value can't take the worker down).
- notifications admin GET/PUT expose reminder_hour; PUT clamps, persists, and
live-reschedules the worker.
- Admin Email Notifications card: a "Reminder send time" select (account-wide).
It's a global setting by design — there's one shared daily job, so a per-user
hour would require an hourly-worker refactor (out of scope) and would otherwise
be a dead setting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>