import type { FastifyInstance } from 'fastify'; import { one } from '../../core/db.js'; import { requirePermission } from '../../core/auth.js'; import { query } from '../../core/db.js'; import { readFile } from 'node:fs/promises'; import type { KcModule } from '../../core/module.js'; /** Backup-Zustand aus der Statusdatei des Backup-Laufs (siehe docs/betrieb-backup-restore.md). Ohne Datei: nicht eingerichtet. */ async function backupState() { const file = process.env.BACKUP_STATUS_FILE ?? '/var/lib/kundencenter/backup-status.json'; let st: any; try { st = JSON.parse(await readFile(file, 'utf8')); } catch { return { configured: false as const }; } const hours = (iso?: string) => (iso ? (Date.now() - new Date(iso).getTime()) / 3600000 : null); const run = st.lastRun; const test = st.lastRestoreTest; return { configured: true as const, lastRunAt: run?.at ?? null, ok: !!run?.ok, ageHours: hours(run?.at), stale: !run || !run.ok || (hours(run.at) ?? 999) > 26, error: run?.error ?? null, file: run?.file ?? null, sizeBytes: run?.sizeBytes ?? null, targets: (run?.targets ?? []).map((t: any) => ({ name: t.name, ok: t.ok })), lastRestoreTestAt: test?.at ?? null, restoreOk: test ? !!test.ok : null, restoreStale: !test || !test.ok || (hours(test.at) ?? 999) > 24 * 10, restoreError: test?.error ?? null, }; } export const systemModule: KcModule = { name: 'system', register(app: FastifyInstance) { app.get('/health', async () => ({ status: 'ok' })); // Liveness app.get('/ready', async (_req, reply) => { // Readiness: DB erreichbar try { await one('SELECT 1 AS ok'); return { status: 'ready' }; } catch { return reply.code(503).send({ status: 'db_unavailable' }); } }); app.get('/admin/system', async (req) => { requirePermission(req, 'jobs.read'); const jobs = await query('SELECT status, COUNT(*) AS n FROM jobs GROUP BY status'); const oldest = await one('SELECT MIN(run_at) AS t FROM jobs WHERE status IN (\'scheduled\',\'retrying\')'); return { jobs: Object.fromEntries(jobs.map((j) => [j.status, Number(j.n)])), oldestPendingJob: oldest?.t ?? null, serverTime: new Date().toISOString(), backup: await backupState() }; }); }, };