fix: sync_days hard-clamped to 90 (SimpleFIN Bridge limit)
- bankSyncConfigService: SYNC_DAYS_MAX=90, getBankSyncConfig clamps on read, setSyncDays rejects >90 with explanation - bankSyncService: every sync requests full sync_days window, dedup handles already-seen transactions - dataSources status endpoint returns sync_days alongside enabled - BankSyncAdminCard: input max 90, live clamp, description cites Bridge limit - BankSyncSection: third stat tile showing History window X days
This commit is contained in:
parent
820fedd58e
commit
1d8ae4f511
|
|
@ -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() {
|
|||
</div>
|
||||
|
||||
{/* Transaction history lookback */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Transaction history</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="730"
|
||||
max="90"
|
||||
step="1"
|
||||
value={syncDays}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">days</span>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div key={conn.id} className="px-6 py-4 space-y-3">
|
||||
{isStale(conn) && (
|
||||
{warning && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5 text-sm text-amber-700 dark:text-amber-400">
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="font-medium">Sync error</span>
|
||||
{conn.last_error && (
|
||||
<span className="ml-1 font-normal opacity-90">— {conn.last_error}</span>
|
||||
<span className="font-medium">{warning.label}</span>
|
||||
<span className="ml-1 font-normal opacity-90">— {conn.last_error}</span>
|
||||
{warning.kind === 'partial' && (
|
||||
<span className="block text-xs mt-0.5 opacity-75">
|
||||
Re-authenticate the affected institutions in your SimpleFIN Bridge account to restore full sync.
|
||||
</span>
|
||||
)}
|
||||
{conn.last_sync_at && (
|
||||
{warning.kind === 'error' && conn.last_sync_at && (
|
||||
<span className="block text-xs mt-0.5 opacity-75">
|
||||
Last successful sync: {fmtDate(conn.last_sync_at)}
|
||||
</span>
|
||||
|
|
@ -415,6 +426,10 @@ export default function BankSyncSection({ onConnectionChange }) {
|
|||
{conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border/60 bg-muted/20 px-3 py-2">
|
||||
<p className="text-muted-foreground">History window</p>
|
||||
<p className="font-medium mt-0.5">{syncDays} days</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accounts section */}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bill-tracker",
|
||||
"version": "0.33.2",
|
||||
"version": "0.33.3",
|
||||
"description": "Monthly bill tracking system",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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 ────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 : [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue