Setzt den in #84 skizzierten Workflow um: record.html nimmt im Handy-Browser eine Sprachnotiz per MediaRecorder auf und laedt sie unauthentifiziert zu POST /api/v1/agents/{id}/recordings hoch. Ein Klick auf die Personen-Figur jedes registrierten Agenten in der Buero-Visualisierung verlinkt dorthin, ein Zaehler am Schreibtisch zeigt offene Aufnahmen. Der Agent selbst holt seine Warteschlange ueber die bestehenden Api-Key-authentifizierten Endpunkte ab (GET .../recordings, GET /recordings/{id}/audio) und markiert eine Aufnahme nach der Transkription per PATCH als erledigt - die eigentliche Transkription (whisper.cpp/faster-whisper) bleibt agent-seitig, siehe fds2610/Agent#17. Schliesst #85
633 lines
31 KiB
Markdown
633 lines
31 KiB
Markdown
# 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`, `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.
|
||
|
||
### `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› ] }`. Jeder Agent traegt
|
||
zusaetzlich `pending_recordings` (Issue #85): Anzahl offener Sprachaufnahmen in seiner
|
||
Warteschlange (`status: "new"`), Grundlage fuer den Zaehler auf seinem Schreibtisch in der
|
||
Buero-Visualisierung, 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;"
|
||
```
|
||
|
||
### `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`).
|
||
|
||
#### 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
|
||
|
||
Issue #85 (Folge-Ticket aus #84): Sprachnotizen fuer einen Agenten, aufgenommen ueber
|
||
`record.html` im Handy-Browser. Die eigentliche Transkription passiert agent-seitig
|
||
(whisper.cpp/faster-whisper, siehe [fds2610/Agent#17](https://codeberg.org/fds2610/Agent/issues/17)) -
|
||
diese API liefert nur die Rohaufnahme mit Status.
|
||
|
||
In der Buero-Visualisierung verlinkt ein Klick auf die Personen-Figur jedes registrierten
|
||
Agenten auf `record.html?agent=‹id›`; ein kleiner Zaehler an seinem Schreibtisch zeigt, wie
|
||
viele Aufnahmen dort noch auf Abholung warten (`pending_recordings`, siehe
|
||
[Agenten](#agenten)).
|
||
|
||
### `POST /api/v1/agents/{id}/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. Die Agenten-Id kommt als URL-Segment mit.
|
||
|
||
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, "agent_id": 3, "mime_type": "audio/webm", "size_bytes": 48213,
|
||
"status": "new", "created_at": "2026-09-03T10:00:00Z", "processed_at": null
|
||
}
|
||
}
|
||
```
|
||
|
||
`404`, falls die Agenten-Id nicht existiert.
|
||
|
||
### `GET /api/v1/agents/{id}/recordings` — Eigene Warteschlange abfragen
|
||
|
||
Auth: `X-Api-Key` des betroffenen Agenten (ein Agent darf nur seine eigene Warteschlange
|
||
abfragen, sonst `403`) - fuer das regelmaessige Abfragen aus [fds2610/Agent#17](https://codeberg.org/fds2610/Agent/issues/17).
|
||
|
||
Query-Parameter `status` optional (`new` oder `done`), ohne Filter kommen beide zurueck.
|
||
|
||
Response: `{ "recordings": [ ‹Aufnahme-Objekte wie oben› ] }`, sortiert nach `created_at`.
|
||
|
||
### `GET /api/v1/recordings/{id}/audio` — Audiodatei herunterladen
|
||
|
||
Auth: `X-Api-Key` des Agenten, dem die Aufnahme gehoert (sonst `403`).
|
||
|
||
Response: die Rohdatei mit dem beim Upload erkannten `Content-Type`, kein JSON.
|
||
|
||
### `PATCH /api/v1/recordings/{id}` — Als verarbeitet markieren
|
||
|
||
Auth: `X-Api-Key` des Agenten, dem die Aufnahme gehoert (sonst `403`). Nach erfolgreicher
|
||
Abholung/Transkription markiert der Agent die Aufnahme so, damit sie nicht erneut in
|
||
`GET .../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", "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.
|
||
|
||
### `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://<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).
|
||
|
||
### `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`) **seit dem letzten Start des `api`-Containers** - nicht seit Projektbeginn: die
|
||
Zaehldatei wird bei jedem Start dieses Containers zurueckgesetzt (`docker/entrypoint.sh`,
|
||
`MONITOR_HOOKS_COUNTER_FILE`). Ein Neustart des `sync`-Containers alleine setzt den Zaehler
|
||
nicht zurueck, da nur der `api`-Container Webhooks empfaengt. Das Frontend zeigt den Wert als
|
||
eigene, immer sichtbare Kachel 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) |
|