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
+166
View File
@@ -0,0 +1,166 @@
import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@/generated/prisma/client";
import { alertsAbgleichen, warnregelnSicherstellen } from "@/server/alerts/abgleich";
import { hostsNachAblauf, offeneAufgaben } from "@/server/queries/dashboard";
import { hostListe } from "@/server/queries/hostliste";
import { kennzahlen, suchIndex } from "@/server/queries/hosts";
import { kundenListe, offeneRechnungspunkteProKunde } from "@/server/queries/kunden";
import { rechnungsListe } from "@/server/queries/rechnungen";
/**
* Lasttest: legt eine große Datenmenge an und misst die Abfragen der wichtigsten Seiten.
* Läuft gegen die Testdatenbank (`TEST_DATABASE_URL`), damit die Entwicklungsdaten unberührt bleiben.
*
* npm run lasttest
*/
const HOSTS = Number(process.env.LAST_HOSTS ?? 200);
const KUNDEN = Number(process.env.LAST_KUNDEN ?? 25);
const JAHRE = Number(process.env.LAST_JAHRE ?? 5);
const REPORTS_PRO_HOST = JAHRE * 365;
const url = process.env.TEST_DATABASE_URL ?? process.env.DATABASE_URL;
if (!url) throw new Error("TEST_DATABASE_URL oder DATABASE_URL setzen.");
process.env.DATABASE_URL = url;
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: url }) });
async function messen<T>(name: string, arbeit: () => Promise<T>): Promise<T> {
const start = performance.now();
const ergebnis = await arbeit();
const dauer = performance.now() - start;
const anzahl = Array.isArray(ergebnis) ? ` (${ergebnis.length} Einträge)` : "";
console.log(` ${name.padEnd(38)} ${dauer.toFixed(0).padStart(6)} ms${anzahl}`);
return ergebnis;
}
async function datenAnlegen() {
console.log(`Lege Testdaten an: ${KUNDEN} Kunden, ${HOSTS} Hosts, je ${REPORTS_PRO_HOST} Reports …`);
const start = performance.now();
await prisma.$executeRawUnsafe(`
TRUNCATE TABLE "Alert","Benachrichtigung","Report","Laufzeit","ProxmoxRechnungPosition",
"ProxmoxRechnung","KundenrechnungPosition","Kundenrechnung","ServerIdHistorie",
"SubscriptionKeyHistorie","AgentToken","Host","Standort","Kunde" RESTART IDENTITY CASCADE
`);
for (let k = 0; k < KUNDEN; k++) {
await prisma.kunde.create({
data: { id: `k${k}`, name: `Kunde ${k} GmbH`, kundennummer: `K-${9000 + k}` },
});
}
const heute = new Date();
for (let h = 0; h < HOSTS; h++) {
const ablauf = new Date(heute.getTime() + ((h % 400) - 30) * 86_400_000);
await prisma.host.create({
data: {
id: `h${h}`,
agentId: `last-${h}`,
hostname: `pve-last-${String(h).padStart(3, "0")}`,
produkt: h % 7 === 0 ? "PBS" : "PVE",
kundeId: h % 20 === 0 ? null : `k${h % KUNDEN}`,
subKey: `pve1c-${String(h).padStart(10, "0")}`,
subStatus: h % 11 === 0 ? "EXPIRED" : "ACTIVE",
subNextduedate: ablauf,
subLizenzierteSockets: 1,
sockets: h % 13 === 0 ? 2 : 1,
ips: [`10.0.${Math.floor(h / 254)}.${h % 254}`],
letzterReportAm: new Date(heute.getTime() - (h % 5) * 86_400_000),
},
});
// Reports in Blöcken, sonst dauert es unnötig lange.
const reports = Array.from({ length: REPORTS_PRO_HOST }, (_, i) => ({
reportId: `last-${h}-${i}`,
hostId: `h${h}`,
erhobenAm: new Date(heute.getTime() - i * 86_400_000),
subStatus: "ACTIVE" as const,
subKey: `pve1c-${String(h).padStart(10, "0")}`,
rohdaten: {},
}));
for (let i = 0; i < reports.length; i += 2000) {
await prisma.report.createMany({ data: reports.slice(i, i + 2000) });
}
// Fünf Laufzeiten je Host, jede zweite mit Rechnungen.
for (let j = 0; j < JAHRE; j++) {
const beginn = new Date(Date.UTC(heute.getUTCFullYear() - j, h % 12, ((h + j) % 27) + 1));
const laufzeit = await prisma.laufzeit.create({
data: {
hostId: `h${h}`,
key: `pve1c-${String(h).padStart(10, "0")}`,
start: beginn,
ende: new Date(Date.UTC(beginn.getUTCFullYear() + 1, beginn.getUTCMonth(), beginn.getUTCDate() - 1)),
status: "BESTAETIGT",
},
});
if ((h + j) % 2 === 0) {
const rechnung = await prisma.proxmoxRechnung.create({
data: {
nummer: `PX-LAST-${h}-${j}`,
datum: beginn,
faelligAm: new Date(beginn.getTime() + 14 * 86_400_000),
nettoBetrag: 115,
bezahlt: j > 0,
positionen: { create: [{ bezeichnung: "Laufzeit", nettoBetrag: 115 }] },
},
include: { positionen: true },
});
await prisma.laufzeit.update({
where: { id: laufzeit.id },
data: { proxmoxPositionId: rechnung.positionen[0].id },
});
}
}
}
console.log(`Daten angelegt in ${((performance.now() - start) / 1000).toFixed(1)} s\n`);
}
async function main() {
await datenAnlegen();
await warnregelnSicherstellen();
const [hosts, reports, laufzeiten] = await Promise.all([
prisma.host.count(),
prisma.report.count(),
prisma.laufzeit.count(),
]);
console.log(`Bestand: ${hosts} Hosts, ${reports} Reports, ${laufzeiten} Laufzeiten\n`);
console.log("Dashboard:");
await messen("Kennzahlen", () => kennzahlen());
await messen("Hosts nach Ablauf", () => hostsNachAblauf());
await messen("Offene Aufgaben", () => offeneAufgaben());
await messen("Kundenliste", () => kundenListe());
await messen("Rechnungspunkte je Kunde", () => offeneRechnungspunkteProKunde());
console.log("\nHost-Tabelle:");
await messen("Erste Seite", () => hostListe({}));
await messen("Suche nach Key", () => hostListe({ q: "pve1c-0000000100" }));
await messen("Filter: bald ablaufend", () => hostListe({ ablauf: "30" }));
await messen("Filter: offene Rechnungen", () => hostListe({ rechnung: "offen" }));
await messen("Sortierung nach Ablauf", () => hostListe({ sortierung: "ablauf" }));
console.log("\nWeitere Seiten:");
await messen("Rechnungsliste (offen)", () => rechnungsListe({ status: "offen" }));
await messen("Suchindex (Strg+K)", () => suchIndex());
await messen("Warnregeln auswerten", () => alertsAbgleichen());
const offeneAlerts = await prisma.alert.count({ where: { erledigtAm: null } });
console.log(`\nOffene Warnungen nach der Auswertung: ${offeneAlerts}`);
}
main()
.then(async () => {
await prisma.$disconnect();
process.exit(0);
})
.catch(async (fehler) => {
console.error(fehler);
await prisma.$disconnect();
process.exit(1);
});