Seit dem Praesenz-Timeout aus Ticket #17 verschwindet ein Agent aus der Buero-Visualisierung, sobald last_seen_at aelter als zehn Minuten ist. Damit sich das auf der Produktionsmaschine nachvollziehen laesst, dokumentiert GET /api/v1/agents jetzt, dass der Endpunkt unauthentifiziert ist (last_seen_at also per curl abrufbar ist) und wie sich derselbe Wert notfalls direkt aus der SQLite-Datei auslesen laesst. Schliesst #20
12 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 |
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"
},
"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 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}'
Ein Agent gilt im Frontend nur noch als anwesend, solange last_seen_at hoechstens zehn
Minuten zurueckliegt (isAgentPresent, html/js/office-scene.js, Issue #17) - laeuft der
Timeout ab, bleibt sein Schreibtisch leer, obwohl er weiterhin registriert ist. Bei direktem
Zugriff auf die Produktionsmaschine laesst sich derselbe Wert auch direkt aus der
SQLite-Datei lesen (MONITOR_DB_PATH, Standard api/data/monitor.sqlite):
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, macht sofort einen ersten Lauf und wiederholt ihn dann alle
MONITOR_SYNC_INTERVAL Sekunden (Default 600, siehe .env.example). Er nutzt dasselbe
Image und dieselbe SQLite-Datei wie api und verbindet sich ausschliesslich ausgehend nach
codeberg.org - es ist also kein Port-Mapping und keine Firewall-Freigabe 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.
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) |