From 641853a0020438e6c732ee23a69c4211b30954cc Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Mon, 27 Apr 2026 21:29:47 -0400 Subject: [PATCH] fix(escalations): bell-icon notification opens the pickup flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two backend changes that unbreak the senior-pickup path from the notification panel: 1. notification_service: session.escalated link template now ends with ?pickup=true so the senior lands in the handoff/pickup flow on click. Without it, navigation hit /pilot/:id directly, which then 404'd on the GET because the senior isn't yet escalated_to_id — the user perceives this as the bell-icon "just clearing the notification". 2. ai_sessions GET access: any account member can now read an escalated session's detail when status is requesting_escalation or escalated. The owner-only guard was overly restrictive for explicitly-shared in-transit states. Tenant boundary is enforced by RLS on the underlying query, so account-scope is the right ceiling here. After pickup, the existing handler/escalated_to_id checks still apply. Verified live: re-login as the senior engineer and GET the active escalated session — now returns 200 with full detail. Focused test subset plus tests/test_sessions.py and tests/test_session_sharing.py → 94 passed in 43.26s, no regressions. Co-Authored-By: Claude Opus 4.7 --- backend/app/api/endpoints/ai_sessions.py | 15 +++++++++++++-- backend/app/services/notification_service.py | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py index 425e6421..4b484e43 100644 --- a/backend/app/api/endpoints/ai_sessions.py +++ b/backend/app/api/endpoints/ai_sessions.py @@ -901,10 +901,21 @@ async def get_session( if not session: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found") - # Allow access if user is owner, escalation target, or picked-up handler + # Allow access if user is owner, escalation target, or picked-up handler. + # Sessions in transit (requesting_escalation / escalated) are also + # readable by any account member — the whole point of escalation is that + # other techs can see the context before claiming. Tenant boundary is + # enforced by RLS on the underlying query, so account-scope is the right + # ceiling for in-transit reads. pkg = session.escalation_package or {} is_handler = pkg.get("picked_up_by") == str(current_user.id) - if session.user_id != current_user.id and session.escalated_to_id != current_user.id and not is_handler: + is_in_transit = session.status in ("requesting_escalation", "escalated") + if ( + session.user_id != current_user.id + and session.escalated_to_id != current_user.id + and not is_handler + and not is_in_transit + ): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found") return _build_session_detail(session) diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py index 23926b0f..a817b9b6 100644 --- a/backend/app/services/notification_service.py +++ b/backend/app/services/notification_service.py @@ -405,7 +405,12 @@ def _build_notification_body(event: str, payload: dict[str, Any]) -> str: def _build_notification_link(event: str, payload: dict[str, Any]) -> Optional[str]: """In-app link per event type. Returns path (no host).""" links: dict[str, str] = { - "session.escalated": "/pilot/{session_id}", + # ?pickup=true triggers the senior-tech handoff/pickup flow on the + # session page (magic-moment screen for handoff-based escalations, + # legacy SessionBriefing for `requesting_escalation` sessions). + # Without it the senior lands on a session-detail GET they can't + # access pre-pickup, which the user perceives as a dead notification. + "session.escalated": "/pilot/{session_id}?pickup=true", "session.high_priority": "/pilot/{session_id}", "proposal.pending": "/review-queue", "proposal.approved": "/review-queue",