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>
Full-deployment QA against a copy of the real DB surfaced launch/require
references the server .cts migration didn't update. All are non-source
tooling/edge paths, which is why typecheck/check/tests stayed green:
- e2e/setup/prepare-db.js: require('../../db/database') and
require('../../scripts/seedDemoData') → .cts. This broke `npm run
test:e2e*` entirely (harness couldn't load). [material]
- playwright.config.js + scripts/prod-smoke.sh: webServer/boot command
`node server.js` → `node server.cts`. Also blocked the e2e + prod-smoke
suites. [material]
- setup/firstRun.js → .cts, require('../services/auditService') → .cts,
and server.cts call site → require('./setup/firstRun.cts'). Latent:
server.cts only reaches firstRun when userCount===0, but database.cts
auto-seeds a default admin during getDb() first, so the path isn't hit
in normal boot — fixed defensively so it can't crash if ever reached.
- .env.example: doc reference `node server.js` → server.cts.
Verified: typecheck:server + check:server clean; server test suite
226/226; e2e probe 17/17 (was 0 — suite couldn't boot before); the
production Docker image builds and boots on a copy of the real
production DB (44 bills, 1159 payments) with /api/health 200 and all 64
page endpoints returning 2xx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the JS→TS server migration. server.js → server.cts (Node
type-stripping, no build step), and every launch/config reference now
points at it: package.json (main/dev:api/start/check:server) and the
Docker CMD.
scripts/seedDemoData.js is required at runtime by routes/user.cts, so it
crosses into the server runtime — migrated to .cts and its require of
db/database updated to the .cts path. The remaining scripts/*.js are
standalone dev/ops tooling (outside the runtime and the check:server
boundary); their stale extensionless requires of now-.cts modules were
repaired so they still run, and the PM2 ecosystem config entry point was
updated to server.cts.
Verified: 0 runtime .js remain in the server tree; typecheck:server and
check:server clean; 226/226 tests pass; `node server.cts` boots and
/api/health returns 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every routes/*.js → .cts, with handler signatures typed against the shared
http Req/Res/Next types. Routes carry @ts-nocheck for now — they're thin glue
over the (fully typed) services, so full route-level type-checking is deferred;
the handler-param typing is a head-start. server.js + test requires updated to
the explicit .cts paths.
Behavior-preserving. Verified: check:server 0, typecheck:server 0, suite 226/226,
real boot → /api/health + /api/about 200 (all routes mount).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the services/ TypeScript migration (0 .js remain). These four are the
largest, most dynamic modules (subscription-catalog matcher, xlsx/spreadsheet
parser, OIDC protocol client, SQLite user-DB importer); converted to .cts with a
top-of-file @ts-nocheck so they run via Node type-stripping now, with rich typing
deferred as an incremental follow-up. The other ~46 services are fully typed.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CSV parser + import primitives (also shared by the OFX importer).
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tracker aggregation core (getTracker, safe-to-spend, bank tracking,
overdue count). Aggregation-heavy so money helpers are plain-required (any)
to avoid Dollars|null arithmetic friction, matching analyticsService.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of the large foundational services. billsService (validation/normalization,
balance math) is required by many others; notificationService (email + push +
drift digest) uses the shared http types indirectly.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch of low-blast-radius modules: utils/apiError, and services statusRuntime,
loginFingerprint, auditService, transactionMatchState, updateCheckService,
advisoryFilterService, bankSyncConfigService.
- types/http.d.ts: shared permissive Express-ish Req/Res/Next/Router types
(@types/express isn't installed; handlers are dynamic) — for apiError's
errorHandler now and the routes/middleware batches next.
- Every require of a migrated module updated to the explicit .cts extension.
- Confirmed + documented the Node rule: a .cts needs at least one `import`
statement or type-stripping never activates (node --check fails on the first
type token); import-less modules lead with an `import type`.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
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 lockfile had drifted (written by local npm 11 / Node 25) into a form that
node:22-alpine's npm 10.9.8 reads as incomplete — `npm ci` failed with
"Missing: esbuild@0.28.1 from lock file", breaking the Docker image build
(Dockerfile now uses `npm ci`, not `npm install`). Regenerated the lock inside
node:22-alpine with its own npm so `npm ci` resolves cleanly (887 packages,
lockfileVersion 3). No dependency version changes — lockfile metadata only.
Verified: clean `npm ci` in node:22-alpine, full image build, container boots
migrations + workers, full QA (auth/CSRF/admin/security) and a live SimpleFIN
sync all pass.
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>
- Dockerfile: npm ci (reproducible, lock-pinned, matches CI) instead of npm
install; add a HEALTHCHECK against the /api/health liveness route.
- package.json: pin engines node>=22.18.0 — the server require()s .mts/.cts and
relies on Node's default TS type-stripping (unflagged only on >=22.18), which
was previously undocumented on a floating base image.
- .dockerignore: exclude .env / .env.* so a stray env file can't be baked into
the image by COPY . .
- docker-compose: set TRUST_PROXY=true (behind the reverse proxy, so Secure
cookies + client IP for rate-limit/audit are correct); flip CSRF_HTTP_ONLY to
true (SPA reads the token from an endpoint, so no JS cookie access needed);
remove the active default INIT_ADMIN_PASS; document TOKEN_ENCRYPTION_KEY (set
it to move at-rest secrets off the DB-resident key); drop obsolete version key.
Not included: TOKEN_ENCRYPTION_KEY itself — that's a secret to generate and
inject into the deployment env (the app self-migrates to the env-key scheme on
next boot); documented in compose + .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Wrap each source's account/transaction upserts + the status update in one
better-sqlite3 db.transaction() — a single commit instead of one implicit
commit per row (far fewer fsyncs on large syncs) and atomic (a mid-sync
failure can't leave a half-written account). The write block is synchronous;
the async SimpleFIN fetch already completed before it.
- Key the dedup lookup on (user_id, provider_transaction_id) to match the UNIQUE
index and the reconnect-stable provider id. A data_source_id-keyed lookup
missed the prior row after a disconnect/reconnect, so pending→posted and
amount-refresh were skipped; now they survive a reconnect.
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>
120-line brand guidelines document covering the v0.42.0 brand
system: naming ('Bill Tracker' with the space, not 'BillTracker'),
asset paths, lockup usage rules (light vs dark variants, mono
mark, social preview), the canonical source-of-truth in
client/lib/brand.ts, and the GitHub-Open-Graph card dimensions.
This is commit 4 (and the last) of the v0.42.0 follow-up batch.
Docs-only; no code change. Depends on 68e57cf (favicon + PWA).
index.html: favicon and apple-touch-icon now point at the SVG mark
(plus the legacy PNG fallback for browsers that don't honour SVG
favicons). vite.config.mjs: VitePWA manifest icons reference the
SVG mark + social-preview, theme_color and background_color
inherit from the new brand tokens.
No behaviour change for end users — the PNG derivatives stay in
the same directory and are still used by the PWA splash / app-icon
paths that need raster.
This is commit 3 of the v0.42.0 follow-up batch. Depends on
1c04dfd (Brand components).
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>
- HISTORY.md: 'Release Image' section with the Doing my part image
(img/doingmypart.jpg) under the latest entry; the release image
the new Brand module + app-primitives UI kit is going out with.
- README.md: 'BillTracker' -> 'Bill Tracker' (with the space) so
the product name in the docs matches BRAND.name on the live site.
This is commit 9 of 9 in the v0.42.0 brand refresh. Docs-only; no
code change. Depends on 8ceea06 (lib).
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>