Single notification path via ntfy HTTP publish for reliable Android delivery; remove VAPID, push subscriptions, and SW push handlers. PWA settings show topic subscribe link; humor texts and deep-link actions unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
7.6 KiB
TakeYourMeds
Persönliche Medikamenten-PWA mit dezenten ntfy-Erinnerungen, Einnahme-Logging und sarkastischem Humor.
Domain: medis.schwenk.online
Features
Kern
- ntfy-Erinnerungen um 8:00 und 12:00 (
Europe/Berlin) — dezent, 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)
- 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 | ntfy (ntfy.schwenk.online) |
| 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, JWT_SECRET, NTFY_TOKEN anpassen
# 3. 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, NTFY_TOKEN, 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) |
NTFY_URL |
ntfy-Server (Default: https://ntfy.schwenk.online) |
NTFY_TOPIC |
Topic für Reminder (Default: takeyourmeds) |
NTFY_TOKEN |
Access-Token zum Publizieren (Bearer) |
NTFY_CLICK_URL |
PWA-URL für Tap/Actions (Default: https://medis.schwenk.online) |
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).
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /api/auth/pin |
{ "pin": "1234" } → { "token": "..." } |
| GET | /api/notify-config |
ntfy Topic + Subscribe-URL |
| 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 |
ntfy einrichten
- Server:
NTFY_TOKENin.envauf boka setzen, Container neu starten - Handy: ntfy-App → Topic
takeyourmedsabonnieren (oder Link unter Einstellungen in der PWA) - Optional: PWA auf Homescreen — für Loggen/Snooze; Reminder kommen über ntfy
Notification-Actions in ntfy: Genommen ✓, +15 Min, +30 Min (öffnen PWA mit Deep-Link)
Test von der Shell:
curl -d "Test" -H "Title: Med-Time" -H "Authorization: Bearer $NTFY_TOKEN" \
https://ntfy.schwenk.online/takeyourmeds
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 |
| ntfy vom Server statt Web Push | Zuverlässig auf Android; eine Notification-Logik |
| 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 ntfy |
| 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
└── data/ # SQLite (gitignored)
Entwicklung
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 |
|---|---|
| Reminder kommt nicht | NTFY_TOKEN in .env? Topic in ntfy-App abonniert? |
| „ntfy nicht konfiguriert" | NTFY_URL + NTFY_TOPIC in .env setzen |
| 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.