From bff14167c906cc6874aeda1a3426bf69315ef79e Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Wed, 8 Jul 2026 08:25:34 +0200 Subject: [PATCH] Revert "feat: replace Web Push with ntfy for medication reminders" This reverts commit 1cada423704012e3f6f21be6ff26d7a20aeaeaea. --- .env.example | 9 ++- README.md | 56 +++++++++--------- SOUL.md | 95 ------------------------------ compose.yml | 7 +-- public/app.js | 28 ++++----- public/index.html | 7 +-- public/lib/api.js | 4 +- public/lib/pwa.js | 20 +++++++ public/sw.js | 58 +++++++++++++++++- pyproject.toml | 3 +- server/app.py | 29 +++++---- server/notify.py | 78 ------------------------ server/push.py | 140 ++++++++++++++++++++++++++++++++++++++++++++ server/scheduler.py | 6 +- server/settings.py | 7 +-- tools/gen_vapid.py | 19 ++++++ tools/schema.sql | 8 +++ 17 files changed, 323 insertions(+), 251 deletions(-) delete mode 100644 SOUL.md delete mode 100644 server/notify.py create mode 100644 server/push.py create mode 100644 tools/gen_vapid.py diff --git a/.env.example b/.env.example index f5732d0..c2d0192 100644 --- a/.env.example +++ b/.env.example @@ -5,11 +5,10 @@ APP_PIN=1234 JWT_SECRET=change-me-to-a-long-random-string JWT_EXPIRE_DAYS=90 -# ntfy — Erinnerungen an Topic publizieren (ntfy-App abonnieren) -NTFY_URL=https://ntfy.schwenk.online -NTFY_TOPIC=takeyourmeds -NTFY_TOKEN= -NTFY_CLICK_URL=https://medis.schwenk.online +# VAPID keys für Web Push — generieren mit: python tools/gen_vapid.py +VAPID_PRIVATE_KEY= +VAPID_PUBLIC_KEY= +VAPID_CLAIMS_EMAIL=mailto:admin@schwenk.online # OpenRouter (optional — Roast-of-the-Day) OPENROUTER_API_KEY= diff --git a/README.md b/README.md index b1a739e..cbe17dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TakeYourMeds -Persönliche Medikamenten-PWA mit dezenten ntfy-Erinnerungen, Einnahme-Logging und sarkastischem Humor. +Persönliche Medikamenten-PWA mit dezenten Push-Erinnerungen, Einnahme-Logging und sarkastischem Humor. **Domain:** [medis.schwenk.online](https://medis.schwenk.online) @@ -11,7 +11,7 @@ Persönliche Medikamenten-PWA mit dezenten ntfy-Erinnerungen, Einnahme-Logging u ## Features ### Kern -- **ntfy-Erinnerungen** um 8:00 und 12:00 (`Europe/Berlin`) — dezent, kein Alarm +- **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 @@ -38,7 +38,7 @@ Persönliche Medikamenten-PWA mit dezenten ntfy-Erinnerungen, Einnahme-Logging u | Backend | FastAPI + uvicorn | | DB | SQLite (`data/medis.sqlite`) | | Frontend | Vanilla JS (kein Build-Step) | -| Push | ntfy ([ntfy.schwenk.online](https://ntfy.schwenk.online/)) | +| Push | Web Push (pywebpush + VAPID) | | Scheduler | APScheduler (8:00, 12:00, 21:00) | | KI (optional) | OpenRouter | | Deployment | Docker Compose + Traefik | @@ -57,9 +57,13 @@ pip install -e . # 2. Konfiguration cp .env.example .env -# APP_PIN, JWT_SECRET, NTFY_TOKEN anpassen +# APP_PIN und JWT_SECRET anpassen -# 3. Starten +# 3. VAPID keys (für Push) +python tools/gen_vapid.py +# Output in .env eintragen + +# 4. Starten uvicorn server.app:app --reload --host 0.0.0.0 --port 8000 ``` @@ -71,7 +75,7 @@ App: http://localhost:8000 ```bash cp .env.example .env -# .env ausfüllen (PIN, JWT_SECRET, NTFY_TOKEN, optional OPENROUTER_API_KEY) +# .env ausfüllen (PIN, JWT_SECRET, VAPID keys, optional OPENROUTER_API_KEY) docker compose up -d --build ``` @@ -94,10 +98,9 @@ Volumes: | `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`) | +| `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` | @@ -127,12 +130,12 @@ Texte frei editierbar — kein Neustart nötig (wird beim ersten Request geladen ## API -Alle Endpunkte unter `/api/*`. Auth via `Authorization: Bearer ` (außer `/api/auth/pin`). +Alle Endpunkte unter `/api/*`. Auth via `Authorization: Bearer ` (außer `/api/auth/pin` und `/api/vapid-public-key`). | Methode | Pfad | Beschreibung | |---------|------|--------------| | POST | `/api/auth/pin` | `{ "pin": "1234" }` → `{ "token": "..." }` | -| GET | `/api/notify-config` | ntfy Topic + Subscribe-URL | +| 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?" }` | @@ -140,23 +143,18 @@ Alle Endpunkte unter `/api/*`. Auth via `Authorization: Bearer ` (außer | 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 | --- -## ntfy einrichten +## PWA / Push 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 +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 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 -``` +Notification-Actions: **Genommen ✓**, **+15 Min**, **+30 Min** --- @@ -175,7 +173,7 @@ curl -d "Test" -H "Title: Med-Time" -H "Authorization: Bearer $NTFY_TOKEN" \ | **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 | +| **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 | @@ -183,7 +181,7 @@ curl -d "Test" -H "Title: Med-Time" -H "Authorization: Bearer $NTFY_TOKEN" \ | **`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 | +| **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 | --- @@ -199,7 +197,7 @@ takeyourmeds/ ├── icon.png / icon.svg # App-Icons (Quelle) ├── server/ # FastAPI Backend ├── public/ # PWA Frontend -├── tools/ # schema.sql +├── tools/ # schema.sql, gen_vapid.py └── data/ # SQLite (gitignored) ``` @@ -220,8 +218,8 @@ Service Worker cached aggressiv — für SW-Änderungen: DevTools → Applicatio | 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 | +| 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 | diff --git a/SOUL.md b/SOUL.md deleted file mode 100644 index 0af7698..0000000 --- a/SOUL.md +++ /dev/null @@ -1,95 +0,0 @@ -# SOUL.md — TakeYourMeds - -Persönliche Medikamenten-PWA — nicht der Chat-Mood (siehe `MOOD.md`). - ---- - -## Agent Quick Start - -| | | -|---|---| -| **Was** | Single-User PWA: Med-Erinnerungen, Einnahme-Log, Streak/Heatmap, sarkastischer Humor | -| **Domain** | [medis.schwenk.online](https://medis.schwenk.online) | -| **Stack** | FastAPI + SQLite + Vanilla JS PWA, ntfy, APScheduler | -| **Deploy** | Docker Compose auf `boka` → `/home/frank/medis.schwenk.online` (Fränky deployt) | -| **Secrets** | `.env` only — PIN, JWT, NTFY_TOKEN, OpenRouter | -| **Tests** | Pragmatisch — keine Pflicht-Suite; manuell ntfy + PWA prüfen | -| **Commit** | Nur auf explizite Anweisung | - ---- - -## Product Name - -**TakeYourMeds** (intern: medis) - -## One-Liner - -Persönliche Medikamenten-PWA mit dezenten Erinnerungen, Einnahme-Logging und sarkastischem Humor — für ADHS-realistische Routinen. - -## Vision - -Medikamente nicht vergessen, ohne Schuld-Trip oder Alarm-Hölle. Die App erinnert dezent, loggt Einnahmen, zeigt Fortschritt (Streak, Heatmap) und liefert kleine Dopamin-Hits — weil ADHS-Gehirne Belohnung brauchen, nicht noch eine To-do-Liste. - -## Audience - -**Fränky** — Single-User, persönliches Gerät (Android PWA + Desktop). Erwartet: zuverlässige Reminder, schnelle „genommen"-Aktion, ehrlicher Ton, kein Corporate-Bullshit. - -## Tone & Wording - -Wie in `messages.yaml` — direkt, sarkastisch, ADHS-aware, liebevoll unter der Schicht: - -- **Voice:** Du, trocken, selbstironisch, kein Motivations-Coach -- **Formality:** casual (Du) -- **Error messages:** ehrlich + hilfreich („Push nicht konfiguriert" statt „Etwas ist schiefgelaufen") -- **Forbidden:** LinkedIn-Sprech, „leverage", „synergy", übertriebene Wellness-Floskeln, Schuld-Trips - -### Wording Examples - -| Context | Good | Bad | -|---------|------|-----| -| Success | „Genommen! Dein Gehirn gibt dir einen langsamen High-Five." | „Großartig! Du hast heute wieder Großes geleistet! 🎉" | -| Error | „Reminder kommt nicht — NTFY_TOKEN in .env?" | „Ein unerwarteter Fehler ist aufgetreten." | -| Empty state | „Noch nichts geloggt. Typisch." | „Beginne deine Wellness-Journey!" | - -## Design - -- **Palette:** `#F12F12` (theme_color), utilitarian PWA -- **Feel:** minimal, schnell, mobile-first — keine Deko-Orgie - -## Non-Goals - -- Kein Multi-User / keine Accounts für andere -- Kein SaaS, kein App-Store-Launch -- Keine Dark Patterns (Streak-Shaming, Paywall, Engagement-Bait) -- Kein Medizin-Ratgeber — nur Erinnerung & Logging -- Kein Build-Step-Frontend (Vanilla JS bleibt) - -## Infrastructure (project-local) - -| | | -|---|---| -| Server | `boka` (Debian 12) | -| Path | `/home/frank/medis.schwenk.online` | -| Domain | `medis.schwenk.online` | -| DB | `data/medis.sqlite` (Volume) | -| Config | `meds.yaml`, `messages.yaml` (read-only mounts) | -| Push | ntfy → [ntfy.schwenk.online](https://ntfy.schwenk.online/) Topic `takeyourmeds` | - -## Project-Specific Rules - -- **ntfy only** — Server publiziert Reminder; ntfy-App auf dem Handy empfängt. Kein Web Push/VAPID. -- **`reminder_window_minutes: 90`** — nicht sofort „verpasst" um 08:01; ADHS-realistisch -- **Abend-Check 21:00** — überfällige Slots → `missed` -- **Roast/Motivation** — OpenRouter optional; Notifications nutzen statische `messages.yaml` -- **PIN in .env** — Single-User, `hmac.compare_digest`; kein Over-Engineering -- **Kein nginx** — FastAPI serviert API + Static (KISS) -- **Service Worker** — aggressives Caching; bei SW-Änderungen `version.json` bumpen - ---- - -## Agent Instructions - -- `SOUL.md` > `STANDARDS.md` für UI/UX und Produkt-Ton -- Secrets nie committen — Token/Keys nur `.env` -- Deploy: Fränky, außer explizit anders gesagt -- Neue Regeln hier oder in `BOUNDARIES.md` — Agent schlägt vor, Fränky bestätigt diff --git a/compose.yml b/compose.yml index b9cf815..b70a315 100644 --- a/compose.yml +++ b/compose.yml @@ -6,10 +6,9 @@ services: - APP_PIN=${APP_PIN} - JWT_SECRET=${JWT_SECRET} - JWT_EXPIRE_DAYS=${JWT_EXPIRE_DAYS:-90} - - NTFY_URL=${NTFY_URL:-https://ntfy.schwenk.online} - - NTFY_TOPIC=${NTFY_TOPIC:-takeyourmeds} - - NTFY_TOKEN=${NTFY_TOKEN:-} - - NTFY_CLICK_URL=${NTFY_CLICK_URL:-https://medis.schwenk.online} + - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY} + - VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY} + - VAPID_CLAIMS_EMAIL=${VAPID_CLAIMS_EMAIL:-mailto:admin@schwenk.online} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} - OPENROUTER_MODEL=${OPENROUTER_MODEL:-google/gemini-2.0-flash-001} - DB_PATH=/data/medis.sqlite diff --git a/public/app.js b/public/app.js index 5d32e29..a305067 100644 --- a/public/app.js +++ b/public/app.js @@ -2,7 +2,7 @@ import { api } from "./lib/api.js"; import { getToken, setToken, clearToken, isLoggedIn } from "./lib/auth.js"; import { setStreakBadge } from "./lib/badge.js"; import { enqueue, flushQueue } from "./lib/offlineQueue.js"; -import { registerPwa, wirePwaInstall } from "./lib/pwa.js"; +import { registerPwa, wirePwaInstall, subscribePush } from "./lib/pwa.js"; import { renderSlots, renderHeatmap, updateStats, showToast, showConfetti, showEasterEgg } from "./lib/ui.js"; const STATUS = { upcoming: "Noch nicht", pending: "Jetzt!", overdue: "Überfällig", snoozed: "Snoozed", taken: "Genommen", missed: "Verpasst" }; @@ -153,20 +153,20 @@ async function initApp() { location.reload(); }); - try { - const notify = await api.notifyConfig(); - const topicEl = document.getElementById("notifyTopic"); - const linkEl = document.getElementById("notifySubscribeLink"); - if (notify.configured && notify.subscribe_url) { - topicEl.textContent = `Topic: ${notify.topic}`; - linkEl.href = notify.subscribe_url; - } else { - topicEl.textContent = "ntfy nicht konfiguriert (NTFY_URL + NTFY_TOPIC in .env)."; - linkEl.hidden = true; + document.getElementById("enablePush").addEventListener("click", async () => { + const statusEl = document.getElementById("pushStatus"); + try { + const perm = await Notification.requestPermission(); + if (perm !== "granted") { statusEl.textContent = "Permission verweigert."; return; } + const { key } = await api.vapidKey(); + if (!key) { statusEl.textContent = "VAPID keys nicht konfiguriert."; return; } + const sub = await subscribePush(key); + await api.pushSubscribe(sub); + statusEl.textContent = "Push aktiv! Du wirst dezent erinnert."; + } catch (e) { + statusEl.textContent = e.message || "Push-Setup fehlgeschlagen."; } - } catch { - document.getElementById("notifyTopic").textContent = "Notify-Config konnte nicht geladen werden."; - } + }); await flushQueue((data) => api.log(data)); await refreshDashboard(); diff --git a/public/index.html b/public/index.html index 22af6c4..9f81e31 100644 --- a/public/index.html +++ b/public/index.html @@ -74,10 +74,9 @@