Files

261 lines
7.9 KiB
Markdown

# 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 <agent_token>
```
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 <user-token>`) wird auf `/agent/*` **nicht** akzeptiert.
---
## Endpoint Summary
Alle Pfade sind relativ zur Base URL. Alle erfordern `Authorization: Bearer <agent_token>`.
| 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`.