1cada42370
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>
234 lines
7.6 KiB
Markdown
234 lines
7.6 KiB
Markdown
# TakeYourMeds
|
||
|
||
Persönliche Medikamenten-PWA mit dezenten ntfy-Erinnerungen, Einnahme-Logging und sarkastischem Humor.
|
||
|
||
**Domain:** [medis.schwenk.online](https://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](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 (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`
|
||
|
||
```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.
|