BillTracker/routes/status.js

296 lines
8.7 KiB
JavaScript

const express = require('express');
const router = express.Router();
const fs = require('fs');
const os = require('os');
const { getDb, getSetting } = require('../db/database');
const { getStatusRuntime, recordError } = require('../services/statusRuntime');
const { listBackups } = require('../services/backupService');
const { getScheduleStatus } = require('../services/backupScheduler');
const startTime = Date.now();
let pkg;
try { pkg = require('../package.json'); } catch { pkg = { name: 'bill-tracker', version: '1.0.0' }; }
function errorMessage(err) {
return err?.message || String(err);
}
function monthRange(now) {
const year = now.getFullYear();
const month = now.getMonth() + 1;
const daysInMonth = new Date(year, month, 0).getDate();
const monthStr = String(month).padStart(2, '0');
return {
year,
month,
start: `${year}-${monthStr}-01`,
end: `${year}-${monthStr}-${String(daysInMonth).padStart(2, '0')}`,
};
}
// GET /api/status
router.get('/', (req, res) => {
const runtimeState = getStatusRuntime();
const now = new Date();
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
let db;
let database = {
ok: false,
connected: false,
status: 'error',
size_bytes: 0,
size_mb: '0.00',
last_modified: null,
last_error: null,
};
let stats = {
active_sessions: null,
users: null,
active_bills: null,
total_payments: null,
};
let notifications = {
ok: true,
enabled: false,
configured: false,
smtp_configured: false,
last_test_at: runtimeState.notifications.last_test_at,
last_sent_at: null,
last_error: runtimeState.notifications.last_error,
};
let backups = {
ok: true,
enabled: false,
configured: false,
last_backup_at: null,
last_backup_path: null,
backup_count: null,
keep_count: null,
last_error: null,
};
let tracker = {
ok: true,
current_year: now.getFullYear(),
current_month: now.getMonth() + 1,
bill_count: null,
payment_count: null,
bills_this_month: null,
payments_this_month: null,
overdue_count: null,
skipped_count: null,
last_error: null,
};
try {
db = getDb();
db.prepare('SELECT 1 AS ok').get();
const dbPath = db.name;
let dbSizeBytes = 0;
let dbLastModified = null;
try {
const stat = fs.statSync(dbPath);
dbSizeBytes = stat.size;
dbLastModified = stat.mtime.toISOString();
} catch {
// The DB may be in-memory or on a volume that cannot be stat'ed.
}
database = {
ok: true,
connected: true,
status: 'connected',
// Filesystem path intentionally omitted — not safe to expose to all users
size_bytes: dbSizeBytes,
size_mb: (dbSizeBytes / 1024 / 1024).toFixed(2),
last_modified: dbLastModified,
last_error: null,
};
} catch (err) {
database.last_error = errorMessage(err);
recordError('Database', err);
}
if (db) {
try {
stats = {
active_sessions: db.prepare("SELECT COUNT(*) AS n FROM sessions WHERE expires_at > datetime('now')").get().n,
users: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
active_bills: db.prepare('SELECT COUNT(*) AS n FROM bills WHERE active = 1').get().n,
total_payments: db.prepare('SELECT COUNT(*) AS n FROM payments').get().n,
};
} catch (err) {
recordError('Status Statistics', err);
}
try {
const enabled = getSetting('notify_smtp_enabled') === 'true';
const host = getSetting('notify_smtp_host');
const sender = getSetting('notify_sender_address');
const port = getSetting('notify_smtp_port');
const username = getSetting('notify_smtp_username');
const password = getSetting('notify_smtp_password');
const configured = !!(host && sender && port && (!username || password));
const lastSent = db.prepare('SELECT MAX(sent_date) AS sent_date FROM notifications').get()?.sent_date || null;
notifications = {
...notifications,
ok: (!enabled || configured) && !notifications.last_error,
enabled,
configured,
smtp_configured: configured,
last_sent_at: lastSent,
};
} catch (err) {
notifications = {
...notifications,
ok: false,
last_error: errorMessage(err),
};
recordError('Notifications Status', err);
}
try {
const enabled = getSetting('backup_enabled') === 'true';
const keepCount = parseInt(getSetting('backup_keep_count') || '', 10);
const managedBackups = listBackups();
const latestBackup = managedBackups[0] || null;
const schedule = getScheduleStatus();
backups = {
ok: true,
enabled,
configured: true,
scheduled_enabled: schedule.enabled,
scheduled_frequency: schedule.frequency,
scheduled_time: schedule.time,
next_backup_at: schedule.next_run_at,
last_backup_at: latestBackup?.modified_at || null,
last_backup_path: latestBackup?.id || null,
last_backup_id: latestBackup?.id || null,
backup_count: managedBackups.length,
count: managedBackups.length,
keep_count: schedule.retention_count ?? (Number.isInteger(keepCount) ? keepCount : null),
retention_count: schedule.retention_count ?? (Number.isInteger(keepCount) ? keepCount : null),
last_error: schedule.last_error || null,
};
} catch (err) {
backups = {
...backups,
ok: false,
last_error: errorMessage(err),
};
recordError('Backups Status', err);
}
try {
const range = monthRange(now);
const billCount = db.prepare('SELECT COUNT(*) AS n FROM bills WHERE active = 1').get().n;
const paymentCount = db.prepare(
'SELECT COUNT(*) AS n FROM payments WHERE paid_date BETWEEN ? AND ? AND deleted_at IS NULL'
).get(range.start, range.end).n;
const skippedCount = db.prepare(
'SELECT COUNT(*) AS n FROM monthly_bill_state WHERE year = ? AND month = ? AND is_skipped = 1'
).get(range.year, range.month).n;
tracker = {
ok: true,
current_year: range.year,
current_month: range.month,
bill_count: billCount,
payment_count: paymentCount,
bills_this_month: billCount,
payments_this_month: paymentCount,
overdue_count: null,
skipped_count: skippedCount,
last_error: null,
};
} catch (err) {
tracker = {
...tracker,
ok: false,
last_error: errorMessage(err),
};
recordError('Tracker Status', err);
}
}
// Cleanup status — safe read-only summary from settings, no paths or secrets
let cleanup = { ok: true, last_run_at: null, last_result: null };
try {
const raw = getSetting('cleanup_last_result');
cleanup = {
ok: true,
last_run_at: getSetting('cleanup_last_run_at') || null,
last_result: raw ? JSON.parse(raw) : null,
};
} catch { /* non-fatal */ }
const memoryMb = Number((process.memoryUsage().rss / 1024 / 1024).toFixed(1));
const runtime = {
ok: true,
node: process.version,
node_version: process.version,
platform: process.platform,
arch: process.arch,
uptime_seconds: uptimeSeconds,
memory_mb: memoryMb,
};
const server = {
ok: true,
time: now.toISOString(),
now: now.toISOString(),
today: now.toISOString().slice(0, 10),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || process.env.TZ || null,
utc_offset: -now.getTimezoneOffset() / 60,
env: process.env.NODE_ENV || 'development',
};
const worker = {
ok: !runtimeState.worker.last_error,
enabled: runtimeState.worker.enabled,
running: runtimeState.worker.running,
started_at: runtimeState.worker.started_at,
last_run_at: runtimeState.worker.last_run_at,
last_success_at: runtimeState.worker.last_run_at,
next_run_at: runtimeState.worker.next_run_at,
last_error: runtimeState.worker.last_error,
};
const recentErrors = getStatusRuntime().recentErrors;
const ok = database.ok && tracker.ok;
res.json({
ok,
app: {
name: pkg.name,
version: pkg.version,
environment: server.env,
uptime_seconds: uptimeSeconds,
},
runtime,
database,
db: database,
stats,
statistics: stats,
worker,
notifications,
backups,
backup: backups,
server,
tracker,
cleanup,
recent_errors: recentErrors,
errors: recentErrors,
version: pkg.version,
environment: server.env,
uptime_seconds: uptimeSeconds,
node_version: process.version,
platform: process.platform,
hostname: os.hostname(),
});
});
module.exports = router;