fix(escalations): bell-icon notification opens the pickup flow
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user