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>
- 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>
The payment-input gate (well-tested by Phase 1) migrates to TypeScript as
a .cts (CommonJS) module: keeps require/module.exports, imports the branded
Cents type from money.mts, and casts the require(esm) result so toCents/
fromCents keep their branded signatures. This proves the low-churn path for
the ~90 remaining CJS services/routes (no require→import rewrite needed).
Both server-TS patterns now proven end-to-end: .mts (ESM, type-source) +
.cts (CJS). typecheck:server clean; suite 225 + probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dates.mts (ESM, typed) joins money.mts — the two most-required server
leaves are now TypeScript. Added .cts to the server tsconfig for the
CJS-module migration path. typecheck:server clean; suite 225 + probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Establishes the server-TypeScript foundation and migrates utils/money →
utils/money.mts with branded Cents/Dollars (mirroring the client), so the
cents↔dollars boundary — the origin of the reconciliation bug class — is a
compile error for any typed server caller.
Approach (no build step): Node 25 runs .ts natively (type-stripping).
Migrated modules use the explicit-ESM .mts extension (the project is
"type":"commonjs", which disables .ts ESM auto-detection) and are required
from the CJS callers via Node's require(esm) — verified working. Infra:
tsconfig.server.json (allowJs + checkJs:false → incremental like the
client), npm run typecheck:server, check:server extended to .mts, wired
into ci. 28 require sites repointed to money.mts.
typecheck:server clean; full server suite 225 + e2e probe 17/17 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The maintained line, and it forwards rejected async handlers to the error
middleware natively — closing the Express-4 gap where a throw in an async
route became an unhandled rejection that hung the request (this is why no
throwaway asyncHandler wrapper was needed). Small surface: only three
bare-* routes needed the path-to-regexp-v8 named-wildcard form
(/api/*splat, /legacy/*splat, /{*splat}); no removed APIs in use, all
req.query uses are reads. Suite 225 + probe 17/17 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both pages filter the whole list on every keystroke. The input now updates
instantly while the (deferred) filter+render lags behind — React 19's
useDeferredValue, which the app wasn't using anywhere. Typing stays snappy
on large lists; zero behavior change otherwise. typecheck/lint/build clean,
probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parseJsonSafe/upload-body → a structured ResponseBody type (typed error
envelope + index sig) instead of any, no cast churn; declineRecommendation
→ Recommendation; snowballPlans endpoint typed so the hook drops its (d: any).
Annotated the Spending settings-load catch as best-effort (Finding #8) —
cosmetic options fall back to defaults, no mount-time nag. Client 0 any's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: _fetch had no timeout — a hung request (server accepted the
connection but never responds) spun the UI forever with no error. Now
wrapped in AbortSignal.timeout(120s, generous enough for the slowest
legit import/sync/backup on a LAN) and both timeout and network failure
map to a clean ApiError (REQUEST_TIMEOUT / NETWORK_ERROR) so they surface
as a normal error toast instead of a freeze or raw 'Failed to fetch'.
Server: the global error handler now returns next(err) when res.headersSent,
so an error after a partial response can't throw 'cannot set headers'.
(The async-handler-hang gap is folded into the Express 5 upgrade in Phase
2.2, which forwards async rejections natively — no throwaway wrapper.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
billMerchantRuleService.syncBillPaymentsFromSimplefin (a money-CREATING
path): a rule-matched unmatched transaction becomes exactly one
provider_sync payment (cents, tx→matched), re-running is idempotent (no
dup — match-status filter + the UNIQUE(transaction_id) index), and a
non-matching tx is left alone. spendingService budgets: dollars↔cents
round-trip + set/update/clear. Suite 221 green.
(spreadsheet createPaymentFromImport dedupe is analogous to the payment
atomicity already covered in Track A; a full XLSX-parse test is deferred
as lower-value/high-setup.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure-function coverage of the validator that normalizes dollars→cents and
rejects bad input before it touches a balance: required-field errors +
field tag, dollars→cents conversion, positive-amount enforcement (rejects
0/negative/non-numeric; documents the intentional lack of an upper bound),
real-calendar-date validation (Feb 30 / month 13), payment_source
whitelist, and the require* option matrix. Suite 215 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins the auth-core invariants: bcrypt password round-trip, wrong-password
→ bad_password (feeds lockout), unknown user → null (no enumeration);
sessions stored HASHED (sha256), never the raw token; the session
lifecycle (create/lookup/expire/rotate/invalidate/prune); and
rotateSessionId enforcing ownership + killing the old session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zero coverage on the AES-256-GCM + HKDF crypto that protects SimpleFIN
bank tokens, TOTP/recovery secrets, SMTP/OIDC creds and login-history
PII. A silent break in decryptSecret or the startup reEncryptWithEnvKey
migration would render every stored secret unrecoverable, invisibly.
Covers: round-trip on both key paths (env e2: / db v2:), unicode/empty/
large payloads, GCM tamper rejection, legacy (pre-v0.78 SHA-256) decrypt,
the env-key-required error, and the migration (migrate/skip/idempotent/
corrupt-tolerant). Suite 200 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eight in-flight commits targeting v0.41.0 (Track A money integrity,
Track B reconciliation tests, Track C API response typing across
auth/snowball/subscriptions/spending/bank-ledger, Track D 500
handler code:'INTERNAL_ERROR') are not yet pushed. Documenting them
in the manual so it stays current with the code that will land next.
Section updates:
- Header: keep package version 0.40.0, note v0.41.0 is the in-flight
target; 'Last Updated' 2026-07-05.
- Notable changes: 4 new bullets summarizing Track A/B/C/D.
- API client (7): list Track C response typing and Track D error shape
with cross-references to the sections where each is detailed.
- Domain types (7): expand to include User, SnowballProjection,
SnowballSettings, Subscription*, Spending*, BankLedger*, CategoryGroup,
CopyBudgetsResponse. Note single-consumer non-money casts left in
place as a deliberate Track C stop.
- Auth state (7): User moved to @/types, re-exported from useAuth.
- client/lib/ (7): add paymentActions.ts (createPaymentOrConfirm).
- Response conventions (5): add 'INTERNAL_ERROR' for 500s; note
409 DUPLICATE_SUSPECTED on POST /payments.
- Payments (5.5): Track A notes — transactional balance mutations,
409 allow_duplicate flag, 409 DUPLICATE_SUSPECTED response shape,
new tests/paymentsRoute.test.js and tests/reconciliation.test.js.
- Verification checklist (11): list 8 unpushed Track commits with
one-line summaries and 19-file diff stat (813+/415-).
Track C (auth): User lived in @/hooks/useAuth; moved it to @/types
(re-exported from useAuth for the 7 importers) and typed me/login/
totpChallenge/hasUsers, dropping those Admin/Login casts. Left the
single-consumer non-money casts (adminUsers, version/about/privacy/
health docs) and the optimistic-UI rewrite as deliberate low-value/
high-churn stops.
Track D: global 500 handler now emits code:'INTERNAL_ERROR' so an
unexpected error carries the same {error,code} field as structured 4xx.
typecheck/lint(0 err)/build/server-193 all green; probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted SnowballProjection (+details) and SnowballSettings into @/types;
wired snowball (Bill[]), snowballSettings/saveSnowballSettings, and
snowballProjection with get<T>/_fetch<T>. Removed the projection/settings/
bills casts. Plan-lifecycle SnowballPlan casts kept (component-owned type).
typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted SubscriptionsPage's Subscription/SubData/Recommendation/
CatalogMatch/SubTx (+ deps) into @/types and wired subscriptions/
recommendations/transaction-matches/create/match-bill with get<T>/
post<T>. Removed the (r: any) in useSubscriptionRecommendations, the 5
page casts, and an unused CatalogMatch import. typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted SpendingPage's local shapes into @/types (SpendingCategoryEntry,
SpendingSummary, SpendingTransaction(s), SpendingIncome*, SpendingRule,
CategoryGroup, CopyBudgetsResponse) and wired the /spending/* + category-
groups endpoints with get<T>/post<T>. Hooks return typed data; 6 casts
removed; 2 orphaned type aliases dropped. typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promoted BankTransactionsPage's local Tx/Account/Ledger/... interfaces
into @/types (BankLedgerTransaction extends BankTransaction so amounts
stay branded Cents) and wired bankTransactionsLedger/match/unmatch/
ignore/unignore/apply-merchant-match/auto-categorize with get<T>/post<T>.
useBankLedger data is now typed; all 9 as {...} casts in the page removed.
Dropped a dead Cents import. typecheck/lint/build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins that Tracker, Analytics, Bills, and Summary agree on a month's
expected/paid totals — the 'same number, different truth' bug class
(QA-B5/B9). Hand-seeds an off-month annual bill, an occurring quarterly
bill, and monthly bills w/ full+partial payments; asserts in integer
cents that Analytics.expected / Bills-gated-sum / Summary.expense_total
all == Tracker.total_expected, Analytics.actual == Tracker.total_paid,
and the off-month annual inflates no surface. All reconcile today; the
harness makes future gating drift fail loudly. Suite 193 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>