Give the archive a real read, and the debug view a verdict
Three backend pieces the /calls page needs, plus the fix for a debug view that
hid its data exactly when it was wanted.
GET /calls/search — paged, filterable call archive. GET /calls returns every
call in one unordered shot: fine for a node's handful of active calls, useless
as an archive. Only the org scope and the started_at ordering go to Firestore,
since that pair is the one composite index that exists; the rest filters in
Python over a bounded window, the same shape admin.py's debug route uses. The
cursor advances over the scanned window rather than the returned page, or a
sparse filter would re-scan from the same place forever.
Manual attribution. POST /incidents/{id}/calls/{id} only ever wrote the legacy
scalar incident_id, never incident_ids -- which is what the correlator writes
and what the frontend queries with array-contains. A manually attached call was
therefore invisible on the incident page it had just been attached to. It now
maintains both and marks the summary stale. DELETE is new: there was no way to
undo an attachment at all, so a wrong link was permanent.
The debug view no longer filters to AI-enabled systems by default. That filter
emptied the view the moment the flags went off, which is precisely when a
window gets reviewed -- on 2026-08-23 it fell from 100 incidents to 6 between
switching correlation off and opening the tab. ai_systems_only=true restores it.
It also returns a summary block now: corr_path / fit_signal / consensus /
llm_action tallies, transcript coverage on both linked and orphaned calls,
single-call and median-calls-per-incident for fragmentation, max span and
anything past the server-26#22 caps for merging, and the count of incidents
still carrying a fallback "— TGID" title. All of it was being recomputed by
hand from the raw payload on every review.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7ef5704be2
commit
140dfbfc74
@@ -112,15 +112,70 @@ async def summarize_incident(
|
||||
|
||||
@router.post("/{incident_id}/calls/{call_id}")
|
||||
async def link_call_to_incident(incident_id: str, call_id: str, _: dict = Depends(require_admin_token)):
|
||||
"""Manually attach a call to an incident (the /calls page's attribution action)."""
|
||||
doc = await fstore.doc_get("incidents", incident_id)
|
||||
if not doc:
|
||||
raise HTTPException(404, f"Incident '{incident_id}' not found.")
|
||||
call_ids = doc.get("call_ids", [])
|
||||
call = await fstore.doc_get("calls", call_id)
|
||||
if not call:
|
||||
raise HTTPException(404, f"Call '{call_id}' not found.")
|
||||
|
||||
call_ids = list(doc.get("call_ids") or [])
|
||||
if call_id not in call_ids:
|
||||
call_ids.append(call_id)
|
||||
await fstore.doc_update("incidents", incident_id, {
|
||||
"call_ids": call_ids,
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
# A manually attached call changes what the incident is about.
|
||||
"summary_stale": True,
|
||||
})
|
||||
await fstore.doc_update("calls", call_id, {"incident_id": incident_id})
|
||||
return {"ok": True}
|
||||
|
||||
# incident_ids is the canonical link — it is what the correlator writes and
|
||||
# what the frontend queries with array-contains. This route only ever set
|
||||
# the legacy scalar incident_id, so a manually attached call stayed
|
||||
# invisible on the incident's own page.
|
||||
incident_ids = list(call.get("incident_ids") or ([call["incident_id"]] if call.get("incident_id") else []))
|
||||
if incident_id not in incident_ids:
|
||||
incident_ids.append(incident_id)
|
||||
await fstore.doc_update("calls", call_id, {
|
||||
"incident_ids": incident_ids,
|
||||
"incident_id": incident_id,
|
||||
"corr_path": "manual",
|
||||
})
|
||||
return {"ok": True, "incident_ids": incident_ids}
|
||||
|
||||
|
||||
@router.delete("/{incident_id}/calls/{call_id}")
|
||||
async def unlink_call_from_incident(incident_id: str, call_id: str, _: dict = Depends(require_admin_token)):
|
||||
"""
|
||||
Detach a call from an incident — the other half of manual attribution.
|
||||
|
||||
An incident left with no calls is resolved rather than deleted, matching
|
||||
what calls.py's transcript correction does when it empties one.
|
||||
"""
|
||||
doc = await fstore.doc_get("incidents", incident_id)
|
||||
if not doc:
|
||||
raise HTTPException(404, f"Incident '{incident_id}' not found.")
|
||||
|
||||
remaining = [c for c in (doc.get("call_ids") or []) if c != call_id]
|
||||
updates: dict = {
|
||||
"call_ids": remaining,
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"summary_stale": True,
|
||||
}
|
||||
if not remaining:
|
||||
updates["status"] = "resolved"
|
||||
updates["resolved_at"] = datetime.now(timezone.utc).isoformat()
|
||||
await fstore.doc_update("incidents", incident_id, updates)
|
||||
|
||||
call = await fstore.doc_get("calls", call_id)
|
||||
if call:
|
||||
incident_ids = [
|
||||
i for i in (call.get("incident_ids") or ([call["incident_id"]] if call.get("incident_id") else []))
|
||||
if i != incident_id
|
||||
]
|
||||
await fstore.doc_update("calls", call_id, {
|
||||
"incident_ids": incident_ids,
|
||||
"incident_id": incident_ids[0] if incident_ids else None,
|
||||
})
|
||||
return {"ok": True, "incident_emptied": not remaining}
|
||||
|
||||
Reference in New Issue
Block a user