Commit Graph

379 Commits

Author SHA1 Message Date
null e7974cf4c0 Polish brand presentation 2026-07-10 19:49:20 -05:00
null 562da73a76 Polish product artwork and branding 2026-07-10 19:10:42 -05:00
null a31a774f95 chore: dead-code sweep — knip now clean and blocking in CI (Phase 5)
- 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>
2026-07-10 18:15:04 -05:00
null 522eaaf101 chore(deps)!: tailwind 3 -> 4 via @tailwindcss/vite (4i)
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>
2026-07-10 18:03:15 -05:00
null d8365d0e22 chore(deps)!: vite 8 (rolldown) + @vitejs/plugin-react 6 (4h)
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>
2026-07-10 17:53:27 -05:00
null a325c94dd4 chore(brand): refresh SVG lockup + regenerate all PNG assets 2026-07-10 17:52:14 -05:00
null 88a32a983f feat(auth): passkey / security-key UI — WebAuthn ships end to end
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>
2026-07-10 17:30:14 -05:00
null 68ca7cbb43 feat(standards): enforcement ratchets — error-shape + no-console; client toApiError
- 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>
2026-07-10 17:20:42 -05:00
null 9efa74e12b fix(client): redirect to login when the session expires mid-use
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>
2026-07-10 16:33:01 -05:00
null f587d09d4a style: apply Prettier across client + server (no behavior change)
One-time repo-wide `prettier --write` (isolated commit so future diffs
stay reviewable). Mechanical formatting only.

Verified unchanged: typecheck + typecheck:server + check:server clean;
server tests 232/232; client tests 50/50; Vite build succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:23:53 -05:00
null c41f1a8a7b feat(tracker): status row border-left indicators, refined metric cards, search filter, and CSS polish 2026-07-06 13:47:34 -05:00
null df1f61d6cc feat(brand): render the real product glyph SVGs (not inline approximations)
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>
2026-07-06 13:32:37 -05:00
null 1238ac5b14 feat(demo): full "take it for a ride" seed + surgical, correct removal
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>
2026-07-06 13:06:47 -05:00
null b25be070ed Add complete brand asset pack 2026-07-06 12:54:29 -05:00
null 40dfd6e3c1 Polish tracker command header 2026-07-06 12:54:11 -05:00
null 23f79c31e0 feat(ui): layout refinements — sidebar, nav pill, command palette, brand block 2026-07-06 12:28:25 -05:00
null f71b433323 feat(ui): refine OnboardingWizard, LoginPage, and SettingsPage 2026-07-06 12:22:47 -05:00
null 3a14c50e08 chore(brand): update PWA icons, release notes, and About page 2026-07-06 12:13:01 -05:00
null 343cc3a36f feat(tracker): persist Overdue/Drift panel collapse + inline hide
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>
2026-07-06 10:34:39 -05:00
null 1457de487f feat(security): show a key fingerprint (not the key) in the encryption status
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>
2026-07-05 19:15:32 -05:00
null 4638d24a1d fix(payoff): correctness, data-integrity & clarity of the payoff simulator
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>
2026-07-05 18:09:12 -05:00
null 1c04dfdf8c feat(brand): BrandMark/Wordmark/Lockup consume the new SVGs
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).
2026-07-05 17:50:04 -05:00
null 720d31b56e feat(brand): SVG lockups + mark + social preview 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.
2026-07-05 17:49:55 -05:00
null 4b81600afe fix(brand): finish the "BillTracker" → "Bill Tracker" rename (display copy)
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>
2026-07-05 17:31:14 -05:00
null ecd907e508 refactor: remove orphan StatusBadge.tsx (dead-code audit follow-up)
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>
2026-07-05 17:22:49 -05:00
null 86733d457a refactor: remove verified dead code — orphan file, dead HOC, unused api wrappers
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>
2026-07-05 17:21:31 -05:00
null 8ceea06659 chore(lib): trackerUtils + version track the brand refresh
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).
2026-07-05 17:19:10 -05:00
null 1807456da7 refactor(brand): adopt new primitives in 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).
2026-07-05 17:19:03 -05:00
null 0eb024de74 refactor(brand): adopt new primitives in 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).
2026-07-05 17:18:56 -05:00
null b714715ddb refactor(brand): adopt new primitives in 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).
2026-07-05 17:18:49 -05:00
null c2b6a7d1b4 refactor(brand): adopt new primitives in ui/{button,card,badge}
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).
2026-07-05 17:18:38 -05:00
null 36834e6625 refactor(brand): wire brand + new primitives through layout chrome
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).
2026-07-05 17:18:31 -05:00
null 4898987a18 feat(brand): shared app-primitives UI kit
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.
2026-07-05 17:18:23 -05:00
null dfee7f9401 feat(brand): new brand module + logo assets
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).
2026-07-05 17:18:04 -05:00
null 1f57b22ff8 feat(settings): configurable reminder send-time (plan Tier 5)
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>
2026-07-05 16:10:25 -05:00
null 0e6f04c7bc feat(settings): sign out of other devices (plan Tier 7)
- 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>
2026-07-05 16:05:59 -05:00
null 7e5e012434 feat(settings): default landing page (plan Tier 6)
- 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>
2026-07-05 16:03:01 -05:00
null 01030fe7d0 feat(settings): configurable "Due soon" window (plan Tier 4)
- 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>
2026-07-05 16:00:32 -05:00
null 0f24016bcf feat(settings): first day of week (plan Tier 3)
- 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>
2026-07-05 15:57:35 -05:00
null 0fa80a77ef feat(settings): make Currency + Date-format real (plan Tier 2)
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>
2026-07-05 15:54:19 -05:00
null e2d4a9f703 feat(settings): theme "System" + reduce-motion (plan Tier 1)
- 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>
2026-07-05 15:50:37 -05:00
null 1d73c8f13e fix(settings): invalidate ['settings'] cache on save so tracker toggles apply
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>
2026-07-05 15:06:57 -05:00
null 21651b3f67 feat(tracker): a11y live regions + keyboard-shortcut hint (plan D/C5)
- 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>
2026-07-05 15:00:16 -05:00
null 0abe861928 feat(tracker): multi-select bulk pay/skip (plan C3)
- 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>
2026-07-05 14:56:34 -05:00
null 88874f3f3d feat(tracker): export/print the month + calendar link (plan C1/C2)
- 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>
2026-07-05 14:49:09 -05:00
null 313aafcb79 refactor(tracker): move settings/simplefin onto React Query + defer search (plan B)
- 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>
2026-07-05 14:43:13 -05:00
null 03009e2a83 refactor(client): de-duplicate parseUtc + settingEnabled to lib/utils (plan A2/A3)
- 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>
2026-07-05 14:38:12 -05:00
null 3973560e92 refactor(tracker): remove dead code and unreferenced files (plan A1)
- 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>
2026-07-05 14:35:13 -05:00
null db1be4b0bd fix(tracker): pin-Due sort uses reconciled status; add filtered-empty state
- 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>
2026-07-05 14:35:03 -05:00
null a6bc85b18c perf(ui): useDeferredValue on the Bills + Subscriptions search filters (Phase 2 stack leverage)
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>
2026-07-05 13:16:21 -05:00