Seit dem Praesenz-Timeout aus Ticket #17 verschwindet ein Agent aus der Buero-Visualisierung, sobald last_seen_at aelter als zehn Minuten ist. Damit sich das auf der Produktionsmaschine nachvollziehen laesst, dokumentiert GET /api/v1/agents jetzt, dass der Endpunkt unauthentifiziert ist (last_seen_at also per curl abrufbar ist) und wie sich derselbe Wert notfalls direkt aus der SQLite-Datei auslesen laesst. Schliesst #20
296 lines
12 KiB
Markdown
296 lines
12 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)
|
||
- [Tickets](#tickets)
|
||
- [Events](#events)
|
||
- [Systeme](#systeme)
|
||
- [KPIs](#kpis)
|
||
- [Live-Updates (SSE)](#live-updates-sse)
|
||
- [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) | Umgebungsvariable `MONITOR_ADMIN_API_KEY` |
|
||
| `X-Api-Key` | Alle anderen schreibenden Endpunkte | Wird bei der Agenten-Registrierung einmalig zurueckgegeben |
|
||
|
||
Der Klartext-API-Key eines Agenten wird **nur einmal** bei der Registrierung ausgegeben.
|
||
Gespeichert wird ausschliesslich ein SHA-256-Hash in SQLite (Tabelle `agents`,
|
||
Spalte `api_key_hash`) - der Klartext ist danach nicht mehr rekonstruierbar und muss vom
|
||
Agenten sicher verwahrt werden.
|
||
|
||
Fehlt oder passt ein Key nicht, antwortet die API mit `401 Unauthorized`.
|
||
|
||
## Agenten
|
||
|
||
Rollen (F1/F2): `entwickler`, `product_owner`, `qa`, `user`. Ein Agent bekommt bei der
|
||
Registrierung fest einen von `MONITOR_DESK_COUNT` (Standard 8) Schreibtischen zugewiesen
|
||
(niedrigster freier Index). Die Rolle `user` bekommt nie einen Schreibtisch (kein fester
|
||
Sitzplatz). Sind alle Schreibtische belegt, wird trotzdem registriert, aber ohne
|
||
Schreibtisch (`desk_index: null`).
|
||
|
||
### `POST /api/v1/agents` — Agent registrieren
|
||
|
||
Auth: `X-Admin-Key`.
|
||
|
||
```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"
|
||
},
|
||
"api_key": "‹64 Hex-Zeichen, nur jetzt sichtbar›"
|
||
}
|
||
```
|
||
|
||
### `GET /api/v1/agents` — Alle Agenten
|
||
|
||
Response: `{ "agents": [ ‹Agent-Objekte wie oben, ohne api_key› ] }`
|
||
|
||
Oeffentlich, unauthentifiziert (siehe [Grundlagen](#grundlagen)) - damit laesst sich
|
||
`last_seen_at` 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}'
|
||
```
|
||
|
||
Ein Agent gilt im Frontend nur noch als anwesend, solange `last_seen_at` hoechstens zehn
|
||
Minuten zurueckliegt (`isAgentPresent`, `html/js/office-scene.js`, Issue #17) - laeuft der
|
||
Timeout ab, bleibt sein Schreibtisch leer, obwohl er weiterhin registriert ist. Bei direktem
|
||
Zugriff auf die Produktionsmaschine laesst sich derselbe Wert auch direkt aus der
|
||
SQLite-Datei lesen (`MONITOR_DB_PATH`, Standard `api/data/monitor.sqlite`):
|
||
|
||
```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.
|
||
|
||
## Tickets
|
||
|
||
Spalten (F3): `backlog`, `bereit`, `in_arbeit`, `qa`, `testing`, `fertig`, `done`.
|
||
|
||
- `backlog`, `bereit`, `in_arbeit`, `testing` und `done` werden regelmaessig aus
|
||
Codeberg-Issues befuellt (siehe [Codeberg-Sync](#codeberg-sync-backlogdone)); `qa` und
|
||
`fertig` sind fuer zukuenftige Labels reserviert und ergeben sich ausschliesslich aus
|
||
Agenten-Meldungen (F4).
|
||
- Ticket-Typ und Farbe (statt Prioritaet) werden bei Codeberg-Tickets aus dem ersten
|
||
Issue-Label uebernommen (F4).
|
||
- Stapel-Darstellung ("aelteste 4 + Rest als Stapel", F4) ist reine Frontend-Logik;
|
||
die API liefert Tickets dafuer sortiert nach Spalte und Alter.
|
||
|
||
### `GET /api/v1/tickets` — Board-Zustand
|
||
|
||
Query-Parameter: `column` (optional, filtert auf eine Spalte).
|
||
|
||
Response: `{ "tickets": [ { "id", "external_ref", "title", "description", "column_name", "type", "color", "assignee_agent_id", "source", "created_at", "updated_at" }, ... ] }`
|
||
|
||
### `POST /api/v1/tickets` — Ticket anlegen
|
||
|
||
Auth: `X-Api-Key`.
|
||
|
||
```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`.
|
||
|
||
## 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).
|
||
|
||
## KPIs
|
||
|
||
F7: Konkrete Kennzahlen sind noch offen ("kommt spaeter"). Phase 1 stellt nur die
|
||
generische Speicher-/Abfragestruktur bereit.
|
||
|
||
### `POST /api/v1/kpis/{key}` — KPI-Datenpunkt melden
|
||
|
||
Auth: `X-Api-Key`.
|
||
|
||
```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.
|
||
|
||
## Codeberg-Sync (Backlog/Done)
|
||
|
||
`api/bin/sync-codeberg.php` liest offene und geschlossene Issues aus dem konfigurierten
|
||
Codeberg-Repo (`CODEBERG_OWNER`/`CODEBERG_REPO`) und gleicht sie ueber `external_ref`
|
||
(= Issue-Nummer) mit den Tickets ab:
|
||
|
||
- Offene Issues → Spalte je nach Label: `wip` → `in_arbeit`, sonst `testing` → `testing`,
|
||
sonst `automate` → `bereit`, sonst `backlog`.
|
||
- Geschlossene Issues → Spalte `done`.
|
||
- Nur falls das Ticket noch in einer der Sync-Spalten (`backlog`, `bereit`, `in_arbeit`,
|
||
`testing`, `done`) ist - auf `qa`/`fertig` manuell verschobene Tickets werden nicht
|
||
zurueckgesetzt.
|
||
- Ticket-Typ/-Farbe kommen vom ersten Issue-Label (Name/Farbe).
|
||
|
||
Ausgefuehrt wird das Skript vom Compose-Service `sync` (siehe
|
||
[docker-compose.yml](docker-compose.yml)) - kein Host-Cron noetig. Der Service startet mit
|
||
dem Stack, macht sofort einen ersten Lauf und wiederholt ihn dann alle
|
||
`MONITOR_SYNC_INTERVAL` Sekunden (Default 600, siehe `.env.example`). Er nutzt dasselbe
|
||
Image und dieselbe SQLite-Datei wie `api` und verbindet sich ausschliesslich ausgehend nach
|
||
codeberg.org - es ist also kein Port-Mapping und keine Firewall-Freigabe noetig.
|
||
|
||
```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.
|
||
|
||
## 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) |
|