Files
takeyourmeds/README.md
T
Frank Schwenk 88bdf6f34b feat: overhaul with modular API, agent endpoints, migrations, backup and tests
- Split monolithic app.py into server/api/ routers
- Add Pydantic schemas for input validation
- Add /agent/* API with separate AGENT_TOKEN auth
- Replace static schema.sql with versioned migrations (datenerhaltend)
- Add tools/backup_db.py for SQLite backups
- Add Dockerfile HEALTHCHECK and compose.yml AGENT_TOKEN env
- Add pytest tests for user and agent API
- Update README with API docs and deployment safety notes
- Bump version to 1.2.0
2026-09-21 12:46:44 +02:00

284 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TakeYourMeds
Persönliche Medikamenten-PWA mit dezenten Push-Erinnerungen, Einnahme-Logging und sarkastischem Humor.
**Domain:** [medis.schwenk.online](https://medis.schwenk.online)
![App Icon](icon.png)
---
## Features
### Kern
- **Push-Erinnerungen** um 8:00 und 12:00 (`Europe/Berlin`) — dezent (`silent`), kein Alarm
- **Einnahme loggen:** genommen / verpasst / snooze
- **PIN-Login** mit langer JWT-Session (90 Tage)
- **Agent-API** für externe Automatisierung (`/agent/*`) mit separatem Token
- **PWA:** installierbar, offline-fähige App-Shell, Service Worker
- **DB-Migrationen** — versionierte SQL-Skripte für deploymentsichere Schema-Updates
### Spaß
- **Dopamin-Drop** — Animation + Erfolgsspruch beim Loggen
- **Roast-of-the-Day** — täglicher sarkastischer Spruch (OpenRouter, gecacht)
- **Easter Eggs** — Meilenstein-Badges (50/100 Logs, Streak 7/30/100)
- **~95 Humor-Texte** in `messages.yaml` (Reminder, Success, Missed, …)
### Nützlich
- **Snooze-Kette** — +15 / +30 Min (UI + Notification-Actions)
- **90-Tage Compliance-Heatmap**
- **Manifest Shortcuts** — „Genommen (Morgens/Mittags)" vom Homescreen
- **Badge API** — App-Icon zeigt Streak-Zahl
- **Offline-Queue** — Logs in IndexedDB, Sync bei Verbindung
---
## Stack
| Komponente | Technologie |
|------------|-------------|
| Backend | FastAPI + uvicorn |
| DB | SQLite (`data/medis.sqlite`) |
| Frontend | Vanilla JS (kein Build-Step) |
| Push | Web Push (pywebpush + VAPID) |
| Scheduler | APScheduler (8:00, 12:00, 21:00) |
| KI (optional) | OpenRouter |
| Deployment | Docker Compose + Traefik |
**KISS-Entscheidung:** Ein Container statt nginx+api — FastAPI serviert API und statische Dateien. Traefik-Labels aus `compose.example.yml` übernommen.
---
## Schnellstart (lokal)
```bash
# 1. venv + Dependencies
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
# 2. Konfiguration
cp .env.example .env
# APP_PIN und JWT_SECRET anpassen
# 3. Frontend bauen (gehashte Assets)
python tools/build_frontend.py
# 4. VAPID keys (für Push)
python tools/gen_vapid.py
# Output in .env eintragen
# 5. Starten
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
```
App: http://localhost:8000
---
## Deployment (Docker + Traefik)
```bash
cp .env.example .env
# .env ausfüllen (PIN, JWT_SECRET, AGENT_TOKEN, VAPID keys, optional OPENROUTER_API_KEY)
docker compose up -d --build
```
Traefik routet `medis.schwenk.online` → Container Port 8000 (TLS via `myresolver`).
Volumes:
- `./data` — SQLite-Datenbank + automatische Backups (`data/backups/`)
- `./meds.yaml` — Medikamenten-Zeitplan (read-only)
- `./messages.yaml` — Humor-Texte (read-only)
- `./tools/migrations` — Schema-Migrationen (read-only)
### Keine Daten verlieren beim Deploy
1. **DB-Migrationen:** `tools/migrations/*.sql` werden beim Container-Start automatisch angewendet (`schema_version`-Tabelle). Neue Tabellen/Spalten kommen ohne Datenverlust dazu.
2. **Backup vor Updates:**
```bash
python tools/backup_db.py
# oder im Container:
docker compose exec app python tools/backup_db.py
```
3. **Volumes nicht löschen:** `docker compose down` ohne `-v` lässt `./data` bestehen. Nie `data/` manuell löschen.
4. **Healthcheck:** Der Container meldet sich über `/api/health/`; Docker wartet mit dem "healthy"-Status, bis die App antwortet.
---
## Konfiguration
### `.env`
| Variable | Beschreibung |
|----------|--------------|
| `APP_PIN` | Login-PIN (4–6 Ziffern) |
| `JWT_SECRET` | Geheimer Key für JWT (min. 32 Zeichen empfohlen: `openssl rand -hex 32`) |
| `JWT_EXPIRE_DAYS` | Session-Laufzeit (Default: 90) |
| `AGENT_TOKEN` | Langer zufälliger Token für die `/agent/*` API (leer = deaktiviert) |
| `VAPID_PRIVATE_KEY` | Web-Push Private Key (PEM) |
| `VAPID_PUBLIC_KEY` | Web-Push Public Key |
| `VAPID_CLAIMS_EMAIL` | mailto:-Adresse für VAPID |
| `OPENROUTER_API_KEY` | Optional — für Roast-of-the-Day |
| `OPENROUTER_MODEL` | Default: `google/gemini-2.0-flash-001` |
### `meds.yaml`
```yaml
timezone: Europe/Berlin
slots:
- id: morning
time: "08:00"
label: Morgens
meds:
- name: Elvanse
dose: "30mg"
reminder_window_minutes: 90 # Karenzzeit — siehe unten
```
**`reminder_window_minutes`:** Zeit nach der Soll-Uhrzeit, in der eine Dosis noch als „offen" gilt.
Beispiel: 08:00 + 90 Min → bis 09:30 Status `pending`/`overdue`, erst danach wird sie abends (~21:00) automatisch als `missed` markiert.
### `messages.yaml`
Kategorien: `reminder`, `success`, `missed`, `streak`, `snooze`, `easter_egg`, `roast_fallback`.
Texte frei editierbar — kein Neustart nötig (wird beim ersten Request geladen; Container-Neustart lädt neu).
---
## API
Alle Endpunkte unter `/api/*`. Auth via `Authorization: Bearer <token>` (außer `/api/auth/pin`, `/api/health/` und `/api/vapid-public-key`).
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| GET | `/api/health/` | Liveness + App-Version |
| POST | `/api/auth/pin` | `{ "pin": "1234" }` → `{ "token": "..." }` |
| GET | `/api/vapid-public-key` | Public Key für Push-Subscription (Legacy-Pfad) |
| GET | `/api/config` | Meds-Config |
| GET | `/api/today` | Heutige Slots + Status |
| POST | `/api/log` | `{ "slot_id", "status", "day?", "source?" }` |
| POST | `/api/snooze` | `{ "slot_id", "minutes": 15\|30 }` |
| GET | `/api/history?days=90` | Heatmap-Daten + Stats |
| GET | `/api/stats` | Streak, Compliance, Meilensteine |
| GET | `/api/roast` | Roast-of-the-Day |
| GET | `/api/motivation` | Tägliche Motivation |
| POST | `/api/push/subscribe` | Web-Push Subscription speichern |
### Agent-API
Separate API für externe Agents / Automatisierung. Aktivieren mit `AGENT_TOKEN` in `.env`. Auth ebenfalls via `Authorization: Bearer <agent_token>`.
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| GET | `/agent/health` | Status, App-Version, DB-Schema-Version |
| GET | `/agent/status` | Heutiger Stand + Stats |
| POST | `/agent/log` | `{ "slot_id", "status", "day?", "source?" }` |
| GET | `/agent/history?days=90` | Heatmap-Daten + Stats |
| GET | `/agent/config` | Meds-Config |
| POST | `/agent/notify` | `{ "title", "body", "slot_id?", "tag?" }` — Push an alle Subscriptions |
Die Agent-API läuft auf denselben Tabellen wie die App; `source` ist frei wählbar, z. B. `"voice"`, `"watch"`, `"cron"`.
---
## PWA / Push einrichten
1. App im Browser öffnen (HTTPS erforderlich — lokal ohne Push)
2. Einloggen → Tab **Einstellungen** → **Push aktivieren**
3. Optional: **Auf Homescreen** installieren
4. Android-Shortcuts: Long-Press App-Icon → „Genommen (Morgens/Mittags)"
Notification-Actions: **Genommen ✓**, **+15 Min**, **+30 Min**
---
## Icons
- `icon.png` / `icon.svg` — Quelldateien im Repo-Root
- Werden nach `public/` kopiert für PWA (192, 512, 72 px)
- Manifest `theme_color`: `#F12F12`
---
## Entscheidungsprotokoll
| Entscheidung | Begründung |
|--------------|------------|
| **Ein FastAPI-Container** statt nginx+api | KISS — weniger Moving Parts, StaticFiles reicht für Single-User-PWA |
| **Vanilla JS** statt React/Vue | Kein Build-Step, schnelle Iteration, passt zu persönlicher App |
| **SQLite** statt Postgres | Single-User, eine Datei, Backup = `data/medis.sqlite` kopieren |
| **Web Push vom Server** statt Client-Timer | Zuverlässig auch bei geschlossener App; APScheduler im Container |
| **OpenRouter nur für Roast** | Kosten/Latenz — Notifications nutzen statische `messages.yaml` |
| **Model: gemini-2.0-flash** | Günstig, schnell, gut genug für kurze deutsche Roasts |
| **PIN plain in .env** statt Hash | Single-User, unkritische Daten; `hmac.compare_digest` gegen Timing-Leaks |
| **JWT 90 Tage** | Lange Session auf persönlichem Gerät, kein ständiges PIN-Eingeben |
| **`reminder_window_minutes: 90`** | ADHS-realistisch — nicht sofort „verpasst" um 08:01 |
| **Badge = Streak**, nicht offene Dosen | Streak ist motivierender Dopamin-Hook |
| **Icons: PNG für alle Größen** | 556×556 Quelle — Browser skaliert; kein ImageMagick nötig |
| **Abend-Check 21:00** | Markiert überfällige Slots als `missed`, optional Push |
| **Offline-Queue via IndexedDB** | Einfacher als SW-only; Flush bei App-Start + Background-Sync |
| **Agent-API mit separatem Token** | Externe Automatisierung ohne User-PIN; lässt sich gezielt abschalten |
| **Versionierte SQL-Migrationen** | Kein Alembic-Overhead; deploymentsicher, datenerhaltend |
---
## Projektstruktur
```
takeyourmeds/
├── server/ # FastAPI Backend
│ ├── api/ # API-Router (auth, intake, push, stats, content, agent, health)
│ ├── schemas.py # Pydantic-Input-Modelle
│ ├── migrations.py # DB-Migrationssystem
│ └── ...
├── frontend/ # PWA-Quellen (JS, CSS, Templates)
├── public/ # Build-Output (index.html, sw.js, assets/)
├── tools/ # migrations/, gen_vapid.py, build_frontend.py, backup_db.py
├── compose.yml # Docker + Traefik
├── Dockerfile
├── meds.yaml # Medikamenten-Zeitplan
├── messages.yaml # Humor-Pool
├── icon.png / icon.svg # App-Icons (Quelle)
└── data/ # SQLite + Backups (gitignored)
```
---
## Entwicklung
```bash
source .venv/bin/activate
python tools/build_frontend.py # nach Änderungen in frontend/
uvicorn server.app:app --reload --port 8000
```
Quellen liegen in `frontend/`; Build-Output in `public/` (gehashte JS/CSS unter `public/assets/`). PWA-Update-Strategie: `docs/PWA-STRATEGY.md`.
Service Worker: bei Deploy neuer `CACHE`-Name automatisch via Build. Update-Banner erscheint bei veralteter Client-Version.
### Tests
```bash
python -m pytest tests/ -v
```
---
## Troubleshooting
| Problem | Lösung |
|---------|--------|
| Push kommt nicht | VAPID keys in `.env`? HTTPS? Push in Einstellungen aktiviert? |
| „VAPID keys nicht konfiguriert" | `python tools/gen_vapid.py` ausführen |
| KI-Roast zeigt Fallback | `OPENROUTER_API_KEY` fehlt oder API-Fehler — Fallback aus `messages.yaml` |
| Streak = 0 trotz Log | Streak zählt nur Tage wo **alle** Slots genommen wurden |
| SW zeigt alte Version | `version.json` bumpen + Cache leeren |
---
## Lizenz
Privates Projekt — persönliche Nutzung.