Files
takeyourmeds/README.md
T
Frank Schwenk 1cada42370 feat: replace Web Push with ntfy for medication reminders
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>
2026-07-06 10:42:58 +02:00

234 lines
7.6 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 ntfy-Erinnerungen, Einnahme-Logging und sarkastischem Humor.
**Domain:** [medis.schwenk.online](https://medis.schwenk.online)
![App Icon](icon.png)
---
## 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](https://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)
```bash
# 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
```
App: http://localhost:8000
---
## Deployment (Docker + Traefik)
```bash
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 (46 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`
```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
1. **Server:** `NTFY_TOKEN` in `.env` auf boka setzen, Container neu starten
2. **Handy:** ntfy-App → Topic `takeyourmeds` abonnieren (oder Link unter **Einstellungen** in der PWA)
3. 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:
```bash
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
```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 |
|---------|--------|
| 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.