From 22b205ff391202a295b0b9f1bda3ac46cd56cfd0 Mon Sep 17 00:00:00 2001 From: Jack Levy Date: Sun, 1 Mar 2026 12:19:05 -0500 Subject: [PATCH] feat(notifications): add Click link to all ntfy alerts (test + real) Test notification: - Click header -> {PUBLIC_URL}/notifications so tapping the test opens the app Real bill alerts (dispatcher): - Title reformatted: "New Bill Text: HR 1234" (event type + bill identifier) - Body: bill full name on first line, AI summary below (300 chars) - Tags updated per event type (page, memo, siren) instead of generic scroll - Click header was already set from bill_url; no change needed there Authored-By: Jack Levy --- backend/app/api/notifications.py | 8 ++++++- .../app/workers/notification_dispatcher.py | 24 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/backend/app/api/notifications.py b/backend/app/api/notifications.py index c53beea..e7432e2 100644 --- a/backend/app/api/notifications.py +++ b/backend/app/api/notifications.py @@ -11,6 +11,7 @@ from fastapi.responses import Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.config import settings as app_settings from app.core.dependencies import get_current_user from app.database import get_db from app.models.notification import NotificationEvent @@ -115,10 +116,12 @@ async def test_ntfy( if not url: return NotificationTestResult(status="error", detail="Topic URL is required") + base_url = (app_settings.PUBLIC_URL or app_settings.LOCAL_URL).rstrip("/") headers: dict[str, str] = { "Title": "PocketVeto: Test Notification", "Priority": "default", "Tags": "white_check_mark", + "Click": f"{base_url}/notifications", } if body.ntfy_auth_method == "token" and body.ntfy_token.strip(): headers["Authorization"] = f"Bearer {body.ntfy_token.strip()}" @@ -132,7 +135,10 @@ async def test_ntfy( async with httpx.AsyncClient(timeout=10) as client: resp = await client.post( url, - content="Your PocketVeto notification settings are working correctly.".encode("utf-8"), + content=( + "Your PocketVeto notification settings are working correctly. " + "Real alerts will link directly to the relevant bill page." + ).encode("utf-8"), headers=headers, ) resp.raise_for_status() diff --git a/backend/app/workers/notification_dispatcher.py b/backend/app/workers/notification_dispatcher.py index d158f1d..5fda4bd 100644 --- a/backend/app/workers/notification_dispatcher.py +++ b/backend/app/workers/notification_dispatcher.py @@ -22,11 +22,17 @@ logger = logging.getLogger(__name__) NTFY_TIMEOUT = 10 _EVENT_TITLES = { - "new_document": "New Bill Text Published", + "new_document": "New Bill Text", "new_amendment": "Amendment Filed", "bill_updated": "Bill Updated", } +_EVENT_TAGS = { + "new_document": "page_facing_up", + "new_amendment": "memo", + "bill_updated": "rotating_light", +} + @celery_app.task(bind=True, name="app.workers.notification_dispatcher.dispatch_notifications") def dispatch_notifications(self): @@ -93,14 +99,22 @@ def _send_ntfy( payload = event.payload or {} bill_label = payload.get("bill_label", event.bill_id.upper()) bill_title = payload.get("bill_title", "") - message = f"{bill_label}: {bill_title}" + event_label = _EVENT_TITLES.get(event.event_type, "Bill Update") + + # Title line: event type + bill identifier (e.g. "New Bill Text: HR 1234") + title = f"{event_label}: {bill_label}" + + # Body: full bill name, then AI summary if available + lines = [bill_title] if bill_title else [] if payload.get("brief_summary"): - message += f"\n\n{payload['brief_summary'][:280]}" + lines.append("") + lines.append(payload["brief_summary"][:300]) + message = "\n".join(lines) or bill_label headers = { - "Title": _EVENT_TITLES.get(event.event_type, "Bill Update"), + "Title": title, "Priority": "default", - "Tags": "scroll", + "Tags": _EVENT_TAGS.get(event.event_type, "bell"), } if payload.get("bill_url"): headers["Click"] = payload["bill_url"]