monitor/html/js/office-scene.js
fds2610-bot 37693a09a1 Phase 2 der Neuausrichtung: Statische Buero-Szene (SVG, Demo-Daten)
Setzt Phase 2 aus Visualization.md um: Vanilla-SVG-Buero-Szene (Waende, 2 Tueren,
8 Schreibtische, Kanban-Board-Rahmen mit 7 Spalten, Monitor-Wand mit Event-/System-/
KPI-Monitor) ohne Animation, gespeist mit statischen Demo-Daten im Format der
Phase-1-API-Responses. Board-Stapel-Logik (>5 Tickets, Done-Spalte als Dauerstapel)
folgt Detailantworten.md F2/F3.

Reine Layout-Logik (html/js/office-scene.js) ist ausgelagert und mit dem Node-
Testrunner abgedeckt (100% Zeilenabdeckung, tests/frontend/), das DOM-Rendering
(office-render.js) bleibt bewusst duenne Glue-Logik. Docker-Compose um web- (nginx,
serviert html/) und frontend-Service (node, "npm test") erweitert, analog zum
bestehenden api-Service und zum NC-Gantt-Vorbild: kein PHP/Node auf dem Host noetig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 00:19:22 +02:00

105 lines
3.9 KiB
JavaScript

// Reine Layout-/Datenlogik fuer die Buero-Visualisierung (Phase 2, siehe Visualization.md
// Abschnitt 9). Enthaelt keine DOM-Zugriffe, damit die Funktionen ohne Browser (z.B. mit dem
// Node-Testrunner) getestet werden koennen. Das eigentliche Zeichnen uebernimmt office-render.js.
// Spaltenreihenfolge exakt wie API.md ("Tickets" -> Spalten, F3).
export const BOARD_COLUMNS = [
{ key: "backlog", label: "Backlog" },
{ key: "bereit", label: "Bereit" },
{ key: "in_arbeit", label: "In Arbeit" },
{ key: "qa", label: "QA" },
{ key: "testing", label: "Testing" },
{ key: "fertig", label: "Fertig" },
{ key: "done", label: "Done" },
];
// Rollen exakt wie API.md ("Agenten", F1/F2). "user" hat nie einen Schreibtisch.
export const ROLES = {
entwickler: { label: "Entwickler:in", accentColor: "#3b82f6", deco: "star" },
product_owner: { label: "Product Owner:in", accentColor: "#ec4899", deco: "flower" },
qa: { label: "QA / Tester:in", accentColor: "#22c55e", deco: "check" },
user: { label: "User", accentColor: "#f59e0b", deco: "none" },
};
export const MAX_VISIBLE_NOTES_PER_COLUMN = 4;
export const MAX_SYSTEM_TILES = 9;
export const MAX_EVENT_LINES = 5;
export const DEFAULT_DESK_COUNT = 8;
/**
* Baut aus rohen Ticket-Zaehlern pro Spalte die Darstellung fuer das Board.
* Regel (Detailantworten F3): mehr als 5 Tickets in einer Spalte -> die aeltesten 4 werden
* einzeln dargestellt, der Rest als ein "Stapel" mit der Anzahl der zusaetzlichen Tickets.
* Regel (Detailantworten F2): die Spalte "done" wird immer als ein Stapel (Zeit-Historie)
* dargestellt, unabhaengig von der Anzahl.
*
* @param {Record<string, number>} ticketCounts - Anzahl Tickets je Spalten-Key.
* @returns {Array<{key: string, label: string, count: number, visibleNotes: number, stackCount: number}>}
*/
export function buildBoardColumnLayout(ticketCounts = {}) {
return BOARD_COLUMNS.map(({ key, label }) => {
const count = Math.max(0, ticketCounts[key] ?? 0);
if (key === "done") {
return { key, label, count, visibleNotes: 0, stackCount: count };
}
if (count <= MAX_VISIBLE_NOTES_PER_COLUMN + 1) {
return { key, label, count, visibleNotes: count, stackCount: 0 };
}
return {
key,
label,
count,
visibleNotes: MAX_VISIBLE_NOTES_PER_COLUMN,
stackCount: count - MAX_VISIBLE_NOTES_PER_COLUMN,
};
});
}
/**
* Ordnet Agenten-Demo-Daten den Schreibtischen zu. Unbesetzte Schreibtische bleiben leer.
* Ueberzaehlige Agenten (mehr als deskCount) werden ignoriert - Phase 2 zeigt nur den
* statischen Grundzustand, die "weitere Agenten kommen durch die Tuer"-Logik ist Teil von
* Phase 5 (Animation).
*
* @param {Array<{name: string, role: string, pose?: string}>} agents
* @param {number} deskCount
* @returns {Array<{deskIndex: number, agent: object|null}>}
*/
export function buildDeskLayout(agents = [], deskCount = DEFAULT_DESK_COUNT) {
const desks = [];
for (let deskIndex = 0; deskIndex < deskCount; deskIndex++) {
const agent = agents[deskIndex] ?? null;
desks.push({ deskIndex, agent: agent ? { pose: "idle", ...agent } : null });
}
return desks;
}
/**
* Bereitet Systemkacheln fuer den System-Monitor auf: Fehler zuerst (wie GET /api/v1/systems),
* maximal MAX_SYSTEM_TILES Kacheln (F6).
*
* @param {Array<{id: string, name: string, status: "ok"|"error"}>} systems
* @returns {Array<object>}
*/
export function buildSystemTiles(systems = []) {
const sorted = [...systems].sort((a, b) => {
if (a.status === b.status) return 0;
return a.status === "error" ? -1 : 1;
});
return sorted.slice(0, MAX_SYSTEM_TILES);
}
/**
* Bereitet die Zeilen des Event-Monitors auf: neuestes Event zuerst, maximal
* MAX_EVENT_LINES Zeilen (F5). Erwartet bereits absteigend nach Zeit sortierte Events
* (wie GET /api/v1/events sie liefert).
*
* @param {Array<{severity: "warning"|"error"}>} events
* @returns {Array<object>}
*/
export function buildEventLines(events = []) {
return events.slice(0, MAX_EVENT_LINES);
}