# 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) - **PWA:** installierbar, offline-fähige App-Shell, Service Worker ### Spaß - **Dopamin-Drop** — Animation + Erfolgsspruch beim Loggen - **Roast-of-the-Day** — täglicher sarkastischer Spruch (OpenRouter, gecacht) - **KI-Orakel** — wöchentlicher Compliance-Report (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. VAPID keys (für Push) python tools/gen_vapid.py # Output in .env eintragen # 4. 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, 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 - `./meds.yaml` — Medikamenten-Zeitplan (read-only) - `./messages.yaml` — Humor-Texte (read-only) --- ## 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) | | `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 + Orakel | | `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` und `/api/vapid-public-key`). | Methode | Pfad | Beschreibung | |---------|------|--------------| | POST | `/api/auth/pin` | `{ "pin": "1234" }` → `{ "token": "..." }` | | GET | `/api/vapid-public-key` | Public Key für Push-Subscription | | 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/oracle` | Wöchentliches KI-Orakel | | POST | `/api/push/subscribe` | Web-Push Subscription speichern | --- ## 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 + Orakel** | 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 | --- ## Projektstruktur ``` takeyourmeds/ ├── compose.yml # Docker + Traefik ├── Dockerfile ├── meds.yaml # Medikamenten-Zeitplan ├── messages.yaml # Humor-Pool ├── icon.png / icon.svg # App-Icons (Quelle) ├── server/ # FastAPI Backend ├── public/ # PWA Frontend ├── tools/ # schema.sql, gen_vapid.py └── data/ # SQLite (gitignored) ``` --- ## Entwicklung ```bash source .venv/bin/activate uvicorn server.app:app --reload --port 8000 ``` Service Worker cached aggressiv — für SW-Änderungen: DevTools → Application → Clear storage, oder Inkognito. --- ## 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.