From 62e5cb0751edbf35ec581427d8d21cefb560b180 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Fri, 7 Aug 2026 09:51:45 +0200 Subject: [PATCH] Abgelaufene Claude-Anmeldung eines Agenten sichtbar machen Laeuft einem Agenten die Claude-Anmeldung aus, laeuft das Cron-Skript um ihn herum unbeeindruckt weiter: es meldet "working", ruft claude auf, bekommt nichts Brauchbares zurueck und meldet am Ende "idle". last_seen_at ist frisch, der Agent sieht beschaeftigt aus - waehrend Lauf fuer Lauf nichts entsteht. Der Ausfall fiel bisher erst auf, wenn jemand merkte, dass keine PRs mehr kommen. Agenten melden den Fall jetzt ueber zwei neue state-Werte (auth_expiring, auth_expired) am bestehenden Activity-Endpunkt. state ist serverseitig nicht eingeschraenkt, daher ohne Migration und ohne Aenderung an der API. Dargestellt in drei Stufen der Aufmerksamkeit: - Schloss ueber dem Avatar (bei auth_expired pulsierend), entsaettigter Koerper, eingefaerbter Bildschirm und Namensschild am Platz - Header-Banner mit den betroffenen Agenten, gelb bei Vorwarnung, rot bei Ausfall - auch aus einigen Metern Entfernung noch lesbar - KPI-Kachel "Problem: n", die sich nur einfaerbt, wenn n > 0 Kern ist isAgentVisible(), bewusst getrennt von isAgentPresent(): ein blockierter Agent bleibt sichtbar, obwohl er sich nicht mehr meldet, zaehlt aber nicht als "Online" mit. Ohne das verschwaende er nach dem Praesenz-Timeout genau dann vom Schreibtisch, wenn seine Warnung dort stehen soll. MAX_KPI_TILES von 4 auf 5, sonst verdraengen Agenten/Online/Problem/Hooks die gemeldeten KPIs (F7) vollstaendig. Nebenbei: "npm test" lief seit Node 24 nicht mehr (das Verzeichnis-Argument wird nicht mehr als Testverzeichnis aufgeloest), jetzt per Glob. Co-Authored-By: Claude Opus 5 --- API.md | 55 +++++++++++++-- html/css/office.css | 87 +++++++++++++++++++++++ html/js/office-render.js | 75 ++++++++++++++++++-- html/js/office-scene.js | 91 +++++++++++++++++++++++- html/office.html | 5 ++ package.json | 2 +- tests/frontend/office-scene.test.js | 106 +++++++++++++++++++++++++++- 7 files changed, 408 insertions(+), 13 deletions(-) diff --git a/API.md b/API.md index 8afdec1..a26fd1e 100644 --- a/API.md +++ b/API.md @@ -105,7 +105,8 @@ curl -s "$MONITOR_API_BASE/agents" | jq '.agents[] | {id, display_name, last_see `is_present` gilt `true`, solange `last_seen_at` hoechstens zehn Minuten zurueckliegt, sonst `false` (`AgentRepository::isPresent()`, Issue #17/#20) - laeuft der Timeout ab, bleibt der -Schreibtisch des Agenten leer, obwohl er weiterhin registriert ist. Berechnet wird das +Schreibtisch des Agenten leer, obwohl er weiterhin registriert ist (Ausnahme sind die +[Problemzustaende](#problemzustaende-abgelaufene-claude-anmeldung), die sichtbar bleiben). Berechnet wird das serverseitig via `gmdate()`/`time()` (also unabhaengig von der Zeitzone des Clients); das Frontend (`isAgentPresent`, `html/js/office-scene.js`) liest `is_present` nur noch direkt aus der API, statt selbst aus `last_seen_at` zu rechnen - vorher fuehrte das je nach @@ -130,6 +131,47 @@ melden, sonst `403`). Response 200: aktualisiertes Agent-Objekt. +`state` ist bewusst nicht serverseitig eingeschraenkt - welche Zustaende es gibt, entscheidet +allein die Darstellung (`AGENT_STATE_VISUALS` in `html/js/office-scene.js`), unbekannte Werte +fallen dort auf "idle am Schreibtisch" zurueck. Gebraeuchlich sind `idle`, `working` sowie die +Lauf-/Interaktionszustaende der Zustandsmaschine (`walking_to_board`, `interacting_with_board`, +`walking_to_monitor`, `interacting_with_monitor`, `walking_back`). + +#### Problemzustaende: abgelaufene Claude-Anmeldung + +Zwei Zustaende sind Sonderfaelle, weil sie ein menschliches Eingreifen erfordern: + +| `state` | Bedeutung | Darstellung | +| --- | --- | --- | +| `auth_expiring` | Die Claude-Anmeldung des Agenten laeuft demnaechst ab, er arbeitet noch. | gelbes Schloss am Avatar, gelbes Namensschild, gelber Header-Banner | +| `auth_expired` | Die Anmeldung ist abgelaufen, der Agent kommt nicht mehr weiter. | rot pulsierendes Schloss, rotes Namensschild, roter Header-Banner | + +Hintergrund: Laeuft die Anmeldung aus, laeuft das Cron-Skript um den Agenten herum +unbeeindruckt weiter - es meldet zu Beginn `working`, ruft `claude` auf, bekommt nichts +Brauchbares zurueck und meldet am Ende `idle`. Ohne eigenen Zustand ist der Ausfall vom +Normalbetrieb nicht zu unterscheiden: `last_seen_at` ist frisch, der Agent sieht beschaeftigt +aus, waehrend Lauf fuer Lauf nichts entsteht. Der Monitor-API-Key ist von der Claude-Anmeldung +unabhaengig und bleibt gueltig - der Agent kann den Zustand also weiterhin melden. + +Zwei Besonderheiten: + +- **Sie ueberleben den Praesenz-Timeout.** Hat ein Agent `auth_expired` gemeldet, bricht sein + Skript den Lauf ab und meldet bis zum naechsten Cron-Start nichts mehr. Das Frontend zeigt ihn + trotzdem weiter (`isAgentVisible()`), sonst verschwaende er genau dann vom Schreibtisch, wenn + seine Warnung dort stehen soll. `is_present`/`agents_online` bleiben davon unberuehrt - ein + blockierter Agent zaehlt nicht als online. +- **Sie loesen sich nur durch eine neue Meldung.** Es gibt bewusst kein Timeout; der naechste + erfolgreiche Lauf meldet ohnehin `working`/`idle` und raeumt den Zustand damit auf. + +Ergaenzend gehoert ein Event (`POST /api/v1/events`, severity `error` bzw. `warning`) dazu - der +Zustand allein hat kein Datum, das Event beantwortet "seit wann". + +```bash +curl -X POST "$MONITOR_API_BASE/agents/$ID/activity" \ + -H "Content-Type: application/json" -H "X-Api-Key: $KEY" \ + -d '{"state": "auth_expired", "current_ticket": null}' +``` + ## Tickets Spalten (F3): `backlog`, `bereit`, `in_arbeit`, `qa`, `testing`, `fertig`, `done`. @@ -224,12 +266,17 @@ Response: `{ "systems": [ { "id", "name", "type", "status", "detail", "source", F7: Konkrete Kennzahlen sind noch offen ("kommt spaeter"). Phase 1 stellt nur die generische Speicher-/Abfragestruktur bereit. -Zusaetzlich zu hier gemeldeten Kennzahlen zeigt der KPI-Monitor im Frontend drei weitere, +Zusaetzlich zu hier gemeldeten Kennzahlen zeigt der KPI-Monitor im Frontend vier weitere, nicht per `POST /api/v1/kpis/{key}` gemeldete Kacheln, die direkt aus anderen Endpunkten -abgeleitet sind: "Agenten"/"Online" aus `GET /api/v1/agents` (Issue #39) und "Hooks" (ganz -rechts) aus [`GET /api/v1/webhooks/stats`](#get-apiv1webhooksstats--empfangene-webhooks-zaehlen-hooks-kpi), +abgeleitet sind: "Agenten"/"Online"/"Problem" aus `GET /api/v1/agents` (Issue #39) und "Hooks" +(ganz unten) aus [`GET /api/v1/webhooks/stats`](#get-apiv1webhooksstats--empfangene-webhooks-zaehlen-hooks-kpi), siehe Issue #38. +"Problem" zaehlt die Agenten in einem +[Problemzustand](#problemzustaende-abgelaufene-claude-anmeldung) und faerbt sich nur rot, wenn +der Wert ueber 0 liegt - eine dauerhaft rote Kachel mit einer 0 waere genau das Rauschen, das den +Ernstfall untergehen liesse. + ### `POST /api/v1/kpis/{key}` — KPI-Datenpunkt melden Auth: `X-Api-Key`. diff --git a/html/css/office.css b/html/css/office.css index 5214aeb..539f947 100644 --- a/html/css/office.css +++ b/html/css/office.css @@ -89,6 +89,14 @@ header.office-header h1 { display: none; } +/* Reine Vorwarnung (Anmeldung laeuft demnaechst ab, der Agent arbeitet noch): gelb statt rot, + damit das Rot dem tatsaechlichen Stillstand vorbehalten bleibt. Dunkle Schrift, weil Weiss + auf Gelb kaum zu lesen ist. */ +.api-banner.api-banner-warning { + background: #facc15; + color: #422006; +} + #office-scene { flex: 1 1 auto; /* min-height: 0 hebt die Flex-Default-Mindesthoehe (min-content) auf - sonst kann @@ -290,6 +298,18 @@ header.office-header h1 { fill: #7dd3fc; } +/* Blockierter Platz (Claude-Anmeldung abgelaufen): der Bildschirm traegt den Zustand mit, + damit er auch dann am Platz sichtbar bleibt, wenn der Avatar gerade nicht dort steht. */ +.desk-screen.pose-auth_expired { + fill: #7f1d1d; + stroke: #f87171; +} + +.desk-screen.pose-auth_expiring { + fill: #713f12; + stroke: #facc15; +} + .desk-chair { fill: var(--color-desk-chair); } @@ -316,6 +336,18 @@ header.office-header h1 { font-weight: 400; } +/* Der Name eines Agenten am Schreibtisch steht am Tisch, nicht am Avatar (updateAgentsLayer() + blendet .avatar-name am Platz aus) - der Problemzustand faerbt deshalb hier. */ +.desk-name.desk-name-problem-error { + fill: #b91c1c; + font-weight: 700; +} + +.desk-name.desk-name-problem-warning { + fill: #a16207; + font-weight: 700; +} + .desk-deco-star { fill: #fbbf24; } @@ -457,6 +489,51 @@ header.office-header h1 { } } +/* Blockierter Agent (AGENT_PROBLEM_STATES): der Koerper verliert seine Rollenfarbe, damit die + Figur als "steht still" lesbar wird, ohne aus der Szene zu verschwinden. */ +.agent-avatar.pose-auth_expired .person-body, +.agent-avatar.pose-auth_expiring .person-body { + fill: #94a3b8; +} + +.person-prop-lock-body { + fill: #f87171; + stroke: #7f1d1d; + stroke-width: 1.5; +} + +.person-prop-lock-shackle { + fill: none; + stroke: #f87171; + stroke-width: 3; + stroke-linecap: round; +} + +.pose-auth_expiring .person-prop-lock-body { + fill: #facc15; + stroke: #713f12; +} + +.pose-auth_expiring .person-prop-lock-shackle { + stroke: #facc15; +} + +/* Nur der harte Ausfall pulsiert - eine pulsierende Vorwarnung waere auf einem Dauermonitor + Rauschen, das man nach einer Stunde nicht mehr sieht. */ +.agent-avatar.pose-auth_expired .person-prop-lock { + animation: office-lock-pulse 1.4s ease-in-out infinite; +} + +@keyframes office-lock-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.35; + } +} + .person-prop-cup { fill: #f8fafc; stroke: #94a3b8; @@ -489,6 +566,16 @@ header.office-header h1 { font-weight: 600; } +/* Kombiniert notiert, weil .kpi-tile-label weiter unten steht als .severity-* und die Farbe + sonst bei gleicher Spezifitaet ueber die Reihenfolge zurueckgewinnt. */ +.kpi-tile-label.severity-error { + fill: #f87171; +} + +.kpi-tile-label.severity-warning { + fill: #facc15; +} + .kpi-sparkline-bg { fill: rgba(255, 255, 255, 0.04); } diff --git a/html/js/office-render.js b/html/js/office-render.js index f5016c5..2b712b4 100644 --- a/html/js/office-render.js +++ b/html/js/office-render.js @@ -30,7 +30,9 @@ import { mapAgentsToDesks, normalizeAgentForDisplay, resolveAgentVisualState, - isAgentPresent, + isAgentVisible, + agentProblemSeverity, + agentsWithProblems, pickIdleActivity, AnimationQueue, detectDoorVisits, @@ -396,7 +398,9 @@ function renderKpiMonitor(frame, kpis) { text(`${tile.label}: ${tile.currentValue ?? "–"} ${tile.unit}`.trim(), { x: frame.x + 16, y: rowTop + 14, - class: "kpi-tile-label", + // Abgeleitete Kacheln koennen einen Schweregrad tragen ("Problem", siehe + // buildAgentCountKpis) - dieselben Klassen wie die Event-Zeilen, also dieselben Farben. + class: `kpi-tile-label${tile.severity ? ` severity-${tile.severity}` : ""}`, }) ); rowTop += LABEL_HEIGHT; @@ -486,10 +490,20 @@ function deskSeat(deskIndex) { }; } +const DESK_NAME_PROBLEM_CLASS = { + auth_expired: "desk-name-problem-error", + auth_expiring: "desk-name-problem-warning", +}; + function deskScreenPoseClass(pose) { switch (pose) { case "working": return "pose-working"; + case "auth_expired": + case "auth_expiring": + // Der Bildschirm faerbt sich mit - ein Schloss allein am Avatar waere weg, sobald der + // Agent laeuft; der Platz selbst soll den Zustand ebenfalls tragen. + return `pose-${pose}`; default: return "pose-idle"; } @@ -547,11 +561,16 @@ function renderDeskAt(origin, agent, width = DESK.w) { ); } + // Blockierte Agenten faerben ihr Namensschild ein. Der Zustand steckt hier in der Pose, weil + // renderDeskAt() den rohen Agenten gar nicht sieht - normalizeAgentForDisplay() hat state + // bereits auf pose abgebildet. + const problem = DESK_NAME_PROBLEM_CLASS[agent.pose] ?? ""; + g.appendChild( text(agent.name, { x: width / 2, y: 40 + DESK.h + 74, - class: "desk-name", + class: `desk-name ${problem}`.trim(), "text-anchor": "middle", }) ); @@ -674,10 +693,24 @@ function poseExtras(pose) { nodes.push(el("rect", { x: -16, y: -48, width: 32, height: 18, rx: 1, class: "person-prop-newspaper" })); } else if (pose === "talking") { nodes.push(el("circle", { cx: 20, cy: -40, r: 7, class: "person-prop-speech" })); + } else if (pose === "auth_expired" || pose === "auth_expiring") { + nodes.push(renderAuthLock()); } return nodes; } +// Schloss ueber dem Kopf: der Agent sitzt noch da, kommt aber nicht mehr rein (abgelaufene +// Claude-Anmeldung, siehe AGENT_PROBLEM_STATES). Bewusst ein Symbol statt nur einer Einfaerbung - +// auf dem Wandmonitor aus einigen Metern Entfernung ist eine Silhouette schneller zu erfassen +// als ein Farbton. Farbe und Pulsieren kommen ueber die Pose-Klasse aus dem Stylesheet. +function renderAuthLock() { + const g = el("g", { class: "person-prop-lock", transform: "translate(0, -74)" }); + // Buegel: Halbkreis oben, offen nach unten - sitzt auf dem Korpus auf. + g.appendChild(el("path", { d: "M-6,0 v-5 a6,6 0 0 1 12,0 v5", class: "person-prop-lock-shackle" })); + g.appendChild(el("rect", { x: -9, y: 0, width: 18, height: 14, rx: 2, class: "person-prop-lock-body" })); + return g; +} + function createAgentAvatarElement() { const g = el("g", { class: "agent-avatar" }); g.appendChild(el("circle", { cx: 0, cy: -34, r: 16, class: "person-head" })); @@ -1482,6 +1515,10 @@ export function renderOfficeScene(container, data = {}) { currentRegularPlacements = placements; repaintAgentsLayer(); scheduleIdleCosmeticsForCurrentAgents(placements); + + // Demo-Agenten kennen kein state-Feld und loesen den Banner damit nie aus - eine erfundene + // Anmelde-Warnung waere im Fallback so falsch wie eine erfundene Kennzahl. + setAgentProblemBanner(agents); } // Demo-Agenten (Phase 2) tragen bereits {name, role, pose} ohne desk_index/state - diese @@ -1519,7 +1556,9 @@ function agentPlacements(agents, deskLayout) { let overflowIndex = 0; for (const raw of agents) { if (seatedIds.has(raw.id)) continue; - if (!isAgentPresent(raw)) continue; + // isAgentVisible statt isAgentPresent, wie bei den Schreibtischen (mapAgentsToDesks): auch + // ein blockierter Agent ohne festen Platz muss sichtbar bleiben. + if (!isAgentVisible(raw)) continue; const agent = normalizeAgentForDisplay(raw); const homeFeet = { x: 70 + (overflowIndex % 3) * 26, @@ -1543,6 +1582,34 @@ function setApiBanner(visible) { } } +// Ein blockierter Agent ist der einzige Zustand, den der Monitor nicht aussitzen kann: er loest +// sich nicht von selbst, sondern erst, wenn sich jemand neu anmeldet. Deshalb zusaetzlich zum +// Schloss am Avatar ein Banner im Header - der ist auch dann noch zu lesen, wenn der Monitor +// quer durch den Raum haengt und die Szene nur noch als Muster wahrgenommen wird. +function setAgentProblemBanner(agents) { + const banner = document.getElementById("agent-problem-banner"); + if (!banner) { + return; + } + + const names = (severity) => + agentsWithProblems(agents) + .filter((agent) => agentProblemSeverity(agent) === severity) + .map((agent) => agent.display_name ?? agent.name ?? `Agent ${agent.id}`) + .join(", "); + + const expired = names("error"); + const expiring = names("warning"); + + const lines = []; + if (expired) lines.push(`Anmeldung abgelaufen: ${expired}`); + if (expiring) lines.push(`Anmeldung läuft ab: ${expiring}`); + + banner.textContent = lines.join(" · "); + banner.hidden = lines.length === 0; + banner.classList.toggle("api-banner-warning", !expired && Boolean(expiring)); +} + // Rein kosmetischer Countdown im Header (Issue #12): zaehlt unabhaengig vom tatsaechlichen // SSE-/Polling-Takt von MONITOR_SYNC_INTERVAL (aus config.js, siehe office.html) auf 0 // herunter und startet danach von vorn - loest selbst keinen Reload aus. diff --git a/html/js/office-scene.js b/html/js/office-scene.js index 98565e7..1efe51f 100644 --- a/html/js/office-scene.js +++ b/html/js/office-scene.js @@ -325,7 +325,12 @@ export function buildKpiSparklinePoints(history = [], width = 140, height = 32, .join(" "); } -export const MAX_KPI_TILES = 4; +// 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 +// 24 px von 340 (siehe renderKpiMonitor() in office-render.js). +export const MAX_KPI_TILES = 5; /** * Bereitet bis zu MAX_KPI_TILES KPI-Kacheln fuer die Monitor-Wand auf. F7 ist weiterhin @@ -340,6 +345,9 @@ export function buildKpiTiles(kpis = []) { label: kpi.label || kpi.key, unit: kpi.unit || "", currentValue: kpi.current_value, + // 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 || []), })); } @@ -408,6 +416,10 @@ const AGENT_STATE_VISUALS = { reading_newspaper: { location: AGENT_LOCATIONS.DESK, pose: "reading_newspaper" }, at_coffee_corner: { location: AGENT_LOCATIONS.COFFEE_CORNER, pose: "coffee" }, talking_to_colleague: { location: AGENT_LOCATIONS.DESK, pose: "talking" }, + // Abgelaufene Claude-Anmeldung (Problemzustaende, siehe AGENT_PROBLEM_STATES): der Agent + // bleibt an seinem Platz - er ist nicht weg, er kommt nur nicht mehr weiter. + auth_expiring: { location: AGENT_LOCATIONS.DESK, pose: "auth_expiring" }, + auth_expired: { location: AGENT_LOCATIONS.DESK, pose: "auth_expired" }, }; /** @@ -431,6 +443,67 @@ export function isAgentPresent(agent) { return Boolean(agent.is_present); } +/** + * Zustaende, die ein menschliches Eingreifen erfordern: laeuft einem Agenten die + * Claude-Anmeldung aus, laeuft das Cron-Skript drumherum unbeeindruckt weiter - es meldet zu + * Beginn "working", ruft claude auf, bekommt nichts Brauchbares zurueck und meldet am Ende + * "idle". Ohne eigenen Zustand ist der Ausfall vom Normalbetrieb nicht zu unterscheiden; der + * Agent sieht beschaeftigt aus, waehrend Lauf fuer Lauf nichts entsteht. + * + * Der Agent meldet diese Zustaende ueber denselben Endpunkt wie jeden anderen (POST + * /api/v1/agents/{id}/activity) - sein Monitor-API-Key ist von der Claude-Anmeldung unabhaengig + * und bleibt gueltig. Der Zustand bleibt kleben, bis der Agent selbst etwas anderes meldet: + * bewusst kein Timeout, denn ein Blocker soll sichtbar bleiben, bis er behoben ist. + */ +export const AGENT_PROBLEM_STATES = { + auth_expiring: "warning", + auth_expired: "error", +}; + +/** + * @param {object} agent + * @returns {"warning"|"error"|null} Schweregrad des Problemzustands, oder null im Normalbetrieb. + * Die Werte entsprechen den Event-Schweregraden (API.md), damit Kacheln, Banner und + * Event-Zeilen dieselben Farben nutzen koennen. + */ +export function agentProblemSeverity(agent) { + if (!agent) { + return null; + } + return AGENT_PROBLEM_STATES[agent.state] ?? null; +} + +/** + * Ob ein Agent in der Szene gezeigt wird. Bewusst getrennt von isAgentPresent(): hat ein Agent + * "auth_expired" gemeldet, bricht sein Skript den Lauf ab und meldet bis zum naechsten + * Cron-Start nichts mehr. Liegt der Cron-Abstand ueber dem Praesenz-Timeout + * (AgentRepository::PRESENCE_TIMEOUT_SECONDS), verschwaende der Agent genau dann vom + * Schreibtisch, wenn seine Warnung dort stehen soll. + * + * isAgentPresent() bleibt davon unberuehrt - sonst zaehlte die KPI "Online" einen blockierten + * Agenten als arbeitsfaehig mit. + */ +export function isAgentVisible(agent) { + return isAgentPresent(agent) || agentProblemSeverity(agent) !== null; +} + +/** + * @param {Array} agents + * @returns {number} Anzahl Agenten in einem Problemzustand (Grundlage der KPI-Kachel "Problem"). + */ +export function countAgentProblems(agents = []) { + return agents.filter((agent) => agentProblemSeverity(agent) !== null).length; +} + +/** + * @param {Array} agents + * @returns {Array} Agenten mit Problemzustand, gruppiert nach Schweregrad - Grundlage + * fuer den Header-Banner, der die betroffenen Namen nennt. + */ +export function agentsWithProblems(agents = []) { + return agents.filter((agent) => agentProblemSeverity(agent) !== null); +} + /** * Die beiden im KPI-Monitor erwarteten Kennzahlen "Agenten" (registrierte Agenten) und * "Online" (davon innerhalb der letzten 10 Minuten aktiv, siehe isAgentPresent()) werden @@ -445,10 +518,21 @@ export function isAgentPresent(agent) { export function buildAgentCountKpis(agents = []) { const registered = agents.filter(Boolean); const online = registered.filter(isAgentPresent).length; + const problems = countAgentProblems(registered); return [ { key: "agents_total", label: "Agenten", unit: "", current_value: registered.length, history: [] }, { key: "agents_online", label: "Online", unit: "", current_value: online, history: [] }, + // "Problem" faerbt sich nur ein, wenn es wirklich eines gibt - eine dauerhaft rote Kachel + // mit einer 0 waere genau das Rauschen, das den Ernstfall untergehen liesse. + { + key: "agents_problem", + label: "Problem", + unit: "", + current_value: problems, + history: [], + severity: problems > 0 ? "error" : null, + }, ]; } @@ -459,7 +543,8 @@ export function buildAgentCountKpis(agents = []) { * oder alle Schreibtische belegt, desk_index === null) erscheinen hier nicht - sie kommen * gemaess F2 nur anlassbezogen durch eine Tuer und haben keinen festen Platz. Agenten, die laut * is_present-Feld nicht mehr als anwesend gelten (Issue #17/#20), erscheinen ebenfalls nicht - - * ihr Schreibtisch bleibt frei. + * ihr Schreibtisch bleibt frei. Ausnahme sind die Problemzustaende (isAgentVisible()): ein + * blockierter Agent bleibt sitzen, auch wenn er sich laenger nicht gemeldet hat. * * @param {Array} agents * @param {number} [deskCount] @@ -468,7 +553,7 @@ export function buildAgentCountKpis(agents = []) { export function mapAgentsToDesks(agents = [], deskCount = DEFAULT_DESK_COUNT) { const desks = new Array(deskCount).fill(null); for (const agent of agents) { - if (!isAgentPresent(agent)) { + if (!isAgentVisible(agent)) { continue; } const deskIndex = agent.desk_index; diff --git a/html/office.html b/html/office.html index fa18959..a7a1d6d 100644 --- a/html/office.html +++ b/html/office.html @@ -15,6 +15,11 @@ + + +