2026-07-04 21:26:22 -05:00
|
|
|
|
import { useState, useEffect } from 'react';
|
2026-05-30 21:20:51 -05:00
|
|
|
|
import { AlertTriangle, Info } from 'lucide-react';
|
2026-05-28 22:06:15 -05:00
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
import { api } from '@/api';
|
2026-07-05 14:38:12 -05:00
|
|
|
|
import { errMessage, parseUtc } from '@/lib/utils';
|
2026-05-28 22:06:15 -05:00
|
|
|
|
import { Button } from '@/components/ui/button';
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
import { Input } from '@/components/ui/input';
|
2026-05-28 22:06:15 -05:00
|
|
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
|
|
|
|
import { Toggle } from './adminShared';
|
|
|
|
|
|
|
2026-07-04 21:26:22 -05:00
|
|
|
|
interface BankSyncWorker {
|
|
|
|
|
|
running?: boolean;
|
|
|
|
|
|
last_run_at?: string | null;
|
|
|
|
|
|
next_run_at?: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface BankSyncConfig {
|
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
|
sync_interval_hours?: number;
|
|
|
|
|
|
sync_days?: number;
|
|
|
|
|
|
debug_logging?: boolean;
|
|
|
|
|
|
sync_days_max?: number;
|
|
|
|
|
|
seed_days?: number;
|
|
|
|
|
|
worker?: BankSyncWorker;
|
|
|
|
|
|
encryption_key_source?: string;
|
2026-07-05 19:15:32 -05:00
|
|
|
|
key_fingerprint?: string | null;
|
2026-07-04 21:26:22 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function timeAgo(iso: string | null | undefined): string | null {
|
|
|
|
|
|
const d = parseUtc(iso);
|
|
|
|
|
|
if (!d) return null;
|
|
|
|
|
|
const secs = Math.floor((Date.now() - d.getTime()) / 1000);
|
2026-07-06 14:23:53 -05:00
|
|
|
|
if (secs < 60) return 'just now';
|
2026-05-28 22:32:33 -05:00
|
|
|
|
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
|
|
|
|
|
if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
|
|
|
|
|
|
return `${Math.floor(secs / 86400)}d ago`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 21:26:22 -05:00
|
|
|
|
function timeUntil(iso: string | null | undefined): string | null {
|
|
|
|
|
|
const d = parseUtc(iso);
|
|
|
|
|
|
if (!d) return null;
|
|
|
|
|
|
const secs = Math.floor((d.getTime() - Date.now()) / 1000);
|
2026-05-28 22:32:33 -05:00
|
|
|
|
if (secs <= 0) return 'soon';
|
|
|
|
|
|
if (secs < 60) return `${secs}s`;
|
|
|
|
|
|
if (secs < 3600) return `${Math.floor(secs / 60)}m`;
|
|
|
|
|
|
return `${Math.floor(secs / 3600)}h`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 22:06:15 -05:00
|
|
|
|
export default function BankSyncAdminCard() {
|
2026-07-06 14:23:53 -05:00
|
|
|
|
const [config, setConfig] = useState<BankSyncConfig | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [loadError, setLoadError] = useState('');
|
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
const [enabled, setEnabled] = useState(false);
|
2026-07-04 21:26:22 -05:00
|
|
|
|
const [syncInterval, setSyncInterval] = useState<number | string>(4);
|
2026-07-06 14:23:53 -05:00
|
|
|
|
const [syncDays, setSyncDays] = useState<number | string>(30);
|
2026-06-07 19:41:17 -05:00
|
|
|
|
const [debugLogging, setDebugLogging] = useState(false);
|
2026-05-28 22:06:15 -05:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-07-06 14:23:53 -05:00
|
|
|
|
api
|
|
|
|
|
|
.bankSyncConfig()
|
|
|
|
|
|
.then((raw) => {
|
2026-07-04 21:26:22 -05:00
|
|
|
|
const d = raw as BankSyncConfig;
|
2026-05-28 22:06:15 -05:00
|
|
|
|
setConfig(d);
|
|
|
|
|
|
setEnabled(!!d.enabled);
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
setSyncInterval(d.sync_interval_hours ?? 4);
|
2026-06-06 15:51:56 -05:00
|
|
|
|
setSyncDays(d.sync_days ?? 30);
|
2026-06-07 19:41:17 -05:00
|
|
|
|
setDebugLogging(!!d.debug_logging);
|
2026-05-28 22:06:15 -05:00
|
|
|
|
})
|
2026-07-06 14:23:53 -05:00
|
|
|
|
.catch((err) => setLoadError(errMessage(err, 'Failed to load bank sync config')))
|
2026-05-28 22:06:15 -05:00
|
|
|
|
.finally(() => setLoading(false));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
2026-07-04 21:26:22 -05:00
|
|
|
|
const hours = parseFloat(String(syncInterval));
|
2026-07-06 14:23:53 -05:00
|
|
|
|
const days = parseInt(String(syncDays), 10);
|
2026-06-06 15:51:56 -05:00
|
|
|
|
const maxDays = config?.sync_days_max ?? 45;
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) {
|
|
|
|
|
|
toast.error('Sync interval must be between 0.5 and 168 hours.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-06 15:51:56 -05:00
|
|
|
|
if (!Number.isFinite(days) || days < 1 || days > maxDays) {
|
2026-07-06 14:23:53 -05:00
|
|
|
|
toast.error(
|
|
|
|
|
|
`Routine sync lookback must be 1–${maxDays} days. SimpleFIN Bridge enforces a ${maxDays}-day hard limit — values above ${maxDays} return errors.`,
|
|
|
|
|
|
);
|
2026-05-29 01:33:54 -05:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-28 22:06:15 -05:00
|
|
|
|
setSaving(true);
|
|
|
|
|
|
try {
|
2026-07-06 14:23:53 -05:00
|
|
|
|
const result = (await api.setBankSyncConfig({
|
|
|
|
|
|
enabled,
|
|
|
|
|
|
sync_interval_hours: hours,
|
|
|
|
|
|
sync_days: days,
|
|
|
|
|
|
debug_logging: debugLogging,
|
|
|
|
|
|
})) as BankSyncConfig;
|
2026-05-28 22:06:15 -05:00
|
|
|
|
setConfig(result);
|
|
|
|
|
|
setEnabled(!!result.enabled);
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
setSyncInterval(result.sync_interval_hours ?? 4);
|
2026-06-06 15:51:56 -05:00
|
|
|
|
setSyncDays(result.sync_days ?? 30);
|
2026-06-07 19:41:17 -05:00
|
|
|
|
setDebugLogging(!!result.debug_logging);
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
toast.success('Bank sync settings saved.');
|
2026-05-28 22:06:15 -05:00
|
|
|
|
} catch (err) {
|
2026-07-04 21:26:22 -05:00
|
|
|
|
toast.error(errMessage(err, 'Failed to update bank sync setting.'));
|
2026-05-28 22:06:15 -05:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardContent className="py-8 text-center text-muted-foreground text-sm">
|
|
|
|
|
|
Loading…
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
if (loadError) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<CardContent className="py-8 text-center text-sm text-destructive">{loadError}</CardContent>
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
|
const changed =
|
|
|
|
|
|
enabled !== !!config?.enabled ||
|
|
|
|
|
|
parseFloat(String(syncInterval)) !== config?.sync_interval_hours ||
|
|
|
|
|
|
parseInt(String(syncDays), 10) !== config?.sync_days ||
|
|
|
|
|
|
debugLogging !== !!config?.debug_logging;
|
2026-05-28 22:32:33 -05:00
|
|
|
|
const worker = config?.worker;
|
2026-06-06 15:51:56 -05:00
|
|
|
|
const seedDays = config?.seed_days ?? 44;
|
2026-07-06 14:23:53 -05:00
|
|
|
|
const maxDays = config?.sync_days_max ?? 45;
|
2026-05-28 22:06:15 -05:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader className="pb-4">
|
|
|
|
|
|
<CardTitle>Bank Sync (SimpleFIN)</CardTitle>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-1">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
Allow users to connect their own SimpleFIN Bridge account to sync read-only bank
|
|
|
|
|
|
transactions. Each user manages their own connection from the Data page — no bank
|
|
|
|
|
|
credentials are stored.
|
2026-05-28 22:06:15 -05:00
|
|
|
|
</p>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-5">
|
|
|
|
|
|
{/* Enable toggle */}
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium">Allow users to connect SimpleFIN</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
|
When enabled, users see a Bank Sync section on their Data page.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<Toggle checked={enabled} onChange={(v) => setEnabled(v)} label="Enable bank sync" />
|
2026-05-28 22:06:15 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
{/* Sync interval */}
|
|
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium">Auto-sync interval</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
|
How often the background worker checks for new transactions.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0.5"
|
|
|
|
|
|
max="168"
|
|
|
|
|
|
step="0.5"
|
|
|
|
|
|
value={syncInterval}
|
2026-07-06 14:23:53 -05:00
|
|
|
|
onChange={(e) => setSyncInterval(e.target.value)}
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
className="w-20 text-sm text-right"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">hrs</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-30 21:20:51 -05:00
|
|
|
|
{/* Sync window — two-mode explainer */}
|
|
|
|
|
|
<div className="space-y-3">
|
2026-05-29 01:33:54 -05:00
|
|
|
|
<div>
|
2026-05-30 21:20:51 -05:00
|
|
|
|
<p className="text-sm font-medium">Sync lookback windows</p>
|
2026-05-29 01:33:54 -05:00
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
2026-05-30 21:20:51 -05:00
|
|
|
|
SimpleFIN uses two different windows depending on sync type.
|
2026-05-29 01:33:54 -05:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-05-30 21:20:51 -05:00
|
|
|
|
|
|
|
|
|
|
{/* Initial / backfill — read-only */}
|
|
|
|
|
|
<div className="rounded-lg border border-border/50 bg-muted/20 px-4 py-3 space-y-1">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
|
Initial connect & backfill
|
|
|
|
|
|
</p>
|
2026-06-06 15:51:56 -05:00
|
|
|
|
<span className="font-mono text-sm font-bold">{seedDays} days</span>
|
2026-05-30 21:20:51 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
The first sync (and any manual backfill) fetches up to{' '}
|
|
|
|
|
|
<strong>{seedDays} days</strong> of history to build a complete transaction picture.
|
|
|
|
|
|
This is fixed — SimpleFIN Bridge enforces a strict{' '}
|
|
|
|
|
|
<strong>{maxDays}-day hard limit</strong>, so this stays one day under it to avoid
|
2026-06-06 15:51:56 -05:00
|
|
|
|
latency-related errors.
|
2026-05-30 21:20:51 -05:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Routine sync — editable */}
|
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<p className="text-sm font-medium">Routine sync lookback</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
|
How far back each auto-sync and manual "Sync Now" looks after the initial connect.
|
|
|
|
|
|
Recommended: <strong>7–30 days</strong>. Setting this near 45 increases request size
|
|
|
|
|
|
and duplicate-skip work with no benefit once history is established.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
2026-06-06 15:51:56 -05:00
|
|
|
|
max={maxDays}
|
2026-05-30 21:20:51 -05:00
|
|
|
|
step="1"
|
|
|
|
|
|
value={syncDays}
|
2026-07-06 14:23:53 -05:00
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setSyncDays(Math.min(maxDays, Math.max(1, parseInt(e.target.value, 10) || 30)))
|
|
|
|
|
|
}
|
2026-05-30 21:20:51 -05:00
|
|
|
|
className="w-20 text-sm text-right"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">days</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Amber warning at the SimpleFIN limit */}
|
2026-07-04 21:26:22 -05:00
|
|
|
|
{parseInt(String(syncDays), 10) >= maxDays && (
|
2026-05-30 21:20:51 -05:00
|
|
|
|
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/8 px-3 py-2.5">
|
|
|
|
|
|
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 shrink-0 mt-0.5" />
|
|
|
|
|
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
{maxDays} 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.
|
2026-05-30 21:20:51 -05:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Always-visible hard-limit note */}
|
|
|
|
|
|
<div className="flex items-start gap-2 text-xs text-muted-foreground">
|
|
|
|
|
|
<Info className="h-3.5 w-3.5 shrink-0 mt-0.5" />
|
|
|
|
|
|
<span>
|
2026-06-06 15:51:56 -05:00
|
|
|
|
SimpleFIN Bridge enforces a <strong>{maxDays}-day maximum</strong> on all requests.
|
|
|
|
|
|
Any value above {maxDays} will cause sync errors for all users.
|
2026-05-30 21:20:51 -05:00
|
|
|
|
</span>
|
2026-05-29 01:33:54 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-28 22:32:33 -05:00
|
|
|
|
{/* Auto-sync worker status */}
|
|
|
|
|
|
{config?.enabled && worker && (
|
|
|
|
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 px-4 py-3 space-y-2">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
|
|
|
|
|
Auto-sync worker
|
|
|
|
|
|
</p>
|
2026-05-28 22:32:33 -05:00
|
|
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">Status</p>
|
|
|
|
|
|
<p className="font-medium flex items-center gap-1.5">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<span
|
|
|
|
|
|
className={`h-1.5 w-1.5 rounded-full ${worker.running ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500'}`}
|
|
|
|
|
|
/>
|
2026-05-28 22:32:33 -05:00
|
|
|
|
{worker.running ? 'Running' : 'Idle'}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">Last run</p>
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<p className="font-medium">
|
|
|
|
|
|
{worker.last_run_at ? timeAgo(worker.last_run_at) : '—'}
|
|
|
|
|
|
</p>
|
2026-05-28 22:32:33 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">Next run</p>
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<p className="font-medium">
|
|
|
|
|
|
{worker.next_run_at ? `in ${timeUntil(worker.next_run_at)}` : '—'}
|
|
|
|
|
|
</p>
|
2026-05-28 22:32:33 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-06-07 19:41:17 -05:00
|
|
|
|
{/* Debug logging toggle */}
|
|
|
|
|
|
<div className="flex items-center justify-between border-t border-border/50 pt-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium">Debug logging</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
Logs each account, transaction, and auto-match step to the server console. Turn on to
|
|
|
|
|
|
diagnose sync issues, then off when done.
|
2026-06-07 19:41:17 -05:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Toggle
|
|
|
|
|
|
checked={debugLogging}
|
2026-07-06 14:23:53 -05:00
|
|
|
|
onChange={(v) => setDebugLogging(v)}
|
2026-06-07 19:41:17 -05:00
|
|
|
|
label="Enable debug logging"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
{/* Encryption note */}
|
|
|
|
|
|
<p className="text-xs text-muted-foreground border-t border-border/50 pt-3">
|
2026-07-06 14:23:53 -05:00
|
|
|
|
{config?.encryption_key_source === 'env' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
SimpleFIN credentials are encrypted at rest. Encryption key is loaded from{' '}
|
|
|
|
|
|
<code className="font-mono">TOKEN_ENCRYPTION_KEY</code> — stored separately from the
|
|
|
|
|
|
database, so a database backup alone cannot decrypt credentials.
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
SimpleFIN credentials are encrypted, but the key is stored in the same database as the
|
|
|
|
|
|
data. A database backup or file-level read includes everything needed to decrypt
|
|
|
|
|
|
credentials. Set <code className="font-mono">TOKEN_ENCRYPTION_KEY</code> in your
|
|
|
|
|
|
environment to keep the key separate.
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}{' '}
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
Regular database backups preserve all user connections.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
2026-07-05 19:15:32 -05:00
|
|
|
|
{config?.key_fingerprint && (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
Active key fingerprint{' '}
|
2026-07-06 14:23:53 -05:00
|
|
|
|
<code className="font-mono text-foreground">{config.key_fingerprint}</code> — a one-way
|
|
|
|
|
|
check of the current key (never the key itself). Keep your{' '}
|
|
|
|
|
|
<code className="font-mono">TOKEN_ENCRYPTION_KEY</code> in a password manager; this
|
|
|
|
|
|
fingerprint lets you confirm a running instance matches the key you saved, and spot an
|
|
|
|
|
|
accidental key change (which would make existing secrets unrecoverable).
|
2026-07-05 19:15:32 -05:00
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
feat: configurable sync interval, auto-match, encryption note, admin link, SimpleFIN hyperlink
#1 Sync interval in admin UI:
- bankSyncConfigService: reads simplefin_sync_interval_hours from settings
(DB-first, env fallback, default 4h), setSyncIntervalHours() with validation
- bankSyncWorker: live-updates interval from getBankSyncConfig() each tick
- routes/admin: PUT accepts enabled and sync_interval_hours independently
- BankSyncAdminCard: number input (0.5 step, 0.5-168 range), dirty-checks both
#3 Auto-match after background sync:
- matchSuggestionService: autoMatchForUser() auto-applies suggestions ≥80
score (exact amount + date ±1d + name signal), lazy-requires matchTransactionToBill
- bankSyncWorker: calls autoMatchForUser after each successful sync, own try/catch
#4 Encryption note in BankSyncAdminCard below worker status panel
Also: error handling, admin link in tracker sidebar, SimpleFIN bridge hyperlink
2026-05-29 00:28:50 -05:00
|
|
|
|
<div className="flex justify-end">
|
2026-05-28 22:18:20 -05:00
|
|
|
|
<Button onClick={handleSave} disabled={saving || !changed}>
|
2026-05-28 22:06:15 -05:00
|
|
|
|
{saving ? 'Saving…' : 'Save'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|