KPIs: keine Graphen mehr in Kachel und Detail-Modal

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-10-01 11:29:56 +02:00
parent f14d851410
commit 266432c7df
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,
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
`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
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

View file

@ -672,7 +672,7 @@ header.office-header h1 {
text-anchor: middle;
}
/* KPI-Sparklines (Phase 4, F10: kein Chart-Lib, eigenes SVG-Zeichnen) */
/* KPI-Kacheln */
.kpi-tile-label {
fill: var(--color-text-inverse);
@ -690,18 +690,6 @@ header.office-header h1 {
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) */
.office-modal {
@ -815,14 +803,6 @@ header.office-header h1 {
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) */
.office-modal-error {

View file

@ -520,15 +520,6 @@ function renderSystemMonitor(frame, systems, onConfigClick) {
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) {
const tiles = buildKpiTiles(kpis);
const g = el("g", { class: "monitor monitor-kpi" });
@ -549,15 +540,7 @@ function renderKpiMonitor(frame, kpis) {
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;
// Passt zur Punkteberechnung in buildKpiSparklinePoints() (Default-Hoehe 32) - so fuellt die
// Linie den Kasten genau aus.
const SPARKLINE_HEIGHT = 32;
const ROW_GAP = 4;
let rowTop = frame.y + 20;
@ -573,11 +556,6 @@ function renderKpiMonitor(frame, kpis) {
);
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;
});
@ -1561,12 +1539,6 @@ function renderKpiModalBody(body, content) {
label.textContent = `${item.label}: ${item.currentValue ?? "–"} ${item.unit}`;
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);
}
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:
// renderOfficeScene() reserviert bereits die letzte Kachel fuer "Hooks" (Issue #38), womit
// "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).
export const MAX_KPI_TILES = 5;
@ -514,7 +481,6 @@ export function buildKpiTiles(kpis = []) {
// Nur abgeleitete Kacheln setzen einen Schweregrad (aktuell "Problem", siehe
// buildAgentCountKpis()); gemeldete KPIs (F7) kennen kein solches Feld und bleiben neutral.
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
*/
@ -548,7 +514,6 @@ export function buildKpiModalContent(kpis = []) {
unit: kpi.unit || "",
currentValue: kpi.current_value,
history: kpi.history || [],
sparklinePoints: buildKpiSparklinePoints(kpi.history || [], 320, 80),
})),
};
}

View file

@ -23,7 +23,6 @@ import {
buildSystemModalContent,
systemsWithoutHostConfig,
formatSystemCheckTime,
buildKpiSparklinePoints,
buildKpiTiles,
buildKpiModalContent,
buildAgentCountKpis,
@ -415,18 +414,6 @@ test("formatSystemCheckTime liefert null fuer fehlende/ungueltige Werte", () =>
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", () => {
const kpis = Array.from({ length: MAX_KPI_TILES + 2 }, (_, i) => ({
key: `k${i}`,