KPIs: keine Graphen mehr in Kachel und Detail-Modal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f14d851410
commit
266432c7df
5 changed files with 4 additions and 100 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue