From 22df64e5e7b35a46eeba6bfa8cfdb321c6bfc5ef Mon Sep 17 00:00:00 2001 From: null Date: Thu, 28 May 2026 22:32:33 -0500 Subject: [PATCH] feat: auto-sync worker for SimpleFIN bank sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New: services/bankSyncWorker.js — interval-based worker running every 4h (configurable via SIMPLEFIN_SYNC_INTERVAL_HOURS) - Checks bank sync enabled, fetches oldest-synced sources, skips <1h old - Staggers syncs 3s apart, writes last_error on failure, timer.unref() for clean shutdown Modified: server.js — starts worker inside app.listen callback routes/admin.js — GET bank-sync-config includes worker status (running, interval, last/next run) client/components/admin/BankSyncAdminCard.jsx — shows auto-sync worker status panel when enabled .env.example — SIMPLEFIN_SYNC_INTERVAL_HOURS --- .env.example | 3 + client/components/admin/BankSyncAdminCard.jsx | 46 +++++++ package.json | 2 +- routes/admin.js | 3 +- server.js | 7 + services/bankSyncWorker.js | 128 ++++++++++++++++++ 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 services/bankSyncWorker.js diff --git a/.env.example b/.env.example index 16db6c0..98c11c8 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,9 @@ NODE_ENV=production # # How many days back to fetch transactions on first sync (default: 90). # SIMPLEFIN_SYNC_DAYS=90 +# +# How often the background auto-sync worker runs (default: 4 hours, minimum: 0.5). +# SIMPLEFIN_SYNC_INTERVAL_HOURS=4 # ── First-run admin account ──────────────────────────────────────────────────── # Set BOTH on first start to create the admin account automatically. diff --git a/client/components/admin/BankSyncAdminCard.jsx b/client/components/admin/BankSyncAdminCard.jsx index 56c2fa4..fafd0be 100644 --- a/client/components/admin/BankSyncAdminCard.jsx +++ b/client/components/admin/BankSyncAdminCard.jsx @@ -5,6 +5,24 @@ import { Button } from '@/components/ui/button'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Toggle } from './adminShared'; +function timeAgo(iso) { + if (!iso) return null; + const secs = Math.floor((Date.now() - new Date(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((new Date(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); @@ -46,6 +64,7 @@ export default function BankSyncAdminCard() { } const changed = enabled !== !!config?.enabled; + const worker = config?.worker; return ( @@ -74,6 +93,33 @@ export default function BankSyncAdminCard() { /> + {/* 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)}` : '—'}

+
+
+

+ Syncs every {worker.interval_hours}h. Set SIMPLEFIN_SYNC_INTERVAL_HOURS to adjust. +

+
+ )} +