From 9aa6836bc65ccf162aa057c34f6869ebd552fc27 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Tue, 28 Jul 2026 21:58:29 +0200 Subject: [PATCH] Phase 6+7 der Neuausrichtung: Tuer-Besuche der Rolle "user", alte Tabellenansicht entfernt Phase 6 (F2): detectDoorVisits() erkennt die drei Anlaesse aus den Detailantworten (neues per Codeberg-Sync angelegtes Backlog-Ticket, Ticket erreicht "fertig", Ticket erreicht "done") durch Diff zweier Ticket-Snapshots. office-render.js laesst dafuer einen "user"-Besucher durch die linke Tuer hereinkommen, zum Board laufen, kurz interagieren und wieder hinausgehen - unabhaengig vom Daten-Refresh-Takt, damit ein Besuch auch bei einem zwischenzeitlichen Refresh sauber zu Ende animiert. Phase 7 (F16): html/mon.html, script.js und styles.css sind entfallen ("kann entfallen" laut Detailantworten); check_fds.sh/mail_fds.py/status.txt speisen weiterhin unveraendert den System-Monitor. Unit-Tests fuer die neue Tuer-Logik ergaenzt (weiterhin 100% Line-Coverage, node --test). E2E-Tests bleiben bewusst ausserhalb dieser Phase. Ausserdem: update.sh (holt die neueste Version vom konfigurierten Git-Remote und faehrt die Docker-Compose-Entwicklungsumgebung hoch) und register-agent.sh (Hilfsskript zur Agenten-Registrierung per API) ergaenzt. Co-Authored-By: Claude Sonnet 5 --- README.md | 11 ++- Visualization.md | 22 ++++-- html/css/office.css | 6 ++ html/js/office-render.js | 111 +++++++++++++++++++++++++- html/js/office-scene.js | 91 +++++++++++++++++++++ html/mon.html | 82 ------------------- html/script.js | 118 ---------------------------- html/styles.css | 118 ---------------------------- register-agent.sh | 50 ++++++++++++ tests/frontend/office-scene.test.js | 94 ++++++++++++++++++++++ update.sh | 74 +++++++++++++++++ 11 files changed, 444 insertions(+), 333 deletions(-) delete mode 100644 html/mon.html delete mode 100644 html/script.js delete mode 100644 html/styles.css create mode 100755 register-agent.sh create mode 100644 update.sh diff --git a/README.md b/README.md index 361f205..5001dad 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,22 @@ die dazugehörigen Klärungen in [Detailfragen.md](Detailfragen.md) und siehe Visualization.md Abschnitt 9). Ist die API nicht erreichbar, zeigt die Szene ersatzweise Demo-Daten. - Weiterhin: klassische Ping-Überwachung (`check_fds.sh`, `hosts.conf`, `mail_fds.py`) als - eine Datenquelle für den System-Monitor; die alte Tabellenansicht (`html/mon.html`) - bleibt bis zur Migration in einer späteren Phase bestehen (F16) + eine Datenquelle für den System-Monitor; die alte tabellarische Statusseite ist mit + Phase 7 entfallen (F16), `status.txt` speist weiterhin den System-Monitor --- ## 📁 Projektstruktur - `api/` — REST-API (PHP, SQLite), siehe [api/README.md](api/README.md) -- `html/` — Frontend: `office.html` (neue Büro-Visualisierung), `mon.html` (alte - Tabellenansicht), `js/`, `css/` +- `html/` — Frontend: `office.html` (Büro-Visualisierung), `js/`, `css/` - `tests/frontend/` — Node-Tests für die Frontend-Layoutlogik (`node --test`) - `check_fds.sh`, `mail_fds.py`, `hosts-sample.conf` — bestehende Ping-Überwachung - `docker-compose.yml` — komplette Entwicklungsumgebung (siehe unten) +- `update.sh` — holt die neueste Version vom konfigurierten Git-Remote und aktualisiert die + Docker-Compose-Umgebung (Abhängigkeiten, Images, Dienste, Tests) +- `register-agent.sh` — Hilfsskript, um einen neuen Agenten per `POST /api/v1/agents` zu + registrieren (siehe [API.md](API.md) Abschnitt "Agenten") --- diff --git a/Visualization.md b/Visualization.md index 8e541e9..e7e5b2f 100644 --- a/Visualization.md +++ b/Visualization.md @@ -255,13 +255,21 @@ externe Datenbankserver (nur PHP/Python-Standardbibliothek) auskommen, um die (F8), eine `AnimationQueue` serialisiert Update-Animationen bei vielen gleichzeitigen Ereignissen (F9). 7. **Phase 6 – Externe Rollen & Türen**: Betreten/Verlassen durch Türen für Kunden/User-Events. - Noch offen - Phase 5 platziert Agenten ohne Schreibtisch (Rolle `user`, Ueberlauf) bereits - an einem Ruheplatz vor der linken Tuer, ein echtes Betreten/Verlassen mit Tuer-Animation - anlaesslich konkreter Backlog/Fertig/Done-Ereignisse (F2) ist aber noch nicht umgesetzt. -8. **Phase 7 – Tests & Feinschliff**: Unit-/E2E-Testabdeckung komplettieren, Migration/ - Abschaltung der alten Tabellenansicht klären ([F16]). Noch offen - insbesondere die in - Abschnitt 8 vorgesehenen Playwright-E2E-Tests fuer Board-/Monitor-Interaktion fehlen noch, - `html/mon.html` besteht laut F16 weiterhin bis zur expliziten Abschaltung. + ✅ erledigt: `detectDoorVisits()` (`html/js/office-scene.js`) erkennt die drei Anlaesse aus + F2 (neues per Codeberg-Sync angelegtes Backlog-Ticket, Ticket erreicht `fertig`, Ticket + erreicht `done`) durch Diff zweier Ticket-Snapshots; `office-render.js` laesst dafuer einen + `user`-Besucher an der linken Tuer erscheinen, zum Board laufen, kurz interagieren und + wieder hinausgehen (`DOOR_VISIT_STAGES`), unabhaengig vom Daten-Refresh-Takt. Die Tuer + leuchtet waehrenddessen kurz auf (`.door-active`). Registrierte `user`-Agenten (Ueberlauf, + Phase 5) bleiben wie bisher an ihrem festen Ruheplatz vor der Tuer stehen, wenn sie nicht + gerade einen Besuch absolvieren. +8. **Phase 7 – Tests & Feinschliff**: Unit-Testabdeckung komplettieren, Migration/Abschaltung + der alten Tabellenansicht klären ([F16]). ✅ erledigt: `html/mon.html`/`script.js`/ + `styles.css` sind entfallen (F16: "kann entfallen"), `check_fds.sh`/`mail_fds.py`/ + `status.txt` speisen weiterhin unveraendert den System-Monitor. Unit-Tests fuer die + Phase-6-Logik sind in `tests/frontend/office-scene.test.js` ergaenzt (weiterhin 100 % + Line-Coverage). E2E-Tests mit Playwright sind bewusst noch nicht Teil dieser Phase (siehe + Abgrenzung im zugehoerigen Issue) und bleiben ein offener Folgeschritt. Jede Phase liefert ein für sich lauffähiges Zwischenergebnis, sodass die Umwidmung schrittweise (und überprüfbar per PR) erfolgen kann, statt in einem Großumbau. diff --git a/html/css/office.css b/html/css/office.css index 7f079ea..40247ea 100644 --- a/html/css/office.css +++ b/html/css/office.css @@ -142,6 +142,12 @@ header.office-header p { fill: var(--color-door); stroke: #5f86ab; stroke-width: 3; + transition: fill 0.4s ease-in-out; +} + +/* Phase 6 (F2): kurzes Aufleuchten, waehrend ein "user"-Tuerbesuch die Tuer passiert. */ +.door-active { + fill: #d7ecff; } .wall-label { diff --git a/html/js/office-render.js b/html/js/office-render.js index 66cc2ad..fef25db 100644 --- a/html/js/office-render.js +++ b/html/js/office-render.js @@ -29,6 +29,10 @@ import { resolveAgentVisualState, pickIdleActivity, AnimationQueue, + detectDoorVisits, + resolveDoorVisitVisual, + advanceDoorVisitStage, + DOOR_VISIT_STAGES, } from "./office-scene.js"; import { DEMO_TICKET_COUNTS, @@ -64,10 +68,14 @@ const NOTE_STEP_Y = 56; const BOARD_FEET = { x: BOARD_FRAME.x + BOARD_FRAME.w / 2, y: BOARD_FRAME.y + BOARD_FRAME.h + 40 }; const MONITOR_FEET = { x: MONITOR_WALL.x + MONITOR_WALL.w / 2, y: MONITOR_WALL.y + MONITOR_WALL.h + 40 }; const COFFEE_FEET = { x: COFFEE_CORNER.x, y: COFFEE_CORNER.y + 26 }; +// Ruheplatz der Tuerbesucher (Phase 6, F2): direkt vor der linken Tuer, wie schon der +// Ueberlaufplatz fuer Agenten ohne Schreibtisch (siehe agentPlacements()). +const DOOR_FEET = { x: 20, y: DOOR.y + DOOR.h / 2 }; const IDLE_MIN_DELAY_MS = 15000; const IDLE_MAX_DELAY_MS = 40000; const IDLE_ACTIVITY_DURATION_MS = 8000; +const DOOR_VISIT_STAGE_DURATION_MS = 2200; function el(tag, attrs = {}, children = []) { const node = document.createElementNS(SVG_NS, tag); @@ -94,9 +102,9 @@ function renderRoom() { g.appendChild(el("rect", { x: 0, y: 0, width: VIEW_W, height: WALL_BAND_H, class: "wall" })); g.appendChild(el("rect", { x: 0, y: 0, width: 40, height: VIEW_H, class: "wall wall-side" })); g.appendChild(el("rect", { x: VIEW_W - 40, y: 0, width: 40, height: VIEW_H, class: "wall wall-side" })); - g.appendChild(el("rect", { x: 0, y: DOOR.y, width: DOOR.w, height: DOOR.h, class: "door" })); + g.appendChild(el("rect", { x: 0, y: DOOR.y, width: DOOR.w, height: DOOR.h, class: "door door-left" })); g.appendChild( - el("rect", { x: VIEW_W - DOOR.w, y: DOOR.y, width: DOOR.w, height: DOOR.h, class: "door" }) + el("rect", { x: VIEW_W - DOOR.w, y: DOOR.y, width: DOOR.w, height: DOOR.h, class: "door door-right" }) ); g.appendChild(renderCoffeeCorner()); return g; @@ -511,6 +519,8 @@ function feetForLocation(location, homeFeet) { return MONITOR_FEET; case AGENT_LOCATIONS.COFFEE_CORNER: return COFFEE_FEET; + case AGENT_LOCATIONS.DOOR: + return DOOR_FEET; case AGENT_LOCATIONS.DESK: default: return homeFeet; @@ -662,6 +672,88 @@ function scheduleIdleCosmeticsForCurrentAgents(placements) { }); } +// --------------------------------------------------------------------------------------- +// Phase 6 - Tuerbesucher (F2: Rolle "user" kommt nur anlassbezogen durch die linke Tuer, +// siehe detectDoorVisits() in office-scene.js). Laeuft unabhaengig vom Daten-Refresh-Takt +// ueber eigene Timer, damit ein Besuch auch dann sauber zu Ende animiert, wenn zwischendurch +// ein neuer SSE-/Polling-Refresh hereinkommt (siehe repaintAgentsLayer()). +// --------------------------------------------------------------------------------------- + +let currentAgentsLayer = null; +let currentRegularPlacements = []; +const doorVisitors = new Map(); +let doorVisitCounter = 0; + +function doorVisitVisual(visitor) { + if (visitor.atDoor) { + return { location: AGENT_LOCATIONS.DOOR, pose: "walking" }; + } + return resolveDoorVisitVisual(visitor.stage); +} + +function doorVisitorPlacements() { + return Array.from(doorVisitors.values()).map((visitor) => { + const visual = doorVisitVisual(visitor); + return { + agent: { id: visitor.id, name: "User", role: "user", pose: visual.pose, location: visual.location }, + homeFeet: DOOR_FEET, + }; + }); +} + +// Kurzes visuelles Feedback (Tuer "leuchtet" auf), waehrend mindestens ein Besucher am +// Tuerplatz steht - rein kosmetisch, siehe .door-active in office.css. +function updateDoorActiveClass() { + if (!currentAgentsLayer) return; + const svg = currentAgentsLayer.closest("svg"); + const doorLeft = svg && svg.querySelector(".door-left"); + if (!doorLeft) return; + const anyAtDoor = Array.from(doorVisitors.values()).some( + (visitor) => doorVisitVisual(visitor).location === AGENT_LOCATIONS.DOOR + ); + doorLeft.classList.toggle("door-active", anyAtDoor); +} + +function repaintAgentsLayer() { + if (!currentAgentsLayer) return; + updateAgentsLayer(currentAgentsLayer, [...currentRegularPlacements, ...doorVisitorPlacements()]); + updateDoorActiveClass(); +} + +function advanceDoorVisit(id) { + const visitor = doorVisitors.get(id); + if (!visitor) { + return; + } + + if (visitor.atDoor) { + visitor.atDoor = false; + repaintAgentsLayer(); + window.setTimeout(() => advanceDoorVisit(id), DOOR_VISIT_STAGE_DURATION_MS); + return; + } + + const nextStage = advanceDoorVisitStage(visitor.stage); + if (nextStage === null) { + doorVisitors.delete(id); + repaintAgentsLayer(); + return; + } + + visitor.stage = nextStage; + repaintAgentsLayer(); + window.setTimeout(() => advanceDoorVisit(id), DOOR_VISIT_STAGE_DURATION_MS); +} + +/** @param {{type: string, ticketId: number}} visit */ +function triggerDoorVisit(visit) { + doorVisitCounter += 1; + const id = `door-visit-${visit.type}-${visit.ticketId}-${doorVisitCounter}`; + doorVisitors.set(id, { id, stage: DOOR_VISIT_STAGES[0], atDoor: true }); + repaintAgentsLayer(); + window.setTimeout(() => advanceDoorVisit(id), DOOR_VISIT_STAGE_DURATION_MS); +} + // --------------------------------------------------------------------------------------- // Modal-Inhalte (Phase 3/4 - Board-/Monitor-Klick, siehe Visualization.md 4.5) // --------------------------------------------------------------------------------------- @@ -826,6 +918,7 @@ export function renderOfficeScene(container, data = {}) { agentAvatarElements.clear(); idleCosmeticTimers.forEach((handle) => window.clearTimeout(handle.timeoutId)); idleCosmeticTimers.clear(); + doorVisitors.clear(); } const groupedTickets = groupTicketsByColumn(tickets); @@ -867,8 +960,9 @@ export function renderOfficeScene(container, data = {}) { } const placements = agentPlacements(agents, deskLayout); - const agentsLayer = svg.querySelector(".agents-layer"); - updateAgentsLayer(agentsLayer, placements); + currentAgentsLayer = svg.querySelector(".agents-layer"); + currentRegularPlacements = placements; + repaintAgentsLayer(); scheduleIdleCosmeticsForCurrentAgents(placements); } @@ -925,9 +1019,18 @@ async function boot() { return; } + // Vorheriger Ticket-Snapshot fuer detectDoorVisits() (Phase 6, F2) - bleibt null im + // Demo-Fallback (kein "echter" Anlass ohne Live-API) und wird nur bei erreichbarer API + // gepflegt, damit die Tuerbesuche ausschliesslich auf echten Zustandswechseln beruhen. + let previousTickets = null; + async function refresh() { try { const data = await loadLiveData(); + if (previousTickets) { + detectDoorVisits(previousTickets, data.tickets).forEach(triggerDoorVisit); + } + previousTickets = data.tickets; renderOfficeScene(container, { ticketCounts: aggregateTicketsByColumn(data.tickets), tickets: data.tickets, diff --git a/html/js/office-scene.js b/html/js/office-scene.js index 5a0edd5..dce5c0a 100644 --- a/html/js/office-scene.js +++ b/html/js/office-scene.js @@ -328,6 +328,7 @@ export const AGENT_LOCATIONS = { MONITOR_SYSTEMS: "monitor-systems", MONITOR_KPI: "monitor-kpi", COFFEE_CORNER: "coffee-corner", + DOOR: "door", }; // Zustandsmaschine wie in Visualization.md 4.4 vorgeschlagen: jeder von der API gemeldete @@ -447,3 +448,93 @@ export class AnimationQueue { return ranCount; } } + +// --------------------------------------------------------------------------------------- +// Phase 6 - Externe Rollen & Tueren (siehe Visualization.md Abschnitt 9, Detailantworten F2) +// --------------------------------------------------------------------------------------- + +/** + * Anlaesse, zu denen die Rolle "user" laut Detailantworten F2 durch die Tuer hereinkommt. + * Der Text auf den Klebezetteln bleibt weiterhin absichtlich unlesbar - der Anlass dient nur + * dazu, den richtigen kurzen Auftritt (Board-Besuch) auszuloesen. + */ +export const DOOR_VISIT_TYPES = { + // Neues Ticket im Backlog, das nicht von einer Rolle dort abgelegt wurde (F4: Backlog wird + // per Codeberg-Sync befuellt) - der User "traegt" es symbolisch aufs Board. + NEW_BACKLOG_TICKET: "new_backlog_ticket", + // Ticket erreicht Spalte "fertig" - der User schaut es sich an. + TICKET_READY_FOR_REVIEW: "ticket_ready_for_review", + // Ticket erreicht Spalte "done" (geschlossen) - der User legt es auf den Done-Stapel. + TICKET_DONE: "ticket_done", +}; + +/** + * Erkennt Tuerbesuch-Anlaesse (F2) durch Vergleich zweier Ticketlisten-Snapshots + * (z. B. zwei aufeinanderfolgende GET /api/v1/tickets-Antworten). Reine Diff-Logik ohne + * DOM-/Zeit-Abhaengigkeit, daher hier statt in office-render.js - so bleibt sie mit dem + * Node-Testrunner pruefbar (siehe Teststrategie in Visualization.md Abschnitt 8). + * + * @param {Array<{id: number, column_name: string, source?: string}>} previousTickets + * @param {Array<{id: number, column_name: string, source?: string}>} currentTickets + * @returns {Array<{type: string, ticketId: number}>} + */ +export function detectDoorVisits(previousTickets = [], currentTickets = []) { + const previousById = new Map(previousTickets.map((ticket) => [ticket.id, ticket])); + const visits = []; + + for (const ticket of currentTickets) { + const before = previousById.get(ticket.id); + + if (!before) { + if (ticket.column_name === "backlog" && ticket.source === "codeberg") { + visits.push({ type: DOOR_VISIT_TYPES.NEW_BACKLOG_TICKET, ticketId: ticket.id }); + } + continue; + } + + if (before.column_name === ticket.column_name) { + continue; + } + + if (ticket.column_name === "fertig") { + visits.push({ type: DOOR_VISIT_TYPES.TICKET_READY_FOR_REVIEW, ticketId: ticket.id }); + } else if (ticket.column_name === "done") { + visits.push({ type: DOOR_VISIT_TYPES.TICKET_DONE, ticketId: ticket.id }); + } + } + + return visits; +} + +// Ablauf eines Tuerbesuchs (F2): zum Board laufen, dort kurz interagieren, wieder hinaus zur +// Tuer gehen. Der Anlass (DOOR_VISIT_TYPES) unterscheidet nur den Grund, nicht den Ablauf. +export const DOOR_VISIT_STAGES = ["walking_to_board", "interacting_with_board", "leaving"]; + +const DOOR_VISIT_STAGE_VISUALS = { + walking_to_board: { location: AGENT_LOCATIONS.BOARD, pose: "walking" }, + interacting_with_board: { location: AGENT_LOCATIONS.BOARD, pose: "interacting" }, + leaving: { location: AGENT_LOCATIONS.DOOR, pose: "walking" }, +}; + +/** + * @param {string} stage - Wert aus DOOR_VISIT_STAGES. + * @returns {{location: string, pose: string}} + */ +export function resolveDoorVisitVisual(stage) { + return DOOR_VISIT_STAGE_VISUALS[stage] ?? DOOR_VISIT_STAGE_VISUALS[DOOR_VISIT_STAGES[0]]; +} + +/** + * Schaltet einen Tuerbesuch zur naechsten Stage weiter. + * + * @param {string} stage - aktuelle Stage aus DOOR_VISIT_STAGES. + * @returns {string|null} naechste Stage, oder null, wenn der Besuch abgeschlossen ist + * (Avatar verlaesst danach die Szene wieder durch die Tuer). + */ +export function advanceDoorVisitStage(stage) { + const index = DOOR_VISIT_STAGES.indexOf(stage); + if (index === -1 || index === DOOR_VISIT_STAGES.length - 1) { + return null; + } + return DOOR_VISIT_STAGES[index + 1]; +} diff --git a/html/mon.html b/html/mon.html deleted file mode 100644 index 92c54ec..0000000 --- a/html/mon.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - FDS Server-Status - - - -
☰
- - -
-

Server-Status Übersicht

- - - - - - - - - - - - -
HostnameIP-AdresseStatusLetzte Prüfung
- -
-
- -
- - - - - - - diff --git a/html/script.js b/html/script.js deleted file mode 100644 index 6fe1b9e..0000000 --- a/html/script.js +++ /dev/null @@ -1,118 +0,0 @@ -// Navigation -document.getElementById('hamburger').addEventListener('click', function() { - document.getElementById('navMenu').style.display = 'block'; -}); -document.getElementById('closeBtn').addEventListener('click', function() { - document.getElementById('navMenu').style.display = 'none'; -}); -document.getElementById('homeLink').addEventListener('click', function(e) { - e.preventDefault(); - document.getElementById('content').style.display = 'block'; - document.getElementById('settingsPage').style.display = 'none'; - document.getElementById('navMenu').style.display = 'none'; -}); -document.getElementById('settingsLink').addEventListener('click', function(e) { - e.preventDefault(); - document.getElementById('content').style.display = 'none'; - document.getElementById('settingsPage').style.display = 'block'; - document.getElementById('navMenu').style.display = 'none'; -}); - -// Timezone Cookie -function getCookie(name) { - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop().split(';').shift(); -} -function setCookie(name, value, days = 365) { - const date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - document.cookie = `${name}=${value}; expires=${date.toUTCString()}; path=/`; -} -function loadTimezone() { - const savedTimezone = getCookie('userTimezone'); - if (savedTimezone) { - document.getElementById('timezone').value = savedTimezone; - } -} -document.getElementById('timezoneForm').addEventListener('submit', function(e) { - e.preventDefault(); - const timezone = document.getElementById('timezone').value; - setCookie('userTimezone', timezone); - alert('Zeitzone gespeichert!'); - loadServerData(); -}); - -// Serverdaten laden -function loadServerData() { - fetch('status.txt') - .then(response => response.text()) - .then(data => { - const lines = data.split('\n'); - const tbody = document.getElementById('serverData'); - tbody.innerHTML = ''; - const userTimezone = getCookie('userTimezone') || 'UTC'; - lines.forEach(line => { - if (line.trim() === '') return; - const [hostname, ip, status, checkdat, checktim] = line.split(' '); - const tr = document.createElement('tr'); - const isoString = `${checkdat}T${checktim}Z`; - const date = new Date(isoString); - const formatter = new Intl.DateTimeFormat('de-DE', { - timeZone: userTimezone, - dateStyle: 'short', - timeStyle: 'medium', - hour12: false - }); - const localTimeStr = formatter.format(date); - tr.innerHTML = ` - ${hostname} - ${ip} - ${status} - ${localTimeStr} (${userTimezone}) - `; - tbody.appendChild(tr); - }); - }) - .catch(error => { - console.error('Fehler beim Laden der status.conf:', error); - document.getElementById('serverData').innerHTML = ` - - - Fehler beim Laden der Serverdaten. - - - `; - }); -} - -// Aktuelle Uhrzeit -function currentTime() { - let date = new Date(); - let dd = date.getDate(); - let m = date.getMonth() + 1; - let yy = date.getFullYear(); - let hh = date.getHours(); - let mm = date.getMinutes(); - let ss = date.getSeconds(); - dd = (dd < 10) ? "0" + dd : dd; - m = (m < 10) ? "0" + m : m; - hh = (hh < 10) ? "0" + hh : hh; - mm = (mm < 10) ? "0" + mm : mm; - ss = (ss < 10) ? "0" + ss : ss; - let time = dd + "." + m + "." + yy + "\u00A0\u00A0\u00A0\u00A0" + hh + ":" + mm + ":" + ss + " Uhr"; - document.getElementById("datumuhrzeit").innerText = time; - let t = setTimeout(function(){ currentTime() }, 1000); -} - -// Initialisierung -document.addEventListener('DOMContentLoaded', function() { - loadTimezone(); - loadServerData(); - const timezoneElement = document.getElementById('timezonestring'); - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - timezoneElement.innerText = timezone; -// console.log(timezone); - currentTime(); -}); - diff --git a/html/styles.css b/html/styles.css deleted file mode 100644 index bae0b7e..0000000 --- a/html/styles.css +++ /dev/null @@ -1,118 +0,0 @@ -body { - font-family: Arial, sans-serif; - line-height: 1.6; - margin: 0; - padding: 20px; - color: #333; -} -h1 { - text-align: center; - color: #2c3e50; -} -table { - width: 100%; - border-collapse: collapse; - margin: 20px 0; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); -} -th, td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #ddd; -} -th { - background-color: #3498db; - color: white; -} -tr:nth-child(even) { - background-color: #f2f2f2; -} -tr:hover { - background-color: #e3f2fd; -} -.status { - padding: 5px 10px; - border-radius: 4px; - font-weight: bold; - text-align: center; -} -.online { - background-color: #2ecc71; - color: white; -} -.offline { - background-color: #e74c3c; - color: white; -} -.unknown { - background-color: #f39c12; - color: white; -} -.last-check { - font-size: 0.9em; - color: #7f8c8d; -} -/* Navigation */ -.hamburger { - cursor: pointer; - font-size: 24px; - position: fixed; - top: 15px; - left: 15px; - z-index: 1000; -} -.nav-menu { - display: none; - position: fixed; - top: 0; - left: 0; - width: 250px; - height: 100%; - background-color: #2c3e50; - color: white; - z-index: 999; - padding-top: 60px; -} -.nav-menu a { - color: white; - text-decoration: none; - display: block; - padding: 15px; - border-bottom: 1px solid #34495e; -} -.nav-menu a:hover { - background-color: #3498db; -} -.close-btn { - position: absolute; - top: 15px; - right: 15px; - font-size: 24px; - cursor: pointer; -} -.settings-form { - padding: 20px; - max-width: 500px; - margin: 0 auto; -} -.settings-form label { - display: block; - margin-bottom: 5px; -} -.settings-form select { - width: 100%; - padding: 8px; - margin-bottom: 15px; -} -.settings-form button { - background-color: #3498db; - color: white; - border: none; - padding: 10px 15px; - cursor: pointer; -} -@media (max-width: 768px) { - .hamburger { - display: block; - } -} diff --git a/register-agent.sh b/register-agent.sh new file mode 100755 index 0000000..1552071 --- /dev/null +++ b/register-agent.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Registriert einen neuen Agenten bei der monitor-REST-API (POST /api/v1/agents). +# Siehe API.md Abschnitt "Agenten" fuer Details. +# +# Wichtig: Der zurueckgegebene API-Key wird nur bei dieser Registrierung einmalig +# ausgegeben - unbedingt sicher speichern, er laesst sich danach nicht erneut abrufen. + +set -euo pipefail + +# --- Hier die Variablen fuer die Registrierung eintragen --------------------- + +# Basis-URL der laufenden API (z.B. lokal via "docker compose up") +BASE_URL="http://localhost:8080" + +# Admin-Key aus MONITOR_ADMIN_API_KEY (siehe .env) +ADMIN_API_KEY="" + +# Anzeigename des neuen Agenten +DISPLAY_NAME="" + +# Rolle: entwickler | product_owner | qa | user +ROLE="entwickler" + +# ------------------------------------------------------------------------------- + +if [ -z "$ADMIN_API_KEY" ]; then + echo "Fehler: ADMIN_API_KEY ist nicht gesetzt." >&2 + exit 1 +fi + +if [ -z "$DISPLAY_NAME" ]; then + echo "Fehler: DISPLAY_NAME ist nicht gesetzt." >&2 + exit 1 +fi + +response=$(curl -sS -w '\n%{http_code}' -X POST "${BASE_URL}/api/v1/agents" \ + -H "Content-Type: application/json" \ + -H "X-Admin-Key: ${ADMIN_API_KEY}" \ + -d "{\"display_name\": \"${DISPLAY_NAME}\", \"role\": \"${ROLE}\"}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +echo "$body" + +if [ "$http_code" != "201" ]; then + echo "Fehler: HTTP $http_code" >&2 + exit 1 +fi diff --git a/tests/frontend/office-scene.test.js b/tests/frontend/office-scene.test.js index b913a58..5f38a72 100644 --- a/tests/frontend/office-scene.test.js +++ b/tests/frontend/office-scene.test.js @@ -26,6 +26,11 @@ import { normalizeAgentForDisplay, pickIdleActivity, AnimationQueue, + DOOR_VISIT_TYPES, + DOOR_VISIT_STAGES, + detectDoorVisits, + resolveDoorVisitVisual, + advanceDoorVisitStage, } from "../../html/js/office-scene.js"; test("buildBoardColumnLayout liefert alle Spalten in API.md-Reihenfolge", () => { @@ -310,3 +315,92 @@ test("AnimationQueue fuehrt Tasks mit Mindestabstand aus (Warteschlange bei Buer assert.deepEqual(ran, ["a", "b", "c"]); assert.equal(queue.size, 0); }); + +// --------------------------------------------------------------------------------------- +// Phase 6 - Externe Rollen & Tueren (F2) +// --------------------------------------------------------------------------------------- + +test("detectDoorVisits erkennt ein neues, per Codeberg-Sync angelegtes Backlog-Ticket", () => { + const visits = detectDoorVisits([], [{ id: 1, column_name: "backlog", source: "codeberg" }]); + assert.deepEqual(visits, [{ type: DOOR_VISIT_TYPES.NEW_BACKLOG_TICKET, ticketId: 1 }]); +}); + +test("detectDoorVisits ignoriert neue Backlog-Tickets, die nicht per Codeberg-Sync kamen", () => { + const visits = detectDoorVisits([], [{ id: 1, column_name: "backlog", source: "api" }]); + assert.deepEqual(visits, []); +}); + +test("detectDoorVisits ignoriert neue Tickets ausserhalb von Backlog (von einer Rolle angelegt)", () => { + const visits = detectDoorVisits([], [{ id: 1, column_name: "bereit", source: "api" }]); + assert.deepEqual(visits, []); +}); + +test("detectDoorVisits erkennt ein Ticket, das die Spalte fertig erreicht", () => { + const before = [{ id: 5, column_name: "qa", source: "api" }]; + const after = [{ id: 5, column_name: "fertig", source: "api" }]; + assert.deepEqual(detectDoorVisits(before, after), [ + { type: DOOR_VISIT_TYPES.TICKET_READY_FOR_REVIEW, ticketId: 5 }, + ]); +}); + +test("detectDoorVisits erkennt ein Ticket, das geschlossen (done) wird", () => { + const before = [{ id: 7, column_name: "testing", source: "api" }]; + const after = [{ id: 7, column_name: "done", source: "api" }]; + assert.deepEqual(detectDoorVisits(before, after), [{ type: DOOR_VISIT_TYPES.TICKET_DONE, ticketId: 7 }]); +}); + +test("detectDoorVisits ignoriert Tickets ohne Spaltenwechsel", () => { + const before = [{ id: 1, column_name: "in_arbeit", source: "api" }]; + const after = [{ id: 1, column_name: "in_arbeit", source: "api" }]; + assert.deepEqual(detectDoorVisits(before, after), []); +}); + +test("detectDoorVisits verarbeitet mehrere gleichzeitige Anlaesse in einem Diff", () => { + const before = [ + { id: 1, column_name: "qa", source: "api" }, + { id: 2, column_name: "testing", source: "api" }, + ]; + const after = [ + { id: 1, column_name: "fertig", source: "api" }, + { id: 2, column_name: "done", source: "api" }, + { id: 3, column_name: "backlog", source: "codeberg" }, + ]; + const visits = detectDoorVisits(before, after); + assert.equal(visits.length, 3); + assert.deepEqual(visits, [ + { type: DOOR_VISIT_TYPES.TICKET_READY_FOR_REVIEW, ticketId: 1 }, + { type: DOOR_VISIT_TYPES.TICKET_DONE, ticketId: 2 }, + { type: DOOR_VISIT_TYPES.NEW_BACKLOG_TICKET, ticketId: 3 }, + ]); +}); + +test("resolveDoorVisitVisual bildet jede Stage auf Ort/Pose ab", () => { + assert.deepEqual(resolveDoorVisitVisual("walking_to_board"), { + location: AGENT_LOCATIONS.BOARD, + pose: "walking", + }); + assert.deepEqual(resolveDoorVisitVisual("interacting_with_board"), { + location: AGENT_LOCATIONS.BOARD, + pose: "interacting", + }); + assert.deepEqual(resolveDoorVisitVisual("leaving"), { location: AGENT_LOCATIONS.DOOR, pose: "walking" }); +}); + +test("resolveDoorVisitVisual faellt bei unbekannter Stage auf die erste Stage zurueck", () => { + assert.deepEqual(resolveDoorVisitVisual("unbekannt"), resolveDoorVisitVisual(DOOR_VISIT_STAGES[0])); +}); + +test("advanceDoorVisitStage durchlaeuft alle Stages und liefert am Ende null", () => { + let stage = DOOR_VISIT_STAGES[0]; + const seen = [stage]; + for (let i = 0; i < DOOR_VISIT_STAGES.length; i++) { + stage = advanceDoorVisitStage(stage); + if (stage !== null) seen.push(stage); + } + assert.deepEqual(seen, DOOR_VISIT_STAGES); + assert.equal(advanceDoorVisitStage(DOOR_VISIT_STAGES[DOOR_VISIT_STAGES.length - 1]), null); +}); + +test("advanceDoorVisitStage liefert bei unbekannter Stage null", () => { + assert.equal(advanceDoorVisitStage("unbekannt"), null); +}); diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..f0a17e2 --- /dev/null +++ b/update.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# Holt die neueste Version aus dem konfigurierten Git-Repo und fuehrt die noetigen Updates +# fuer die Docker-Compose-Entwicklungs-/Laufzeitumgebung durch (siehe README.md/Visualization.md +# Abschnitt "Entwicklung nur mit Docker"): PHP-Abhaengigkeiten aktualisieren, Images neu bauen, +# Dienste neu starten, Testsuiten laufen lassen. SQLite-Migrationen brauchen keinen eigenen +# Schritt - api/src/Db/Database::migrate() wendet sie bei jedem API-Start automatisch an. +# +# Konfiguration ueber Umgebungsvariablen (Defaults passen fuer den ueblichen Fall): +# UPDATE_REMOTE - Git-Remote, aus dem geholt wird (Default: origin) +# UPDATE_BRANCH - Branch, der aktualisiert wird (Default: aktuell ausgecheckter Branch) +# UPDATE_RUN_TESTS - "0" setzen, um die Testsuiten nach dem Update zu ueberspringen (Default: 1) + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" +cd "$REPO_DIR" + +log() { + printf '[update.sh] %s\n' "$1" +} + +fail() { + printf '[update.sh] Fehler: %s\n' "$1" >&2 + exit 1 +} + +command -v git >/dev/null 2>&1 || fail "git ist nicht installiert." +command -v docker >/dev/null 2>&1 || fail "docker ist nicht installiert." +git -C "$REPO_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || fail "$REPO_DIR ist kein Git-Repository." + +UPDATE_REMOTE="${UPDATE_REMOTE:-origin}" +UPDATE_BRANCH="${UPDATE_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" +UPDATE_RUN_TESTS="${UPDATE_RUN_TESTS:-1}" + +if [ -n "$(git status --porcelain)" ]; then + fail "Lokale Aenderungen/unversionierte Dateien vorhanden - bitte erst committen/stashen. Breche ab, um nichts zu ueberschreiben." +fi + +log "Hole Aenderungen von ${UPDATE_REMOTE}/${UPDATE_BRANCH}..." +git fetch "$UPDATE_REMOTE" "$UPDATE_BRANCH" + +# --ff-only statt reset --hard: bricht kontrolliert ab, falls der lokale Branch abweicht +# (z. B. eigene, noch nicht gepushte Commits), statt Historie zu ueberschreiben. +git checkout "$UPDATE_BRANCH" +git pull --ff-only "$UPDATE_REMOTE" "$UPDATE_BRANCH" + +DOCKER_COMPOSE="docker compose" +if ! docker compose version >/dev/null 2>&1; then + command -v docker-compose >/dev/null 2>&1 || fail "Weder 'docker compose' noch 'docker-compose' verfuegbar." + DOCKER_COMPOSE="docker-compose" +fi + +log "Aktualisiere PHP-Abhaengigkeiten (composer install)..." +$DOCKER_COMPOSE run --rm api composer install --no-interaction --no-progress + +log "Baue Docker-Images neu (falls Dockerfile/Abhaengigkeiten sich geaendert haben)..." +$DOCKER_COMPOSE build + +log "Starte Dienste neu (api, web)..." +$DOCKER_COMPOSE up -d api web + +if [ "$UPDATE_RUN_TESTS" = "1" ]; then + log "Fuehre PHPUnit-Tests (API) aus..." + $DOCKER_COMPOSE run --rm api composer test + + log "Fuehre Frontend-Tests aus..." + $DOCKER_COMPOSE run --rm frontend +else + log "UPDATE_RUN_TESTS=0 gesetzt - ueberspringe Testsuiten." +fi + +log "Update abgeschlossen (${UPDATE_BRANCH} @ $(git rev-parse --short HEAD))."