# 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)
- **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. 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
```

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-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` 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 |
| 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** | 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/
├── frontend/            # PWA-Quellen (JS, CSS, Templates)
├── compose.yml          # Docker + Traefik
├── Dockerfile
├── meds.yaml            # Medikamenten-Zeitplan
├── messages.yaml        # Humor-Pool
├── icon.png / icon.svg  # App-Icons (Quelle)
├── server/              # FastAPI Backend
├── public/              # Build-Output (index.html, sw.js, assets/)
├── tools/               # schema.sql, gen_vapid.py, build_frontend.py
└── data/                # SQLite (gitignored)
```

---

## Entwicklung

```bash
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.

---

## 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.
