import type { Posten } from "@/lib/benachrichtigung/posten"; import { ablaufStufe, fingerabdruck, type AnlassArt } from "@/lib/benachrichtigung/regeln"; import { prisma } from "@/lib/db"; import { tageBis } from "@/lib/format"; /** * Sammelt die meldbaren Anlaesse aus der Datenbank (#028). Der Abgleich selbst meldet nichts: * er schreibt Abweichungen und Erreichbarkeit, und diese Datei liest sie. So bleibt der * Abgleich-Ablauf unberuehrt, und ein verpasster Versand holt sich der naechste Lauf. */ const AUSGEROLLT = ["AUSGEROLLT", "ABWEICHUNG"] as const; async function drift(): Promise { const offen = await prisma.abweichung.findMany({ where: { behobenAm: null, art: { not: "HOSTKEY_GEAENDERT" }, host: { archiviertAm: null } }, select: { id: true, art: true, zielbenutzer: true, fingerprint: true, details: true, host: { select: { id: true, name: true, adresse: true } }, }, orderBy: { eroeffnetAm: "asc" }, }); return offen.map((a) => ({ art: "drift", anlass: fingerabdruck.drift(a.id), host: { hostId: a.host.id, name: a.host.name, adresse: a.host.adresse }, befund: { art: a.art, zielbenutzer: a.zielbenutzer, fingerprint: a.fingerprint, text: ((a.details ?? {}) as { text?: unknown }).text?.toString() ?? null, }, })); } async function hostKey(): Promise { const offen = await prisma.abweichung.findMany({ where: { behobenAm: null, art: "HOSTKEY_GEAENDERT", host: { archiviertAm: null } }, select: { id: true, fingerprint: true, details: true, host: { select: { id: true, name: true, adresse: true } }, }, orderBy: { eroeffnetAm: "asc" }, }); return offen.map((a) => { const details = (a.details ?? {}) as { erwartet?: unknown; algorithmus?: unknown }; return { art: "hostkey", anlass: fingerabdruck.hostkey(a.id), host: { hostId: a.host.id, name: a.host.name, adresse: a.host.adresse }, befund: { algorithmus: typeof details.algorithmus === "string" ? details.algorithmus : "unbekannt", erwartet: typeof details.erwartet === "string" ? details.erwartet : null, gesehen: a.fingerprint ?? "unbekannt", }, }; }); } async function unerreichbar(): Promise { const hosts = await prisma.host.findMany({ where: { archiviertAm: null, bootstrapAm: { not: null }, erreichbar: false, // Ein geaenderter Host-Key laesst den Abgleich ebenfalls scheitern; dafuer gibt es die // dringende Meldung (e) – hier waere sie nur ein irrefuehrendes Echo. abweichungen: { none: { art: "HOSTKEY_GEAENDERT", behobenAm: null } }, }, select: { id: true, name: true, adresse: true, zuletztGeprueftAm: true, letzterAbgleichAm: true }, }); return hosts.map((h) => ({ art: "unerreichbar", anlass: fingerabdruck.unerreichbar(h.id, h.letzterAbgleichAm), host: { hostId: h.id, name: h.name, adresse: h.adresse, zuletztGeprueftAm: h.zuletztGeprueftAm, letzterAbgleichAm: h.letzterAbgleichAm, }, })); } async function ablauf(jetzt: Date): Promise { const bis = new Date(jetzt.getTime() + 31 * 86_400_000); // Feste select-Liste: Private-Key-Felder kommen hier nie vorbei (CLAUDE.md). const schluessel = await prisma.schluessel.findMany({ where: { status: "AKTIV", widerrufEingeleitetAm: null, ablaufAm: { gt: jetzt, lte: bis } }, select: { id: true, name: true, fingerprint: true, ablaufAm: true }, }); const posten: Posten[] = []; for (const s of schluessel) { if (!s.ablaufAm) continue; const stufe = ablaufStufe(s.ablaufAm, jetzt); if (stufe === null) continue; posten.push({ art: "ablauf", anlass: fingerabdruck.ablauf(s.id, stufe, s.ablaufAm), schluessel: { schluesselId: s.id, name: s.name, fingerprint: s.fingerprint, ablaufAm: s.ablaufAm, tage: tageBis(s.ablaufAm, jetzt), stufe, }, }); } return posten; } async function abgelaufen(jetzt: Date): Promise { const schluessel = await prisma.schluessel.findMany({ where: { status: { in: ["AKTIV", "ABGELAUFEN"] }, ablaufAm: { lte: jetzt }, OR: [ { publicVerwendungen: { some: { zustand: { in: [...AUSGEROLLT] } } } }, { privateVerwendungen: { some: { zustand: { in: [...AUSGEROLLT] } } } }, ], }, select: { id: true, name: true, fingerprint: true, ablaufAm: true, publicVerwendungen: { where: { zustand: { in: [...AUSGEROLLT] } }, select: { host: { select: { name: true } }, hostgruppe: { select: { name: true } } }, }, privateVerwendungen: { where: { zustand: { in: [...AUSGEROLLT] } }, select: { host: { select: { name: true } } }, }, }, }); return schluessel.flatMap((s): Posten[] => { if (!s.ablaufAm) return []; const hosts = new Set(); for (const v of s.publicVerwendungen) { if (v.host) hosts.add(v.host.name); else if (v.hostgruppe) hosts.add(`Gruppe ${v.hostgruppe.name}`); } for (const v of s.privateVerwendungen) hosts.add(v.host.name); return [ { art: "abgelaufen", anlass: fingerabdruck.abgelaufen(s.id, s.ablaufAm), schluessel: { schluesselId: s.id, name: s.name, fingerprint: s.fingerprint, ablaufAm: s.ablaufAm, hosts: [...hosts].sort((a, b) => a.localeCompare(b, "de")), }, }, ]; }); } /** Alle Posten der eingeschalteten Anlaesse. */ export async function postenSammeln( arten: readonly AnlassArt[], jetzt: Date = new Date(), ): Promise { const sammler: Record Promise> = { drift, hostkey: hostKey, unerreichbar, ablauf: () => ablauf(jetzt), abgelaufen: () => abgelaufen(jetzt), }; const teile = await Promise.all(arten.map((art) => sammler[art]())); return teile.flat(); }