# Agent API Handbook Externe Automatisierung (andere Agents, Cronjobs, Sprachassistenten) spricht mit TakeYourMeds über **`/agent/*`**. Diese Routen verwenden ein separates, langes Token — nicht die User-JWT-Session der PWA. Base URL (lokal): `http://localhost:8000/agent` Production: `https://medis.schwenk.online/agent` --- ## Mental Model: Slots + Status Die App dreht sich um **Slots** aus `meds.yaml`. Jeder Slot steht für eine feste Einnahmezeit am Tag (z.B. `morning` um 08:00). Ein Slot kann genau einen Status haben: | Status | Bedeutung | | ---------- | -------------------------------------------------------------------- | | `upcoming` | Zeit noch nicht erreicht | | `pending` | Zeit ist da, liegt aber noch innerhalb der `reminder_window_minutes` | | `overdue` | Fenster überschritten, aber noch nicht als `missed` gebucht | | `taken` | Genommen geloggt | | `missed` | Verpasst geloggt (oder vom 21:00-Abend-Check gesetzt) | | `snoozed` | Verschoben via `/agent/log` mit `status: snoozed` oder `/api/snooze` | **Streak** zählt nur Tage, an denen **alle** Slots den Status `taken` haben. Vergessene Slots setzen die Streak auf 0. --- ## Authentication Sende das Shared Secret bei **jedem** Request: ```http Authorization: Bearer ``` Konfiguriert wird der Token über die Umgebungsvariable **`AGENT_TOKEN`** (siehe `.env.example`). Ist `AGENT_TOKEN` leer, liefern alle `/agent/*`-Endpunkte **`503`** (Service Unavailable). | Situation | HTTP Status | Detail | | --------------------------- | ----------- | -------------------------------- | | Fehlender Header | `401` | `Invalid or missing agent token` | | Falscher Token | `401` | `Invalid or missing agent token` | | `AGENT_TOKEN` nicht gesetzt | `503` | `Agent API is disabled` | User JWT (`Authorization: Bearer `) wird auf `/agent/*` **nicht** akzeptiert. --- ## Endpoint Summary Alle Pfade sind relativ zur Base URL. Alle erfordern `Authorization: Bearer `. | Method | Path | Zweck | | ------ | ------------------ | -------------------------------------------------- | | `GET` | `/health` | Liveness: `status`, `version`, `db_version` | | `GET` | `/status` | Heutige Slots + aktuelle Stats | | `POST` | `/log` | Eine Einnahme loggen | | `GET` | `/history?days=90` | Heatmap-Daten der letzten N Tage | | `GET` | `/config` | Medikamenten-Konfiguration (Slots, Zeitzone) | | `POST` | `/notify` | Push-Benachrichtigung an alle Subscriptions senden | --- ## Request / Response Shapes ### `POST /log` Body (`AgentLogEntry`): ```json { "slot_id": "morning", "status": "taken", "day": "2026-09-22", "logged_at": "2026-09-22T08:05:00+02:00", "source": "voice" } ``` | Feld | Pflicht | Bemerkung | | ----------- | ------- | ------------------------------------------------------------- | | `slot_id` | ja | Muss in `meds.yaml` existieren | | `status` | nein | Default: `taken`. Erlaubt: `taken`, `missed`, `snoozed` | | `day` | nein | ISO-Datum. Default: heute in `meds.yaml.timezone` | | `logged_at` | nein | ISO-Zeitstempel. Default: jetzt (UTC) | | `source` | nein | Default: `agent`. Frei wählbar, z.B. `voice`, `watch`, `cron` | Response: ```json { "ok": true, "slot_id": "morning", "day": "2026-09-22", "status": "taken" } ``` Unbekannter `slot_id` liefert `400` (`Unknown slot`). ### `GET /status` Response: ```json { "today": { "day": "2026-09-22", "timezone": "Europe/Berlin", "now": "2026-09-22T08:05:00+02:00", "slots": [ { "id": "morning", "time": "08:00", "label": "Morgens", "meds": [{ "name": "Elvanse", "dose": "30mg" }], "status": "taken", "logged_at": "2026-09-22T08:05:00+02:00", "window_end": "2026-09-22T09:30:00+02:00", "snooze_until": null } ] }, "stats": {/* siehe /history response */} } ``` ### `GET /history?days=90` Response: ```json { "days": [ { "day": "2026-09-22", "level": "good", "slots": { "morning": "taken" } } ], "stats": { "streak": 5, "compliance_percent": 92.3, "compliance_window_days": 90, "days_active": 120, "first_day": "2026-05-25", "total_taken": 238, "milestones": [ { "id": "log_50", "title": "Pharma-Intern: 50 Logs. Dein Arzt wäre stolz. Vielleicht.", "unlocked_at": "2026-07-10T..." } ] } } ``` | `level` | Bedeutung | | --------- | ------------------------------------------------------- | | `good` | Alle Slots an diesem Tag `taken` | | `bad` | Mindestens ein Slot `missed` | | `partial` | Einige Slots geloggt, aber nicht alle und kein `missed` | | `none` | Noch nichts geloggt | `days` ist auf `365` limitiert. ### `GET /config` Response: ```json { "timezone": "Europe/Berlin", "slots": [ { "id": "morning", "time": "08:00", "label": "Morgens", "meds": [{ "name": "Elvanse", "dose": "30mg" }], "reminder_window_minutes": 90 } ], "version": "1.2.0" } ``` ### `POST /notify` Body (`AgentNotifyRequest`): ```json { "title": "Medis", "body": "Elvanse warten. Dein Gehirn auch.", "slot_id": "morning", "tag": "agent-reminder" } ``` | Feld | Pflicht | Bemerkung | | --------- | ------- | --------------------------------------------------- | | `title` | ja | Max. 120 Zeichen | | `body` | ja | Max. 512 Zeichen | | `slot_id` | nein | Wird an den Client durchgereicht | | `tag` | nein | Notification-Tag zum Ersetzen älterer Notifications | Response: ```json { "ok": true, "sent": 2 } ``` `sent` ist die Anzahl erfolgreich versendeter Subscriptions. Fehlgeschlagene Push-Zustellungen werden still ignoriert (siehe Server-Logs). --- ## Example Workflow ```bash export BASE=http://localhost:8000/agent export TOKEN=your-agent-token # Health curl -s -H "Authorization: Bearer $TOKEN" "$BASE/health" # Agent-Status abfragen curl -s -H "Authorization: Bearer $TOKEN" "$BASE/status" # Genommen loggen curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"slot_id":"morning","status":"taken","source":"voice"}' \ "$BASE/log" # History der letzten 7 Tage curl -s -H "Authorization: Bearer $TOKEN" "$BASE/history?days=7" ``` --- ## Common Errors | Code | Wann | | ----- | -------------------------------------------------- | | `400` | Unbekannter `slot_id` | | `401` | Fehlender oder falscher Agent-Token | | `422` | Ungültiger JSON-Body (z.B. `status` nicht erlaubt) | | `503` | Agent-API deaktiviert (`AGENT_TOKEN` leer) | --- ## Rules of Thumb - `day` nicht manuell setzen, wenn du „heute“ meinst — der Server füllt es aus der konfigurierten Zeitzone. - `status: snoozed` loggt den Slot als verschoben; er taucht dann in `/status` als `snoozed` auf, bis der Status überschrieben wird. - `/notify` sendet an **alle** gespeicherten Push-Subscriptions. Für 1:1-Notifications brauchst du die App-Logik über den Client. - Für Produktvision, UI-Wording und Datenschutz: siehe `SOUL.md` und `README.md`.