296 lines
8.7 KiB
JavaScript
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;
|