Merge pull request 'KPIs: keine Graphen mehr in Kachel und Detail-Modal' (#106) from fds2610-bot/monitor:main into main

Reviewed-on: https://codeberg.org/fds2610/monitor/pulls/106
This commit is contained in:
fds2610 2026-10-01 11:30:42 +02:00
commit ca6772f8c0
5 changed files with 4 additions and 100 deletions

View file

@ -244,7 +244,7 @@ externe Datenbankserver (nur PHP/Python-Standardbibliothek) auskommen, um die
5. **Phase 4 – Monitor-Wand live**: Event-, System- und KPI-Monitor zeigen echte Daten, 5. **Phase 4 – Monitor-Wand live**: Event-, System- und KPI-Monitor zeigen echte Daten,
Klick-Modals. ✅ erledigt: Events/Systeme/KPIs kommen live von der API, ein neuer Klick-Modals. ✅ erledigt: Events/Systeme/KPIs kommen live von der API, ein neuer
SSE-Endpoint (`GET /api/v1/stream`, F13) stoesst Frontend-Updates an (Polling-Fallback in SSE-Endpoint (`GET /api/v1/stream`, F13) stoesst Frontend-Updates an (Polling-Fallback in
`office-realtime.js`), KPI-Monitor zeichnet eine eigene SVG-Sparkline (F10, kein Chart-Lib) `office-realtime.js`), KPI-Monitor zeichnet nur Zahlenwerte (keine Graphen)
sobald KPIs gemeldet wurden. "Agenten" (registrierte Agenten) und "Online" (davon sobald KPIs gemeldet wurden. "Agenten" (registrierte Agenten) und "Online" (davon
praesent, isAgentPresent()) sind als erste zwei KPI-Kacheln immer sichtbar, direkt aus der praesent, isAgentPresent()) sind als erste zwei KPI-Kacheln immer sichtbar, direkt aus der
Agentenliste abgeleitet statt gemeldet (Issue #39). Ganz rechts kommt als vierte, ebenfalls Agentenliste abgeleitet statt gemeldet (Issue #39). Ganz rechts kommt als vierte, ebenfalls

View file

@ -672,7 +672,7 @@ header.office-header h1 {
text-anchor: middle; text-anchor: middle;
} }
/* KPI-Sparklines (Phase 4, F10: kein Chart-Lib, eigenes SVG-Zeichnen) */ /* KPI-Kacheln */
.kpi-tile-label { .kpi-tile-label {
fill: var(--color-text-inverse); fill: var(--color-text-inverse);
@ -690,18 +690,6 @@ header.office-header h1 {
fill: #facc15; fill: #facc15;
} }
.kpi-sparkline-bg {
fill: rgba(255, 255, 255, 0.04);
}
.kpi-sparkline-line {
fill: none;
stroke: #7dd3fc;
stroke-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
}
/* Modal (Phase 3/4: Board-/Monitor-Detailansicht, Visualization.md 4.5) */ /* Modal (Phase 3/4: Board-/Monitor-Detailansicht, Visualization.md 4.5) */
.office-modal { .office-modal {
@ -815,14 +803,6 @@ header.office-header h1 {
font-style: italic; font-style: italic;
} }
.kpi-modal-sparkline {
width: 100%;
height: 80px;
margin-top: 8px;
background: #10131a;
border-radius: 6px;
}
/* Projekte-Modal (Issue #27: Klick auf den PO -> Projekte des Codeberg-Sync verwalten) */ /* Projekte-Modal (Issue #27: Klick auf den PO -> Projekte des Codeberg-Sync verwalten) */
.office-modal-error { .office-modal-error {

View file

@ -520,15 +520,6 @@ function renderSystemMonitor(frame, systems, onConfigClick) {
return g; return g;
} }
function renderSparkline(points, width, height, x, y) {
const g = el("g", { class: "kpi-sparkline", transform: `translate(${x}, ${y})` });
g.appendChild(el("rect", { x: 0, y: 0, width, height, class: "kpi-sparkline-bg" }));
if (points) {
g.appendChild(el("polyline", { points, class: "kpi-sparkline-line" }));
}
return g;
}
function renderKpiMonitor(frame, kpis) { function renderKpiMonitor(frame, kpis) {
const tiles = buildKpiTiles(kpis); const tiles = buildKpiTiles(kpis);
const g = el("g", { class: "monitor monitor-kpi" }); const g = el("g", { class: "monitor monitor-kpi" });
@ -549,15 +540,7 @@ function renderKpiMonitor(frame, kpis) {
return g; return g;
} }
// Die Zeilen stehen direkt untereinander, statt die Monitorhoehe gleichmaessig unter den
// Kacheln aufzuteilen: Zaehler-Kacheln wie "Agenten"/"Online" (Issue #39) und "Hooks"
// (Issue #38) haben history: [] und damit keine Sparkline - verteilt man die Hoehe trotzdem,
// klaffen zwischen den Werten grosse leere Sparkline-Kaesten. Ein Verlauf bekommt seinen
// Platz deshalb nur, wenn es ihn wirklich gibt.
const LABEL_HEIGHT = 20; const LABEL_HEIGHT = 20;
// Passt zur Punkteberechnung in buildKpiSparklinePoints() (Default-Hoehe 32) - so fuellt die
// Linie den Kasten genau aus.
const SPARKLINE_HEIGHT = 32;
const ROW_GAP = 4; const ROW_GAP = 4;
let rowTop = frame.y + 20; let rowTop = frame.y + 20;
@ -573,11 +556,6 @@ function renderKpiMonitor(frame, kpis) {
); );
rowTop += LABEL_HEIGHT; rowTop += LABEL_HEIGHT;
if (tile.sparklinePoints) {
g.appendChild(renderSparkline(tile.sparklinePoints, frame.w - 32, SPARKLINE_HEIGHT, frame.x + 16, rowTop));
rowTop += SPARKLINE_HEIGHT;
}
rowTop += ROW_GAP; rowTop += ROW_GAP;
}); });
@ -1561,12 +1539,6 @@ function renderKpiModalBody(body, content) {
label.textContent = `${item.label}: ${item.currentValue ?? "–"} ${item.unit}`; label.textContent = `${item.label}: ${item.currentValue ?? "–"} ${item.unit}`;
li.appendChild(label); li.appendChild(label);
const svg = document.createElementNS(SVG_NS, "svg");
svg.setAttribute("viewBox", "0 0 320 80");
svg.setAttribute("class", "kpi-modal-sparkline");
svg.appendChild(el("polyline", { points: item.sparklinePoints, class: "kpi-sparkline-line" }));
li.appendChild(svg);
list.appendChild(li); list.appendChild(li);
} }
body.appendChild(list); body.appendChild(list);

View file

@ -458,43 +458,10 @@ export function formatSystemCheckTime(value) {
}); });
} }
/**
* Erzeugt die Punkte-Liste (SVG <polyline points="...">) fuer eine einfache
* KPI-Sparkline - bewusst ohne Chart-Lib gezeichnet (F10). Die API liefert die Historie
* absteigend (neuester Wert zuerst, siehe KpiRepository::history()), fuer die Sparkline
* wird chronologisch (aeltester zuerst) von links nach rechts gezeichnet.
*
* @param {Array<{value: number}>} history
* @param {number} [width]
* @param {number} [height]
* @param {number} [padding]
* @returns {string}
*/
export function buildKpiSparklinePoints(history = [], width = 140, height = 32, padding = 4) {
if (history.length === 0) {
return "";
}
const chronological = [...history].reverse();
const values = chronological.map((point) => point.value);
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const stepX = chronological.length > 1 ? (width - padding * 2) / (chronological.length - 1) : 0;
return chronological
.map((point, index) => {
const x = padding + index * stepX;
const y = height - padding - ((point.value - min) / range) * (height - padding * 2);
return `${x.toFixed(1)},${y.toFixed(1)}`;
})
.join(" ");
}
// Fuenf statt vier Kacheln, seit "Problem" (abgelaufene Agenten-Anmeldung) hinzugekommen ist: // Fuenf statt vier Kacheln, seit "Problem" (abgelaufene Agenten-Anmeldung) hinzugekommen ist:
// renderOfficeScene() reserviert bereits die letzte Kachel fuer "Hooks" (Issue #38), womit // renderOfficeScene() reserviert bereits die letzte Kachel fuer "Hooks" (Issue #38), womit
// "Agenten"/"Online"/"Problem" die uebrigen drei fuellen wuerden - fuer die gemeldeten KPIs (F7) // "Agenten"/"Online"/"Problem" die uebrigen drei fuellen wuerden - fuer die gemeldeten KPIs (F7)
// bliebe dann kein Platz mehr. Der KPI-Monitor ist hoch genug: eine Zeile ohne Sparkline braucht // bliebe dann kein Platz mehr. Der KPI-Monitor ist hoch genug: eine Zeile braucht
// 24 px von 340 (siehe renderKpiMonitor() in office-render.js). // 24 px von 340 (siehe renderKpiMonitor() in office-render.js).
export const MAX_KPI_TILES = 5; export const MAX_KPI_TILES = 5;
@ -514,7 +481,6 @@ export function buildKpiTiles(kpis = []) {
// Nur abgeleitete Kacheln setzen einen Schweregrad (aktuell "Problem", siehe // Nur abgeleitete Kacheln setzen einen Schweregrad (aktuell "Problem", siehe
// buildAgentCountKpis()); gemeldete KPIs (F7) kennen kein solches Feld und bleiben neutral. // buildAgentCountKpis()); gemeldete KPIs (F7) kennen kein solches Feld und bleiben neutral.
severity: kpi.severity ?? null, severity: kpi.severity ?? null,
sparklinePoints: buildKpiSparklinePoints(kpi.history || []),
})); }));
} }
@ -535,7 +501,7 @@ export function buildHooksKpi(count) {
} }
/** /**
* Inhaltsmodell fuer das KPI-Monitor-Modal (groessere Sparkline als die Wandkachel). * Inhaltsmodell fuer das KPI-Monitor-Modal .
* *
* @param {Array<object>} kpis * @param {Array<object>} kpis
*/ */
@ -548,7 +514,6 @@ export function buildKpiModalContent(kpis = []) {
unit: kpi.unit || "", unit: kpi.unit || "",
currentValue: kpi.current_value, currentValue: kpi.current_value,
history: kpi.history || [], history: kpi.history || [],
sparklinePoints: buildKpiSparklinePoints(kpi.history || [], 320, 80),
})), })),
}; };
} }

View file

@ -23,7 +23,6 @@ import {
buildSystemModalContent, buildSystemModalContent,
systemsWithoutHostConfig, systemsWithoutHostConfig,
formatSystemCheckTime, formatSystemCheckTime,
buildKpiSparklinePoints,
buildKpiTiles, buildKpiTiles,
buildKpiModalContent, buildKpiModalContent,
buildAgentCountKpis, buildAgentCountKpis,
@ -415,18 +414,6 @@ test("formatSystemCheckTime liefert null fuer fehlende/ungueltige Werte", () =>
assert.equal(formatSystemCheckTime("nicht-valide"), null); assert.equal(formatSystemCheckTime("nicht-valide"), null);
}); });
test("buildKpiSparklinePoints liefert leeren String ohne Historie und einen Punkt pro Messwert sonst", () => {
assert.equal(buildKpiSparklinePoints([]), "");
const points = buildKpiSparklinePoints([{ value: 3 }, { value: 1 }, { value: 2 }], 100, 20);
assert.equal(points.split(" ").length, 3);
});
test("buildKpiSparklinePoints behandelt konstante Werte ohne Division durch 0", () => {
const points = buildKpiSparklinePoints([{ value: 5 }, { value: 5 }], 100, 20);
assert.equal(points.split(" ").length, 2);
assert.ok(points.length > 0);
});
test("buildKpiTiles kappt bei MAX_KPI_TILES", () => { test("buildKpiTiles kappt bei MAX_KPI_TILES", () => {
const kpis = Array.from({ length: MAX_KPI_TILES + 2 }, (_, i) => ({ const kpis = Array.from({ length: MAX_KPI_TILES + 2 }, (_, i) => ({
key: `k${i}`, key: `k${i}`,