kundencenter/apps/api/src/modules/system/index.ts
2026-09-27 00:51:32 +02:00

35 lines
2.2 KiB
TypeScript

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() };
});
},
};