Merge pull request 'KPI-Monitor zeigt Agenten/Online-Kacheln wieder an' (#40) from fds2610-bot/monitor:main into main

Reviewed-on: https://codeberg.org/fds2610/monitor/pulls/40
This commit is contained in:
fds2610 2026-08-04 13:02:34 +02:00
commit 5dcf080b27
4 changed files with 57 additions and 4 deletions

View file

@ -245,8 +245,10 @@ externe Datenbankserver (nur PHP/Python-Standardbibliothek) auskommen, um die
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)
sobald KPIs gemeldet wurden - vorher weiterhin der Platzhalter aus Phase 2 (F7 ist
inhaltlich weiterhin offen).
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) - darueber hinaus ist F7 inhaltlich
weiterhin offen, ohne gemeldete KPIs zeigt die Wand nur diese beiden Kacheln.
6. **Phase 5 – Agenten & Animation**: Zustandsmaschine, Laufwege, Tipp-/Lese-/Kaffee-Animation,
Reaktion auf neue Ticket-/Event-/Systemstatus-Meldungen. ✅ erledigt: `resolveAgentVisualState`
bildet die von der API gemeldeten Zustaende auf Ort/Pose ab, Agenten-Avatare sind eine

View file

@ -24,6 +24,7 @@ import {
buildSystemModalContent,
buildKpiTiles,
buildKpiModalContent,
buildAgentCountKpis,
mapAgentsToDesks,
normalizeAgentForDisplay,
resolveAgentVisualState,
@ -1401,6 +1402,11 @@ export function renderOfficeScene(container, data = {}) {
// is_present-Feld wie registrierte Agenten (Issue #19).
poIsLive = isLive;
// "Agenten"/"Online" (Issue #39) stehen vor den gemeldeten KPIs (F7), damit sie auf der
// Wandkachel (MAX_KPI_TILES) immer sichtbar sind, statt von gemeldeten Kennzahlen verdraengt
// zu werden.
const displayKpis = [...buildAgentCountKpis(agents), ...kpis];
const groupedTickets = groupTicketsByColumn(tickets);
const boardGroup = renderBoard(ticketCounts, groupedTickets, (columnKey) => {
@ -1417,10 +1423,10 @@ export function renderOfficeScene(container, data = {}) {
svg.insertBefore(boardGroup, svg.querySelector(".agents-layer"));
}
const wallGroup = renderMonitorWall(systems, events, kpis, {
const wallGroup = renderMonitorWall(systems, events, displayKpis, {
onEventsClick: () => openModal(buildEventModalContent(events), renderEventModalBody),
onSystemsClick: () => openModal(buildSystemModalContent(systems), renderSystemModalBody),
onKpiClick: () => openModal(buildKpiModalContent(kpis), renderKpiModalBody),
onKpiClick: () => openModal(buildKpiModalContent(displayKpis), renderKpiModalBody),
});
const existingWall = svg.querySelector(".monitor-wall");
if (existingWall) {

View file

@ -415,6 +415,27 @@ export function isAgentPresent(agent) {
return Boolean(agent.is_present);
}
/**
* Die beiden im KPI-Monitor erwarteten Kennzahlen "Agenten" (registrierte Agenten) und
* "Online" (davon innerhalb der letzten 10 Minuten aktiv, siehe isAgentPresent()) werden
* anders als die per POST /api/v1/kpis/{key} gemeldeten Kennzahlen (F7, weiterhin offen)
* direkt aus der ohnehin geladenen Agentenliste abgeleitet statt separat gemeldet.
* Gibt KPI-foermige Eintraege zurueck (gleiche Form wie GET /api/v1/kpis), damit sie sich
* mit den gemeldeten KPIs vor buildKpiTiles()/buildKpiModalContent() zusammenfuehren lassen.
*
* @param {Array<object|null>} agents - z.B. leere Schreibtisch-Plaetze der Demo-Daten sind null.
* @returns {Array<object>}
*/
export function buildAgentCountKpis(agents = []) {
const registered = agents.filter(Boolean);
const online = registered.filter(isAgentPresent).length;
return [
{ key: "agents_total", label: "Agenten", unit: "", current_value: registered.length, history: [] },
{ key: "agents_online", label: "Online", unit: "", current_value: online, history: [] },
];
}
/**
* Ordnet eine rohe Agentenliste (GET /api/v1/agents) den Schreibtischen zu - anders als
* buildDeskLayout()/Demo-Daten steht der Sitzplatz hier nicht in der Array-Reihenfolge,

View file

@ -20,6 +20,7 @@ import {
buildKpiSparklinePoints,
buildKpiTiles,
buildKpiModalContent,
buildAgentCountKpis,
MAX_KPI_TILES,
AGENT_LOCATIONS,
resolveAgentVisualState,
@ -331,6 +332,29 @@ test("buildKpiModalContent zeigt alle KPIs (kein Limit) inkl. Historie", () => {
assert.equal(content.items[0].label, "Offene Tickets");
});
test("buildAgentCountKpis liefert Anzahl registrierter und online-Agenten (Issue #39)", () => {
const kpis = buildAgentCountKpis([
{ id: 1, is_present: true },
{ id: 2, is_present: false },
{ id: 3, is_present: true },
]);
assert.equal(kpis.length, 2);
assert.deepEqual(kpis[0], { key: "agents_total", label: "Agenten", unit: "", current_value: 3, history: [] });
assert.deepEqual(kpis[1], { key: "agents_online", label: "Online", unit: "", current_value: 2, history: [] });
});
test("buildAgentCountKpis ignoriert leere Schreibtisch-Plaetze (null) und faellt ohne is_present auf anwesend zurueck", () => {
const kpis = buildAgentCountKpis([{ id: 1 }, null, { id: 2 }, null]);
assert.equal(kpis[0].current_value, 2);
assert.equal(kpis[1].current_value, 2);
});
test("buildAgentCountKpis liefert 0/0 ohne Agenten", () => {
const kpis = buildAgentCountKpis([]);
assert.equal(kpis[0].current_value, 0);
assert.equal(kpis[1].current_value, 0);
});
// ---------------------------------------------------------------------------------------
// Phase 5 - Agenten-Zustandsmaschine & Animation
// ---------------------------------------------------------------------------------------