perf(alerts): Warnungen sammelweise anlegen, Dark-Mode und Mobilansicht verbessern (#015)

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>
This commit is contained in:
2026-09-17 23:27:23 +02:00
co-authored by Claude Opus 5
parent 9647c19136
commit 4c65c9ddab
22 changed files with 354 additions and 63 deletions
+31 -14
View File
@@ -82,26 +82,27 @@ export async function alertsAbgleichen(jetzt: Date = new Date()): Promise<Abglei
const kontext = await kontextLaden(jetzt);
const kandidaten = alleRegelnPruefen(kontext);
const offene = await prisma.alert.findMany({ where: { erledigtAm: null } });
// 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));
for (const kandidat of neu) {
// Ein früher erledigter Fall taucht wieder auf: denselben Eintrag erneut öffnen.
await prisma.alert.upsert({
where: { fingerprint: kandidat.fingerprint },
update: {
titel: kandidat.titel,
beschreibung: kandidat.beschreibung,
schwere: kandidat.schwere,
erledigtAm: null,
quittiertAm: null,
geoeffnetAm: jetzt,
},
create: {
// 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,
@@ -111,6 +112,22 @@ export async function alertsAbgleichen(jetzt: Date = new Date()): Promise<Abglei
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,
},
});
}
+7 -3
View File
@@ -47,8 +47,12 @@ export async function hostsNachAblauf(jetzt: Date = new Date()) {
});
}
/** Offene Rechnungspunkte als Aufgabenliste fürs Dashboard. */
export async function offeneAufgaben() {
/**
* Offene Rechnungspunkte als Aufgabenliste fürs Dashboard.
* Begrenzt, weil die Karte sonst bei vielen Hosts endlos lang wird – die vollständige
* Liste steht unter „Rechnungen“.
*/
export async function offeneAufgaben(grenze = 12) {
const laufzeiten = await prisma.laufzeit.findMany({
where: {
status: "BESTAETIGT",
@@ -116,5 +120,5 @@ export async function offeneAufgaben() {
}
}
return aufgaben;
return { aufgaben: aufgaben.slice(0, grenze), gesamt: aufgaben.length };
}