Commit Graph

145 Commits

Author SHA1 Message Date
null 1fe71a7d5e docs(history): React Query migration of all 7 manual-fetch pages (R5)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:26:32 -05:00
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 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 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 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 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 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 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 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 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
null b3168fca70 fix(qa): retention GC orphaned matched transactions on bill purge (QA-B5-04)
Found probing a copy of the live SimpleFIN DB: 3 transactions were
match_status='matched' with matched_bill_id=NULL. Bills are soft-deleted
(retained for recovery), then the retention GC hard-deletes them past the
30-day window. transactions.matched_bill_id is ON DELETE SET NULL, so the
purge nulled the pointer but left match_status='matched' — a limbo row
excluded from spending/analytics (match_status != 'matched') yet attributed
to no bill, silently dropping that spend.

pruneSoftDeletedFinancialRecords now releases those matches back to
'unmatched' in the same transaction and self-heals pre-existing orphans;
retention behaviour is unchanged. Verified on a live-DB copy (3→0 orphans,
0 transactions lost). Regression: 3 tests in backupAndCleanup.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:04:59 -05:00
null a5671ab3be fix(qa): harden DB file permissions — was world-readable 644 (QA-B16-02)
docker-entrypoint chmod 700'd the data dir but never the DB file; SQLite created
bills.db/-wal/-shm at umask 644 (world-readable), holding financial data +
encrypted SimpleFIN token/sessions/secrets. Add `umask 077` (files 600, dirs 700)
+ explicit chmod 600 of any pre-existing DB files on upgrade. Found on the live
nebula deploy (BillTracker.db was 644).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:32:39 -05:00
null 2963d11d1b fix(qa): version check is opt-out-able (QA-B16-01)
- updateCheckService: gate the external request on `update_check_enabled`
  (default on); when off, no network call, returns { disabled: true }
- aboutAdmin: GET/PUT /update-check-setting (admin-only) to toggle it
- StatusPage: a Switch on the admin System Status card to enable/disable
- privacy.js: state that an admin can disable it (was called "optional" with
  no actual opt-out)
- tests/updateCheckOptOut.test.js: proves no external fetch when disabled
- docs: archive QA-B16-01, B16 

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:05:37 -05:00
null c31d8cbe9e fix(qa): escape bill name in reminder email HTML — XSS via bill name (B14-04)
- notificationService buildEmailHtml: the message line interpolated bill.name
  raw (`<strong>${bill.name}</strong> is due…`) while the detail table escaped
  it; a `<img src=x onerror=…>` name landed unescaped in the email HTML. Now
  escaped everywhere. (self-XSS — reminders go to the bill's owner — but a clear
  inconsistent-escaping defect)
- expose buildEmailHtml via _email; add an escaping test across all 4 email types
- docs: archive QA-B14-04

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:18:05 -05:00
null 2050e13407 fix(qa): notification _push export was clobbered → "Send test push" 500'd (B10-01)
- notificationService: `module.exports._push = {...}` was set BEFORE the final
  `module.exports = {...}`, which wiped it, so routes/notifications.js got
  `_push || {}` → sendTestPush undefined → POST /api/notifications/test-push
  always threw "Push service not initialised". Scheduled reminders were fine
  (in-scope calls). Moved the _push assignment after the reassignment.
- add tests/notificationDelivery.test.js (7 tests: ntfy/gotify/discord payloads,
  dispatch, error handling, unknown channel, no token leak in the body)
- docs: archive QA-B10-01

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:11:34 -05:00
null 819cfdfa9f fix(qa): bank-tracking unpaid_this_month gates by occurrence (QA-B5-02)
- routes/summary buildBankTracking: fetch unpaid candidates and filter by
  resolveDueDate in JS so annual / off-month quarterly bills don't inflate the
  SimpleFIN "unpaid this month" metric (completes the occurrence-gating family)
- add tests/summaryBankTracking.test.js (isolated route test)
- docs: archive QA-B5-02; Active Findings Log now empty (0 open)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:41:33 -05:00
null 1bd282f47b fix(qa): Analytics "expected" gates by occurrence (matches Tracker/Summary)
- analyticsService: only add a bill's expected_amount in months it actually
  occurs (resolveDueDate), so annual / off-month quarterly bills no longer
  inflate the expected-vs-actual line every month (QA-B5-03, same root as B5-01)
- add a Tracker<->Analytics reconciliation guard to e2e/api.probe.spec.js
- docs: archive QA-B5-03; cycle log

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:23:37 -05:00
null a15ff056b3 fix(qa): Summary excludes bills not due in the month (reconciles with Tracker)
- routes/summary: filter the expense list by resolveDueDate so annual and
  off-month quarterly bills no longer inflate the monthly total / "monthly
  result" — the Summary now agrees with the Tracker for the same month (QA-B5-01)
- add a Tracker<->Summary reconciliation guard in e2e/api.probe.spec.js
- docs: archive QA-B5-01; track QA-B5-02 (SimpleFIN unpaid_this_month residual)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:19:35 -05:00
null 72d06aa2d8 fix(qa): cent-exact toCents rounding + money.js test coverage
- utils/money: toCents rounds off the shortest decimal string instead of
  Math.round(n*100), so 1.005 -> 101 (not 100). Output is identical for all
  integer / <=2-decimal / "$1,234.56" inputs, so no downstream change (QA-B7-01)
- add tests/money.test.js (9 tests; the money core previously had none)
- docs: archive QA-B7-01 to HISTORY v0.41.0; QA cycle 1 now 0 open findings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:11:12 -05:00
null 98c8fab176 fix(qa): resolve a11y nested-interactive on Categories & Snowball rows
- CategoriesPage: category rows are now a plain container with a dedicated
  chevron toggle button, instead of role=button rows nesting action buttons
- PlanStatusBanner: split the collapsible header into a name/progress toggle,
  sibling action buttons, and a chevron toggle (actions no longer nested in the
  trigger button)
- add e2e/categories.spec.js expand regression; all 8 authed pages now pass axe
- docs: archive QA-B14-02 to HISTORY v0.41.0; QA plan status/cycle-log

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:02:15 -05:00
null 127b69ffc2 chore(qa): vendor chunk splitting, remove unused markdown deps, remove dead totalInterestPaid (batch 0.41.0 QA cleanup) 2026-07-02 20:47:50 -05:00
null 029c227685 fix(qa): seed demo data amounts, bill amount validation, negative USD format, a11y aria-labels, Playwright E2E setup (batch 0.41.0 QA) 2026-07-02 20:36:09 -05:00
null bdbf231538 docs: add v0.41.0 changelog entry 2026-06-16 04:15:16 -05:00
null d0835b86ab chore(cleanup): remove legacy/public HTML files, retire /legacy route, update docs and About page 2026-06-11 23:50:27 -05:00
null 4b74a456d9 chore(release): bump to v0.38.4, update HISTORY, remove Stage 2 from FUTURE 2026-06-11 20:14:02 -05:00
null 79b51b1c9a fix(bank-sync): transaction matching, services, and worker updates 2026-06-07 20:07:27 -05:00
null 71e783a799 fix(ui): calendar settings improvements 2026-06-07 16:52:50 -05:00
null 13e41aec74 feat: iCal feed for bills (Apple/Google calendar export) 2026-06-07 15:53:46 -05:00
null 72d95065d0 fix: mobile tracker row and bucket rendering polish 2026-06-07 14:58:37 -05:00
null 6811eb8be5 feat: payment accounting service, SQL schema + migration, backend route refactor, test updates 2026-06-07 01:05:48 -05:00
null 1ebb2da50a feat: tracker bucket rollover logic, utils cleanup, HISTORY update 2026-06-07 00:41:07 -05:00
null 12d869d400 feat(subscriptions): cadence sort toggle, in-place bill edits after save 2026-06-06 22:25:58 -05:00
null 83e6afa9e6 feat(subscriptions): simplified SubscriptionsPage, inline actions, improved matching card, Service Catalog route
- Extracted known-service catalog to dedicated /subscriptions/catalog route
- Simplified main Subscriptions page to focus on tracked services + bank-backed recommendations
- Replaced inline Pause/Resume with Edit + MoreHorizontal dropdown on subscription rows
- Added 'Improve Matching' card linking to Service Catalog
- Vite proxy respects API_PORT env var for dev flexibility
- Added top_200_us_subscriptions_researched dataset
- Updated HISTORY.md with v0.35.0 changes
2026-06-06 22:09:34 -05:00
null 17478ebd9c 1 2026-06-06 19:47:05 -05:00