docs: add Agent API docs, OpenAPI field descriptions, and project context files
This commit is contained in:
+260
@@ -0,0 +1,260 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user