From 9af2f828019909ed4d4b61a8f7bafeb4482cd1f6 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Wed, 26 Aug 2026 21:21:05 +0200 Subject: [PATCH] Neue Agenten-Rolle "ops" mit Schreibtischplatz (Issue #66) Fuer den Betrieb fehlte bisher eine Rolle: "user" meldet zwar Daten, bleibt in der Buero-Visualisierung aber unsichtbar, weil sie nie einen Schreibtisch bekommt. Der Ping-Check aus check_fds.sh (Weg 3 im README) lief deshalb als "user" und tauchte im Buero nicht auf. "ops" fuellt die Luecke: die Schreibtischvergabe in AgentRepository::register() greift automatisch, sobald die Rolle erlaubt ist - nur "user" bleibt ohne Platz. Der CHECK-Constraint auf agents.role braucht dafuer eine eigene Migration. Ein Edit von 001_initial.sql wuerde nur frische Datenbanken erreichen: bestehende Installationen haben die Migration laut schema_migrations laengst angewandt und wuerden bei role='ops' weiterhin abbrechen. SQLite kann CHECK nicht per ALTER TABLE aendern, 004 baut die Tabelle deshalb neu (Verfahren aus der SQLite-Doku, foreign_keys kurz aus, weil tickets.assignee_agent_id auf agents zeigt). Im Frontend bekommt die Rolle Label "OPs", eine Akzentfarbe und ein Zahnrad als Schreibtisch-Deko - ohne Eintrag in der ROLES-Map rendert der Avatar grau und ohne Label. Neu ist ausserdem register-ops-agent.sh neben der docker-compose.yml: es liest Admin-Key und Basis-URL aus der .env, registriert den lokalen OPs-Agenten und schreibt den nur einmalig ausgegebenen API-Key mit chmod 600 nach monitor-api.conf - also genau die beiden Schritte aus Weg 3, die hosts.conf und credentials.conf nicht abdecken. Eine vorhandene monitor-api.conf ueberschreibt es nicht, und ein zweiter Agent gleichen Namens (der einen weiteren der acht Schreibtische belegen wuerde) wird vorher abgefangen. Co-Authored-By: Claude Opus 5 --- API.md | 8 +- README.md | 19 ++- api/src/Db/migrations/004_agent_role_ops.sql | 40 ++++++ api/src/Repository/AgentRepository.php | 2 +- .../Unit/Repository/AgentRepositoryTest.php | 14 ++ html/css/office.css | 10 ++ html/js/demo-data.js | 2 +- html/js/office-render.js | 24 ++++ html/js/office-scene.js | 1 + monitor-api-sample.conf | 7 +- register-agent.sh | 2 +- register-ops-agent.sh | 130 ++++++++++++++++++ 12 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 api/src/Db/migrations/004_agent_role_ops.sql create mode 100755 register-ops-agent.sh diff --git a/API.md b/API.md index 1673d2f..5e9c5da 100644 --- a/API.md +++ b/API.md @@ -63,12 +63,18 @@ Fehlt oder passt ein Key nicht, antwortet die API mit `401 Unauthorized`. ## Agenten -Rollen (F1/F2): `entwickler`, `product_owner`, `qa`, `user`. Ein Agent bekommt bei der +Rollen (F1/F2): `entwickler`, `product_owner`, `qa`, `ops`, `user`. Ein Agent bekommt bei der Registrierung fest einen von `MONITOR_DESK_COUNT` (Standard 8) Schreibtischen zugewiesen (niedrigster freier Index). Die Rolle `user` bekommt nie einen Schreibtisch (kein fester Sitzplatz). Sind alle Schreibtische belegt, wird trotzdem registriert, aber ohne Schreibtisch (`desk_index: null`). +`ops` (Issue #66) ist die Rolle fuer den Betrieb - etwa den Ping-Check aus `check_fds.sh`, +der seine Ergebnisse per REST meldet (siehe README, "Weg 3"). Sie bekommt wie die uebrigen +Rollen einen Schreibtisch, in der Buero-Visualisierung mit dem Label "OPs" und einem +Zahnrad als Deko. Registrieren laesst sich der lokale OPs-Agent mit +`register-ops-agent.sh`, das zugleich `monitor-api.conf` schreibt. + ### `POST /api/v1/agents` — Agent registrieren Auth: `X-Admin-Key`. diff --git a/README.md b/README.md index 8fcf4f1..a9099e9 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ die dazugehörigen Klärungen in [Detailfragen.md](Detailfragen.md) und [README_UPDATE.md](README_UPDATE.md) - `register-agent.sh` — Hilfsskript, um einen neuen Agenten per `POST /api/v1/agents` zu registrieren (siehe [API.md](API.md) Abschnitt "Agenten") +- `register-ops-agent.sh` — registriert den lokalen OPs-Agenten (Rolle `ops`) und legt dabei + `monitor-api.conf` für die Statusmeldung des Ping-Checks an (siehe "Weg 3" unten) --- @@ -134,10 +136,19 @@ Kombiniert beide Wege und ist der richtige Weg, wenn der Ping-Check auf einem an läuft als die API — dann fällt der Umweg über die Datei `status.txt` weg: 1. Wie in Weg 1 die `hosts.conf` pflegen und `check_fds.sh` per Cron laufen lassen. -2. Wie in Weg 2 einen Agenten registrieren (Rolle `user`, damit er keinen Schreibtisch in der - Büro-Visualisierung belegt). -3. `monitor-api-sample.conf` nach `monitor-api.conf` neben `check_fds.sh` kopieren, dort - `MONITOR_API_BASE` und `MONITOR_API_KEY` eintragen und die Datei auf `chmod 600` setzen. +2. Den OPs-Agenten registrieren — das erledigt `register-ops-agent.sh` neben der + `docker-compose.yml` in einem Schritt (die API muss dafür laufen): + ```bash + ./register-ops-agent.sh # optional: ./register-ops-agent.sh "OPs Rechenzentrum" + ``` + Das Skript liest `MONITOR_ADMIN_API_KEY` und `MONITOR_API_BASE` aus der `.env`, legt einen + Agenten der Rolle `ops` an (sitzt als „OPs" sichtbar an einem Schreibtisch, Issue #66) und + schreibt den nur einmalig ausgegebenen API-Key mit `chmod 600` nach `monitor-api.conf`. + Eine bestehende `monitor-api.conf` überschreibt es nicht (`--force` erzwingt es). + + Von Hand geht es weiterhin so: Agenten wie in Weg 2 registrieren (Rolle `ops`), + `monitor-api-sample.conf` nach `monitor-api.conf` neben `check_fds.sh` kopieren, dort + `MONITOR_API_BASE` (ohne `/api/v1`) und `MONITOR_API_KEY` eintragen, `chmod 600` setzen. `check_fds.sh` meldet dann nach jedem Ping zusätzlich per REST (`online` → `ok`, `offline` → `error`) und schreibt `status.txt` unverändert weiter für den alten Wandmonitor. diff --git a/api/src/Db/migrations/004_agent_role_ops.sql b/api/src/Db/migrations/004_agent_role_ops.sql new file mode 100644 index 0000000..f7a44d0 --- /dev/null +++ b/api/src/Db/migrations/004_agent_role_ops.sql @@ -0,0 +1,40 @@ +-- Issue #66: Neue Agenten-Rolle "ops" fuer den Betrieb (z.B. den Ping-Check aus +-- check_fds.sh). Anders als "user" bekommt sie einen festen Schreibtisch in der +-- Buero-Visualisierung; die Vergabe in AgentRepository::register() greift automatisch, +-- sobald die Rolle erlaubt ist. +-- +-- SQLite kennt kein "ALTER TABLE ... DROP/ADD CONSTRAINT", der CHECK aus +-- 001_initial.sql laesst sich also nur ueber einen Tabellen-Neubau erweitern (Verfahren +-- aus der SQLite-Doku, "Making Other Kinds Of Table Schema Changes"). Ein Edit von +-- 001_initial.sql reicht nicht: bestehende Installationen haben die Migration laut +-- schema_migrations laengst angewandt und wuerden bei role='ops' weiterhin abbrechen. +-- +-- foreign_keys wird dafuer kurz abgeschaltet, weil tickets.assignee_agent_id auf agents +-- zeigt - mit aktiven Fremdschluesseln wuerde DROP TABLE die Zuweisungen mitnehmen. Nach +-- dem Umbenennen traegt die neue Tabelle wieder den Namen "agents", der FK in tickets +-- zeigt also unveraendert richtig. +PRAGMA foreign_keys = OFF; + +CREATE TABLE agents_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + display_name TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('entwickler', 'product_owner', 'qa', 'ops', 'user')), + api_key_hash TEXT NOT NULL UNIQUE, + desk_index INTEGER, + state TEXT NOT NULL DEFAULT 'idle', + current_ticket_id INTEGER, + created_at TEXT NOT NULL, + last_seen_at TEXT, + FOREIGN KEY (current_ticket_id) REFERENCES tickets (id) +); + +INSERT INTO agents_new (id, display_name, role, api_key_hash, desk_index, state, + current_ticket_id, created_at, last_seen_at) + SELECT id, display_name, role, api_key_hash, desk_index, state, + current_ticket_id, created_at, last_seen_at + FROM agents; + +DROP TABLE agents; +ALTER TABLE agents_new RENAME TO agents; + +PRAGMA foreign_keys = ON; diff --git a/api/src/Repository/AgentRepository.php b/api/src/Repository/AgentRepository.php index c1efb9d..c6aa9a7 100644 --- a/api/src/Repository/AgentRepository.php +++ b/api/src/Repository/AgentRepository.php @@ -12,7 +12,7 @@ use Monitor\Api\Http\NotFoundException; final class AgentRepository { /** @var string[] */ - public const ROLES = ['entwickler', 'product_owner', 'qa', 'user']; + public const ROLES = ['entwickler', 'product_owner', 'qa', 'ops', 'user']; /** * Praesenz-Timeout (Issue #17/#20): ein Agent gilt nur noch als anwesend, solange diff --git a/api/tests/Unit/Repository/AgentRepositoryTest.php b/api/tests/Unit/Repository/AgentRepositoryTest.php index c94c276..a2d3230 100644 --- a/api/tests/Unit/Repository/AgentRepositoryTest.php +++ b/api/tests/Unit/Repository/AgentRepositoryTest.php @@ -42,6 +42,20 @@ final class AgentRepositoryTest extends TestCase self::assertNull($result['agent']['desk_index']); } + /** + * Issue #66: "ops" ist die Rolle fuer den Betrieb (z.B. der Ping-Check) und sitzt - + * anders als "user" - sichtbar an einem Schreibtisch. Der Test deckt zugleich den + * erweiterten CHECK-Constraint aus 004_agent_role_ops.sql ab: ohne ihn wuerde das + * INSERT scheitern, nicht erst die Zuweisung. + */ + public function testOpsRoleGetsADesk(): void + { + $result = $this->repository->register('OPs', 'ops'); + + self::assertSame('ops', $result['agent']['role']); + self::assertSame(0, $result['agent']['desk_index']); + } + public function testOverflowAgentGetsNoDeskWhenAllOccupied(): void { $this->repository->register('Alice', 'entwickler'); diff --git a/html/css/office.css b/html/css/office.css index 656a843..5c5bb89 100644 --- a/html/css/office.css +++ b/html/css/office.css @@ -378,6 +378,16 @@ header.office-header h1 { stroke-linejoin: round; } +.desk-deco-gear circle, +.desk-deco-gear rect { + fill: #a78bfa; +} + +/* Heller Kern, damit der gefuellte Kreis als Zahnrad-Ring lesbar wird (wie beim Bluetenkern). */ +.desk-deco-gear-center { + fill: #ede9fe !important; +} + .desk-empty .desk-chair { opacity: 0.5; } diff --git a/html/js/demo-data.js b/html/js/demo-data.js index 027996b..f027c16 100644 --- a/html/js/demo-data.js +++ b/html/js/demo-data.js @@ -24,7 +24,7 @@ export const DEMO_AGENTS = [ { name: "Quentin", role: "qa", pose: "idle" }, { name: "Dirk", role: "entwickler", pose: "working" }, { name: "Nadja", role: "qa", pose: "coffee" }, - null, + { name: "Olaf", role: "ops", pose: "working" }, null, ]; diff --git a/html/js/office-render.js b/html/js/office-render.js index b646034..622329e 100644 --- a/html/js/office-render.js +++ b/html/js/office-render.js @@ -639,6 +639,30 @@ function renderDeskAt(origin, agent, width = DESK.w) { g.appendChild( el("path", { d: `M${width - 58},24 l8,8 16,-18`, class: "desk-deco desk-deco-check" }) ); + } else if (role.deco === "gear") { + const cx = width - 45; + const cy = 22; + // Zahnrad = Ring plus sechs Zaehne, jeder um 60 Grad um die Mitte gedreht. + const teeth = []; + for (let i = 0; i < 6; i++) { + teeth.push( + el("rect", { + x: cx - 2.5, + y: cy - 15, + width: 5, + height: 7, + rx: 1, + transform: `rotate(${i * 60}, ${cx}, ${cy})`, + }) + ); + } + g.appendChild( + el("g", { class: "desk-deco desk-deco-gear" }, [ + ...teeth, + el("circle", { cx, cy, r: 9 }), + el("circle", { cx, cy, r: 3.5, class: "desk-deco-gear-center" }), + ]) + ); } // Blockierte Agenten faerben ihr Namensschild ein. Der Zustand steckt hier in der Pose, weil diff --git a/html/js/office-scene.js b/html/js/office-scene.js index e60e94c..80ed31b 100644 --- a/html/js/office-scene.js +++ b/html/js/office-scene.js @@ -18,6 +18,7 @@ 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" }, + ops: { label: "OPs", accentColor: "#8b5cf6", deco: "gear" }, user: { label: "User", accentColor: "#f59e0b", deco: "none" }, }; diff --git a/monitor-api-sample.conf b/monitor-api-sample.conf index 35c82c5..1a602f7 100644 --- a/monitor-api-sample.conf +++ b/monitor-api-sample.conf @@ -7,9 +7,10 @@ # Diese Datei nach "monitor-api.conf" neben check_fds.sh kopieren und ausfuellen. Fehlt sie # oder bleibt ein Wert leer, meldet check_fds.sh wie bisher nur nach status.txt und per Mail. # -# Der Key gehoert einem eigens registrierten Agenten der Rolle "user" - die bekommt nie einen -# Schreibtisch in der Buero-Visualisierung (AgentRepository::register). Anlegen mit -# register-agent.sh; der Key wird nur bei der Registrierung einmalig ausgegeben. +# Der Key gehoert einem eigens registrierten Agenten der Rolle "ops" (Issue #66), der in der +# Buero-Visualisierung als "OPs" an einem Schreibtisch sitzt. Am einfachsten legt +# register-ops-agent.sh beides an - Agent und diese Datei; der Key wird nur bei der +# Registrierung einmalig ausgegeben. # # Enthaelt ein Secret: chmod 600 monitor-api.conf diff --git a/register-agent.sh b/register-agent.sh index 1552071..ddfaccb 100755 --- a/register-agent.sh +++ b/register-agent.sh @@ -19,7 +19,7 @@ ADMIN_API_KEY="" # Anzeigename des neuen Agenten DISPLAY_NAME="" -# Rolle: entwickler | product_owner | qa | user +# Rolle: entwickler | product_owner | qa | ops | user ROLE="entwickler" # ------------------------------------------------------------------------------- diff --git a/register-ops-agent.sh b/register-ops-agent.sh new file mode 100755 index 0000000..1cac5b1 --- /dev/null +++ b/register-ops-agent.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# +# Registriert den lokalen OPs-Agenten (Rolle "ops", Issue #66) bei der monitor-API und legt +# die Zugangsdaten fuer check_fds.sh in monitor-api.conf ab - also genau die beiden Schritte +# aus "Weg 3" im README, die sich nicht in hosts.conf/credentials.conf erledigen lassen. +# +# Aufruf (im Verzeichnis mit docker-compose.yml, die API muss laufen): +# +# ./register-ops-agent.sh [Anzeigename] # Standard-Anzeigename: "OPs" +# +# Ueberschreibbar per Umgebungsvariable: MONITOR_API_BASE, MONITOR_ADMIN_API_KEY. +# +# Wichtig: Den API-Key gibt die API nur bei der Registrierung einmalig heraus - deshalb +# ueberschreibt das Skript eine vorhandene monitor-api.conf nicht (--force erzwingt es, +# der alte Agent bleibt dann mit seinem Schreibtisch registriert). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/.env" +API_CONF="${MONITOR_API_CONF:-$SCRIPT_DIR/monitor-api.conf}" + +DISPLAY_NAME="OPs" +FORCE=0 +for arg in "$@"; do + case "$arg" in + -f|--force) FORCE=1 ;; + -h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; exit 0 ;; + -*) echo "Unbekannte Option: $arg" >&2; exit 1 ;; + *) DISPLAY_NAME="$arg" ;; + esac +done + +if ! command -v curl >/dev/null 2>&1; then + echo "Fehler: curl wird benoetigt." >&2 + exit 1 +fi + +# Werte aus der .env lesen, statt sie zu sourcen: die Datei enthaelt Kommentare und Werte, +# die als Shell-Code anders bedeuten wuerden, als sie Docker Compose liest. +env_value() { + [ -f "$ENV_FILE" ] || return 0 + sed -n "s/^[[:space:]]*$1=//p" "$ENV_FILE" | tail -n1 \ + | sed -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/" +} + +ADMIN_API_KEY="${MONITOR_ADMIN_API_KEY:-$(env_value MONITOR_ADMIN_API_KEY)}" +API_BASE="${MONITOR_API_BASE:-$(env_value MONITOR_API_BASE)}" +API_BASE="${API_BASE:-http://localhost:8091}" + +# In der .env steht die Basis-URL inklusive "/api/v1" (so braucht sie das Frontend), +# monitor-api.conf dagegen ohne - check_fds.sh haengt den Pfad selbst an. +API_ROOT="${API_BASE%/}" +API_ROOT="${API_ROOT%/api/v1}" + +if [ -z "$ADMIN_API_KEY" ]; then + echo "Fehler: MONITOR_ADMIN_API_KEY steht weder in der Umgebung noch in $ENV_FILE." >&2 + exit 1 +fi +if [ "$ADMIN_API_KEY" = "change-me" ]; then + echo "Fehler: MONITOR_ADMIN_API_KEY ist noch der Beispielwert 'change-me'." >&2 + exit 1 +fi +if [ -e "$API_CONF" ] && [ "$FORCE" -ne 1 ]; then + echo "Fehler: $API_CONF existiert bereits - der bisherige OPs-Agent ist offenbar schon" >&2 + echo " registriert. Zum Neuanlegen die Datei wegsichern oder --force angeben." >&2 + exit 1 +fi + +json_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +# Ein zweiter Agent gleichen Namens wuerde einen weiteren der acht Schreibtische belegen. +# GET /api/v1/agents ist unauthentifiziert (API.md "Agenten") und dient hier auch als +# Erreichbarkeitspruefung. +agents_json=$(curl -sS --max-time 10 "$API_ROOT/api/v1/agents" 2>/dev/null) || { + echo "Fehler: API unter $API_ROOT nicht erreichbar. Laeuft 'docker compose up'?" >&2 + exit 1 +} +if printf '%s' "$agents_json" | grep -q "\"display_name\":\"$(json_escape "$DISPLAY_NAME")\""; then + echo "Fehler: Ein Agent namens \"$DISPLAY_NAME\" ist bereits registriert." >&2 + echo " Anderen Anzeigenamen uebergeben oder den alten Agenten loeschen." >&2 + exit 1 +fi + +response=$(curl -sS -w '\n%{http_code}' --max-time 10 -X POST "$API_ROOT/api/v1/agents" \ + -H 'Content-Type: application/json' \ + -H "X-Admin-Key: $ADMIN_API_KEY" \ + -d "{\"display_name\": \"$(json_escape "$DISPLAY_NAME")\", \"role\": \"ops\"}") + +http_code=$(printf '%s' "$response" | tail -n1) +body=$(printf '%s' "$response" | sed '$d') + +if [ "$http_code" != "201" ]; then + echo "Fehler: Registrierung fehlgeschlagen (HTTP $http_code)." >&2 + echo "$body" >&2 + exit 1 +fi + +api_key=$(printf '%s' "$body" | grep -oE '"api_key":"[0-9a-f]+"' | head -n1 | cut -d'"' -f4) +agent_id=$(printf '%s' "$body" | grep -oE '"id":[0-9]+' | head -n1 | cut -d: -f2) +desk_index=$(printf '%s' "$body" | grep -oE '"desk_index":(null|[0-9]+)' | head -n1 | cut -d: -f2) + +if [ -z "$api_key" ]; then + echo "Fehler: Die Antwort enthielt keinen api_key:" >&2 + echo "$body" >&2 + exit 1 +fi + +# Erst die Rechte setzen, dann schreiben - sonst stuende das Secret kurzzeitig lesbar da. +umask 077 +cat > "$API_CONF" <> /var/log/check_fds.log 2>&1"