# 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 ` (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 `. | 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.