BillTracker/services/statusRuntime.js

128 lines
3.7 KiB
JavaScript

'use strict';
const MAX_RECENT_ERRORS = 10;
// Keys written to the settings table for cross-restart persistence
const DB_KEY = {
workerLastRun: '_worker_last_run_at',
workerNextRun: '_worker_next_run_at',
workerError: '_worker_last_error',
workerStarted: '_worker_started_at',
};
const state = {
worker: {
enabled: false,
running: false,
started_at: null,
last_run_at: null,
next_run_at: null,
last_error: null,
},
notifications: {
last_test_at: null,
last_error: null,
},
recentErrors: [],
};
// Seed from DB on first load so status survives container restarts.
// Wrapped in try/catch — DB may not be ready at require-time in tests.
function seedFromDb() {
try {
const { getSetting } = require('../db/database');
state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null;
state.worker.next_run_at = getSetting(DB_KEY.workerNextRun) || null;
state.worker.last_error = getSetting(DB_KEY.workerError) || null;
state.worker.started_at = getSetting(DB_KEY.workerStarted) || null;
if (state.worker.last_run_at) state.worker.enabled = true;
} catch { /* DB not ready — will be populated when workers run */ }
}
seedFromDb();
function dbSet(key, value) {
try {
const { setSetting } = require('../db/database');
setSetting(key, value == null ? '' : String(value));
} catch { /* non-fatal */ }
}
function toMessage(error) {
if (!error) return null;
if (typeof error === 'string') return error;
return error.message || String(error);
}
function recordError(source, error) {
const message = toMessage(error);
if (!message) return;
state.recentErrors.unshift({ timestamp: new Date().toISOString(), source, message });
state.recentErrors = state.recentErrors.slice(0, MAX_RECENT_ERRORS);
}
function markWorkerStarted(nextRunAt = null) {
const now = new Date().toISOString();
state.worker.enabled = true;
state.worker.running = true;
state.worker.started_at = state.worker.started_at || now;
state.worker.next_run_at = nextRunAt;
dbSet(DB_KEY.workerStarted, state.worker.started_at);
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
}
function markWorkerSuccess(nextRunAt = null) {
const now = new Date().toISOString();
state.worker.running = false;
state.worker.last_run_at = now;
state.worker.last_error = null;
if (nextRunAt !== undefined) state.worker.next_run_at = nextRunAt;
dbSet(DB_KEY.workerLastRun, now);
dbSet(DB_KEY.workerError, '');
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
}
function markWorkerError(error, nextRunAt = null) {
const now = new Date().toISOString();
state.worker.running = false;
state.worker.last_run_at = now;
state.worker.last_error = toMessage(error);
if (nextRunAt !== undefined) state.worker.next_run_at = nextRunAt;
dbSet(DB_KEY.workerLastRun, now);
dbSet(DB_KEY.workerError, state.worker.last_error || '');
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
recordError('Daily Worker', error);
}
function markNotificationTestSuccess() {
state.notifications.last_test_at = new Date().toISOString();
state.notifications.last_error = null;
}
function markNotificationError(error) {
state.notifications.last_error = toMessage(error);
recordError('Notifications', error);
}
function markNotificationSuccess() {
state.notifications.last_error = null;
}
function getStatusRuntime() {
return {
worker: { ...state.worker },
notifications: { ...state.notifications },
recentErrors: [...state.recentErrors],
};
}
module.exports = {
getStatusRuntime,
markNotificationError,
markNotificationSuccess,
markNotificationTestSuccess,
markWorkerError,
markWorkerStarted,
markWorkerSuccess,
recordError,
};