Files
Cortex-Inara/cortex/routers/push.py
Scott Idem 3c7ecf4e4f feat: notification test endpoints — POST /api/push/test and /api/push/reminders/check
- POST /api/push/test: sends "Test notification from Cortex" via the
  user's configured notification channel (web_push / NCT / email / etc.)
- POST /api/push/reminders/check: runs the daily reminder check immediately
  for the current user, returns reminders_found count

Both require an active session cookie. Useful for verifying channel setup
without waiting for the 09:00 scheduler job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:34:58 -04:00

121 lines
3.9 KiB
Python

"""
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}