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>
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>
- 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>
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>
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>
- 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>
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>
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>
The client had ~15 hand-rolled currency formatters (local `fmt`/`money`/
`fmtFull`/`fmtDollars`/…) plus a canonical `fmt` in lib/utils used at ~190
sites — same rules copy-pasted, with inconsistent handling of negatives,
whole-dollar, cents vs dollars, and blank input.
Add client/lib/money.js as the one implementation:
- formatUSD(dollars) — "$1,234.56" (whole/dash options)
- formatUSDWhole(dollars) — "$1,235"
- formatCentsUSD(cents) — from integer cents; signed "+/-" and dash options
Inputs are coerced so null/''/NaN never render as "$NaN", and -0 is
normalized so it never shows as "-$0.00".
lib/utils.fmt now delegates to formatUSD (byte-identical — the existing
utils.test.js fmt suite is the regression guard), and the 15 local
formatters delegate to money.js. No display currency formatting remains
outside money.js; the /100 conversions left behind are calculations
(form prefill), not display.
Tests: client/lib/money.test.js (13). Full client suite 46 pass; build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
Replace all Save buttons on the Settings page with debounced auto-save:
- useAutoSave hook: debounce with latest-payload-wins, flush() for blur,
pending-edit flush on unmount, status machine (idle/saving/saved/error)
with saved fading back to idle. Covered by 6 Vitest tests (fake timers).
- SaveStatus pill (framer-motion) in the page header and notification card
headers — Saving…/Saved/Save failed.
- Timing per control: toggles/selects/channel ~150-400ms; typed inputs
(email, URLs, grace period, drift pct) 900ms + flush on blur.
- Push token never auto-saves mid-type: saves on blur only, so a partial
token can never overwrite a working one.
- Notification cards no longer refetch parent settings on save (would
clobber in-flight edits under auto-save).
- Decision: no undo toast — settings are non-destructive and instantly
re-editable; undo would add noise without safety.
- vitest include now picks up .jsx tests; jsdom + @testing-library/react
added as devDependencies.
- Use controlled Dialog state (setDialogOpen) instead of immediate onClose()
to let Radix cleanup properly before unmount
- Amber 'Pending' badge now only shows for bank-linked bills — unlinked
bills skip the pending-cleared check and show 'Paid' directly
- TrackerPage onSave no longer nullifies edit state before BillModal can
animate closed
(batch 0.37.4)
- Shorten 'Live Sync' label to 'Live' for space-constrained layouts
- Add existing bill due_day fallback in validateBillData to prevent
spurious required-field errors during partial PATCH updates
(batch 0.37.2)
- 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
- Wire four push channels into runNotifications() with urgency mapping
- push_url and push_token encrypted at rest via AES-256-GCM
- Profile page Push card with master toggle, channel picker, test button
- Calendar CashFlowCard with period/month projections and negative alert
- Tracker card shows projected amount when cashflow data available
- TrackerPage Pending badge: consistent styling and tooltip text
- CalendarPage money map: handle edge cases when bank tracking is active but no pending payments
- trackerService: deduplicate pending payment query, handle zero-pending state
- Opt-in Bank Budget Tracking mode replaces manual starting amounts with live bank balance
- Calendar money map shows Balance / Pending / Unpaid Bills / After Bills in bank mode
- Pending badge (amber) on tracker rows within configurable pending window (0-7 days)
- New GET /api/data-sources/accounts/all endpoint for account picker
- Tracker starting-amounts card shows account name and live balance hint
- Copy pipeline-report.py from Pipeline project into scripts/
- Update TOOLS.md and MEMORY.md to reflect workflow consolidation
- (includes all uncommitted v0.36.0 changes from prior session)
Detects when a bill's recent payments have diverged from its configured
expected amount for 2+ consecutive months and surfaces it in a new
collapsible amber panel on the Tracker page.
- Migration v0.71: adds `drift_snoozed_until` to bills and
`notify_amount_change` to users
- New `driftService.getDriftReport()`: computes per-bill payment median
over last 3 months, flags drift above a user-configurable threshold
(default 5%, minimum $1 delta)
- New `GET /api/bills/drift-report` and `POST /api/bills/:id/snooze-drift`
routes (registered before `/:id` to avoid routing conflict)
- `runDriftNotifications()` added to daily worker — sends amber digest
email per user listing all changed bills with old → new amounts
- `notify_amount_change` wired through profile and notifications routes
- `DriftInsightPanel`: collapsible amber panel with per-bill
strikethrough old → new amount, ±% badge, TrendingUp/TrendingDown
icons, "Update to $X.XX" (with undo toast) and "Dismiss" (30 days)
actions; teal palette for price decreases
- `drift_threshold_pct` setting added to SettingsPage Billing Behavior
- "Notify on price changes" toggle added to ProfilePage notifications
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Roadmap grid now adapts columns based on how many priority lanes have items
- With only LOW items, lane uses full width instead of narrow 5-column slot
- cleanupService: use BACKUP_DIR import, handle .xlsx export file cleanup
- backupScheduler: export computeNextRun for external use
- Added backupAndCleanup.test.js for coverage
- Removed hasBoth, compact prop, 2xl:min-w-[700px], 2xl:hidden Last Month,
narrowed columns — back to original single-column tracker layout
- Removed 2xl:max-w-[2000px] from Layout/Sidebar/AdminShell/footer/nav
- Kept S badge in all 4 locations and bucket remaining/Done header
- Added mkdocs/ to .gitignore
- Bump v0.33.8.6 -> v0.33.8.7
- New backfillDataSource export and POST route for 89-day history pull
- Auto-seed 89 days on first connect, 30 days for routine syncs
- sinceEpoch() replaced with sinceEpochDays(days) with explicit param
- Status page errorRow now filters AND status = 'error'
- Full status page redesign: colored top borders, card icons, section labels,
health banner with glowing dot, consistent spacing
- Bump v0.33.8.3 -> v0.33.8.4
- Sub badge (indigo) in all 4 locations, toggleable in Bills prefs
- SimpleFIN Sync card on Status page
- dailyWorker.start() now called on startup
- tracker.overdue_count uses real SQL query
- Status page accuracy: dynamic headers, Degraded state, worker last_error check
- Removed SimpleFIN prefix from Recommendations title
- Bump v0.33.8.2 -> v0.33.8.3
- Removed duplicate unmatchTransaction API entry in api.js
- Unmonitored accounts: no chevron, click-to-expand disabled, tx panel hidden
- matched_bill_name included via LEFT JOIN bills in accounts query
- BillPickerDialog resets search/selection on open
- Link to bill: marks historical txs matched, stores merchant rule,
applyMerchantRules catches other unmatched txs from same merchant
- Track (new subscription): creates bill with is_subscription=1, stores
merchant rule for ongoing tracking
- SimpleFIN sync: applyMerchantRules runs after tx insert, auto-matches
by merchant rule with payment_source='auto_match'
- Auto-match payments have transaction_id set, treated same as manual matches
- New services/billMerchantRuleService.js for rule storage and matching
- Migration for bill_merchant_rules table
- Amount-bucket grouping ensures consistent charges are grouped together
- Catalog lookup names and boosts the result
- Deduplication ensures one recommendation per known service
- Removed catalog-first rewrite
Backend:
- v0.64 migration: monitored column on financial_accounts
- GET/PUT data-sources accounts endpoints for monitored toggle + tx listing
- matchSuggestionService: excludes unmonitored accounts from match scoring
Frontend:
- BankSyncSection rebuild: accounts panel with monitored switch, expand for
last 50 transactions, match status badges, optimistic toggle
- TransactionMatchingSection: toast on bills load failure
- DataPage: toast on import history load failure
- ProfilePage: toast on both login history fetch failures