TakeYourMeds
Persönliche Medikamenten-PWA mit dezenten Push-Erinnerungen, Einnahme-Logging und sarkastischem Humor.
Domain: medis.schwenk.online
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)
# 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
Deployment (Docker + Traefik)
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
- DB-Migrationen:
tools/migrations/*.sqlwerden beim Container-Start automatisch angewendet (schema_version-Tabelle). Neue Tabellen/Spalten kommen ohne Datenverlust dazu. - Backup vor Updates:
python tools/backup_db.py # oder im Container: docker compose exec app python tools/backup_db.py - Volumes nicht löschen:
docker compose downohne-vlässt./databestehen. Niedata/manuell löschen. - 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
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
- App im Browser öffnen (HTTPS erforderlich — lokal ohne Push)
- Einloggen → Tab Einstellungen → Push aktivieren
- Optional: Auf Homescreen installieren
- 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
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
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.