# 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](Visualization.md), Abschnitt 9). Implementierung: Vanilla-PHP (kein Framework), Persistenz in SQLite (`api/data/monitor.sqlite`). Details zum lokalen Setup: [api/README.md](api/README.md). ## Inhaltsverzeichnis - [Grundlagen](#grundlagen) - [Authentifizierung](#authentifizierung) - [Agenten](#agenten) - [Sprachaufnahmen](#sprachaufnahmen) - [Tickets](#tickets) - [Events](#events) - [Systeme](#systeme) - [KPIs](#kpis) - [Live-Updates (SSE)](#live-updates-sse) - [Projekte (Codeberg-Sync konfigurieren)](#projekte-codeberg-sync-konfigurieren) - [Hosts (Ping-Ueberwachung konfigurieren)](#hosts-ping-ueberwachung-konfigurieren) - [Codeberg-Sync (Backlog/Done)](#codeberg-sync-backlogdone) - [Fehlerformat](#fehlerformat) ## 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](#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](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), `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](#post-apiv1webhookscodeberg--sofort-sync-per-webhook). 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](#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](https://codeberg.org/fds2610/Agent). ### `POST /api/v1/agents` — Agent registrieren Auth: `X-Admin-Key`. ```json // Request { "display_name": "Alice", "role": "entwickler" } ``` ```json // 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](#sprachaufnahmen). Oeffentlich, unauthentifiziert (siehe [Grundlagen](#grundlagen)) - damit laesst sich `last_seen_at`/`is_present` jedes Agenten jederzeit von aussen pruefen, z.B. auf der Produktionsmaschine: ```bash 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](#problemzustaende-abgelaufene-claude-anmeldung), 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: ```bash 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). ```bash 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`). ```json // 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_expired` gemeldet, 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_online` bleiben 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`/`idle` und 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". ```bash 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](https://codeberg.org/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](#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`. ```json // 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. ```json // 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`, `testing` und `done` werden regelmaessig aus Codeberg-Issues aus mehreren konfigurierten Projekten befuellt (siehe [Codeberg-Sync](#codeberg-sync-backlogdone)); `fertig` ist 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` (Format `owner/name`) identifiziert bei Codeberg-Tickets das Quell-Projekt und macht `external_ref` (die Issue-Nummer) nur je Projekt eindeutig - Issue #5 in `fds2610/monitor` und Issue #5 in `fds2610/Steuerkram` sind 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", "created_at", "updated_at" }, ... ] }` ### `POST /api/v1/tickets` — Ticket anlegen Auth: `X-Api-Key`. ```json // 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](#codeberg-sync-backlogdone)) 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`. ```json { "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 ()"`, `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`. ```json { "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}`](#hosts-ping-ueberwachung-konfigurieren) - 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`](#get-apiv1webhooksstats--empfangene-webhooks-zaehlen-hooks-kpi), siehe Issue #38. "Problem" zaehlt die Agenten in einem [Problemzustand](#problemzustaende-abgelaufene-claude-anmeldung) 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-apiv1webhookscodeberg--sofort-sync-per-webhook). ### `POST /api/v1/kpis/{key}` — KPI-Datenpunkt melden Auth: `X-Api-Key`. ```json { "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](https://developer.mozilla.org/de/docs/Web/API/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): ```json // 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`: liest `permissions.push` aus `GET /repos/{owner}/{repo}` - erfordert lediglich, dass das Projekt fuer den Token sichtbar ist. - `webhook`: listet `GET /repos/{owner}/{repo}/hooks` und sucht einen aktiven Hook mit `content_type: json`, dessen URL auf `/api/v1/webhooks/codeberg` endet. Das Auflisten von Hooks erfordert Admin-Rechte auf das Projekt - fehlen die, liefert der Check `ok: false` mit 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 1. `GET /api/v1/hosts/conf` mit dem Agenten-Key des OPs-Agenten. - `200`: der Body ist die neue `hosts.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 laedt `check_fds.sh` seine bestehende `hosts.conf` per `POST /api/v1/hosts/import` hoch, 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. 2. `POST /api/v1/hosts/import` nimmt 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](#projekte-codeberg-sync-konfigurieren)) und gleicht sie je Projekt ueber `repo` + `external_ref` (= Issue-Nummer) mit den Tickets ab: - Offene Issues → Spalte je nach Label: `wip` → `in_arbeit`, sonst `testing` → `testing`, sonst `help wanted` → `qa`, sonst `automate` → `bereit`, sonst `backlog`. - Geschlossene Issues → Spalte `done`. - Nur falls das Ticket noch in einer der Sync-Spalten (`backlog`, `bereit`, `in_arbeit`, `qa`, `testing`, `done`) ist - auf `fertig` manuell 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](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. ```bash 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](#projekte-codeberg-sync-konfigurieren)), 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:///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). ### `GET /api/v1/webhooks/stats` — Empfangene Webhooks zaehlen (Hooks-KPI) Auth: keine (oeffentlich wie die anderen `GET`-Endpunkte). ```json { "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) |