diff --git a/client/components/admin/BankSyncAdminCard.jsx b/client/components/admin/BankSyncAdminCard.jsx index 1706f3c..e0eb6e3 100644 --- a/client/components/admin/BankSyncAdminCard.jsx +++ b/client/components/admin/BankSyncAdminCard.jsx @@ -52,8 +52,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 > 730) { - toast.error('Transaction history must be between 1 and 730 days.'); + 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.'); return; } setSaving(true); @@ -146,21 +146,21 @@ export default function BankSyncAdminCard() { {/* Transaction history lookback */} -
+

Transaction history

- How far back to fetch on first connect. Re-syncs only fetch recent activity. + How far back to fetch transactions. Maximum 90 days — this is a hard limit imposed by SimpleFIN Bridge and cannot be exceeded.

setSyncDays(e.target.value)} + onChange={e => setSyncDays(Math.min(90, Math.max(1, parseInt(e.target.value, 10) || 90)))} className="w-20 text-sm text-right" /> days diff --git a/client/components/data/BankSyncSection.jsx b/client/components/data/BankSyncSection.jsx index 5b0deda..a798470 100644 --- a/client/components/data/BankSyncSection.jsx +++ b/client/components/data/BankSyncSection.jsx @@ -184,6 +184,7 @@ function AccountRow({ account, sourceId, expanded, onToggleExpand, onToggleMonit export default function BankSyncSection({ onConnectionChange }) { const [enabled, setEnabled] = useState(null); + const [syncDays, setSyncDays] = useState(90); const [connections, setConnections] = useState([]); const [accountsBySource, setAccountsBySource] = useState({}); const [accountsLoading, setAccountsLoading] = useState({}); @@ -220,6 +221,7 @@ export default function BankSyncSection({ onConnectionChange }) { api.dataSources({ type: 'provider_sync' }), ]); setEnabled(status.enabled); + setSyncDays(status.sync_days ?? 90); const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []; setConnections(conns); onConnectionChange?.(conns[0] || null); @@ -252,7 +254,11 @@ export default function BankSyncSection({ onConnectionChange }) { setSyncing(id); try { const result = await api.syncDataSource(id); - toast.success(`Synced — ${result.transactionsNew} new transaction(s).`); + if (result.errlist) { + toast.warning(`Synced ${result.transactionsNew} new transaction(s), but some connections need attention: ${result.errlist}`); + } else { + toast.success(`Synced — ${result.transactionsNew} new transaction(s).`); + } await load(); } catch (err) { toast.error(err.message || 'Sync failed'); @@ -302,10 +308,11 @@ export default function BankSyncSection({ onConnectionChange }) { } }; - function isStale(conn) { - if (!conn.last_error) return false; - if (!conn.last_sync_at) return true; - return Date.now() - new Date(conn.last_sync_at).getTime() > 24 * 60 * 60 * 1000; + function connWarning(conn) { + if (!conn.last_error) return null; + if (conn.status === 'error') return { kind: 'error', label: 'Sync error' }; + // Partial errlist: sync succeeded but some bank connections need attention + return { kind: 'partial', label: 'Some connections need attention' }; } if (enabled === null) { @@ -338,17 +345,21 @@ export default function BankSyncSection({ onConnectionChange }) { const accsError = accountsErrorBySource[conn.id]; const monitoredCount = accounts.filter(a => a.monitored).length; + const warning = connWarning(conn); return (
- {isStale(conn) && ( + {warning && (
- Sync error - {conn.last_error && ( - — {conn.last_error} + {warning.label} + — {conn.last_error} + {warning.kind === 'partial' && ( + + Re-authenticate the affected institutions in your SimpleFIN Bridge account to restore full sync. + )} - {conn.last_sync_at && ( + {warning.kind === 'error' && conn.last_sync_at && ( Last successful sync: {fmtDate(conn.last_sync_at)} @@ -415,6 +426,10 @@ export default function BankSyncSection({ onConnectionChange }) { {conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)}

+
+

History window

+

{syncDays} days

+
{/* Accounts section */} diff --git a/package.json b/package.json index bc91a71..43c126b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.33.2", + "version": "0.33.3", "description": "Monthly bill tracking system", "main": "server.js", "scripts": { diff --git a/routes/dataSources.js b/routes/dataSources.js index ec0cd1f..23fec47 100644 --- a/routes/dataSources.js +++ b/routes/dataSources.js @@ -62,8 +62,8 @@ router.get('/', (req, res) => { // ─── GET /api/data-sources/simplefin/status ────────────────────────────────── router.get('/simplefin/status', (req, res) => { - const { enabled } = getBankSyncConfig(); - res.json({ enabled }); + const { enabled, sync_days } = getBankSyncConfig(); + res.json({ enabled, sync_days }); }); // ─── POST /api/data-sources/simplefin/connect ──────────────────────────────── diff --git a/services/bankSyncConfigService.js b/services/bankSyncConfigService.js index 99629bb..0e86721 100644 --- a/services/bankSyncConfigService.js +++ b/services/bankSyncConfigService.js @@ -2,6 +2,7 @@ const { getSetting, setSetting } = require('../db/database'); +const SYNC_DAYS_MAX = 90; // SimpleFIN Bridge hard limit const SYNC_DAYS_DEFAULT = 90; const SYNC_INTERVAL_DEFAULT = 4; // hours @@ -20,11 +21,12 @@ function getBankSyncConfig() { const syncDaysDb = parseInt(getSetting('simplefin_sync_days') || '', 10); const syncDaysEnv = parseInt(process.env.SIMPLEFIN_SYNC_DAYS || '', 10); - const syncDays = Number.isFinite(syncDaysDb) && syncDaysDb > 0 + const rawSyncDays = Number.isFinite(syncDaysDb) && syncDaysDb > 0 ? syncDaysDb : Number.isFinite(syncDaysEnv) && syncDaysEnv > 0 ? syncDaysEnv : SYNC_DAYS_DEFAULT; + const syncDays = Math.min(rawSyncDays, SYNC_DAYS_MAX); const intervalDb = parseFloat(getSetting('simplefin_sync_interval_hours') || ''); const intervalEnv = parseFloat(process.env.SIMPLEFIN_SYNC_INTERVAL_HOURS || ''); @@ -57,8 +59,8 @@ function setSyncIntervalHours(hours) { function setSyncDays(days) { const n = parseInt(days, 10); - if (!Number.isFinite(n) || n < 1 || n > 730) { - throw Object.assign(new Error('sync_days must be between 1 and 730'), { status: 400 }); + if (!Number.isFinite(n) || n < 1 || n > SYNC_DAYS_MAX) { + throw Object.assign(new Error(`sync_days must be between 1 and ${SYNC_DAYS_MAX} (SimpleFIN Bridge hard limit)`), { status: 400 }); } setSetting('simplefin_sync_days', String(n)); return getBankSyncConfig(); diff --git a/services/bankSyncService.js b/services/bankSyncService.js index df75ac4..33aa6eb 100644 --- a/services/bankSyncService.js +++ b/services/bankSyncService.js @@ -11,12 +11,7 @@ const { const { getBankSyncConfig } = require('./bankSyncConfigService'); const { decorateDataSource } = require('./transactionService'); -function sinceEpoch(dataSource) { - if (dataSource.last_sync_at) { - // Overlap by 2 days to catch late-posted transactions - const ts = new Date(dataSource.last_sync_at).getTime(); - if (Number.isFinite(ts)) return Math.floor((ts - 2 * 86400 * 1000) / 1000); - } +function sinceEpoch() { const { sync_days } = getBankSyncConfig(); return Math.floor((Date.now() - sync_days * 86400 * 1000) / 1000); } @@ -84,7 +79,7 @@ function insertTransactionIfNew(db, txRow) { async function runSync(db, userId, dataSource) { const accessUrl = decryptSecret(dataSource.encrypted_secret); - const since = sinceEpoch(dataSource); + const since = sinceEpoch(); const raw = await fetchAccountsAndTransactions(accessUrl, since); const accounts = Array.isArray(raw.accounts) ? raw.accounts : [];