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;