diff --git a/HISTORY.md b/HISTORY.md
index 0757777..97d5000 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,28 +1,59 @@
# Bill Tracker β Changelog
-## v0.34.1.2
+## v0.34.1.3
### π Features
- **Reordering across management pages** β Bills, Subscriptions, Categories, and Snowball now expose tracker-style drag/up/down controls. Bill-backed pages persist through `sort_order`; Categories adds its own persisted `sort_order` API.
+- **Snowball readiness warning** β Clicking "Start Snowball Plan" when any readiness checklist items are still incomplete now shows an AlertDialog listing the pending items and asking for confirmation before proceeding.
+
+- **Payoff Simulator β all bills load** β Simulator now loads from `api.bills()` (all active bills) instead of the snowball-only endpoint, so any bill with a `current_balance` appears in the dropdown regardless of category.
+
+- **Payoff Simulator β Custom mode** β Added a "Custom β not in Bill Tracker" option to the dropdown. Selecting it reveals Name (optional) and Balance (required) inputs, letting users simulate any loan or debt without creating a bill. Apply-to-budget and minimum-payment UI are hidden in custom mode.
+
+- **Payoff Simulator β print** β Added a Print button (top-right of page header) that triggers `window.print()`. Print styles isolate the simulator region, hide interactive controls, and inject a summary line showing the simulated parameters.
+
+- **Summary bill ordering** β Summary expenses now use the persisted bill order and support tracker-style drag/up/down reordering, while hiding reorder controls from printed/PDF summaries.
+- **Unified bill schedule editing** β Edit Bill now uses one canonical "Billing Schedule" field instead of separate Billing Cycle and Cycle Type controls.
+
### π§ Changed
-- **Bump** β `0.34.1.1` β `0.34.1.2`
+- **Bump** β `0.34.1.2` β `0.34.1.3`
+
+- **Payoff Simulator β subscriptions excluded** β Added explicit `!is_subscription` guard to the bill filter so subscription bills never appear in the payoff dropdown even if a balance is accidentally set on one.
+
+- **SimpleFIN sync window corrected** β Hard limit updated from 90 β 45 days (actual SimpleFIN Bridge cap). Initial seed and backfill use 44 days (1-day buffer). Routine sync default remains 30 days. The admin `sync_days` setting was previously stored but never read β it now correctly drives routine auto-sync and manual "Sync Now" lookback.
+- **Backups status badge fixed** β System Status page Backups card previously showed "Enabled" (green) whenever `backup_enabled` was true, even with no schedule configured. Badge now reflects the scheduler state: "Scheduled" (green) when automatic backups are active, "Manual Only" (amber) when enabled but unscheduled, "Disabled" (amber) when off. Added Schedule and Next Backup rows to the card.
+
+- **SimpleFIN admin card β sync explainer** β "Transaction history" field replaced with two clearly labelled blocks: Initial connect & backfill (fixed 44 days, read-only) and Routine sync lookback (editable, 1β45 days, default 30). Amber warning appears when the routine value reaches the 45-day limit. Persistent info note keeps the hard limit visible at all times.
+
+- **SimpleFIN account monitoring** β Turning off tracking for an account now prevents new transaction ingestion for that account and excludes existing transactions from matching, merchant-rule sync, and subscription recommendation/search flows.
+- **Snowball extra payment focus** β The extra monthly budget input now uses a brighter, professional focus panel with the live monthly amount called out.
+
+- **Snowball drag behavior** β Snowball custom ordering now uses the same native drag/drop pattern and visual feedback as the Tracker page.
+
+- **Scheduled backup retention** β The database backup scheduler now starts with the server only when enabled in Admin settings and prunes only scheduled backups, keeping the configured default of 2 without deleting manual, imported, or pre-restore backups.
+- **Billing schedule migration** β Added migration v0.76 to backfill legacy billing-cycle values into `cycle_type`, normalize `cycle_day`, and derive the legacy `billing_cycle` value from the canonical schedule.
---
-## v0.34.1.1
+## v0.34.1
### π Features
- **Persistent tracker bill ordering** β Added `sort_order` on bills, `PUT /api/bills/reorder`, and tracker drag/up/down controls so bill order can be changed and remembered.
+
- **Bill archive endpoint** β Added `PUT /api/bills/:id/archived` to hide or restore bills without deleting them.
+
- **Subscription catalog matching** β Subscription recommendations now use the DB-backed `subscription_catalog` as a strong matching signal alongside the existing recurrence algorithm. Known services can surface as high-confidence recommendations, with catalog name/type/website carried into the Track flow.
+
- **Claude.ai catalog seed** β Updated the known subscription catalog so Claude.ai/Anthropic transaction descriptors match the Claude Pro subscription entry.
+
- **Subscription transaction match search** β Added `/api/subscriptions/transaction-matches` for the Subscriptions page. Bank transaction search now annotates known catalog hits, shows "Known: service" badges, and pre-fills new subscriptions from catalog metadata when available.
- **Payoff Simulator page** β New `/payoff` route in sidebar. Select any debt from a dropdown; inputs auto-populate from bill rate, minimum, and expected amount (all editable). Live-updating custom SVG chart with 3 tracks: slate dashed (min-only), indigo dashed (snowball plan), amber solid (simulation). Stats cards show interest saved vs minimum, time saved, and total paid breakdown. "Apply to budget" pushes sim payment back to bill's expected amount with undo support.
- **Snowball plan lifecycle** β Snowball page now supports committing to a plan. "Start Snowball Plan" button appears once β₯3 readiness items are checked. Active plan shows a collapsible emerald banner with pulsing status dot, per-debt progress bars, and on-track/ahead/behind indicators computed from the plan's initial snapshot vs. current balances. Actions: Pause Β· Resume Β· Complete Β· Abandon Β· New Plan (with AlertDialog confirmation).
+
- **Snowball plan history** β Collapsible history panel at the bottom of the Snowball page lists all past plans (completed, abandoned, paused) with status badges, date ranges, and expandable debt snapshot tables showing starting balance, projected payoff, projected interest, and current balance with "Paid off β" on cleared debts.
- **`snowball_plans` table** β Migration v0.73 adds persistent plan storage: status, method, extra_payment, started/paused/completed timestamps, and a JSON plan_snapshot of the initial projection and per-debt starting balances. 8 new API endpoints under `/api/snowball/plans`.
- **Price Change Insights panel** β Tracker page now shows a collapsible amber panel when recurring bills have been paid at a different amount than expected for 2+ consecutive months. Per-bill "Update to $X.XX" action (with undo toast) and "Dismiss" (hidden for 30 days). TrendingUp/TrendingDown icons and teal palette for decreases.
@@ -40,7 +71,7 @@
- Updated status: Keyboard Navigation/Shortcuts β partial (Esc + Cmd+K done, arrow-key grid not)
- Confirmed not implemented: Projected Cash Flow, Recurring Payment Rules (partial), Calendar Agenda, Filtered Exports, Payment Method Tracking, Unit Tests, Bill Grouping, Form State Management β all remain in FUTURE.md
- ### π Internal
+### π Internal
- **Migration hardening** β Made late snooze/drift migrations idempotent for fresh databases.
- **Subscription matching tests** β Added coverage for known catalog recommendations and catalog-annotated subscription transaction search.
diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx
index 3ec9bfa..02201c5 100644
--- a/client/components/BillModal.jsx
+++ b/client/components/BillModal.jsx
@@ -16,6 +16,12 @@ import {
} from '@/components/ui/select';
import { api } from '@/api';
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
+import {
+ BILLING_SCHEDULE_OPTIONS,
+ billingCycleForSchedule,
+ defaultCycleDayForSchedule,
+ scheduleValue,
+} from '@/lib/billingSchedule';
function getOrdinalSuffix(day) {
if (day > 3 && day < 21) return 'th';
@@ -27,10 +33,6 @@ function getOrdinalSuffix(day) {
}
}
-function defaultCycleDayFor(type) {
- return type === 'weekly' || type === 'biweekly' ? 'monday' : '1';
-}
-
// Radix Select crashes on empty string value
const CAT_NONE = 'none';
const PAYMENT_METHODS = [
@@ -121,9 +123,9 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
const [dueDay, setDueDay] = useState(String(sourceBill?.due_day || ''));
const [expectedAmount, setExpected] = useState(String(sourceBill?.expected_amount || ''));
const [interestRate, setInterestRate] = useState(sourceBill?.interest_rate == null ? '' : String(sourceBill.interest_rate));
- const [billingCycle, setCycle] = useState(sourceBill?.billing_cycle || 'monthly');
- const [cycleType, setCycleType] = useState(sourceBill?.cycle_type || 'monthly');
- const [cycleDay, setCycleDay] = useState(sourceBill?.cycle_day || '1');
+ const initialCycleType = scheduleValue(sourceBill || {});
+ const [cycleType, setCycleType] = useState(initialCycleType);
+ const [cycleDay, setCycleDay] = useState(sourceBill?.cycle_day || defaultCycleDayForSchedule(initialCycleType));
const [autopay, setAutopay] = useState(!!sourceBill?.autopay_enabled);
const [autodraftStatus, setAutodraftStatus] = useState(sourceBill?.autodraft_status || (sourceBill?.autopay_enabled ? 'assumed_paid' : 'none'));
const [autoMarkPaid, setAutoMarkPaid] = useState(!!sourceBill?.auto_mark_paid);
@@ -296,7 +298,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
const handleCycleTypeChange = (value) => {
setCycleType(value);
- setCycleDay(defaultCycleDayFor(value));
+ setCycleDay(defaultCycleDayForSchedule(value));
};
function resetPaymentForm() {
@@ -439,7 +441,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
override_due_date: sourceBill?.override_due_date,
expected_amount: parseFloat(expectedAmount) || 0,
interest_rate: parsedInterestRate,
- billing_cycle: billingCycle,
+ billing_cycle: billingCycleForSchedule(cycleType),
cycle_type: cycleType,
cycle_day: cycleDay,
autopay_enabled: autopay,
@@ -578,35 +580,17 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
)}
- {/* Billing Cycle */}
+ {/* Billing Schedule */}
-
-
-
-
- {/* Cycle Type */}
-
-
+
diff --git a/client/components/BillsTableInner.jsx b/client/components/BillsTableInner.jsx
index 1f0c671..13f7cad 100644
--- a/client/components/BillsTableInner.jsx
+++ b/client/components/BillsTableInner.jsx
@@ -1,5 +1,6 @@
import { ArrowDown, ArrowUp, Copy, GripVertical, PenLine, EyeOff, Eye, Clock, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
+import { scheduleLabel } from '@/lib/billingSchedule';
import { MobileBillRow } from '@/components/MobileBillRow';
function ordinal(n) {
@@ -130,7 +131,7 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
{/* Meta row */}
- {prefs.showCycle && {bill.billing_cycle || 'monthly'}}
+ {prefs.showCycle && {scheduleLabel(bill)}}
{prefs.showCycle && prefs.showDueDay && Β·}
diff --git a/client/components/CommandPalette.jsx b/client/components/CommandPalette.jsx
index 29a63c9..490a4a0 100644
--- a/client/components/CommandPalette.jsx
+++ b/client/components/CommandPalette.jsx
@@ -7,6 +7,7 @@ import {
import { toast } from 'sonner';
import { api } from '@/api';
import { cn, fmt } from '@/lib/utils';
+import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
@@ -68,7 +69,8 @@ function billSearchText(bill) {
bill.name,
bill.category_name,
bill.notes,
- bill.billing_cycle,
+ scheduleValue(bill),
+ scheduleLabel(bill),
bill.bucket,
bill.website,
amountSearchText(
diff --git a/client/components/MobileBillRow.jsx b/client/components/MobileBillRow.jsx
index 96e8dc8..623f2cb 100644
--- a/client/components/MobileBillRow.jsx
+++ b/client/components/MobileBillRow.jsx
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { ArrowDown, ArrowUp, Copy, GripVertical, History } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
+import { scheduleLabel } from '@/lib/billingSchedule';
function hasHistoricalVisibility(bill) {
const visibility = bill.history_visibility;
@@ -137,7 +138,7 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o
Cycle
-
{bill.billing_cycle || 'monthly'}
+
{scheduleLabel(bill)}
diff --git a/client/components/admin/BackupManagementCard.jsx b/client/components/admin/BackupManagementCard.jsx
index fa31a6d..8747e38 100644
--- a/client/components/admin/BackupManagementCard.jsx
+++ b/client/components/admin/BackupManagementCard.jsx
@@ -21,7 +21,7 @@ const DEFAULT_SETTINGS = {
enabled: false,
frequency: 'daily',
time: '02:00',
- retention_count: 14,
+ retention_count: 2,
last_run_at: null,
next_run_at: null,
last_error: null,
@@ -142,7 +142,7 @@ export default function BackupManagementCard() {
enabled: !!settings.enabled,
frequency: settings.frequency,
time: settings.time,
- retention_count: parseInt(settings.retention_count, 10) || 14,
+ retention_count: parseInt(settings.retention_count, 10) || 2,
});
setSettings({ ...DEFAULT_SETTINGS, ...saved });
toast.success('Backup schedule saved.');
diff --git a/client/components/admin/BankSyncAdminCard.jsx b/client/components/admin/BankSyncAdminCard.jsx
index e0eb6e3..989c9f6 100644
--- a/client/components/admin/BankSyncAdminCard.jsx
+++ b/client/components/admin/BankSyncAdminCard.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
+import { AlertTriangle, Info } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api';
import { Button } from '@/components/ui/button';
@@ -52,8 +53,8 @@ export default function BankSyncAdminCard() {
toast.error('Sync interval must be between 0.5 and 168 hours.');
return;
}
- if (!Number.isFinite(days) || days < 1 || days > 90) {
- toast.error('Transaction history must be between 1 and 90 days β SimpleFIN Bridge does not support longer windows.');
+ if (!Number.isFinite(days) || days < 1 || days > 45) {
+ toast.error('Routine sync lookback must be 1β45 days. SimpleFIN Bridge enforces a 45-day hard limit β values above 45 return errors.');
return;
}
setSaving(true);
@@ -145,25 +146,72 @@ export default function BankSyncAdminCard() {
- {/* Transaction history lookback */}
-
+ {/* Sync window β two-mode explainer */}
+
-
Transaction history
+
Sync lookback windows
- How far back to fetch transactions. Maximum 90 days β this is a hard limit imposed by SimpleFIN Bridge and cannot be exceeded.
+ SimpleFIN uses two different windows depending on sync type.
+ The first sync (and any manual backfill) always fetches the maximum 44 days of history
+ to build a complete transaction picture. This is fixed β SimpleFIN Bridge enforces a
+ strict 45-day hard limit and will return an error for any request beyond it.
+
+
+
+ {/* Routine sync β editable */}
+
+
+
Routine sync lookback
+
+ How far back each auto-sync and manual "Sync Now" looks after the initial connect.
+ Recommended: 7β30 days. Setting this near 45 increases request size
+ and duplicate-skip work with no benefit once history is established.
+
+
+ {/* Amber warning at the SimpleFIN limit */}
+ {parseInt(syncDays, 10) >= 45 && (
+
+
+
+ 45 days is SimpleFIN Bridge's maximum. Requests at this limit may occasionally
+ fail due to request latency β 30 days or less is recommended for reliable routine syncs.
+
+
+ )}
+
+ {/* Always-visible hard-limit note */}
+
+
+
+ SimpleFIN Bridge enforces a 45-day maximum on all requests.
+ Any value above 45 will cause sync errors for all users.
+