Commit Graph

408 Commits

Author SHA1 Message Date
null 65c61d71fa docs(history): React correctness audit + ESLint enforcement (R1-R4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:58:54 -05:00
null 1387f7c2d7 fix(client): guard param-driven data loaders against out-of-order responses (R3)
The month/filter-driven loaders on Analytics, Summary, and Spending (x2)
fetched + setState with no race guard, so a slow response for old params could
overwrite fresher data (or setState after unmount) on rapid month/category nav.
Added the request-sequence guard already used by the Bank ledger (newest
request wins; stale ignored). Bills/Subscriptions load once ([] deps) so they
weren't at risk; BankTransactions already had the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:57:04 -05:00
null 02395b9ad4 refactor(client): remove orphaned dead logic flagged by ESLint (R2c)
- MobileBillRow: an autopayClass useMemo computed a badge class that was never
  rendered (dead since a refactor) -> removed.
- CategoriesPage: onRowKeyDown handler was never wired (keyboard toggle is now
  the dedicated chevron button, per the QA-B14-02 a11y fix) -> removed.
- BillModal: unused isDebtCategory derived flag -> removed.
Remaining lint warnings are unused imports / HMR / vestigial destructures
(non-correctness, non-blocking, now tracked by eslint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:51:21 -05:00
null b8d394061b fix(client): resolve all 13 exhaustive-deps warnings (R2b)
- BankSyncSection: handleBtSave read btLateGraceDays but it was missing from the
  useCallback deps -> saving could persist a STALE late-grace value (real bug).
- ProfilePage: EditProfile sync effect now depends on [profile].
- Stabilized identity of derived arrays/objects feeding useMemo deps (they were
  recreated every render, defeating memoization): TrackerPage filters + rows,
  HealthPage bills, BankTransactionsPage transactions -> wrapped in useMemo.
- BillModal + TransactionMatchingSection: intentional id/filter-scoped effects
  documented with a targeted eslint-disable + reason.
- Removed 2 stale eslint-disable directives (confirm-dialog, useAuth).

exhaustive-deps + rules-of-hooks now both 0. Build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:47:14 -05:00
null 5e267e4fa7 fix(client): fix ESLint errors — real latent bugs (R2a)
ESLint surfaced 6 errors, incl. real bugs invisible before:
- ImportTransactionCsvSection called importErrorState() without importing it —
  its own error handler would throw ReferenceError on a failed CSV import.
- client/components/MobileTrackerRow.jsx was a dead duplicate (unused; the live
  one is tracker/MobileTrackerRow.jsx) with undefined-setter refs → deleted.
- StatusPage dbOk had a dead '?? true' (constant boolean LHS) — restored the
  intended default-true-when-unknown.
- MobileBillRow redundant !! in a ternary condition.
Lint is now 0 errors; wired 'npm run lint' into 'npm run ci'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:42:52 -05:00
null c679022592 build(lint): add ESLint 9 + eslint-plugin-react-hooks/react-refresh (R1)
There was no linting at all — nothing enforced rules-of-hooks (conditional-hook
crashes) or exhaustive-deps (stale closures) across 125 client components/pages.
Added an ESLint flat config (eslint.config.mjs) scoped to client/, an 'npm run
lint' script, and the devDeps. First run: 0 rules-of-hooks violations (good),
6 errors + 13 exhaustive-deps warnings to work through next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:41:24 -05:00
null 38417ad8da docs(history): BM2 bill-modal decompose summary (v0.41.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:25:15 -05:00
null 20b46c81df refactor(bill-modal): extract TemplateSection (BM2, 7/n)
The save-as-template toggle + name input move to their own presentational
component. Behavior-preserving — BillModal 1105 -> 1090 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:23:57 -05:00
null ad68d965b6 refactor(bill-modal): extract LinkedTransactionsSection (BM2, 6/n)
Bank-matching rules (+ Sync action) and the linked-transaction list (+ Unmatch)
move to LinkedTransactionsSection. The big inline sync onClick is lifted to a
named parent handler (handleSyncBillPayments) sharing a refreshAfterImport helper
with handleRulesChanged. Dropped now-unused imports (BillMerchantRules, Link2,
Link2Off, RefreshCw, transactionDate, fmtTransactionAmount). Behavior-preserving
— BillModal 1193 -> 1105 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:22:58 -05:00
null 301bb152ef refactor(bill-modal): extract UnmatchDialogs + shared transactionDisplay (BM2, 5/n)
The three unmatch flows (choice dialog, single-unmatch confirm, bulk review with
optional merchant-rule removal) move to UnmatchDialogs; the transaction display/
matching helpers (transactionTitle/Date, fmtTransactionAmount, isSimilarPayee)
move to a shared transactionDisplay.js used by both the parent and the dialogs.
Dropped now-unused imports (Layers, Checkbox, formatCentsUSD). Behavior-
preserving — BillModal 1432 -> 1193 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:20:02 -05:00
null c61e3d84a5 refactor(bill-modal): extract PaymentFormFields (BM2, 4/n)
The add/edit manual-payment form (+ its PAYMENT_METHODS list) moves to its own
presentational component; the parent keeps the form state + submit handler.
Behavior-preserving — BillModal 1502 -> 1432 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:16:10 -05:00
null afba78e86b refactor(bill-modal): extract PaymentHistoryList (BM2, 3/n)
The payment-history list (rows with source badges + edit/remove for manual
payments) and its 4 display helpers (isTransactionLinkedPayment,
isHistoryOnlyPayment, paymentSourceLabel/Tone) move to their own presentational
component; the parent keeps payment state + add/edit/delete handlers and passes
them as props. Behavior-preserving — BillModal 1603 -> 1502 lines; build +
client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:11:05 -05:00
null 9d670985fe refactor(bill-modal): extract AutopayTrustIndicator (BM2, 2/n)
The edit-mode autopay trust panel (12-mo success rate, mark-verified, staleness/
failure warnings) moves to its own presentational component. Behavior-preserving
— BillModal 1637 -> 1603 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:07:35 -05:00
null 1018c55bb3 refactor(bill-modal): extract DebtDetailsSection (BM2, 1/n)
First BillModal decompose step: the collapsible Debt/Snowball fields (interest
rate, current balance, minimum payment, snowball visibility) move to
client/components/bill-modal/DebtDetailsSection.jsx as a presentational
component; state stays in the parent (the save action reads it). Behavior-
preserving — BillModal 1723 -> 1637 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:06:12 -05:00
null 2b710c459b feat(tracker): summary-card redesign — de-dup Paid, surface Remaining + progress (T3)
The summary row showed 'Paid' twice (this month + last month), indistinguishable,
while the actionable remaining only appeared in the CashFlow card below. Now:
- previous-month becomes a muted 'Last month: $X' hint under Total Paid (not a
  second green box);
- a Remaining card (existing unused blue type) surfaces summary.remaining when
  there's no bank hero (bank mode already shows projected remaining on the hero);
- a compact '$X of $Y paid · N bills left' progress line under the cards.
Respects tracker_show_summary_cards. Visual baseline for / may need a refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:49:35 -05:00
null 4a76eb9b92 feat(tracker): optimistic pay/skip + toast.promise on syncs (M1/M2)
M1: marking a bill paid/unpaid flips the row instantly via local optimistic
state (cleared when fresh data arrives, rolled back on error) on both desktop
and mobile rows, instead of waiting for the server round-trip.

M2: bank sync (Tracker) and the bill-modal Sync use sonner toast.promise —
one toast transitioning loading -> done -> error, replacing the manual
spinner-flag + separate success/error toasts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:47:32 -05:00
null 55b515c401 feat(tracker): Pay-all-due per bucket + reversible quick-pay with specific toasts (T4)
- Each bucket header gains a 'Pay all due (N)' action: one bulkPay for every
  unpaid gated bill in the bucket, behind a confirm (count + total) and a single
  Undo toast that deletes the created payments.
- Quick-pay and mark-paid now show a specific toast ('Rent - $1,200 paid');
  quick-pay gained an Undo action to match un-pay.
- Per-bill snooze already existed in the Overdue Command Center.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:43:01 -05:00
null 995f635d35 refactor(tracker): consolidate isPaidStatus + rowOutstanding + toast gap (T5)
Added a single isPaidStatus(status) (+ PAID_STATUSES) to statusService and a
matching client helper in trackerUtils, routing the unambiguous settled-status
checks through it (trackerService, StatusBadge, CalendarPage, rowIsPaid). The
intentionally paid-only counts stay distinct. Replaced two inline
Math.max(r.balance||0,0) with rowOutstanding, and gave the Tracker settings
load a quiet toast instead of a silent swallow. Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:36:30 -05:00
null d92cc38116 fix(bill-modal): correctness + toast fallbacks + validator consolidation (BM1)
- handleBlur now takes the field value explicitly (was positional guessing that
  fell through to interestRate for unmapped fields).
- Three copy-pasted money validators -> one shared validateNonNegativeMoney in
  client/lib/money.js; expected-amount copy 'positive' -> 'non-negative' (0 ok).
- Removed the save action's duplicate due-day/interest-rate re-validation
  (validateForm already covers it); kept the parses.
- Added err.message fallbacks to save/deactivate/verify-autopay toasts.
- Save toasts now name the bill.

Test: client/lib/money.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:32:25 -05:00
null e9c5e4d1d3 test(tracker): cover paymentAccounting + analytics money services (X2)
Added tests for two untested money-critical services:
- paymentAccountingService: bank-backed predicate, accounting-active SQL, and
  the manual-vs-bank override invariant (bank overrides provisional manual so
  it isn't double-counted; balance restored; manual reactivates + re-applies
  balance when the override is removed).
- analyticsService: month-window math (year-boundary + leap edges) and
  validateSummaryQuery defaults/range errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:28:12 -05:00
null 9f4b53d37a perf(tracker): batch getTracker per-bill queries (kill N+1) (P1)
getTracker ran a payments query per bill plus computeAmountSuggestion per bill
(up to 12 queries each: 6 months x 2) inside bills.map — ~70-450 queries for a
35-bill account per load. Now one query fetches all cycle payments (grouped in
JS per bill range) and two compute all amount suggestions
(computeAmountSuggestionsBatch). Behavior-preserving.

Test: tests/amountSuggestionService.test.js pins batch == per-bill output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:25:55 -05:00
null 73631ab812 fix(tracker): route error handling + autopay write atomicity (T2)
routes/tracker.js had no try/catch and returned a plain {error} shape; the
three GET handlers now wrap in try/catch + standardizeError (including the
invalid-month path). applyAutopaySuggestions ran INSERT + applyBalanceDelta
as two un-transactional steps on a GET — wrapped both in one db.transaction.

Tests: autopay creates one payment + drops balance (idempotent), route
returns standardized error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:21:28 -05:00
null c91c97ef41 feat(tracker): live cross-query invalidation for the app shell (X3)
The app never called invalidateQueries; a Tracker mutation only refetch()'d
the one tracker query. So the sidebar overdue badge (['overdue-count'],
2-min staleTime), drift report, and bills list stayed stale after pay/skip/
edit — you could clear your last overdue bill and still see '3' for minutes.
Added useInvalidateTrackerData() (tracker + overdue-count + drift-report +
bills) and wired it into rows, BillModal.onSave, bank-sync, reorder, and the
payment/late-attribution handlers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:19:30 -05:00
null 10e159352a fix(notifications): honor per-bill reminder_days_before + expose for all bills (BM3)
The notifier used a hard-coded 3-day early reminder and never read
reminder_days_before, so the modal's 'Reminder Days' control was a no-op.
The early reminder now fires at the bill's own lead (>= 2 days so it never
collides with the 1-day/same-day reminders); email subject+body say 'due in
N days'. Lead-time selection extracted to a pure exported reminderTypeFor()
for unit testing. The Reminder Days control now shows for every bill and a
non-subscription save no longer clobbers the column to 3.

Test: tests/notificationLeadTime.test.js

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:16:10 -05:00
null ad1f5bebf6 fix(bill-modal): SimpleFIN import refreshes payments + Tracker live (BM4)
The Sync button and merchant-rule historical import both CREATE payments but
only reloaded linked transactions, so the modal's Payment History stayed stale
and the Tracker row behind the modal didn't update (kept showing due/overdue)
until close+reopen. Both now await Promise.all([loadPayments(),
loadLinkedTransactions()]) then onSave?.(), matching the unmatch handlers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:11:28 -05:00
null 4a38cc8614 fix(tracker): gate bank card unpaid/remaining by occurrence + add trackerService tests (T1)
buildBankTracking summed expected_amount for all active unpaid bills with no
resolveDueDate gate, so annual/off-month bills inflated unpaid_this_month and
the bank remaining (same class as QA-B5-02, live on the bank path). getTracker
now derives the unpaid total from the already-gated rows (netting partials) and
passes it in. summary.remaining/total_remaining now use the bank card's own
remaining in bank mode (agreeing with safe-to-spend), and a stray balance/100
is now fromCents.

New tests/trackerService.test.js: gating fix, summary totals, bank-mode
remaining agreement, cents<->dollars, getOverdueCount gating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:10:28 -05:00
null d689ff6e68 fix(tracker): quick-pay duplicate guard + atomic balance write (X1)
POST /payments/quick had no dedupe and non-atomic INSERT+applyBalanceDelta,
unlike /payments/bulk. A double-click/retry/two-tab pay created a second
payment and dropped the balance twice; a mid-way failure left a payment with
no balance adjustment. Now checks the bill_id+paid_date+amount composite key
(idempotent 200) and wraps the write in db.transaction.

Test: tests/paymentsQuickRoute.test.js

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:05:19 -05:00
null 836dbdb9ae fix(snowball): surface projection errors + polish (Snowball #2,#3,#8)
- The payoff projection panel swallowed fetch errors silently; now shows a
  "Couldn't load … Try again" state (no projection) and a subtle "showing the
  last result" retry banner when a refresh fails.
- loadProjection() now uses the currently-typed extra payment (via a ref that
  mirrors the input), consistent with the debounced live preview, so refreshing
  after a balance edit never drops an in-progress extra.
- Copy: extra-payment validation says "non-negative" (0 is valid); the capped
  banner now reads "one or more debts won't pay off at this rate" (accurate for
  the unpayable-debt case from the #1 fix, not just >50 years).

(#9 unsaved-preview hint was unnecessary — the input already auto-saves on blur.)
Build clean; client suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:08:41 -05:00
null 2b315f5d18 refactor(snowball): consolidate plan endpoints, standardize errors, fix N+1 (Snowball #4,#6,#7)
- The four plan-lifecycle routes (pause/resume/complete/abandon) were near
  -duplicate copies returning a plain {error} shape; folded into one
  transitionPlan(req,res,{allowedFrom,setSql,action,past}) helper that returns
  standardizeError {message, code}, keeps the state guards and ownership scoping.
- Standardized the remaining plan endpoints' error responses (start/list/active/
  patch) to standardizeError too.
- enrichPlanWithProgress fetched each snapshot bill one-by-one and wasn't user
  -scoped; now a single WHERE id IN (…) AND user_id = ? batch.

Test: tests/snowballPlanRoute.test.js (transitions, INVALID_PLAN_STATE guard,
ownership 404, dollar-denominated current_debts). Server 154 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:04:31 -05:00
null db4d33513e fix(debt): honest payoff date for unpayable debts + math tests (Snowball #1,#5)
The payoff simulation reported months_to_freedom by taking Math.max over each
debt's payoff month — but a debt whose minimum never overcomes its interest (and
that the rolling snowball can't cover) never reaches $0, so its "never" counted
as 0 and the projection showed the OTHER debts' last month as the freedom date.
Now months_to_freedom/payoff_date are null when any active debt never clears, the
result is flagged capped, and each such debt is marked never_paid
(services/snowballService.js + the same guard in aprService calculateMinimumOnly).

Also adds tests/snowballMath.test.js (12) — the debt-payoff engine had zero
coverage. Hand-calculated examples for amortization (0% + 12% APR), snowball
rolling, avalanche vs snowball interest, skip reasons, APR snapshot, and the
unpayable-debt edge. Server suite 151 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:01:17 -05:00
null 877e4c6d3c docs(data): record the Data page overhaul + sign-off (Batch 6)
HISTORY: "Data page overhaul" section (redesign + OFX/QFX import + richer export
+ erase-my-data). QA plan: cycle-log row.

Sign-off: server 139 + client 46 + build green; axe on /data zero critical/
serious (added to a11y.authed); full probe suite 17/17. No /data visual baseline
existed to update (only login screenshots).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:25:25 -05:00
null d53a64b604 feat(data): "Erase my data" danger zone (Batch 5)
New services/userDataService.js eraseUserData() permanently wipes a user's
financial + imported data in one transaction (child → parent order for FK
safety): bills (+ cascading payments/monthly_bill_state/bill_history_ranges),
transactions/accounts/data_sources, categories/groups, templates, snowball,
spending rules/budgets, merchant rules, imports, and per-user hint tables. It
PRESERVES the account, sessions, 2FA/WebAuthn, login history and preferences —
this resets your data, not your account — then re-seeds default categories and
writes an audit row to import_history.

- POST /api/user/erase-data — rate-limited (demoDataLimiter), requires a
  type-to-confirm token ("ERASE"), structured errors.
- UI: EraseDataSection danger-zone card (Export & backups pane) — red-accented,
  "download a backup first" nudge, type-to-confirm AlertDialog, toasts; on
  success DataPage reloads all state.

Tests: tests/eraseUserData.test.js — wipes user A only, preserves user B +
account + session, re-seeds categories, audited. Server 139 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:21:07 -05:00
null 314e4ff45e feat(export): JSON export + date-range/format payments export (Batch 4)
- GET /api/export now accepts a date range (?from=&to= on paid_date) in addition
  to ?year=, for CSV or JSON; filename derived from the range. Validates the
  range (both bounds, from<=to).
- New GET /api/export/user-json — full portable JSON of the user's data, reusing
  the same getUserExportData assembly as the SQLite/Excel exports (money via
  fromCents).
- UI (DownloadMyDataSection): a JSON export card plus a "Payments export" with
  From/To dates and a CSV/JSON toggle; shared blob-download helper; toasts and
  client-side range validation.

Tests: tests/exportRicher.test.js (JSON assembly in dollars, year vs range
filtering, CSV filename, bad-range rejection). Server 134 pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:15:36 -05:00
null bd1eee00b0 feat(import): OFX/QFX transaction import (Batch 3)
New services/ofxImportService.js parses OFX 1.x (SGML, unclosed leaf tags),
OFX 2.x (XML) and QFX (+ Intuit tags ignored) into the same normalized shape the
CSV path produces, then writes through the SAME shared primitives (session table,
(user_id, data_source_id, provider_transaction_id) dedupe, import_history) — now
exported from csvTransactionImportService (additive; CSV tests still pass).

- Routes POST /api/import/ofx/{preview,commit} mirror the CSV two-step (raw
  upload → structured commit; no column mapping since OFX is structured).
- UI: ImportOfxSection (upload → preview list → import) in the Import pane;
  amounts shown via formatCentsUSD; toasts on preview/commit/malformed.
- Gap handling: signed TRNAMT → signed cents; DTPOSTED → YYYY-MM-DD; FITID →
  stable provider id (hash fallback); non-OFX / empty files rejected clearly.

Tests: tests/ofxImportService.test.js (SGML + XML/QFX parse, entity decode,
signed cents, preview→commit, re-import dedupe, import_history). Server 129 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:11:59 -05:00
null c7b110cd68 feat(data): actionable badges, health dots, at-a-glance stats, palette links (Batch 2)
- Transactions nav shows a live "N to review" badge (unmatched count from the
  bank-ledger summary, limit:1 so it's cheap; refreshes on sync/import).
- Bank sync nav shows a green/amber/grey health dot (connected / needs-attention
  / off), mirroring the hero tone.
- Connection hero connected line now shows the transaction count at a glance
  ("SimpleFIN · 1,159 transactions · synced 2h ago · syncs automatically").
- Command palette gains Data section deep-links (Bank sync / Transactions /
  Import / Export) via ?section=.
- Count/stat fetch is non-blocking (.catch → 0), never blocks the page.

Build clean; client suite 46 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:05:25 -05:00
null 6a1b2f62b2 feat(data): two-pane shell + connection hero + deep-linking (Batch 1)
Rewrite the Data page shell into a settings-style two-pane layout (sticky goal
-nav on desktop, segmented on mobile) with:
- ConnectionHero — 5 states so a network blip is never mistaken for "not
  connected" and a server without SimpleFIN never shows a dead Connect button
  (loading / disabled / error+retry / not-connected / connected±needs-attention);
  Sync-now handles partial errors, 429, and failure with toasts.
- DataNav — <nav> landmark, aria-current, keyboard, responsive.
- ?section= deep-linking via useSearchParams (URL source of truth → localStorage
  → default; migrates the old 3-tab key), so refresh/back-button work.
- Goal-based regroup into 4 panes with plain-language titles/subtitles/icons
  passed via cardProps (every section component reused unchanged).
- Lazy panes: ImportSpreadsheet/ImportMyData code-split (own chunks) + only the
  active pane mounts; framer-motion cross-fade (reduced-motion aware);
  focus-to-heading on switch.
- Repoint BankTransactions "Open Data" → ?section=bank-sync; add /data to the
  authed axe sweep.

Build clean (heavy panes split into their own chunks); client suite 46 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:02:36 -05:00
null 212117a61a feat(data): modernize SectionCard chrome (Batch 0 — Data overhaul)
Replace the tiny grey uppercase section titles with a modern header: optional
leading icon in a soft chip, sentence-case high-contrast title, calm subtitle,
a right-aligned rotating chevron, and optional statusDot/badge slots. API is
unchanged (title/subtitle/collapsible/summary/storageKey/actions preserved) so
no section internals change — purely the shared card chrome for the Data page.

Build clean; client suite 46 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:57:03 -05:00
null 5aa5c0cc0e docs(qa): mark IMP-CODE-02 fully shipped (database.js 4174 -> 1297)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:34:37 -05:00
null 12d9d4c5a8 refactor(db): extract legacy-reconcile migrations + lock version-sync (IMP-CODE-02)
Second half of the migrations split: moved the ~830-line reconcileLegacyMigrations
array into db/migrations/legacyReconcileMigrations.js via the same
buildLegacyReconcileMigrations(deps) factory (same injected db + helpers; no
inline requires in this one). database.js is now 1,297 lines — down from 4,174
at the start of IMP-CODE-02 (~69%).

Added tests/migrationModules.test.js locking the invariants database.js depends
on: both modules build, versioned versions are unique, and every legacy-reconcile
version has a versioned counterpart (the drift the in-app assertion warns about).

Verified: full suite 125 pass; fresh DB applies all 79 migrations (reconcile
path included — a fresh schema.sql DB triggers it) and is idempotent; real prod
DB copy (v1.06) migrates as a no-op with data intact and no version-sync drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:34:01 -05:00
null 026c6a56b8 refactor(db): extract the versioned migrations array into its own module (IMP-CODE-02)
db/database.js carried a ~1,740-line inline `const migrations = [...]` inside
runMigrations(). Moved it to db/migrations/versionedMigrations.js as a factory,
buildVersionedMigrations(deps), injected with the live `db` connection and the
few schema helpers the migration bodies close over (isValidColumnName,
isValidSqlDefinition, ensureTransactionFoundationSchema, and the four
run*Migration helpers). Behavior is identical — the run/check closures resolve
the same bindings, just passed in rather than captured.

Fixed the two path references that broke by moving one directory deeper: the
inline require('../services/...') calls and the __dirname docs JSON require now
use ../../.

database.js: 3,859 → 2,119 lines. Verified: full server suite 122 pass; a fresh
DB applies all 79 migrations and is idempotent on a second boot; the real prod
DB copy (v1.06) migrates as a clean no-op with data intact and no version-sync
drift between the runMigrations and reconcileLegacyMigrations version lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:31:00 -05:00
null 78ad63dda4 docs(qa): IMP-UX-02 state audit complete — no gaps found
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:07:56 -05:00
null 318ffa368e docs(qa): archive IMP-CODE-02 partial (catalog seed extracted)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:05:44 -05:00
null 7f2faeaede refactor(db): extract subscription-catalog seed data out of database.js (IMP-CODE-02)
db/database.js was 4,174 lines. The largest self-contained block was ~315
lines of static subscription_catalog seed rows (SUBSCRIPTION_CATALOG_ROWS +
SUBSCRIPTION_CATALOG_V2_ROWS) — pure data, no logic. Moved it to
db/subscriptionCatalogSeed.js and imported it; database.js is now 3,859 lines
and the seed data lives in a cohesive, obvious module.

Behavior-preserving: a fresh DB still seeds 291 catalog rows; full server
suite (122) passes — migrations and the catalog seed run on every fresh test DB.

The migrations array (~2,700 lines) is the other big block but is the DB core;
splitting it safely needs a dedicated pass (tracked in the backlog), not a blind
move — a mistake there corrupts every DB on boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:05:16 -05:00
null 9aa312082d docs(qa): archive IMP-CODE-03 (canonical match-state writer)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:02:47 -05:00
null fa2432265c refactor(match): one canonical writer for transaction match state (IMP-CODE-03)
match_status, matched_bill_id and ignored must move together, but they were
updated by copy-pasted inline UPDATEs across six routes/services — exactly how
they drift apart (QA-B5-04 left match_status='matched' with a NULL bill).

Add services/transactionMatchState.js (markMatched / markUnmatched / markIgnored,
each ownership-scoped, returning rows changed) and route the six single-
transaction transitions through it: matchTransactionToBill, unmatchTransaction,
ignoreTransaction, unignoreTransaction (transactionMatchService), the match/
unmatch handlers (routes/matches), and unmatch-on-payment-delete (routes/
transactions, routes/payments).

Guarded bulk auto-match sweeps (subscription tracking, merchant-rule matching,
historical import) and the retention purge intentionally keep their own queries
— their WHERE clauses carry idempotency guards (AND match_status='unmatched')
the simple helper must not silently drop.

Test: tests/transactionMatchState.test.js (transitions + ownership scoping).
transactionMatchService/subscriptionService regression suites still pass;
server 122 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:02:10 -05:00
null c47638a373 docs(qa): archive IMP-UX-01 (recently-deleted restore view)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:57:08 -05:00
null aace5a4356 feat(bills): "Recently deleted" restore view for the 30-day window (IMP-UX-01)
Bills soft-delete and are retained 30 days, but the only way back was the
transient "Undo" toast — dismiss it and a bill deleted an hour ago was
unrecoverable from the UI (even though the API and retention kept it).

- GET /api/bills/deleted lists soft-deleted bills still inside the recovery
  window, newest first, with days_left (declared before /:id). User-scoped.
- BillsPage shows a "Recently deleted (N)" button when any exist, opening a
  dialog to restore each one; restoring refreshes the active list too.
- The list fetch is non-blocking (never blanks the page); restore is
  try/catch + toast; dialog has empty and per-row busy states.

Tests: tests/billsDeletedRoute.test.js (window filter, ordering, days_left,
money serialization, user isolation). Server 116 pass; client 46; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:56:45 -05:00
null e09025430b docs(qa): archive IMP-IA-01 (Data in primary nav)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:50:43 -05:00
null 0b1c6a8322 feat(nav): surface Data in the primary app menu (IMP-IA-01)
Import/export/backups (/data) was only reachable from the account overflow
dropdown + command palette — buried for how central it is. Move it into the
main Tracker nav menu alongside Bills/Categories/Spending/… so it appears in
both the desktop dropdown and the mobile nav.

Preserves the existing gate: Data stays hidden for the default-admin account
(new `accountToolsOnly` flag on the nav item, filtered by the same
`!user.is_default_admin` check the account dropdown used). Removed the now
-redundant account-dropdown entry; command palette entry unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:50:07 -05:00
null 54484ec8a0 docs(qa): archive IMP-CODE-01 (money formatter consolidation)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:47:07 -05:00