Commit Graph

136 Commits

Author SHA1 Message Date
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
null 7455dff5b8 feat: v0.37.0 — auto-learn merchant rules, ambiguous match protection, session hashing, geolocation opt-in 2026-06-06 18:30:21 -05:00
null 99abca9868 security: WebAuthn / FIDO2 hardware security key 2FA 2026-06-05 22:05:23 -05:00
null 2c9cc37593 feat: copy last month budgets, monthly income section on summary page 2026-06-04 21:57:42 -05:00
null 59d32f4686 perf: composite DB indexes, notification N+1 batching, spending page double-fetch fix 2026-06-04 21:00:59 -05:00
null 3f0078b930 feat: spending income section, rules manager, error handling improvements 2026-06-04 19:53:38 -05:00
null 653dd72e12 feat: TOTP 2FA for login & profile setup flow 2026-06-04 04:10:14 -05:00
null 26b6fb13e5 feat: login history with geolocation, encryption, new device alerts, session detection 2026-06-04 03:38:32 -05:00
null 3745ef79b7 docs: update HISTORY.md with bank tracking, merchant rules, and UI changes 2026-06-04 02:48:32 -05:00
null 48f5577031 fix: bank tracking pending deduction corrected, projected month-end balance, settings loading fix 2026-06-04 01:36:07 -05:00