fix(dashboard): topology reflects connection health + clears finished requests (#7672)

The provider topology only lit nodes from live/recent traffic, so between
requests (and right after a restart) it went blank even though 50+ connections
were healthy — which reads as "lost providers". Two root causes:

1. Stuck-green latch: request.completed/request.failed are declared in the
   dashboard event map and consumed by useLiveRequests to drain the active-request
   set, but they were never emitted (only request.started was). A node's green
   "active" pulse therefore only cleared on a page reload, and accumulated over a
   session. Emit the terminal event from persistAttemptLogs — keyed by the same
   traceId as request.started — through a pure resolveRequestLifecycleEvent()
   helper (2xx/3xx + no error => completed, else failed).

2. No at-rest state: the map had nothing to show when idle. Colour each node by
   connection health (green connected / red error / grey idle) as a base layer,
   with live/recent traffic still taking precedence and pulsing brighter on top.
   edgeStyle() gains an optional trailing `healthy` param (static dim green) and
   StatusDot a `pulse` prop (static dot for connected-at-rest); both backward
   compatible. Legend "Active" -> "Connected".

Tests: resolveRequestLifecycleEvent success/failure/token-alias units, edgeStyle
healthy variant + precedence, and source guards for the emit wiring (traceId
threaded into persistAttemptLogs) and the health-colour wiring.

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
This commit is contained in:
danscMax
2026-07-20 02:52:43 +03:00
committed by GitHub
parent 1b7209034e
commit 18a8da6df7
10 changed files with 359 additions and 31 deletions

View File

@@ -464,9 +464,27 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
}, [selectedProvider, models]);
const topologyProviders = useMemo(() => {
const byProvider = new Map<string, { id: string; provider: string; name?: string }>();
type ProviderHealth = "active" | "error" | "idle";
const byProvider = new Map<
string,
{ id: string; provider: string; name?: string; status: ProviderHealth }
>();
const providerConfig = AI_PROVIDERS as Record<string, { name?: string }>;
// Connection-health per provider, so the topology node reflects "what is connected"
// at rest (green healthy / red error) instead of going blank between requests. A
// provider with ≥1 healthy connection is "active"; if none are healthy but some are
// errored it is "error"; otherwise "idle". Live/recent traffic still overrides this.
const healthByProvider = new Map<string, ProviderHealth>();
for (const stat of providerStats) {
const canonical = normalizeProviderId(stat.id);
if (!canonical) continue;
healthByProvider.set(
canonical,
stat.connected > 0 ? "active" : stat.errors > 0 ? "error" : "idle"
);
}
const addProvider = (providerId?: string | null, name?: string) => {
const rawProviderId = typeof providerId === "string" ? providerId.trim() : "";
if (!rawProviderId) return;
@@ -484,6 +502,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
id: canonicalProviderId,
provider: canonicalProviderId,
name: resolvedName,
status: healthByProvider.get(canonicalProviderId) ?? "idle",
});
};

View File

@@ -13,6 +13,8 @@ type TopologyProvider = {
id: string;
provider: string;
name?: string;
/** Connection-health base state, so the topology can colour a node at rest. */
status?: "active" | "error" | "idle";
};
export function HomeProviderTopologySection({

View File

@@ -7,7 +7,7 @@ import { AI_PROVIDERS } from "@/shared/constants/providers";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { FlowCanvas } from "@/shared/components/flow/FlowCanvas";
import { StatusDot } from "@/shared/components/flow/StatusDot";
import { edgeStyle } from "@/shared/components/flow/edgeStyles";
import { edgeStyle, FLOW_EDGE_COLORS } from "@/shared/components/flow/edgeStyles";
import { resolveTopologyNodeLabel } from "./topologyLabel";
// Rings: [capacity, rx, ry]. Each successive ring fits ~6 more nodes.
@@ -37,17 +37,27 @@ type ProviderNodeData = {
providerId: string;
active: boolean;
error: boolean;
/** Connection-health base state: a healthy connection with no in-flight traffic. */
healthy: boolean;
};
function ProviderNode({ data }: { data: ProviderNodeData }) {
const { label, color, providerId, active, error } = data;
const { label, color, providerId, active, error, healthy } = data;
const GREEN = FLOW_EDGE_COLORS.active;
const RED = FLOW_EDGE_COLORS.error;
return (
<div
className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg border-2 transition-all duration-300 bg-bg"
style={{
borderColor: error ? "#ef4444" : active ? color : "var(--color-border)",
boxShadow: error ? `0 0 12px #ef444430` : active ? `0 0 12px ${color}30` : "none",
borderColor: error ? RED : active ? color : healthy ? GREEN : "var(--color-border)",
boxShadow: error
? `0 0 12px ${RED}30`
: active
? `0 0 12px ${color}30`
: healthy
? `0 0 10px ${GREEN}20`
: "none",
minWidth: "136px",
}}
>
@@ -85,12 +95,16 @@ function ProviderNode({ data }: { data: ProviderNodeData }) {
<span
className="text-xs font-medium truncate flex-1"
style={{ color: active ? color : error ? "#ef4444" : "var(--color-text-main)" }}
style={{
color: active ? color : error ? RED : healthy ? GREEN : "var(--color-text-main)",
}}
>
{label}
</span>
{(active || error) && <StatusDot color={color} error={error} />}
{(active || error || healthy) && (
<StatusDot color={active ? color : GREEN} error={error} pulse={active || error} />
)}
</div>
);
}
@@ -143,7 +157,8 @@ const nodeTypes: NodeTypes = {
router: RouterNode as any,
};
type ProviderEntry = { id?: string; provider: string; name?: string };
type ProviderHealth = "active" | "error" | "idle";
type ProviderEntry = { id?: string; provider: string; name?: string; status?: ProviderHealth };
function getHandles(angle: number, cx: number): { sourceHandle: string; targetHandle: string } {
const rel = (((angle + Math.PI / 2) % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
@@ -180,18 +195,20 @@ function buildLayout(
if (providers.length === 0) return { nodes, edges };
// Sort: active → error → last-used → rest (alpha within groups)
// Sort: active → error → last-used → healthy(connected) → rest (alpha within groups)
const sorted = [...providers].sort((a, b) => {
const aId = a.provider.toLowerCase();
const bId = b.provider.toLowerCase();
const rank = (id: string) => {
const rank = (p: ProviderEntry) => {
const id = p.provider.toLowerCase();
if (activeSet.has(id)) return 0;
if (errorSet.has(id)) return 1;
if (errorSet.has(id) || p.status === "error") return 1;
if (lastSet.has(id)) return 2;
return 3;
if (p.status === "active") return 3;
return 4;
};
const d = rank(aId) - rank(bId);
return d !== 0 ? d : aId.localeCompare(bId); // teknik sıralama: ASCII kasıtlı
const d = rank(a) - rank(b);
return d !== 0
? d
: a.provider.toLowerCase().localeCompare(b.provider.toLowerCase()); // ASCII kasıtlı
});
let provIdx = 0;
@@ -203,8 +220,14 @@ function buildLayout(
const p = sorted[provIdx++];
const pid = p.provider.toLowerCase();
const active = activeSet.has(pid);
const error = !active && errorSet.has(pid);
const last = !active && !error && lastSet.has(pid);
// Traffic signals (live/recent request) take precedence; connection health is the
// base state shown when a provider has no in-flight or recent traffic, so the map
// still reflects "what is connected" at rest instead of going blank after a restart.
const trafficError = !active && errorSet.has(pid);
const last = !active && !trafficError && lastSet.has(pid);
const healthError = !active && !trafficError && !last && p.status === "error";
const healthy = !active && !trafficError && !last && !healthError && p.status === "active";
const error = trafficError || healthError;
const config = getProviderConfig(p.provider);
const nodeId = `provider-${p.provider}`;
@@ -223,6 +246,7 @@ function buildLayout(
providerId: p.provider,
active,
error,
healthy,
} satisfies ProviderNodeData,
draggable: false,
});
@@ -234,7 +258,7 @@ function buildLayout(
target: nodeId,
targetHandle,
animated: active,
style: edgeStyle(active, last, error),
style: edgeStyle(active, last, error, healthy),
});
}
}

View File

@@ -10,21 +10,34 @@ type StatusDotProps = {
* value used by ProviderTopology so the home pulse is pixel-identical.
*/
sizeClass?: string;
/**
* Whether to render the `animate-ping` halo. Defaults to true (live/active pulse).
* Pass false for a static presence dot — e.g. a connection that is healthy but has
* no in-flight traffic, which should read as "connected" without implying activity.
*/
pulse?: boolean;
};
/**
* The pulsing presence indicator extracted from `ProviderTopology` (U0). Renders
* an `animate-ping` halo plus a solid dot. Callers decide *whether* to show it
* (e.g. only when a node is active or errored); this component only draws it.
* The presence indicator extracted from `ProviderTopology` (U0). Renders an optional
* `animate-ping` halo plus a solid dot. Callers decide *whether* to show it (e.g. only
* when a node is active, healthy, or errored); this component only draws it.
*/
export function StatusDot({ color, error = false, sizeClass = "size-1.5" }: StatusDotProps) {
export function StatusDot({
color,
error = false,
sizeClass = "size-1.5",
pulse = true,
}: StatusDotProps) {
const dotColor = error ? FLOW_EDGE_COLORS.error : color;
return (
<span className={`relative flex ${sizeClass} shrink-0`}>
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-70"
style={{ backgroundColor: dotColor }}
/>
{pulse && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-70"
style={{ backgroundColor: dotColor }}
/>
)}
<span
className={`relative inline-flex rounded-full ${sizeClass}`}
style={{ backgroundColor: dotColor }}

View File

@@ -21,12 +21,22 @@ export interface FlowEdgeStyle {
/**
* Resolve the stroke style for an edge given its state. Precedence is
* error > active > last-used > idle — identical to the original ProviderTopology
* implementation (do not reorder without updating the home regression).
* error > active > last-used > healthy > idle — the first three are identical to the
* original ProviderTopology implementation (do not reorder without updating the home
* regression). `healthy` is the connection-health base state (a configured provider with
* a live/healthy connection but no in-flight traffic): a static, dimmer green that makes
* the map meaningful at rest, distinct from the animated `active` pulse. It is an optional
* trailing param so existing callers (Combo/Compression studios) stay unaffected.
*/
export function edgeStyle(active: boolean, last: boolean, error: boolean): FlowEdgeStyle {
export function edgeStyle(
active: boolean,
last: boolean,
error: boolean,
healthy = false
): FlowEdgeStyle {
if (error) return { stroke: FLOW_EDGE_COLORS.error, strokeWidth: 2, opacity: 0.85 };
if (active) return { stroke: FLOW_EDGE_COLORS.active, strokeWidth: 2.5, opacity: 1 };
if (last) return { stroke: FLOW_EDGE_COLORS.last, strokeWidth: 1.5, opacity: 0.6 };
if (healthy) return { stroke: FLOW_EDGE_COLORS.active, strokeWidth: 1.5, opacity: 0.4 };
return { stroke: FLOW_EDGE_COLORS.idle, strokeWidth: 1, opacity: 0.3 };
}