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'; import { Input } from '@/components/ui/input'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Toggle } from './adminShared'; function parseUtc(str) { if (!str) return null; const normalized = str.includes('T') ? str : str.replace(' ', 'T') + 'Z'; return new Date(normalized); } function timeAgo(iso) { if (!iso) return null; const secs = Math.floor((Date.now() - parseUtc(iso).getTime()) / 1000); if (secs < 60) return 'just now'; 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`; } function timeUntil(iso) { if (!iso) return null; const secs = Math.floor((parseUtc(iso).getTime() - Date.now()) / 1000); 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`; } export default function BankSyncAdminCard() { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [saving, setSaving] = useState(false); const [enabled, setEnabled] = useState(false); const [syncInterval, setSyncInterval] = useState(4); const [syncDays, setSyncDays] = useState(30); const [debugLogging, setDebugLogging] = useState(false); useEffect(() => { api.bankSyncConfig() .then(d => { setConfig(d); setEnabled(!!d.enabled); setSyncInterval(d.sync_interval_hours ?? 4); setSyncDays(d.sync_days ?? 30); setDebugLogging(!!d.debug_logging); }) .catch(err => setLoadError(err.message || 'Failed to load bank sync config')) .finally(() => setLoading(false)); }, []); const handleSave = async () => { const hours = parseFloat(syncInterval); const days = parseInt(syncDays, 10); const maxDays = config?.sync_days_max ?? 45; if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) { toast.error('Sync interval must be between 0.5 and 168 hours.'); return; } if (!Number.isFinite(days) || days < 1 || days > maxDays) { toast.error(`Routine sync lookback must be 1–${maxDays} days. SimpleFIN Bridge enforces a ${maxDays}-day hard limit — values above ${maxDays} return errors.`); return; } setSaving(true); try { const result = await api.setBankSyncConfig({ enabled, sync_interval_hours: hours, sync_days: days, debug_logging: debugLogging }); setConfig(result); setEnabled(!!result.enabled); setSyncInterval(result.sync_interval_hours ?? 4); setSyncDays(result.sync_days ?? 30); setDebugLogging(!!result.debug_logging); toast.success('Bank sync settings saved.'); } catch (err) { toast.error(err.message || 'Failed to update bank sync setting.'); } finally { setSaving(false); } }; if (loading) { return ( Loading… ); } if (loadError) { return ( {loadError} ); } const changed = enabled !== !!config?.enabled || parseFloat(syncInterval) !== config?.sync_interval_hours || parseInt(syncDays, 10) !== config?.sync_days || debugLogging !== !!config?.debug_logging; const worker = config?.worker; const seedDays = config?.seed_days ?? 44; const maxDays = config?.sync_days_max ?? 45; return ( Bank Sync (SimpleFIN)

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.

{/* Enable toggle */}

Allow users to connect SimpleFIN

When enabled, users see a Bank Sync section on their Data page.

setEnabled(v)} label="Enable bank sync" />
{/* Sync interval */}

Auto-sync interval

How often the background worker checks for new transactions.

setSyncInterval(e.target.value)} className="w-20 text-sm text-right" /> hrs
{/* Sync window — two-mode explainer */}

Sync lookback windows

SimpleFIN uses two different windows depending on sync type.

{/* Initial / backfill — read-only */}

Initial connect & backfill

{seedDays} days

The first sync (and any manual backfill) fetches up to {seedDays} days of history to build a complete transaction picture. This is fixed — SimpleFIN Bridge enforces a strict {maxDays}-day hard limit, so this stays one day under it to avoid latency-related errors.

{/* 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.

setSyncDays(Math.min(maxDays, Math.max(1, parseInt(e.target.value, 10) || 30)))} className="w-20 text-sm text-right" /> days
{/* Amber warning at the SimpleFIN limit */} {parseInt(syncDays, 10) >= maxDays && (

{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.

)} {/* Always-visible hard-limit note */}
SimpleFIN Bridge enforces a {maxDays}-day maximum on all requests. Any value above {maxDays} will cause sync errors for all users.
{/* Auto-sync worker status */} {config?.enabled && worker && (

Auto-sync worker

Status

{worker.running ? 'Running' : 'Idle'}

Last run

{worker.last_run_at ? timeAgo(worker.last_run_at) : '—'}

Next run

{worker.next_run_at ? `in ${timeUntil(worker.next_run_at)}` : '—'}

)} {/* Debug logging toggle */}

Debug logging

Logs each account, transaction, and auto-match step to the server console. Turn on to diagnose sync issues, then off when done.

setDebugLogging(v)} label="Enable debug logging" />
{/* Encryption note */}

{config?.encryption_key_source === 'env' ? <>SimpleFIN credentials are encrypted at rest. Encryption key is loaded from TOKEN_ENCRYPTION_KEY — 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 TOKEN_ENCRYPTION_KEY in your environment to keep the key separate. }{' '} Regular database backups preserve all user connections.

); }