Der Codeberg-Sync speichert den Ersteller (user.login) von Issues und PRs in der neuen Ticket-Spalte author (Migration 010). Die Buero-Visualisierung zaehlt offene Tickets (alles ausser Done), deren Ersteller weder fds2610 noch fds2610-bot ist, und zeigt die Anzahl in einer kleinen roten Fahne auf dem PO-Tisch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 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
- Sprachaufnahmen
- Tickets
- Events
- Systeme
- KPIs
- Live-Updates (SSE)
- Projekte (Codeberg-Sync konfigurieren)
- Hosts (Ping-Ueberwachung konfigurieren)
- 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), POST/PATCH/DELETE /api/v1/repo-configs (Projekte des Codeberg-Sync), POST /api/v1/tickets/resync (Board neu einlesen) |
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, ops, voice, 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.
voice ist die Rolle des Voice-Bots: des Bots, der die Sprachaufnahmen abholt,
transkribiert und daraus Tickets anlegt (siehe Sprachaufnahmen). Auch
er bekommt einen Schreibtisch - mit einem Mikrofon als Deko, dem Zaehler offener Aufnahmen
und als einzige Figur einem Link auf record.html. Registriert wird er von der Maschine
aus, auf der er laeuft: scripts/register-voice-agent.sh in
fds2610/Agent.
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› ], "pending_recordings": 0 }.
pending_recordings ist die Anzahl offener Sprachaufnahmen (status: "new") in der
gemeinsamen Warteschlange - ein Wert fuers ganze Buero, kein Feld je Agent, seit die
Warteschlange niemandem mehr gehoert. Das Frontend zeichnet den Zaehler damit an den
Schreibtisch des Voice-Bots, siehe Sprachaufnahmen.
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 (Ausnahme sind die
Problemzustaende, die sichtbar bleiben). 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;"
last_seen_at frischt nicht nur POST /api/v1/agents/{id}/activity auf, sondern jeder
mit gueltigem X-Api-Key authentifizierte Request (AgentRepository::touch(), Zustand und
Ticket bleiben dabei unveraendert). Wer sich mit seinem Key meldet, ist offensichtlich da - so
bleibt z.B. der OPs-Agent eines check_fds.sh mit aelterer monitor-api.conf ohne
MONITOR_AGENT_ID ueber seine Systemmeldungen praesent, obwohl er nie activity aufruft.
GET /api/v1/agents/me — Eigenen Agenten abfragen
Auth: X-Api-Key. Response 200: { "agent": ‹Agent-Objekt, ohne api_key› } des Agenten,
dem der Key gehoert. Dient Clients, die zwar ihren Key, aber nicht ihre ID kennen:
check_fds.sh traegt so eine in monitor-api.conf fehlende MONITOR_AGENT_ID selbst nach
(ohne Admin-Key, den der Ping-Rechner nicht hat).
curl -s "$MONITOR_API_BASE/agents/me" -H "X-Api-Key: $MONITOR_API_KEY" | jq '.agent.id'
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.
state ist bewusst nicht serverseitig eingeschraenkt - welche Zustaende es gibt, entscheidet
allein die Darstellung (AGENT_STATE_VISUALS in html/js/office-scene.js), unbekannte Werte
fallen dort auf "idle am Schreibtisch" zurueck. Gebraeuchlich sind idle, working sowie die
Lauf-/Interaktionszustaende der Zustandsmaschine (walking_to_board, interacting_with_board,
walking_to_monitor, interacting_with_monitor, walking_back).
transcribing meldet der Voice-Bot, sobald er eine neue Sprachnachricht empfangen hat; der
Bildschirm an seinem Schreibtisch ist aktiv und es laufen keine idle-Aktivitaeten (Kaffee,
Zeitung). Nach der Transkription meldet er wieder idle.
Problemzustaende: abgelaufene Claude-Anmeldung
Zwei Zustaende sind Sonderfaelle, weil sie ein menschliches Eingreifen erfordern:
state |
Bedeutung | Darstellung |
|---|---|---|
auth_expiring |
Die Claude-Anmeldung des Agenten laeuft demnaechst ab, er arbeitet noch. | gelbes Schloss am Avatar, gelbes Namensschild, gelber Header-Banner |
auth_expired |
Die Anmeldung ist abgelaufen, der Agent kommt nicht mehr weiter. | rot pulsierendes Schloss, rotes Namensschild, roter Header-Banner |
Hintergrund: Laeuft die Anmeldung aus, laeuft das Cron-Skript um den Agenten herum
unbeeindruckt weiter - es meldet zu Beginn working, ruft claude auf, bekommt nichts
Brauchbares zurueck und meldet am Ende idle. Ohne eigenen Zustand ist der Ausfall vom
Normalbetrieb nicht zu unterscheiden: last_seen_at ist frisch, der Agent sieht beschaeftigt
aus, waehrend Lauf fuer Lauf nichts entsteht. Der Monitor-API-Key ist von der Claude-Anmeldung
unabhaengig und bleibt gueltig - der Agent kann den Zustand also weiterhin melden.
Zwei Besonderheiten:
- Sie ueberleben den Praesenz-Timeout. Hat ein Agent
auth_expiredgemeldet, bricht sein Skript den Lauf ab und meldet bis zum naechsten Cron-Start nichts mehr. Das Frontend zeigt ihn trotzdem weiter (isAgentVisible()), sonst verschwaende er genau dann vom Schreibtisch, wenn seine Warnung dort stehen soll.is_present/agents_onlinebleiben davon unberuehrt - ein blockierter Agent zaehlt nicht als online. - Sie loesen sich nur durch eine neue Meldung. Es gibt bewusst kein Timeout; der naechste
erfolgreiche Lauf meldet ohnehin
working/idleund raeumt den Zustand damit auf.
Ergaenzend gehoert ein Event (POST /api/v1/events, severity error bzw. warning) dazu - der
Zustand allein hat kein Datum, das Event beantwortet "seit wann".
curl -X POST "$MONITOR_API_BASE/agents/$ID/activity" \
-H "Content-Type: application/json" -H "X-Api-Key: $KEY" \
-d '{"state": "auth_expired", "current_ticket": null}'
Sprachaufnahmen
Sprachnotizen, aufgenommen ueber record.html im Handy-Browser. Die eigentliche
Transkription passiert bot-seitig (whisper.cpp, siehe bin/voice-recording-agent.sh in
fds2610/Agent) - diese API liefert nur die Rohaufnahme
mit Status.
Eine Warteschlange fuer alle, ohne Empfaenger. Bis einschliesslich Migration
006_voice_recordings.sql hing jede Aufnahme an einer Agenten-Id, und jede Agentenfigur in
der Visualisierung verlinkte auf ihre eigene Warteschlange. Das hat Warteschlangen erzeugt,
die nie jemand geleert hat: abgeholt wird nur von dem Bot, auf dem whisper.cpp laeuft.
Seit 007_voice_recordings_global.sql gibt es genau eine Warteschlange; welches Ticket in
welchem Repo entsteht, entscheidet der abholende Bot anhand des Transkripts - nicht die
Aufnahme anhand einer Id.
Abgeholt wird sie vom Voice-Bot: einem Agenten mit der Rolle voice (siehe
Agenten). In der Buero-Visualisierung verlinkt nur seine Personen-Figur auf
record.html, und der Zaehler an seinem Schreibtisch zeigt, wie viele Aufnahmen noch auf
Abholung warten (pending_recordings).
POST /api/v1/recordings — Aufnahme hochladen
Auth: keine. Der Upload kommt aus record.html, einer oeffentlich verlinkten Seite im
Handy-Browser eines Menschen, nicht von einem registrierten Agenten - es gibt dort keinen
API-/Admin-Key einzugeben.
Request: multipart/form-data mit Feld audio (Datei). Erlaubte MIME-Typen: audio/webm,
audio/ogg, audio/mp4, audio/mpeg, audio/wav, audio/x-wav - andere lehnt der
Endpunkt mit 422 ab. Groessenlimit: MONITOR_MAX_RECORDING_BYTES (Default 25 MiB), darueber
413.
// Response 201
{
"recording": {
"id": 1, "mime_type": "audio/webm", "size_bytes": 48213,
"status": "new", "created_at": "2026-09-03T10:00:00Z", "processed_at": null
}
}
GET /api/v1/recordings — Warteschlange abfragen
Auth: X-Api-Key eines beliebigen registrierten Agenten. Keinen Ownership-Check mehr: die
Warteschlange gehoert niemandem, in der Praxis fragt sie der Voice-Bot alle fuenf Minuten
ab. Ein zweiter Bot mit eigenem Key waere sonst ohne Not ausgesperrt.
Query-Parameter status optional (new oder done), ohne Filter kommen beide zurueck.
Response: { "recordings": [ ‹Aufnahme-Objekte wie oben› ] }, aelteste zuerst
(created_at aufsteigend) - in dieser Reihenfolge arbeitet der Bot sie ab.
GET /api/v1/recordings/{id}/audio — Audiodatei herunterladen
Auth: X-Api-Key eines registrierten Agenten.
Response: die Rohdatei mit dem beim Upload erkannten Content-Type, kein JSON.
PATCH /api/v1/recordings/{id} — Als verarbeitet markieren
Auth: X-Api-Key eines registrierten Agenten. Nach erfolgreicher Abholung/Transkription
markiert der Bot die Aufnahme so, damit sie nicht erneut in
GET /api/v1/recordings?status=new auftaucht.
// Request
{ "status": "done" }
Response 200: aktualisiertes Aufnahme-Objekt (status: "done", processed_at gesetzt).
Tickets
Spalten (F3): backlog, bereit, in_arbeit, qa, testing, fertig, done.
backlog,bereit,in_arbeit,qa,testingunddonewerden regelmaessig aus Codeberg-Issues aus mehreren konfigurierten Projekten befuellt (siehe Codeberg-Sync);fertigist fuer ein zukuenftiges Label reserviert und ergibt sich ausschliesslich aus Agenten-Meldungen (F4).- Ticket-Typ (statt Prioritaet) wird bei Codeberg-Tickets aus dem ersten Issue-Label uebernommen, die Farbe dagegen fest aus dem Quell-Projekt (F4/F26) - so bleiben Tickets aus unterschiedlichen Codeberg-Projekten auf einen Blick unterscheidbar, unabhaengig davon, welche Labels dort jeweils existieren.
repo(Formatowner/name) identifiziert bei Codeberg-Tickets das Quell-Projekt und machtexternal_ref(die Issue-Nummer) nur je Projekt eindeutig - Issue #5 infds2610/monitorund Issue #5 infds2610/Steuerkramsind unterschiedliche Tickets.- 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", "repo", "title", "description", "column_name", "type", "color", "assignee_agent_id", "source", "is_pull_request", "author", "created_at", "updated_at" }, ... ] }
author ist der Codeberg-Login, der Issue bzw. PR angelegt hat (vom Codeberg-Sync gepflegt,
bei per API angelegten Tickets null, Issue #113).
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.
POST /api/v1/tickets/resync — Board komplett neu einlesen
Auth: X-Admin-Key. Issue #51: manueller "Reload"-Knopf am Kanban-Board im Frontend fuer den
Fall, dass Tickets (geloescht, verschoben, o.ae.) vom regulaeren Codeberg-Sync nicht mehr
bereinigt werden. Liest synchron alle konfigurierten Codeberg-Projekte neu ein (siehe
Codeberg-Sync) und entfernt dabei je Projekt zusaetzlich Tickets,
deren Issue nicht mehr unter den offenen/geschlossenen Issues auftaucht - selbst wenn sie
manuell auf eine nicht vom Sync verwaltete Spalte wie fertig bewegt wurden. Tickets ohne
Projektbezug (z.B. per API angelegt) bleiben unberuehrt. Anders als der Webhook-Trigger kein
asynchroner Anstoss des sync-Containers, sondern derselbe Ablauf direkt im api-Container, damit
die Antwortzeit die tatsaechliche Sync-Dauer widerspiegelt.
Response 200 mit { "tickets": [...] } (neuer Board-Zustand), 502 falls die Codeberg-API
nicht erreichbar war (bereits verarbeitete Projekte bleiben dann auf dem neuen Stand, noch
nicht erreichte auf dem alten - der naechste periodische Sync-Lauf oder Webhook gleicht das an).
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.
POST /api/v1/events/system — Event des Monitorings selbst melden
Wie POST /api/v1/events, aber Auth per X-Admin-Key. Genutzt von update.sh, das nach einem
erfolgreichen Update name: "Monitor", message: "Update eingespielt (<Commit>)",
severity: "warning" meldet.
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).
DELETE /api/v1/systems/{id} — Kachel entfernen
Auth: X-Admin-Key. Issue #70: Entfernt ein System aus dem Monitor. Gedacht fuer Kacheln,
die niemand mehr meldet - ein einmal gemeldetes System blieb sonst fuer immer stehen, nur
eben ohne Aktualisierung. Response 200 mit { "deleted": true|false } (false = es gab
kein System mit dieser Id). Meldet sich das System spaeter erneut, legt
POST /api/v1/systems/{id}/status es unveraendert neu an.
Einen Host der Ping-Ueberwachung entfernt man dagegen ueber
DELETE /api/v1/hosts/{id} - der nimmt seine Kachel
selbst mit.
KPIs
F7: Konkrete Kennzahlen sind noch offen ("kommt spaeter"). Phase 1 stellt nur die generische Speicher-/Abfragestruktur bereit.
Zusaetzlich zu hier gemeldeten Kennzahlen zeigt der KPI-Monitor im Frontend vier weitere,
nicht per POST /api/v1/kpis/{key} gemeldete Kacheln, die direkt aus anderen Endpunkten
abgeleitet sind: "Agenten"/"Online"/"Problem" aus GET /api/v1/agents (Issue #39) und "Hooks"
(ganz unten) aus GET /api/v1/webhooks/stats,
siehe Issue #38.
"Problem" zaehlt die Agenten in einem Problemzustand und faerbt sich nur rot, wenn der Wert ueber 0 liegt - eine dauerhaft rote Kachel mit einer 0 waere genau das Rauschen, das den Ernstfall untergehen liesse.
Issue #59: die Kennzahl open_prs ("Offene PRs") wird nicht per POST /api/v1/kpis/{key}
gemeldet, sondern am Ende jedes Sync-Laufs (CodebergSyncService::sync()/resync()) automatisch
aus der Summe der offenen Pull Requests aller konfigurierten Projekte berechnet und ueber das
generische KPI-Repository gespeichert. Ein Sync-Lauf startet u.a. bei jedem eingehenden
Codeberg-Webhook (auch PR-Aenderungen) sowie periodisch, siehe
POST /api/v1/webhooks/codeberg.
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.
Projekte (Codeberg-Sync konfigurieren)
Seit Issue #27 ist die Projektliste des Codeberg-Sync (vorher CODEBERG_REPOS) in der
Datenbank gepflegt statt in einer Umgebungsvariable - editierbar per API bzw. ueber das
Frontend-Modal (Klick auf den PO in der Buero-Visualisierung). Erster Start seedet die drei
bisherigen Default-Projekte fds2610/monitor (gelb), fds2610/Steuerkram (gruen) und
fds2610/nc-gantt (blau), siehe api/src/Db/migrations/003_repo_configs.sql.
| Endpunkt | Auth | Beschreibung |
|---|---|---|
GET /api/v1/repo-configs |
- | Liste aller konfigurierten Projekte, sortiert nach sort_order |
POST /api/v1/repo-configs |
X-Admin-Key |
Legt ein Projekt an (owner, repo, color als 6-stelliger Hex-Wert, mit oder ohne #) |
PATCH /api/v1/repo-configs/{id} |
X-Admin-Key |
Aendert owner/repo/color eines Projekts |
DELETE /api/v1/repo-configs/{id} |
X-Admin-Key |
Entfernt ein Projekt (der Sync liest es danach nicht mehr) und loescht dessen Tickets aus dem Kanban (Issue #43) |
POST /api/v1/repo-configs/{id}/check |
X-Admin-Key |
Prueft Schreibrechte + Webhook des Projekts (Issue #53, siehe unten) |
owner+repo muessen zusammen eindeutig sein (409 -> hier als 422 mit Fehlermeldung, da
kein Konflikt-Statuscode im Fehlerformat vorgesehen ist). Der laufende sync-Dienst
(--watch) liest die Projektliste bei jedem Lauf frisch ein - eine Aenderung wirkt also ohne
Neustart des Containers.
POST /api/v1/repo-configs/{id}/check — Schreibrechte + Webhook pruefen
Issue #53: Das Frontend-Modal ruft diesen Endpunkt automatisch nach dem Anlegen/Bearbeiten
eines Projekts auf und zeigt das Ergebnis als gruenen Haken bzw. rotes Kreuz rechts neben den
Bearbeiten-/Loeschen-Symbolen (Tooltip = message). Beide Checks nutzen CODEBERG_TOKEN
(das Konto, unter dem der Sync laeuft):
// Response 200
{
"checks": {
"write_access": { "ok": true, "message": "fds2610-bot ist Mitarbeiter mit Schreibrechten auf dieses Projekt." },
"webhook": { "ok": false, "message": "Kein aktiver Webhook auf \"/api/v1/webhooks/codeberg\" gefunden - unter Einstellungen -> Webhooks einrichten." }
}
}
write_access: liestpermissions.pushausGET /repos/{owner}/{repo}- erfordert lediglich, dass das Projekt fuer den Token sichtbar ist.webhook: listetGET /repos/{owner}/{repo}/hooksund sucht einen aktiven Hook mitcontent_type: json, dessen URL auf/api/v1/webhooks/codebergendet. Das Auflisten von Hooks erfordert Admin-Rechte auf das Projekt - fehlen die, liefert der Checkok: falsemit entsprechender Meldung statt eines Fehlers.
Hosts (Ping-Ueberwachung konfigurieren)
Seit Issue #70 steht die Liste der per Ping ueberwachten Hosts in der Datenbank statt nur in
der hosts.conf neben check_fds.sh - pflegbar ueber das Zahnrad am Wandmonitor "Systeme"
in der Buero-Visualisierung. Jeder Host gehoert dem OPs-Agenten (Issue #66), der ihn anpingt:
API und Ping-Check laufen nicht zwangslaeufig auf demselben Rechner, die API kann die
hosts.conf also nicht selbst schreiben. Stattdessen holt check_fds.sh seine Liste bei
jedem Lauf mit dem eigenen Agenten-Key ab und schreibt daraus die lokale Datei.
| Endpunkt | Auth | Beschreibung |
|---|---|---|
GET /api/v1/hosts |
- | Alle Hosts ueber alle Ping-Rechner, je mit agent_name und system_ids (siehe unten) |
POST /api/v1/hosts |
X-Admin-Key |
Legt einen Host an (agent_id, hostname, address) |
PATCH /api/v1/hosts/{id} |
X-Admin-Key |
Aendert hostname/address; der zustaendige Agent bleibt, wie er ist |
DELETE /api/v1/hosts/{id} |
X-Admin-Key |
Nimmt den Host aus der Ueberwachung und loescht dessen Kachel im System-Monitor |
GET /api/v1/hosts/conf |
X-Api-Key |
Die hosts.conf des anfragenden Ping-Rechners als text/plain |
POST /api/v1/hosts/import |
X-Api-Key |
Erstbefuellung aus einer hochgeladenen hosts.conf (text/plain) |
hostname und address duerfen keine Leerzeichen enthalten und nicht mit # beginnen -
hosts.conf ist zeilen- und leerzeichengetrennt, ein solcher Wert wuerde dort still
verstuemmelt. hostname muss je Ping-Rechner eindeutig sein (422 bei einem Duplikat),
ueber verschiedene Rechner hinweg ist er erlaubt. agent_id muss zu einem registrierten
Agenten der Rolle ops gehoeren.
system_ids nennt die Ids, unter denen dieser Host im System-Monitor auftauchen kann - es
sind zwei, weil beide Meldewege ihn unterschiedlich benennen: der Legacy-Import aus
status.txt nimmt den Hostnamen unveraendert, die REST-Meldung aus check_fds.sh dessen
kleingeschriebenen Slug (Nextcloud(lokal) -> nextcloud-lokal). Das Frontend erkennt daran,
welche Kachel zu welchem Host gehoert; DELETE /api/v1/hosts/{id} raeumt beide ab.
Ablauf im Ping-Check
GET /api/v1/hosts/confmit dem Agenten-Key des OPs-Agenten.200: der Body ist die neuehosts.conf- auch wenn er keinen Host enthaelt (dann wurden im Frontend alle entfernt und es wird nichts mehr geprueft).204: fuer diesen Rechner ist noch nie etwas konfiguriert worden. Dann laedtcheck_fds.shseine bestehendehosts.confperPOST /api/v1/hosts/importhoch, damit die bereits ueberwachten Hosts nicht von Hand nachgetragen werden muessen.- Alles andere (inkl. nicht erreichbarer API): die zuletzt geschriebene lokale Datei bleibt in Kraft - eine Stoerung der API darf die Ueberwachung nicht anhalten.
POST /api/v1/hosts/importnimmt nur den allerersten Upload eines Rechners an und antwortet danach mit{ "imported": 0 }. Sonst wuerde ein Ping-Rechner mit alter lokaler Datei die im Frontend geloeschten Hosts wieder anlegen.
Codeberg-Sync (Backlog/Done)
api/bin/sync-codeberg.php liest offene und geschlossene Issues aus den konfigurierten
Codeberg-Projekten (siehe Projekte) und gleicht sie
je Projekt ueber repo + external_ref (= Issue-Nummer) mit den Tickets ab:
- Offene Issues → Spalte je nach Label:
wip→in_arbeit, sonsttesting→testing, sonsthelp wanted→qa, sonstautomate→bereit, sonstbacklog. - Geschlossene Issues → Spalte
done. - Nur falls das Ticket noch in einer der Sync-Spalten (
backlog,bereit,in_arbeit,qa,testing,done) ist - auffertigmanuell verschobene Tickets werden nicht zurueckgesetzt. - Ticket-Typ kommt vom ersten Issue-Label, die Farbe fest vom Projekt (siehe oben).
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 aller konfigurierten Projekte (siehe
Projekte), nicht nur das des ausloesenden Repos. Vom Aendern des Issues bis zum
Board vergehen so wenige Sekunden (Webhook → Sync → SSE-Push ans Frontend).
Bei mehreren Projekten muss der Webhook in jedem Repo eingerichtet werden, dessen
Aenderungen sofort statt erst beim naechsten periodischen Lauf ankommen sollen - ein
fehlender Webhook auf einem der Projekte faengt der periodische Sync (MONITOR_SYNC_INTERVAL)
weiterhin ab.
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).
GET /api/v1/webhooks/stats — Empfangene Webhooks zaehlen (Hooks-KPI)
Auth: keine (oeffentlich wie die anderen GET-Endpunkte).
{ "count": 12 }
count zaehlt jede angenommene Zustellung an POST /api/v1/webhooks/codeberg (auch
ping) am heutigen Tag (Serverdatum); ab Mitternacht beginnt der Zaehler bei 0. Die
Zaehldatei (MONITOR_HOOKS_COUNTER_FILE) enthaelt Datum und Anzahl. Das Frontend zeigt den
Wert als eigene, immer sichtbare Kachel "Hooks heute" ganz rechts im KPI-Monitor (Issue #38).
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) |