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>
This commit is contained in:
Frank Schwenk
2026-07-06 10:42:58 +02:00
parent 24ac7f2f48
commit 1cada42370
17 changed files with 251 additions and 323 deletions
+78
View File
@@ -0,0 +1,78 @@
from __future__ import annotations
import json
import logging
import httpx
from server.settings import settings
logger = logging.getLogger(__name__)
def _publish_url() -> str | None:
if not settings.ntfy_url or not settings.ntfy_topic:
return None
return f"{settings.ntfy_url.rstrip('/')}/{settings.ntfy_topic}"
def _click_base() -> str:
return settings.ntfy_click_url.rstrip("/")
async def send_slot_reminder(
slot_id: str,
title: str,
body: str,
*,
silent: bool = True,
) -> int:
url = _publish_url()
if not url:
logger.warning(
"Notify skipped slot=%s reason=ntfy_not_configured (NTFY_URL + NTFY_TOPIC)",
slot_id,
)
return 0
click_base = _click_base()
headers: dict[str, str] = {
"Title": title,
"Click": f"{click_base}/?slot={slot_id}",
"Priority": "2" if silent else "3",
"Tags": "pill",
"Actions": json.dumps(
[
{
"action": "view",
"label": "Genommen ✓",
"url": f"{click_base}/?action=take&slot={slot_id}",
},
{
"action": "view",
"label": "+15 Min",
"url": f"{click_base}/?action=snooze&slot={slot_id}&minutes=15",
},
{
"action": "view",
"label": "+30 Min",
"url": f"{click_base}/?action=snooze&slot={slot_id}&minutes=30",
},
]
),
}
if settings.ntfy_token:
headers["Authorization"] = f"Bearer {settings.ntfy_token}"
logger.info("Notify start slot=%s title=%r topic=%s", slot_id, title, settings.ntfy_topic)
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(url, content=body.encode("utf-8"), headers=headers)
resp.raise_for_status()
except httpx.HTTPError as exc:
logger.warning("Notify failed slot=%s error=%s", slot_id, exc)
return 0
logger.info("Notify ok slot=%s", slot_id)
return 1