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 <noreply@anthropic.com>
This commit is contained in:
parent
f383ce2d33
commit
9aa6836bc6
11 changed files with 444 additions and 333 deletions
11
README.md
11
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
|
siehe Visualization.md Abschnitt 9). Ist die API nicht erreichbar, zeigt die Szene
|
||||||
ersatzweise Demo-Daten.
|
ersatzweise Demo-Daten.
|
||||||
- Weiterhin: klassische Ping-Überwachung (`check_fds.sh`, `hosts.conf`, `mail_fds.py`) als
|
- 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`)
|
eine Datenquelle für den System-Monitor; die alte tabellarische Statusseite ist mit
|
||||||
bleibt bis zur Migration in einer späteren Phase bestehen (F16)
|
Phase 7 entfallen (F16), `status.txt` speist weiterhin den System-Monitor
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 Projektstruktur
|
## 📁 Projektstruktur
|
||||||
|
|
||||||
- `api/` — REST-API (PHP, SQLite), siehe [api/README.md](api/README.md)
|
- `api/` — REST-API (PHP, SQLite), siehe [api/README.md](api/README.md)
|
||||||
- `html/` — Frontend: `office.html` (neue Büro-Visualisierung), `mon.html` (alte
|
- `html/` — Frontend: `office.html` (Büro-Visualisierung), `js/`, `css/`
|
||||||
Tabellenansicht), `js/`, `css/`
|
|
||||||
- `tests/frontend/` — Node-Tests für die Frontend-Layoutlogik (`node --test`)
|
- `tests/frontend/` — Node-Tests für die Frontend-Layoutlogik (`node --test`)
|
||||||
- `check_fds.sh`, `mail_fds.py`, `hosts-sample.conf` — bestehende Ping-Überwachung
|
- `check_fds.sh`, `mail_fds.py`, `hosts-sample.conf` — bestehende Ping-Überwachung
|
||||||
- `docker-compose.yml` — komplette Entwicklungsumgebung (siehe unten)
|
- `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")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,13 +255,21 @@ externe Datenbankserver (nur PHP/Python-Standardbibliothek) auskommen, um die
|
||||||
(F8), eine `AnimationQueue` serialisiert Update-Animationen bei vielen gleichzeitigen
|
(F8), eine `AnimationQueue` serialisiert Update-Animationen bei vielen gleichzeitigen
|
||||||
Ereignissen (F9).
|
Ereignissen (F9).
|
||||||
7. **Phase 6 – Externe Rollen & Türen**: Betreten/Verlassen durch Türen für Kunden/User-Events.
|
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
|
✅ erledigt: `detectDoorVisits()` (`html/js/office-scene.js`) erkennt die drei Anlaesse aus
|
||||||
an einem Ruheplatz vor der linken Tuer, ein echtes Betreten/Verlassen mit Tuer-Animation
|
F2 (neues per Codeberg-Sync angelegtes Backlog-Ticket, Ticket erreicht `fertig`, Ticket
|
||||||
anlaesslich konkreter Backlog/Fertig/Done-Ereignisse (F2) ist aber noch nicht umgesetzt.
|
erreicht `done`) durch Diff zweier Ticket-Snapshots; `office-render.js` laesst dafuer einen
|
||||||
8. **Phase 7 – Tests & Feinschliff**: Unit-/E2E-Testabdeckung komplettieren, Migration/
|
`user`-Besucher an der linken Tuer erscheinen, zum Board laufen, kurz interagieren und
|
||||||
Abschaltung der alten Tabellenansicht klären ([F16]). Noch offen - insbesondere die in
|
wieder hinausgehen (`DOOR_VISIT_STAGES`), unabhaengig vom Daten-Refresh-Takt. Die Tuer
|
||||||
Abschnitt 8 vorgesehenen Playwright-E2E-Tests fuer Board-/Monitor-Interaktion fehlen noch,
|
leuchtet waehrenddessen kurz auf (`.door-active`). Registrierte `user`-Agenten (Ueberlauf,
|
||||||
`html/mon.html` besteht laut F16 weiterhin bis zur expliziten Abschaltung.
|
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
|
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.
|
schrittweise (und überprüfbar per PR) erfolgen kann, statt in einem Großumbau.
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,12 @@ header.office-header p {
|
||||||
fill: var(--color-door);
|
fill: var(--color-door);
|
||||||
stroke: #5f86ab;
|
stroke: #5f86ab;
|
||||||
stroke-width: 3;
|
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 {
|
.wall-label {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ import {
|
||||||
resolveAgentVisualState,
|
resolveAgentVisualState,
|
||||||
pickIdleActivity,
|
pickIdleActivity,
|
||||||
AnimationQueue,
|
AnimationQueue,
|
||||||
|
detectDoorVisits,
|
||||||
|
resolveDoorVisitVisual,
|
||||||
|
advanceDoorVisitStage,
|
||||||
|
DOOR_VISIT_STAGES,
|
||||||
} from "./office-scene.js";
|
} from "./office-scene.js";
|
||||||
import {
|
import {
|
||||||
DEMO_TICKET_COUNTS,
|
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 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 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 };
|
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_MIN_DELAY_MS = 15000;
|
||||||
const IDLE_MAX_DELAY_MS = 40000;
|
const IDLE_MAX_DELAY_MS = 40000;
|
||||||
const IDLE_ACTIVITY_DURATION_MS = 8000;
|
const IDLE_ACTIVITY_DURATION_MS = 8000;
|
||||||
|
const DOOR_VISIT_STAGE_DURATION_MS = 2200;
|
||||||
|
|
||||||
function el(tag, attrs = {}, children = []) {
|
function el(tag, attrs = {}, children = []) {
|
||||||
const node = document.createElementNS(SVG_NS, tag);
|
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: 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: 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: 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(
|
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());
|
g.appendChild(renderCoffeeCorner());
|
||||||
return g;
|
return g;
|
||||||
|
|
@ -511,6 +519,8 @@ function feetForLocation(location, homeFeet) {
|
||||||
return MONITOR_FEET;
|
return MONITOR_FEET;
|
||||||
case AGENT_LOCATIONS.COFFEE_CORNER:
|
case AGENT_LOCATIONS.COFFEE_CORNER:
|
||||||
return COFFEE_FEET;
|
return COFFEE_FEET;
|
||||||
|
case AGENT_LOCATIONS.DOOR:
|
||||||
|
return DOOR_FEET;
|
||||||
case AGENT_LOCATIONS.DESK:
|
case AGENT_LOCATIONS.DESK:
|
||||||
default:
|
default:
|
||||||
return homeFeet;
|
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)
|
// Modal-Inhalte (Phase 3/4 - Board-/Monitor-Klick, siehe Visualization.md 4.5)
|
||||||
// ---------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
@ -826,6 +918,7 @@ export function renderOfficeScene(container, data = {}) {
|
||||||
agentAvatarElements.clear();
|
agentAvatarElements.clear();
|
||||||
idleCosmeticTimers.forEach((handle) => window.clearTimeout(handle.timeoutId));
|
idleCosmeticTimers.forEach((handle) => window.clearTimeout(handle.timeoutId));
|
||||||
idleCosmeticTimers.clear();
|
idleCosmeticTimers.clear();
|
||||||
|
doorVisitors.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedTickets = groupTicketsByColumn(tickets);
|
const groupedTickets = groupTicketsByColumn(tickets);
|
||||||
|
|
@ -867,8 +960,9 @@ export function renderOfficeScene(container, data = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const placements = agentPlacements(agents, deskLayout);
|
const placements = agentPlacements(agents, deskLayout);
|
||||||
const agentsLayer = svg.querySelector(".agents-layer");
|
currentAgentsLayer = svg.querySelector(".agents-layer");
|
||||||
updateAgentsLayer(agentsLayer, placements);
|
currentRegularPlacements = placements;
|
||||||
|
repaintAgentsLayer();
|
||||||
scheduleIdleCosmeticsForCurrentAgents(placements);
|
scheduleIdleCosmeticsForCurrentAgents(placements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -925,9 +1019,18 @@ async function boot() {
|
||||||
return;
|
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() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const data = await loadLiveData();
|
const data = await loadLiveData();
|
||||||
|
if (previousTickets) {
|
||||||
|
detectDoorVisits(previousTickets, data.tickets).forEach(triggerDoorVisit);
|
||||||
|
}
|
||||||
|
previousTickets = data.tickets;
|
||||||
renderOfficeScene(container, {
|
renderOfficeScene(container, {
|
||||||
ticketCounts: aggregateTicketsByColumn(data.tickets),
|
ticketCounts: aggregateTicketsByColumn(data.tickets),
|
||||||
tickets: data.tickets,
|
tickets: data.tickets,
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,7 @@ export const AGENT_LOCATIONS = {
|
||||||
MONITOR_SYSTEMS: "monitor-systems",
|
MONITOR_SYSTEMS: "monitor-systems",
|
||||||
MONITOR_KPI: "monitor-kpi",
|
MONITOR_KPI: "monitor-kpi",
|
||||||
COFFEE_CORNER: "coffee-corner",
|
COFFEE_CORNER: "coffee-corner",
|
||||||
|
DOOR: "door",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Zustandsmaschine wie in Visualization.md 4.4 vorgeschlagen: jeder von der API gemeldete
|
// Zustandsmaschine wie in Visualization.md 4.4 vorgeschlagen: jeder von der API gemeldete
|
||||||
|
|
@ -447,3 +448,93 @@ export class AnimationQueue {
|
||||||
return ranCount;
|
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];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<meta http-equiv="Refresh" content="60">
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
|
||||||
<title>FDS Server-Status</title>
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="hamburger" id="hamburger">☰</div>
|
|
||||||
<div class="nav-menu" id="navMenu">
|
|
||||||
<span class="close-btn" id="closeBtn">×</span>
|
|
||||||
<a href="#" id="homeLink">Startseite</a>
|
|
||||||
<a href="#" id="settingsLink">Einstellungen</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content">
|
|
||||||
<h1>Server-Status Übersicht</h1>
|
|
||||||
<table id="serverTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Hostname</th>
|
|
||||||
<th>IP-Adresse</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Letzte Prüfung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="serverData">
|
|
||||||
<!-- Daten werden per JavaScript eingefügt -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div id="timezonestring"></div>
|
|
||||||
<div id ="datumuhrzeit"onload="currentTime()"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="settingsPage" style="display: none;">
|
|
||||||
<h1>Einstellungen</h1>
|
|
||||||
<form class="settings-form" id="timezoneForm">
|
|
||||||
<label for="timezone">Zeitzone:</label>
|
|
||||||
<select id="timezone" name="timezone">
|
|
||||||
|
|
||||||
<option value="UTC">UTC</option>
|
|
||||||
<option value="Europe/Berlin">UTC+1 / UTC+2 (Berlin, MEZ/MESZ)</option>
|
|
||||||
<option value="UTC">UTC (Koordinierte Weltzeit)</option>
|
|
||||||
<option value="Europe/London">UTC+0 / UTC+1 (London, GMT/BST)</option>
|
|
||||||
<option value="Europe/Berlin">UTC+1 / UTC+2 (Berlin, MEZ/MESZ)</option>
|
|
||||||
<option value="Europe/Moscow">UTC+3 (Moskau, MSK)</option>
|
|
||||||
<option value="Asia/Dubai">UTC+4 (Dubai, GST)</option>
|
|
||||||
<option value="Asia/Karachi">UTC+5 (Karachi, PKT)</option>
|
|
||||||
<option value="Asia/Dhaka">UTC+6 (Dhaka, BST)</option>
|
|
||||||
<option value="Asia/Bangkok">UTC+7 (Bangkok, ICT)</option>
|
|
||||||
<option value="Asia/Shanghai">UTC+8 (Shanghai, CST)</option>
|
|
||||||
<option value="Asia/Tokyo">UTC+9 (Tokio, JST)</option>
|
|
||||||
<option value="Australia/Sydney">UTC+10 / UTC+11 (Sydney, AEST/AEDT)</option>
|
|
||||||
<option value="Pacific/Auckland">UTC+12 / UTC+13 (Auckland, NZST/NZDT)</option>
|
|
||||||
<option value="Pacific/Guadalcanal">UTC+11 (Guadalcanal, SBT)</option>
|
|
||||||
<option value="Asia/Kamchatka">UTC+12 (Kamtschatka, PETT)</option>
|
|
||||||
<option value="Pacific/Chatham">UTC+12:45 (Chatham, CHAST/CHADT)</option>
|
|
||||||
<option value="America/New_York">UTC-5 / UTC-4 (New York, EST/EDT)</option>
|
|
||||||
<option value="America/Chicago">UTC-6 / UTC-5 (Chicago, CST/CDT)</option>
|
|
||||||
<option value="America/Denver">UTC-7 / UTC-6 (Denver, MST/MDT)</option>
|
|
||||||
<option value="America/Los_Angeles">UTC-8 / UTC-7 (Los Angeles, PST/PDT)</option>
|
|
||||||
<option value="America/Anchorage">UTC-9 / UTC-8 (Anchorage, AKST/AKDT)</option>
|
|
||||||
<option value="Pacific/Honolulu">UTC-10 (Honolulu, HST)</option>
|
|
||||||
<option value="Pacific/Pago_Pago">UTC-11 (Pago Pago, SST)</option>
|
|
||||||
<option value="Pacific/Kiritimati">UTC+14 (Kiritimati, LINT)</option>
|
|
||||||
<option value="Atlantic/Azores">UTC-1 / UTC+0 (Azoren, AZOT/AZOST)</option>
|
|
||||||
<option value="Atlantic/Cape_Verde">UTC-1 (Kap Verde, CVT)</option>
|
|
||||||
|
|
||||||
</select>
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
118
html/script.js
118
html/script.js
|
|
@ -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 = `
|
|
||||||
<td>${hostname}</td>
|
|
||||||
<td>${ip}</td>
|
|
||||||
<td><span class="status ${status.toLowerCase()}">${status}</span></td>
|
|
||||||
<td class="last-check">${localTimeStr} (${userTimezone})</td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Laden der status.conf:', error);
|
|
||||||
document.getElementById('serverData').innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="text-align: center; color: red;">
|
|
||||||
Fehler beim Laden der Serverdaten.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
|
|
||||||
118
html/styles.css
118
html/styles.css
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
register-agent.sh
Executable file
50
register-agent.sh
Executable file
|
|
@ -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
|
||||||
|
|
@ -26,6 +26,11 @@ import {
|
||||||
normalizeAgentForDisplay,
|
normalizeAgentForDisplay,
|
||||||
pickIdleActivity,
|
pickIdleActivity,
|
||||||
AnimationQueue,
|
AnimationQueue,
|
||||||
|
DOOR_VISIT_TYPES,
|
||||||
|
DOOR_VISIT_STAGES,
|
||||||
|
detectDoorVisits,
|
||||||
|
resolveDoorVisitVisual,
|
||||||
|
advanceDoorVisitStage,
|
||||||
} from "../../html/js/office-scene.js";
|
} from "../../html/js/office-scene.js";
|
||||||
|
|
||||||
test("buildBoardColumnLayout liefert alle Spalten in API.md-Reihenfolge", () => {
|
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.deepEqual(ran, ["a", "b", "c"]);
|
||||||
assert.equal(queue.size, 0);
|
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);
|
||||||
|
});
|
||||||
|
|
|
||||||
74
update.sh
Normal file
74
update.sh
Normal file
|
|
@ -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))."
|
||||||
Loading…
Add table
Reference in a new issue