BillTracker/services/statusRuntime.cts

157 lines
4.4 KiB
TypeScript

'use strict';
// import required so Node type-strips this .cts (it has no other import).
import type { Db } from '../types/db';
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',
};
interface WorkerState {
enabled: boolean;
running: boolean;
started_at: string | null;
last_run_at: string | null;
next_run_at: string | null;
last_error: string | null;
}
interface NotificationsState {
last_test_at: string | null;
last_error: string | null;
}
interface RuntimeError {
timestamp: string;
source: string;
message: string;
}
const state: {
worker: WorkerState;
notifications: NotificationsState;
recentErrors: RuntimeError[];
} = {
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(): void {
try {
const { getSetting } = require('../db/database.cts');
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: string, value: unknown): void {
try {
const { setSetting } = require('../db/database.cts');
setSetting(key, value == null ? '' : String(value));
} catch {
/* non-fatal */
}
}
function toMessage(error: unknown): string | null {
if (!error) return null;
if (typeof error === 'string') return error;
return (error as any).message || String(error);
}
function recordError(source: string, error: unknown): void {
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: string | null = null): void {
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: string | null = null): void {
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: unknown, nextRunAt: string | null = null): void {
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(): void {
state.notifications.last_test_at = new Date().toISOString();
state.notifications.last_error = null;
}
function markNotificationError(error: unknown): void {
state.notifications.last_error = toMessage(error);
recordError('Notifications', error);
}
function markNotificationSuccess(): void {
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,
};