deploy.sh only reads the sidecar's custom title override, which most conversations never set, so the banner always said "Untitled conversation". /api/deploy-status now looks the session up in the index: the title the list shows (override, else ai-title) and the in-app conversationId. Hidden from a guest who can't see that conversation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""App-level routes: the SSE event stream, deploy status and health."""
|
|
|
|
import queue
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
import schemas
|
|
from conversations.cards import _conv_title
|
|
from conversations.tree import _conv_id, _is_sidechain_path
|
|
from core.http import _r
|
|
from core.state import State
|
|
from deploy_status import read_status as read_deploy_status
|
|
from users import access
|
|
from users.caller import CallerDep
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# What a guest's stream carries: the pings about conversations they can see
|
|
# (and the cross-cutting ones that only make a client refetch a filtered list).
|
|
_GUEST_PASS = {"deploy", "users"}
|
|
|
|
|
|
def _guest_filter(deps, caller):
|
|
def flt(ev: dict):
|
|
kind = ev.get("type")
|
|
if kind in _GUEST_PASS:
|
|
return ev
|
|
if kind == "transcript":
|
|
ids = [i for i in ev.get("ids") or []
|
|
if access.visible(deps, caller, i)]
|
|
return {**ev, "ids": ids} if ids else None
|
|
if kind == "meta":
|
|
cid = ev.get("id")
|
|
return ev if not cid or access.visible(deps, caller, cid) else None
|
|
if kind == "form":
|
|
sid = ev.get("sessionId")
|
|
return ev if sid and access.visible(deps, caller, sid) else None
|
|
if kind == "notification":
|
|
return ev # an id only; the feed it refetches is filtered
|
|
return None
|
|
return flt
|
|
|
|
|
|
@router.get("/api/events")
|
|
def events(deps: State, caller: CallerDep):
|
|
"""Server-Sent Events: ``meta`` / ``transcript`` change pings for the UI."""
|
|
q = deps.hub.subscribe(None if caller.is_owner else _guest_filter(deps, caller))
|
|
|
|
def gen():
|
|
try:
|
|
yield "retry: 3000\n\n"
|
|
while True:
|
|
try:
|
|
data = q.get(timeout=15)
|
|
yield f"data: {data}\n\n"
|
|
except queue.Empty:
|
|
yield ": ping\n\n" # keep proxies from closing the stream
|
|
finally:
|
|
deps.hub.unsubscribe(q)
|
|
|
|
return StreamingResponse(
|
|
gen(), media_type="text/event-stream",
|
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no",
|
|
"Connection": "keep-alive"})
|
|
|
|
|
|
@router.get("/api/deploy-status", responses=_r(schemas.DeployStatus))
|
|
def deploy_status(deps: State, caller: CallerDep):
|
|
"""Current blue-green deploy snapshot (or ``null`` when none is in flight).
|
|
|
|
Written by ``services/ai-agent/deploy.sh`` into the data dir; polled by the
|
|
:class:`DeployWatcher`, which pings a ``deploy`` SSE event on any change.
|
|
"""
|
|
snap = read_deploy_status()
|
|
if snap:
|
|
_attach_conversation(deps, caller, snap)
|
|
return snap
|
|
|
|
|
|
def _attach_conversation(deps, caller, snap: dict) -> None:
|
|
"""Resolve the deploying conversation from the snapshot's ``sessionId``.
|
|
|
|
The script can only stamp the sidecar's custom ``title`` override, which
|
|
most conversations never set — the title the viewer shows is Claude's
|
|
``ai-title`` (else the first prompt), parsed from the transcript. So the
|
|
title and the in-app ``conversationId`` are looked up here, live; the
|
|
script's own ``convTitle`` is only the fallback. A guest who can't see the
|
|
conversation gets neither (nor the script's link).
|
|
"""
|
|
sid = snap.get("sessionId")
|
|
if not sid:
|
|
return
|
|
if not access.visible(deps, caller, sid):
|
|
snap["convTitle"] = snap["convUrl"] = None
|
|
return
|
|
best = None
|
|
for path, s in deps.store.all_summaries():
|
|
if s.get("sessionId") != sid or _is_sidechain_path(path):
|
|
continue
|
|
if best is None or (s.get("endedAt") or "") > (best[1].get("endedAt") or ""):
|
|
best = (path, s)
|
|
if best:
|
|
snap["conversationId"] = _conv_id(best[0])
|
|
snap["convTitle"] = _conv_title(deps, best[1]) or snap.get("convTitle")
|
|
|
|
|
|
@router.get("/api/health")
|
|
def health():
|
|
return {"ok": True}
|