Auf der Produktionsmaschine traten vereinzelt Timeouts nach codeberg.org auf
("Operation timed out after 15003 milliseconds with 0 bytes received"), waehrend
die Verbindung sonst in ~0,1s antwortet.
Bisher fiel der Dauerlaeufer nach so einem Fehlschlag in den regulaeren Takt
zurueck. Weil SyncTrigger::consume() die Webhook-Anforderung schon vor dem Lauf
quittiert, war eine per Webhook angeforderte Aktualisierung damit verloren und
kam erst bis zu MONITOR_SYNC_INTERVAL Sekunden spaeter - genau die Verzoegerung,
die der Webhook abschaffen sollte.
Neu wird der naechste Versuch nach einem Fehlschlag vorgezogen: 5s, 10s, 20s, …
gedeckelt auf das regulaere Intervall (Sync\SyncSchedule). Ein kurzer Aussetzer
ist damit binnen Sekunden ueberstanden, eine laengere Codeberg-Stoerung laeuft
trotzdem nicht in einen Dauerbeschuss. Nach einem erfolgreichen Lauf gilt sofort
wieder das regulaere Intervall.
Die Wartezeit-Berechnung liegt in einer eigenen Klasse, weil die Schleife in
bin/sync-codeberg.php selbst nicht testbar ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
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
- Authentifizierung
- Agenten
- Tickets
- Events
- Systeme
- KPIs
- Live-Updates (SSE)
- Codeberg-Sync (Backlog/Done)
- Fehlerformat
Grundlagen
- Basis-Pfad:
/api/v1 - Request-/Response-Body:
application/json - Alle Zeitstempel: ISO-8601 UTC (
YYYY-MM-DDTHH:mm:ssZ), serverseitig erzeugt viaDatabase::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 UmgebungsvariableMONITOR_API_BASE(siehe.env.exampleunddocker/nginx/default.conf.template). Leerer Wert = gleicher Origin, dann wird der relative Pfad/api/v1verwendet.
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,testingunddonewerden regelmaessig aus Codeberg-Issues befuellt (siehe Codeberg-Sync);qaundfertigsind 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, sonsttesting→testing, sonstautomate→bereit, sonstbacklog. - Geschlossene Issues → Spalte
done. - Nur falls das Ticket noch in einer der Sync-Spalten (
backlog,bereit,in_arbeit,testing,done) ist - aufqa/fertigmanuell 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.
Schlaegt ein Lauf fehl (vereinzelte Timeouts nach codeberg.org kommen vor), wird der
naechste Versuch vorgezogen statt bis zum regulaeren Intervall zu warten: 5s, 10s, 20s,
… bis hoechstens MONITOR_SYNC_INTERVAL (Sync\SyncSchedule). Sonst ginge eine per Webhook
angeforderte Aktualisierung verloren - die Anforderung wird vor dem Lauf quittiert und
waere damit erst nach vollen MONITOR_SYNC_INTERVAL Sekunden nachgeholt. Nach einem
erfolgreichen Lauf gilt sofort wieder das regulaere Intervall. Im Log sieht das so aus:
[sync] Lauf fehlgeschlagen (1x in Folge, naechster Versuch in 5s): Codeberg-API nicht erreichbar: …
[sync] Lauf fehlgeschlagen (2x in Folge, naechster Versuch in 10s): …
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-Typeapplication/json - Secret: derselbe Wert wie
CODEBERG_WEBHOOK_SECRET(z.B.openssl rand -hex 32) - Ereignisse: Benutzerdefiniert →
IssuesundIssue-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) |