monitor/API.md
fds2610-bot 3efa0ee423 Codeberg-Webhook fuer Sofort-Sync statt Warten auf den Polling-Lauf
Aenderungen an Codeberg-Issues landeten bisher erst beim naechsten Lauf des
sync-Services auf dem Board (MONITOR_SYNC_INTERVAL, Default 600s). Neu meldet
Codeberg jede Aenderung aktiv per Webhook, der Weg aufs Board dauert damit
wenige Sekunden (Webhook -> Sync -> SSE-Push ans Frontend).

POST /api/v1/webhooks/codeberg wertet den Payload bewusst nicht aus, sondern
fordert nur einen regulaeren Sync an - CodebergSyncService bleibt die einzige
Stelle, die Issues auf Tickets abbildet, und ein verpasster Webhook ist
folgenlos, weil der periodische Lauf denselben Stand ohnehin nachholt.

Der Sync passiert nicht im Request: Forgejo bricht die Zustellung nach wenigen
Sekunden ab (DELIVER_TIMEOUT, Default 5s), ein Sync macht aber mehrere Requests
nach codeberg.org. Der Endpunkt setzt daher nur eine Trigger-Datei im
gemeinsamen ./api-Volume (Sync\SyncTrigger) und antwortet sofort mit 202; der
sync-Service laeuft jetzt im Watch-Modus und greift sie im Sekundentakt auf.

Authentifiziert wird per HMAC-SHA256 ueber den rohen Request-Body
(X-Forgejo-Signature bzw. X-Gitea-Signature) - Codeberg kennt unsere API-Keys
nicht. Ohne gesetztes CODEBERG_WEBHOOK_SECRET weist der Endpunkt grundsaetzlich
mit 401 ab, damit ein vergessenes Secret keinen offenen Sync-Ausloeser im
Internet hinterlaesst. Request traegt dafuer neu den unveraenderten rawBody.

Anders als der sync-Service braucht der api-Service damit eine eingehende
Freigabe in Proxy und Firewall (POST /api/v1/webhooks/codeberg).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:04:06 +02:00

14 KiB
Raw Blame History

monitor REST-API (Phase 1)

Diese API ist die zentrale Schnittstelle, ueber die entfernte Agenten (Menschen oder automatisierte Systeme) ihre Aktivitaet melden: Tickets anlegen/verschieben, Events melden, Systemstatus berichten, KPIs eintragen und den eigenen Zustand aktualisieren. Sie ist Teil von Phase 1 der Neuausrichtung (siehe Visualization.md, Abschnitt 9).

Implementierung: Vanilla-PHP (kein Framework), Persistenz in SQLite (api/data/monitor.sqlite). Details zum lokalen Setup: api/README.md.

Inhaltsverzeichnis

Grundlagen

  • Basis-Pfad: /api/v1
  • Request-/Response-Body: application/json
  • Alle Zeitstempel: ISO-8601 UTC (YYYY-MM-DDTHH:mm:ssZ), serverseitig erzeugt via Database::now(), sofern nicht explizit im Request angegeben.
  • Lesende Endpunkte (GET) sind oeffentlich, da sie das Frontend bedienen.
  • Schreibende Endpunkte (POST/PATCH) erfordern einen Agenten- oder Admin-Key (siehe Authentifizierung).
  • Alle Antworten tragen Access-Control-Allow-Origin: * (CORS), da Frontend und API im Docker-Compose-Setup auf unterschiedlichen Ports laufen (siehe docker-compose.yml: Frontend :8090, API :8091).
  • Welche Basis-URL das Frontend anspricht, wird nicht geraten, sondern konfiguriert: Der Webserver liefert sie zur Laufzeit als /config.js (window.MONITOR_API_BASE) aus, gespeist aus der Umgebungsvariable MONITOR_API_BASE (siehe .env.example und docker/nginx/default.conf.template). Leerer Wert = gleicher Origin, dann wird der relative Pfad /api/v1 verwendet.

Authentifizierung

Zwei Schluesseltypen, beide statisch (F14):

Header Verwendet fuer Herkunft
X-Admin-Key POST /api/v1/agents (Registrierung neuer Agenten) Umgebungsvariable MONITOR_ADMIN_API_KEY
X-Api-Key Alle anderen schreibenden Endpunkte Wird bei der Agenten-Registrierung einmalig zurueckgegeben

Ausnahme ist der Codeberg-Webhook (POST /api/v1/webhooks/codeberg): Codeberg kennt unsere Keys nicht, signiert den Payload aber mit dem hinterlegten Webhook-Secret - dort gilt die HMAC-Signatur statt eines Keys, siehe Codeberg-Sync.

Der Klartext-API-Key eines Agenten wird nur einmal bei der Registrierung ausgegeben. Gespeichert wird ausschliesslich ein SHA-256-Hash in SQLite (Tabelle agents, Spalte api_key_hash) - der Klartext ist danach nicht mehr rekonstruierbar und muss vom Agenten sicher verwahrt werden.

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 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).

POST /api/v1/agents — Agent registrieren

Auth: X-Admin-Key.

// Request
{ "display_name": "Alice", "role": "entwickler" }
// Response 201
{
  "agent": {
    "id": 1, "display_name": "Alice", "role": "entwickler",
    "desk_index": 0, "state": "idle", "current_ticket_id": null,
    "created_at": "2026-07-27T10:00:00Z", "last_seen_at": "2026-07-27T10:00:00Z",
    "is_present": true
  },
  "api_key": "‹64 Hex-Zeichen, nur jetzt sichtbar›"
}

GET /api/v1/agents — Alle Agenten

Response: { "agents": [ ‹Agent-Objekte wie oben, ohne api_key› ] }

Oeffentlich, unauthentifiziert (siehe Grundlagen) - damit laesst sich last_seen_at/is_present jedes Agenten jederzeit von aussen pruefen, z.B. auf der Produktionsmaschine:

curl -s "$MONITOR_API_BASE/agents" | jq '.agents[] | {id, display_name, last_seen_at, is_present}'

is_present gilt true, solange last_seen_at hoechstens zehn Minuten zurueckliegt, sonst false (AgentRepository::isPresent(), Issue #17/#20) - laeuft der Timeout ab, bleibt der Schreibtisch des Agenten leer, obwohl er weiterhin registriert ist. Berechnet wird das serverseitig via gmdate()/time() (also unabhaengig von der Zeitzone des Clients); das Frontend (isAgentPresent, html/js/office-scene.js) liest is_present nur noch direkt aus der API, statt selbst aus last_seen_at zu rechnen - vorher fuehrte das je nach Client-Zeitzone zu falschen Ergebnissen (Issue #20). Bei direktem Zugriff auf die Produktionsmaschine laesst sich last_seen_at auch direkt aus der SQLite-Datei lesen (MONITOR_DB_PATH, Standard api/data/monitor.sqlite), is_present selbst steht dort allerdings nicht drin, da es erst bei der API-Antwort berechnet wird:

sqlite3 "$MONITOR_DB_PATH" "SELECT id, display_name, last_seen_at FROM agents;"

POST /api/v1/agents/{id}/activity — Eigenen Zustand melden

Auth: X-Api-Key des betroffenen Agenten (ein Agent darf nur seinen eigenen Zustand melden, sonst 403).

// Request
{ "state": "working", "current_ticket": 42 }

Response 200: aktualisiertes Agent-Objekt.

Tickets

Spalten (F3): backlog, bereit, in_arbeit, qa, testing, fertig, done.

  • backlog, bereit, in_arbeit, testing und done werden regelmaessig aus Codeberg-Issues befuellt (siehe Codeberg-Sync); qa und fertig sind fuer zukuenftige Labels reserviert und ergeben sich ausschliesslich aus Agenten-Meldungen (F4).
  • Ticket-Typ und Farbe (statt Prioritaet) werden bei Codeberg-Tickets aus dem ersten Issue-Label uebernommen (F4).
  • Stapel-Darstellung ("aelteste 4 + Rest als Stapel", F4) ist reine Frontend-Logik; die API liefert Tickets dafuer sortiert nach Spalte und Alter.

GET /api/v1/tickets — Board-Zustand

Query-Parameter: column (optional, filtert auf eine Spalte).

Response: { "tickets": [ { "id", "external_ref", "title", "description", "column_name", "type", "color", "assignee_agent_id", "source", "created_at", "updated_at" }, ... ] }

POST /api/v1/tickets — Ticket anlegen

Auth: X-Api-Key.

// Request (nur "title" ist Pflicht)
{ "title": "Neues Bug-Ticket", "description": "...", "column": "backlog", "type": "bug", "color": "#ff0000" }

Response 201 mit dem angelegten Ticket, 422 bei fehlendem title oder ungueltiger column.

PATCH /api/v1/tickets/{id} — Ticket verschieben/aendern

Auth: X-Api-Key. Erlaubte Felder im Body: column, title, description, type, color, assignee_agent_id (alle optional, nur uebergebene Felder werden geaendert).

Response 200 mit dem aktualisierten Ticket, 404 falls unbekannt, 422 bei ungueltiger column.

Events

severity: warning (gelb) oder error (rot). Neuestes Event zuerst (F5).

POST /api/v1/events — Event melden

Auth: X-Api-Key.

{ "system_id": "srv1", "name": "Ping", "message": "Host nicht erreichbar", "severity": "error" }

Pflichtfelder: name, message, severity. occurred_at optional (Default: jetzt). Response 201 mit dem gespeicherten Event.

GET /api/v1/events — Event-Liste

Query-Parameter: limit (Default 5, entspricht der im Plan festgelegten Zeilenzahl des Event-Monitors, F5; Maximum intern 200).

Response: { "events": [ { "id", "system_id", "name", "message", "severity", "occurred_at", "created_at" }, ... ] }

Systeme

Bis zu 9 Kacheln (F6, SystemRepository::MAX_TILES), Fehler zuerst sortiert. Aktuell werden Systeme zusaetzlich aus der bestehenden status.txt (geschrieben von check_fds.sh) importiert; per API gemeldete Systeme (source: "api") haben Vorrang und werden vom Legacy-Import nicht ueberschrieben.

POST /api/v1/systems/{id}/status — Systemstatus melden

Auth: X-Api-Key.

{ "status": "ok", "name": "Webserver", "type": "host", "detail": "optional" }

status: ok oder error. Response 200 mit dem gespeicherten System, 422 bei ungueltigem status.

GET /api/v1/systems — Systemuebersicht

Response: { "systems": [ { "id", "name", "type", "status", "detail", "source", "last_check_at" }, ... ] } (maximal 9 Eintraege).

KPIs

F7: Konkrete Kennzahlen sind noch offen ("kommt spaeter"). Phase 1 stellt nur die generische Speicher-/Abfragestruktur bereit.

POST /api/v1/kpis/{key} — KPI-Datenpunkt melden

Auth: X-Api-Key.

{ "value": 12.5, "label": "Offene Tickets", "unit": "count", "timestamp": "2026-07-27T10:00:00Z" }

Pflichtfeld: value (numerisch). label/unit/timestamp optional. Response 201 mit dem KPI inkl. Verlauf.

GET /api/v1/kpis — Alle KPIs

Response: { "kpis": [ { "key", "label", "unit", "current_value", "history": [ { "value", "recorded_at" }, ... ] }, ... ] }

Live-Updates (SSE)

F13: Das Frontend haelt sich per Server-Sent Events ueber Aenderungen auf dem Laufenden, statt staendig zu pollen.

GET /api/v1/stream — Aenderungs-Feed

Auth: keine (oeffentlich wie die anderen GET-Endpunkte). Antwort: Content-Type: text/event-stream.

Die Verbindung bleibt fuer MONITOR_SSE_MAX_ITERATIONS * MONITOR_SSE_POLL_INTERVAL Sekunden offen (Default: 30 * 2s = ca. 1 Minute) und prueft alle MONITOR_SSE_POLL_INTERVAL Sekunden serverseitig, ob sich Tickets, Events, Systeme, KPIs oder Agenten veraendert haben (billiger "Fingerabdruck" aus Anzahl + juengstem Zeitstempel je Ressource, keine vollstaendige Änderungshistorie). Danach schliesst der Server die Verbindung; EventSource im Browser verbindet automatisch neu (Standardverhalten).

Bei einer Aenderung wird ein update-Event gesendet, das nur benennt, welche Ressourcen sich geaendert haben - das Frontend laedt die betroffenen Endpunkte anschliessend regulaer per GET neu:

event: update
data: {"changed": ["tickets", "events"]}

Ohne Aenderung sendet der Server stattdessen alle MONITOR_SSE_POLL_INTERVAL Sekunden einen Kommentar (: heartbeat) als Keep-Alive, damit Proxys die Verbindung nicht vorzeitig schliessen.

Codeberg-Sync (Backlog/Done)

api/bin/sync-codeberg.php liest offene und geschlossene Issues aus dem konfigurierten Codeberg-Repo (CODEBERG_OWNER/CODEBERG_REPO) und gleicht sie ueber external_ref (= Issue-Nummer) mit den Tickets ab:

  • Offene Issues → Spalte je nach Label: wip → in_arbeit, sonst testing → testing, sonst automate → bereit, sonst backlog.
  • Geschlossene Issues → Spalte done.
  • Nur falls das Ticket noch in einer der Sync-Spalten (backlog, bereit, in_arbeit, testing, done) ist - auf qa/fertig manuell verschobene Tickets werden nicht zurueckgesetzt.
  • Ticket-Typ/-Farbe kommen vom ersten Issue-Label (Name/Farbe).

Ausgefuehrt wird das Skript vom Compose-Service sync (siehe docker-compose.yml) - kein Host-Cron noetig. Der Service startet mit dem Stack (--watch), macht sofort einen ersten Lauf und wiederholt ihn danach aus zwei Gruenden: periodisch alle MONITOR_SYNC_INTERVAL Sekunden (Default 600, siehe .env.example) und sofort, sobald ein Codeberg-Webhook eintrifft (siehe unten). Er nutzt dasselbe Image und dieselbe SQLite-Datei wie api und verbindet sich selbst ausschliesslich ausgehend nach codeberg.org - fuer diesen Service ist also kein Port-Mapping noetig.

docker compose up -d sync                # Sync-Service starten
docker compose logs -f sync              # Laeufe mitlesen
docker compose run --rm api php bin/sync-codeberg.php   # einmalig manuell (Debug)

Beim manuellen Aufruf werden Fehler direkt auf der Konsole sichtbar (Codeberg-API nicht erreichbar: … bzw. Codeberg-API-Fehler (HTTP 4xx)) - der erste Anlaufpunkt, wenn Backlog und Done leer bleiben.

POST /api/v1/webhooks/codeberg — Sofort-Sync per Webhook

Damit Issue-Aenderungen nicht erst beim naechsten periodischen Lauf auf dem Board landen, kann Codeberg jede Aenderung aktiv melden. Der Endpunkt wertet den Payload nicht aus, er fordert nur einen regulaeren Sync an; der sync-Service greift die Anforderung im Sekundentakt auf und laedt den aktuellen Stand. Vom Aendern des Issues bis zum Board vergehen so wenige Sekunden (Webhook → Sync → SSE-Push ans Frontend).

Authentifizierung ist kein API-Key, sondern die HMAC-Signatur, die Codeberg selbst mitschickt: X-Forgejo-Signature (bzw. X-Gitea-Signature) enthaelt den HMAC-SHA256 des rohen Request-Bodys, gebildet mit dem Webhook-Secret aus CODEBERG_WEBHOOK_SECRET. Ist die Variable nicht gesetzt, antwortet der Endpunkt grundsaetzlich mit 401 - ein offener Sync-Ausloeser im Internet waere sonst die Folge.

Antwort Bedeutung
202 mit {"status": "sync angefordert"} Sync wurde angestossen
200 mit {"status": "pong"} ping-Event beim Anlegen des Webhooks
401 Signatur fehlt, ist falsch oder es ist kein Secret konfiguriert

Einrichtung im Codeberg-Repo unter Einstellungen → Webhooks → Forgejo:

  • Ziel-URL: https://<oeffentliche-api-url>/api/v1/webhooks/codeberg
  • HTTP-Methode: POST, Content-Type application/json
  • Secret: derselbe Wert wie CODEBERG_WEBHOOK_SECRET (z.B. openssl rand -hex 32)
  • Ereignisse: Benutzerdefiniert → Issues und Issue-Label (Issue-Kommentare sind fuer das Board irrelevant und wuerden nur ueberfluessige Syncs ausloesen)

Anders als der sync-Service braucht dieser Endpunkt eine eingehende Freigabe: Proxy und Firewall muessen POST /api/v1/webhooks/codeberg aus dem Internet an den api-Container durchlassen. Ob eine Zustellung geklappt hat, zeigt Codeberg direkt beim Webhook unter „Letzte Zustellungen" (inkl. Antwort-Body). Der periodische Sync laeuft unabhaengig weiter und ist das Sicherheitsnetz fuer verpasste Zustellungen (Container-Neustart, Stoerung).

Fehlerformat

Alle Fehlerantworten haben die Form { "error": "‹Meldung›" } mit passendem HTTP-Status:

Status Bedeutung
400 Ungueltige Anfrage (z. B. unbekannter Endpunkt-Parameter)
401 Fehlender/ungueltiger X-Api-Key/X-Admin-Key
403 Aktion nicht erlaubt (z. B. fremde Agent-Aktivitaet melden)
404 Ressource nicht gefunden
405 HTTP-Methode fuer diesen Pfad nicht erlaubt
422 Validierungsfehler (Pflichtfeld fehlt, ungueltiger Enum-Wert)