Loading…
@@ -61,6 +65,14 @@ export default function AdminPage() {
);
}
+ if (loadError) {
+ return (
+
diff --git a/package.json b/package.json
index 64ef000..41e7540 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bill-tracker",
- "version": "0.31.0",
+ "version": "0.32.0",
"description": "Monthly bill tracking system",
"main": "server.js",
"scripts": {
diff --git a/routes/admin.js b/routes/admin.js
index 18267a3..be5cc71 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const { getDb, rollbackMigration } = require('../db/database');
-const { getBankSyncConfig, setBankSyncEnabled } = require('../services/bankSyncConfigService');
+const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours } = require('../services/bankSyncConfigService');
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker');
const { hashPassword } = require('../services/authService');
const { logAudit } = require('../services/auditService');
@@ -407,12 +407,16 @@ router.get('/bank-sync-config', (req, res) => {
// PUT /api/admin/bank-sync-config
router.put('/bank-sync-config', (req, res) => {
- const enabled = req.body?.enabled;
- if (typeof enabled !== 'boolean') {
- return res.status(400).json({ error: 'enabled must be a boolean' });
- }
+ const { enabled, sync_interval_hours } = req.body || {};
try {
- res.json(setBankSyncEnabled(enabled));
+ let config = getBankSyncConfig();
+ if (typeof enabled === 'boolean') {
+ config = setBankSyncEnabled(enabled);
+ }
+ if (sync_interval_hours !== undefined) {
+ config = setSyncIntervalHours(sync_interval_hours);
+ }
+ res.json(config);
} catch (err) {
res.status(err.status || 500).json({ error: err.message || 'Failed to update bank sync config' });
}
diff --git a/services/bankSyncConfigService.js b/services/bankSyncConfigService.js
index b4e4eb4..f96b632 100644
--- a/services/bankSyncConfigService.js
+++ b/services/bankSyncConfigService.js
@@ -2,7 +2,8 @@
const { getSetting, setSetting } = require('../db/database');
-const SYNC_DAYS_DEFAULT = 90;
+const SYNC_DAYS_DEFAULT = 90;
+const SYNC_INTERVAL_DEFAULT = 4; // hours
function getBankSyncConfig() {
const dbValue = getSetting('bank_sync_enabled');
@@ -25,9 +26,18 @@ function getBankSyncConfig() {
? syncDaysEnv
: SYNC_DAYS_DEFAULT;
+ const intervalDb = parseFloat(getSetting('simplefin_sync_interval_hours') || '');
+ const intervalEnv = parseFloat(process.env.SIMPLEFIN_SYNC_INTERVAL_HOURS || '');
+ const syncIntervalHours = Number.isFinite(intervalDb) && intervalDb >= 0.5
+ ? intervalDb
+ : Number.isFinite(intervalEnv) && intervalEnv >= 0.5
+ ? intervalEnv
+ : SYNC_INTERVAL_DEFAULT;
+
return {
enabled,
sync_days: syncDays,
+ sync_interval_hours: syncIntervalHours,
};
}
@@ -36,4 +46,13 @@ function setBankSyncEnabled(enabled) {
return getBankSyncConfig();
}
-module.exports = { getBankSyncConfig, setBankSyncEnabled };
+function setSyncIntervalHours(hours) {
+ const n = parseFloat(hours);
+ if (!Number.isFinite(n) || n < 0.5 || n > 168) {
+ throw Object.assign(new Error('sync_interval_hours must be between 0.5 and 168'), { status: 400 });
+ }
+ setSetting('simplefin_sync_interval_hours', String(Math.round(n * 10) / 10));
+ return getBankSyncConfig();
+}
+
+module.exports = { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours };
diff --git a/services/bankSyncWorker.js b/services/bankSyncWorker.js
index e530245..9d73e92 100644
--- a/services/bankSyncWorker.js
+++ b/services/bankSyncWorker.js
@@ -3,8 +3,8 @@
const { getDb } = require('../db/database');
const { getBankSyncConfig } = require('./bankSyncConfigService');
const { syncDataSource } = require('./bankSyncService');
+const { autoMatchForUser } = require('./matchSuggestionService');
-const DEFAULT_INTERVAL_HOURS = 4;
// Skip a source if it was synced less than this long ago (catches recent manual syncs)
const MIN_SYNC_AGE_MS = 60 * 60 * 1000; // 1 hour
// Pause between each source to avoid hammering SimpleFIN
@@ -16,10 +16,8 @@ let lastRunAt = null;
let nextRunAt = null;
function intervalMs() {
- const hours = parseFloat(process.env.SIMPLEFIN_SYNC_INTERVAL_HOURS);
- return Number.isFinite(hours) && hours >= 0.5
- ? Math.round(hours * 3600000)
- : DEFAULT_INTERVAL_HOURS * 3600000;
+ const { sync_interval_hours } = getBankSyncConfig();
+ return Math.round(sync_interval_hours * 3600000);
}
function needsSync(source) {
@@ -71,6 +69,7 @@ async function runCycle() {
try {
await syncDataSource(db, source.user_id, source.id);
synced++;
+ try { autoMatchForUser(source.user_id); } catch { /* non-fatal */ }
} catch {
// syncDataSource already writes last_error to the data_sources row
failed++;
@@ -107,8 +106,7 @@ function scheduleNext() {
function start() {
if (timer) return;
scheduleNext();
- const hours = intervalMs() / 3600000;
- console.log(`[bankSync] Auto-sync worker started (interval: ${hours}h)`);
+ console.log(`[bankSync] Auto-sync worker started (interval: ${getBankSyncConfig().sync_interval_hours}h)`);
}
function stop() {
@@ -119,7 +117,7 @@ function stop() {
function getStatus() {
return {
running,
- interval_hours: intervalMs() / 3600000,
+ interval_hours: getBankSyncConfig().sync_interval_hours,
last_run_at: lastRunAt,
next_run_at: nextRunAt,
};
diff --git a/services/matchSuggestionService.js b/services/matchSuggestionService.js
index d2c056d..e80fa67 100644
--- a/services/matchSuggestionService.js
+++ b/services/matchSuggestionService.js
@@ -326,7 +326,28 @@ function rejectMatchSuggestion(userId, id) {
};
}
+// Auto-apply high-confidence suggestions (score >= 80) for a user.
+// Called by the background sync worker after each successful source sync.
+// Score of 80+ requires at minimum exact amount + date proximity + name signal,
+// so false-positive risk is low.
+function autoMatchForUser(userId) {
+ const { matchTransactionToBill } = require('./transactionMatchService');
+ const suggestions = listMatchSuggestions(userId, { limit: 50 });
+ let matched = 0;
+ for (const s of suggestions) {
+ if (s.score < 80) break; // sorted descending — safe to stop early
+ try {
+ matchTransactionToBill(userId, s.transactionId, s.billId);
+ matched++;
+ } catch {
+ // Already matched, ignored, bill deleted, or date missing — skip silently
+ }
+ }
+ return matched;
+}
+
module.exports = {
+ autoMatchForUser,
listMatchSuggestions,
parseSuggestionId,
rejectMatchSuggestion,