Files
coreci-chat/apps/control-plane/lib/health.ts
T
CIAgent dfc6b8ff76 feat(P5): Wave E dashboard surfacing — agent status, health, logs, audit export
REQ-014: surface relay agent health (green/yellow/red), target hostname,
  last 100 log lines in admin dashboard. Per-tenant view under RLS.

---ci---
phase: 5
milestone: v0.1
status: execute
---/ci---
2026-08-25 02:21:51 +00:00

61 lines
2.3 KiB
TypeScript

/**
* Health computation for Relay Agent targets (REQ-014, Wave E).
*
* Status is computed from `targets.last_seen_at`:
* green — last_seen within the last 60s (agent is heartbeating)
* yellow — last_seen within the last 5min (degraded — maybe a slow network
* or a restart in progress; agent may still recover)
* red — last_seen more than 5min ago, or NULL (never seen / down)
*
* The thresholds match the Wave E spec + the in-memory `getConnectedAgents()`
* snapshot from the WS server (real-time online check). The dashboard SSE
* fan-out (api/relay/status) and the targets list/detail routes both use this
* helper so the badge color is consistent everywhere.
*/
export type HealthStatus = "green" | "yellow" | "red";
/** green threshold (seconds since last_seen). */
export const GREEN_THRESHOLD_S = 60;
/** yellow threshold (seconds since last_seen). */
export const YELLOW_THRESHOLD_S = 5 * 60;
export interface HealthInput {
/** ISO timestamptz string, or null if the agent never heartbeated. */
lastSeenAt: string | null;
/** Optional: the WS server's in-memory connected-agent snapshot, keyed by
* targetId. If a target is currently connected, we treat it as green even
* if last_seen is slightly stale (heartbeat updates are batched). */
connectedTargetIds?: ReadonlySet<string>;
/** Target id (for the connected lookup). */
targetId?: string;
}
/**
* Compute health from last_seen_at (and optionally the connected-agents set).
* `now` defaults to Date.now() but can be injected for deterministic tests.
*/
export function computeHealth(input: HealthInput, now: number = Date.now()): HealthStatus {
if (input.targetId && input.connectedTargetIds?.has(input.targetId)) {
return "green";
}
if (!input.lastSeenAt) return "red";
const last = Date.parse(input.lastSeenAt);
if (Number.isNaN(last)) return "red";
const ageS = (now - last) / 1000;
if (ageS <= GREEN_THRESHOLD_S) return "green";
if (ageS <= YELLOW_THRESHOLD_S) return "yellow";
return "red";
}
/** Badge color for a health status (used by the dashboard pages). */
export function healthColor(status: HealthStatus): string {
switch (status) {
case "green":
return "#22c55e";
case "yellow":
return "#eab308";
case "red":
return "#ef4444";
}
}