""" Web Push endpoints. GET /api/push/vapid-key → public VAPID key for browser PushManager.subscribe() POST /api/push/subscribe → save a push subscription for the logged-in user DELETE /api/push/subscribe → remove a subscription by endpoint """ import jwt from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel from auth_utils import COOKIE_NAME, decode_token from config import settings import push_utils router = APIRouter(prefix="/api/push") def _require_user(request: Request) -> str: token = request.cookies.get(COOKIE_NAME) if not token: raise HTTPException(status_code=401, detail="Not authenticated") try: return decode_token(token) except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid session") @router.get("/vapid-key") async def get_vapid_key() -> dict: """Return the VAPID public key. Public endpoint — needed before login to subscribe.""" key = settings.vapid_public_key if not key: raise HTTPException(status_code=503, detail="Push notifications not configured") return {"public_key": key} class SubscribeRequest(BaseModel): subscription: dict # full PushSubscription JSON from browser class UnsubscribeRequest(BaseModel): endpoint: str @router.post("/subscribe") async def subscribe(req: SubscribeRequest, request: Request) -> dict: username = _require_user(request) sub = req.subscription if not sub.get("endpoint"): raise HTTPException(status_code=400, detail="subscription.endpoint is required") push_utils.add_subscription(username, sub) return {"ok": True} @router.delete("/subscribe") async def unsubscribe(req: UnsubscribeRequest, request: Request) -> dict: username = _require_user(request) found = push_utils.remove_subscription(username, req.endpoint) return {"ok": True, "found": found} @router.post("/test") async def notify_test(request: Request) -> dict: """Send a test notification via the user's configured notification channel. Useful for verifying channel setup (web push, NCT, email, etc.) without waiting for a cron job or reminder to fire naturally. """ username = _require_user(request) from notification import notify await notify(username, "Test notification from Cortex — your notification channel is working.") return {"ok": True, "user": username} @router.post("/reminders/check") async def reminder_check_now(request: Request) -> dict: """Run the reminder check for the current user immediately. Same logic as the daily 09:00 scheduler job, but scoped to one user and fired on demand. Returns how many reminders were found and whether a notification was sent. """ import re username = _require_user(request) from persona import list_user_personas, set_context from notification import notify total_sent = 0 for persona_name in list_user_personas(username): set_context(username, persona_name) from tools.reminders import load_due_reminders content = load_due_reminders() if not content: continue entries = [] for line in content.splitlines(): m = re.match(r"^\d+\.\s+(.+)", line.strip()) if m: text = re.sub(r"\[(OVERDUE|due TODAY|due: \S+)\]", "", m.group(1)).strip() if text: entries.append(text) if not entries: continue count = len(entries) if count == 1: msg = f"Reminder: {entries[0]}" else: bullet_list = "\n".join(f"• {e}" for e in entries[:3]) tail = f"\n…and {count - 3} more" if count > 3 else "" msg = f"{count} reminders due:\n{bullet_list}{tail}" await notify(username, msg) total_sent += count return {"ok": True, "user": username, "reminders_found": total_sent}