# 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) |