# backend/app/routers/notify.py import uuid from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from ..auth.deps import current_user from ..auth.scope import scoped from ..automation.event_bus import publish from ..db import get_session from ..models import ( Digest, Notification, NotificationGuard, NotifyBucket, NotifyStats, Person, SenderRule, ) from ..schemas import ( DigestOut, GuardOut, GuardPatch, NotificationOut, ReclassifyNotifRequest, SenderRuleCreate, SenderRuleOut, TriageOut, ) from ..services.triage import detect_defer_pattern router = APIRouter() def _out(n: Notification) -> NotificationOut: return NotificationOut( id=n.id, bucket=n.bucket.value, src=n.src, app=n.app, title=n.title, sum=n.sum, time=n.time, why=n.why, tone=n.tone, act=n.act, ) @router.get("/notifications", response_model=TriageOut) def triage(s: Session = Depends(get_session), user: Person = Depends(current_user)): rows = s.exec( scoped(select(Notification), Notification, user.id).order_by(Notification.sort_order) ).all() stats = s.get(NotifyStats, 1) buckets: dict[str, list] = {"now": [], "later": [], "held": []} for n in rows: buckets[n.bucket.value].append(_out(n)) counts = { "now": len(buckets["now"]), "later": len(buckets["later"]) + (stats.later_more if stats else 0), "held": len(buckets["held"]) + (stats.held_more if stats else 0), } return TriageOut( now=buckets["now"], later=buckets["later"], held=buckets["held"], counts=counts, stats=( {"total": stats.total, "seen": stats.seen, "filtered": stats.filtered} if stats else {} ), ) @router.post("/notifications/{nid}/reclassify", response_model=NotificationOut) def reclassify(nid: str, body: ReclassifyNotifRequest, s: Session = Depends(get_session)): n = s.get(Notification, nid) if not n: raise HTTPException(404, "notification not found") prev = n.bucket.value n.bucket = NotifyBucket(body.to_bucket) if body.to_bucket == "later": n.why, n.tone, n.act = "내가 미룸", "muted", "" elif body.to_bucket == "now" and prev == "held": n.why, n.tone = "되돌림", "amber" s.add(n) s.commit() if body.to_bucket == "later": publish("notification.triaged", {"id": nid, "to": "later", "by": "user"}) for cand in detect_defer_pattern(s): publish("automation.suggested", cand) return _out(n) @router.post("/notifications/{nid}/undo", response_model=NotificationOut) def undo(nid: str, s: Session = Depends(get_session)): """held → now 되돌리기.""" n = s.get(Notification, nid) if not n: raise HTTPException(404, "notification not found") n.bucket = NotifyBucket.now n.why, n.tone = "되돌림", "amber" s.add(n) s.commit() publish("notification.triaged", {"id": nid, "to": "now", "by": "undo"}) return _out(n) @router.get("/notifications/guard", response_model=GuardOut) def get_guard(s: Session = Depends(get_session)): g = s.get(NotificationGuard, 1) return GuardOut(on=g.on, until=g.until, held=g.held, quiet=g.quiet) @router.post("/notifications/guard", response_model=GuardOut) def set_guard(body: GuardPatch, s: Session = Depends(get_session)): g = s.get(NotificationGuard, 1) g.on = body.on s.add(g) s.commit() return GuardOut(on=g.on, until=g.until, held=g.held, quiet=g.quiet) @router.get("/notifications/digests", response_model=list[DigestOut]) def digests(s: Session = Depends(get_session)): return s.exec(select(Digest).order_by(Digest.sort_order)).all() @router.get("/notifications/senders", response_model=list[SenderRuleOut]) def senders(s: Session = Depends(get_session)): rows = s.exec(select(SenderRule).order_by(SenderRule.sort_order)).all() return [ SenderRuleOut( id=r.id, who=r.who, from_label=r.from_label, rule=r.rule, tone=r.tone, bucket=r.bucket.value, ) for r in rows ] @router.post("/notifications/senders", response_model=SenderRuleOut) def add_sender(body: SenderRuleCreate, s: Session = Depends(get_session)): rid = "sr-" + uuid.uuid4().hex[:6] last = s.exec(select(SenderRule)).all() r = SenderRule( id=rid, who=body.who, from_label=body.from_label, rule=body.rule, bucket=NotifyBucket(body.bucket), tone=body.tone, sort_order=len(last), ) s.add(r) s.commit() return SenderRuleOut( id=r.id, who=r.who, from_label=r.from_label, rule=r.rule, tone=r.tone, bucket=r.bucket.value, )