Lasttest mit 200 Hosts und 365.000 Reports: Auswertung der Warnregeln von 3813 auf 293 ms. Logo im dunklen Design sichtbar gemacht, Tabellen blenden auf schmalen Bildschirmen Spalten aus, Aufgaben-Karte begrenzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
171 lines
6.3 KiB
TypeScript
171 lines
6.3 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { alleRegelnPruefen, REGELN, type AlertKandidat, type RegelKontext } from "./regeln";
|
|
|
|
/**
|
|
* Lädt den Zustand, wendet die Regeln an und gleicht das Ergebnis mit den offenen Warnungen ab:
|
|
* neue öffnen, verschwundene schließen, bestehende unverändert lassen.
|
|
*/
|
|
|
|
export async function kontextLaden(jetzt: Date = new Date()): Promise<RegelKontext> {
|
|
const [hosts, laufzeiten, einstellungen] = await Promise.all([
|
|
prisma.host.findMany({
|
|
include: {
|
|
kunde: { select: { name: true } },
|
|
serverIdHistorie: { select: { serverId: true, ersteMeldung: true, letzteMeldung: true } },
|
|
},
|
|
}),
|
|
prisma.laufzeit.findMany({
|
|
where: { status: "BESTAETIGT" },
|
|
include: {
|
|
host: { select: { hostname: true, kundeId: true, kunde: { select: { name: true } } } },
|
|
proxmoxPosition: { include: { rechnung: true } },
|
|
kundenPosition: { include: { rechnung: true } },
|
|
},
|
|
}),
|
|
prisma.warnregelEinstellung.findMany(),
|
|
]);
|
|
|
|
return {
|
|
jetzt,
|
|
hosts: hosts.map((host) => ({
|
|
id: host.id,
|
|
hostname: host.hostname,
|
|
kundeId: host.kundeId,
|
|
kundeName: host.kunde?.name ?? null,
|
|
subKey: host.subKey,
|
|
subStatus: host.subStatus,
|
|
subNextduedate: host.subNextduedate,
|
|
subLizenzierteSockets: host.subLizenzierteSockets,
|
|
sockets: host.sockets,
|
|
letzterReportAm: host.letzterReportAm,
|
|
serverIds: host.serverIdHistorie,
|
|
})),
|
|
laufzeiten: laufzeiten.map((laufzeit) => ({
|
|
id: laufzeit.id,
|
|
hostId: laufzeit.hostId,
|
|
hostname: laufzeit.host.hostname,
|
|
kundeId: laufzeit.host.kundeId,
|
|
kundeName: laufzeit.host.kunde?.name ?? null,
|
|
key: laufzeit.key,
|
|
start: laufzeit.start,
|
|
ende: laufzeit.ende,
|
|
einkaufNummer: laufzeit.proxmoxPosition?.rechnung.nummer ?? null,
|
|
einkaufBetrag: laufzeit.proxmoxPosition ? Number(laufzeit.proxmoxPosition.nettoBetrag) : null,
|
|
einkaufBezahlt: laufzeit.proxmoxPosition ? laufzeit.proxmoxPosition.rechnung.bezahlt : null,
|
|
einkaufFaelligAm: laufzeit.proxmoxPosition?.rechnung.faelligAm ?? null,
|
|
verkaufNummer: laufzeit.kundenPosition?.rechnung.nummer ?? null,
|
|
verkaufBetrag: laufzeit.kundenPosition ? Number(laufzeit.kundenPosition.nettoBetrag) : null,
|
|
verkaufErstellt: laufzeit.kundenPosition?.rechnung.erstellt ?? false,
|
|
verkaufBezahlt: laufzeit.kundenPosition?.rechnung.bezahlt ?? false,
|
|
verkaufDatum: laufzeit.kundenPosition?.rechnung.datum ?? null,
|
|
})),
|
|
einstellungen: new Map(
|
|
einstellungen.map((eintrag) => [
|
|
eintrag.regel,
|
|
{
|
|
aktiv: eintrag.aktiv,
|
|
schwellwerte: (eintrag.schwellwerte ?? {}) as Record<string, unknown>,
|
|
},
|
|
]),
|
|
),
|
|
};
|
|
}
|
|
|
|
export interface AbgleichErgebnis {
|
|
geoeffnet: AlertKandidat[];
|
|
erledigt: string[];
|
|
unveraendert: number;
|
|
}
|
|
|
|
/** Vergleicht die Kandidaten mit den offenen Warnungen und schreibt die Unterschiede fort. */
|
|
export async function alertsAbgleichen(jetzt: Date = new Date()): Promise<AbgleichErgebnis> {
|
|
const kontext = await kontextLaden(jetzt);
|
|
const kandidaten = alleRegelnPruefen(kontext);
|
|
|
|
// Alle betroffenen Warnungen in einem Zug laden: die offenen und die, deren Fall wieder auftritt.
|
|
const fingerprints = kandidaten.map((k) => k.fingerprint);
|
|
const [offene, bekannte] = await Promise.all([
|
|
prisma.alert.findMany({ where: { erledigtAm: null } }),
|
|
fingerprints.length > 0
|
|
? prisma.alert.findMany({ where: { fingerprint: { in: fingerprints } } })
|
|
: Promise.resolve([]),
|
|
]);
|
|
|
|
const offeneNachFingerprint = new Map(offene.map((alert) => [alert.fingerprint, alert]));
|
|
const bekannteNachFingerprint = new Map(bekannte.map((alert) => [alert.fingerprint, alert]));
|
|
const kandidatenNachFingerprint = new Map(kandidaten.map((k) => [k.fingerprint, k]));
|
|
|
|
const neu = kandidaten.filter((k) => !offeneNachFingerprint.has(k.fingerprint));
|
|
const verschwunden = offene.filter((alert) => !kandidatenNachFingerprint.has(alert.fingerprint));
|
|
|
|
// Fälle, die es noch nie gab: in einem Rutsch anlegen.
|
|
const wirklichNeu = neu.filter((k) => !bekannteNachFingerprint.has(k.fingerprint));
|
|
if (wirklichNeu.length > 0) {
|
|
await prisma.alert.createMany({
|
|
data: wirklichNeu.map((kandidat) => ({
|
|
fingerprint: kandidat.fingerprint,
|
|
regel: kandidat.regel,
|
|
schwere: kandidat.schwere,
|
|
titel: kandidat.titel,
|
|
beschreibung: kandidat.beschreibung,
|
|
hostId: kandidat.hostId,
|
|
kundeId: kandidat.kundeId,
|
|
laufzeitId: kandidat.laufzeitId,
|
|
geoeffnetAm: jetzt,
|
|
})),
|
|
skipDuplicates: true,
|
|
});
|
|
}
|
|
|
|
// Früher erledigte Fälle, die wieder auftreten: denselben Eintrag erneut öffnen.
|
|
for (const kandidat of neu.filter((k) => bekannteNachFingerprint.has(k.fingerprint))) {
|
|
await prisma.alert.update({
|
|
where: { fingerprint: kandidat.fingerprint },
|
|
data: {
|
|
titel: kandidat.titel,
|
|
beschreibung: kandidat.beschreibung,
|
|
schwere: kandidat.schwere,
|
|
erledigtAm: null,
|
|
quittiertAm: null,
|
|
geoeffnetAm: jetzt,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Beschreibungen bestehender Warnungen nachführen (z. B. „läuft in 9 Tagen ab“).
|
|
for (const alert of offene) {
|
|
const kandidat = kandidatenNachFingerprint.get(alert.fingerprint);
|
|
if (!kandidat) continue;
|
|
if (kandidat.titel !== alert.titel || kandidat.beschreibung !== alert.beschreibung) {
|
|
await prisma.alert.update({
|
|
where: { id: alert.id },
|
|
data: { titel: kandidat.titel, beschreibung: kandidat.beschreibung, schwere: kandidat.schwere },
|
|
});
|
|
}
|
|
}
|
|
|
|
if (verschwunden.length > 0) {
|
|
await prisma.alert.updateMany({
|
|
where: { id: { in: verschwunden.map((alert) => alert.id) } },
|
|
data: { erledigtAm: jetzt },
|
|
});
|
|
}
|
|
|
|
return {
|
|
geoeffnet: neu,
|
|
erledigt: verschwunden.map((alert) => alert.fingerprint),
|
|
unveraendert: kandidaten.length - neu.length,
|
|
};
|
|
}
|
|
|
|
/** Legt fehlende Regel-Einstellungen mit ihren Standardwerten an (Seed und Aktualisierungen). */
|
|
export async function warnregelnSicherstellen() {
|
|
for (const regel of REGELN) {
|
|
await prisma.warnregelEinstellung.upsert({
|
|
where: { regel: regel.name },
|
|
update: {},
|
|
create: { regel: regel.name, aktiv: true, schwellwerte: regel.standardSchwellwerte },
|
|
});
|
|
}
|
|
}
|