diff --git a/drb-c2-core/app/routers/calls.py b/drb-c2-core/app/routers/calls.py index 76527d6..9e5ab39 100644 --- a/drb-c2-core/app/routers/calls.py +++ b/drb-c2-core/app/routers/calls.py @@ -78,10 +78,13 @@ async def close_stale_calls( started_raw = call.get("started_at") if not started_raw: continue - try: - started = datetime.fromisoformat(started_raw.replace("Z", "+00:00")) - except Exception: - continue + if isinstance(started_raw, datetime): + started = started_raw if started_raw.tzinfo else started_raw.replace(tzinfo=timezone.utc) + else: + try: + started = datetime.fromisoformat(str(started_raw).replace("Z", "+00:00")) + except Exception: + continue if started < cutoff: stale.append(call) diff --git a/drb-frontend/app/admin/page.tsx b/drb-frontend/app/admin/page.tsx index 5dc13c1..41464bc 100644 --- a/drb-frontend/app/admin/page.tsx +++ b/drb-frontend/app/admin/page.tsx @@ -961,15 +961,102 @@ function AuditLogTab() { ); } +// --------------------------------------------------------------------------- +// Stale Calls tab +// --------------------------------------------------------------------------- + +function StaleCallsTab() { + const [minutes, setMinutes] = useState(30); + const [result, setResult] = useState<{ dry_run: boolean; count: number; call_ids: string[] } | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function run(dryRun: boolean) { + setLoading(true); + setError(null); + setResult(null); + try { + const res = await c2api.closeStallCalls(minutes, dryRun); + setResult(res); + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + } + + return ( +
+

+ Finds calls stuck in active status because a node rebooted before sending an end-call event. + Preview first, then close. +

+ +
+
+ + setMinutes(Math.min(1440, Math.max(1, Number(e.target.value))))} + className="w-28 bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500" + /> +
+ + +
+ + {error && ( +
+

{error}

+
+ )} + + {result && ( +
+

+ {result.dry_run ? "Preview: " : "Closed: "} + 0 ? "text-amber-400" : "text-green-400"}> + {result.count} stale call{result.count !== 1 ? "s" : ""} + + {result.count === 0 && — nothing to clear} +

+ {result.call_ids.length > 0 && ( +
+ {result.call_ids.map((id) => ( +

{id}

+ ))} +
+ )} +
+ )} +
+ ); +} + // --------------------------------------------------------------------------- // Main admin page // --------------------------------------------------------------------------- -type AdminTab = "features" | "correlation" | "users" | "audit"; +type AdminTab = "features" | "correlation" | "users" | "audit" | "calls"; const TAB_LABELS: { key: AdminTab; label: string }[] = [ { key: "features", label: "AI Features" }, { key: "correlation", label: "Correlation Debug" }, + { key: "calls", label: "Calls" }, { key: "users", label: "Users" }, { key: "audit", label: "Audit Log" }, ]; @@ -985,7 +1072,7 @@ export default function AdminPage() { if (!isAdmin) return null; - // Users/Audit tabs benefit from full width; AI Features / Correlation are narrow + // Users/Audit tabs benefit from full width; everything else is narrow const wide = tab === "users" || tab === "audit"; return ( @@ -1008,6 +1095,7 @@ export default function AdminPage() { {tab === "features" && } {tab === "correlation" && } + {tab === "calls" && } {tab === "users" && } {tab === "audit" && }