Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 93 additions & 22 deletions src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import ProviderIcon from "@/shared/components/ProviderIcon";
// Types
// ─────────────────────────────────────────────────────────────────────────────

type BreakerState = "CLOSED" | "OPEN" | "HALF_OPEN";
type KnownBreakerState = "CLOSED" | "OPEN" | "HALF_OPEN" | "DEGRADED";
type BreakerState = KnownBreakerState | (string & {});

type ProviderBreaker = {
provider: string;
Expand Down Expand Up @@ -83,6 +84,7 @@ type Connection = {

type FeedEventKind =
| "circuit-opened"
| "circuit-degraded"
| "circuit-recovered"
| "circuit-closed"
| "cooldown-added"
Expand Down Expand Up @@ -110,11 +112,11 @@ type FeedFilter = "all" | "circuits" | "cooldowns" | "lockouts" | "sessions" | "

const REFRESH_INTERVAL_MS = 5000;
const FEED_MAX_EVENTS = 50;
const EMPTY_PROVIDER_BREAKERS: ProviderBreaker[] = [];

const BREAKER_TONE: Record<
BreakerState,
{ dot: string; bg: string; ring: string; label: string; icon: string }
> = {
type BreakerTone = { dot: string; bg: string; ring: string; label: string; icon: string };

const BREAKER_TONE: Record<string, BreakerTone> = {
CLOSED: {
dot: "#22c55e",
bg: "rgba(34,197,94,0.10)",
Expand All @@ -129,6 +131,13 @@ const BREAKER_TONE: Record<
label: "RECOV",
icon: "sync",
},
DEGRADED: {
dot: "#f97316",
bg: "rgba(249,115,22,0.10)",
ring: "rgba(249,115,22,0.30)",
label: "DEG",
icon: "warning",
},
OPEN: {
dot: "#ef4444",
bg: "rgba(239,68,68,0.10)",
Expand All @@ -138,8 +147,17 @@ const BREAKER_TONE: Record<
},
};

const FALLBACK_BREAKER_TONE: BreakerTone = {
dot: "#64748b",
bg: "rgba(100,116,139,0.10)",
ring: "rgba(100,116,139,0.30)",
label: "UNK",
icon: "help",
};

const FEED_KIND_META: Record<FeedEventKind, { icon: string; color: string; group: FeedFilter }> = {
"circuit-opened": { icon: "block", color: "#ef4444", group: "circuits" },
"circuit-degraded": { icon: "warning", color: "#f97316", group: "circuits" },
"circuit-recovered": { icon: "sync", color: "#eab308", group: "circuits" },
"circuit-closed": { icon: "check_circle", color: "#22c55e", group: "circuits" },
"cooldown-added": { icon: "ac_unit", color: "#3b82f6", group: "cooldowns" },
Expand Down Expand Up @@ -188,6 +206,16 @@ function untilMs(value: number | string | null | undefined): number {
return 0;
}

function normalizeBreakerState(state: string | null | undefined): string {
return String(state || "")
.trim()
.toUpperCase();
}

function getBreakerTone(normalizedState: string): BreakerTone {
return BREAKER_TONE[normalizedState] || FALLBACK_BREAKER_TONE;
}
Comment on lines +209 to +217

function pushFeed(prev: FeedEvent[], events: FeedEvent[]): FeedEvent[] {
if (events.length === 0) return prev;
const merged = [...events, ...prev];
Expand All @@ -210,24 +238,34 @@ function diffSnapshots(
for (const [provider, nextB] of nextBreakers) {
const prevB = prevBreakers.get(provider);
if (!prevB) continue;
if (prevB.state === nextB.state) continue;
if (nextB.state === "OPEN") {
const prevState = normalizeBreakerState(prevB.state);
const nextState = normalizeBreakerState(nextB.state);
if (prevState === nextState) continue;
if (nextState === "OPEN") {
out.push({
id: `cb-open-${provider}-${nowTs}`,
ts: nowTs,
kind: "circuit-opened",
title: `${provider} circuit OPEN`,
detail: `threshold hit · retry in ${fmtMs(nextB.retryAfterMs)}`,
});
} else if (nextB.state === "HALF_OPEN") {
} else if (nextState === "HALF_OPEN") {
out.push({
id: `cb-half-${provider}-${nowTs}`,
ts: nowTs,
kind: "circuit-recovered",
title: `${provider} HALF_OPEN`,
detail: `probing recovery`,
});
} else if (nextB.state === "CLOSED" && prevB.state !== "CLOSED") {
} else if (nextState === "DEGRADED") {
out.push({
id: `cb-deg-${provider}-${nowTs}`,
ts: nowTs,
kind: "circuit-degraded",
title: `${provider} DEGRADED`,
detail: `${nextB.failureCount} failures · degraded but serving`,
});
} else if (nextState === "CLOSED" && prevState !== "CLOSED") {
Comment on lines +260 to +268
out.push({
id: `cb-close-${provider}-${nowTs}`,
ts: nowTs,
Expand Down Expand Up @@ -423,16 +461,33 @@ export default function RuntimePageClient() {
return connections.filter((c) => c.rateLimitedUntil && untilMs(c.rateLimitedUntil) > 0);
}, [connections]);

const breakers = health?.providerBreakers ?? [];
const breakers = health?.providerBreakers ?? EMPTY_PROVIDER_BREAKERS;
const lockoutEntries = useMemo<Array<[string, LockoutEntry]>>(
() => Object.entries(health?.lockouts ?? {}),
[health]
);

const counts = useMemo(() => {
const openCircuits = breakers.filter((b) => b.state === "OPEN").length;
const halfCircuits = breakers.filter((b) => b.state === "HALF_OPEN").length;
let openCircuits = 0;
let halfCircuits = 0;
let degradedCircuits = 0;
let unknownCircuits = 0;

for (const breaker of breakers) {
const state = normalizeBreakerState(breaker.state);
if (state === "OPEN") {
openCircuits++;
} else if (state === "HALF_OPEN") {
halfCircuits++;
} else if (state === "DEGRADED") {
degradedCircuits++;
} else if (state !== "CLOSED") {
unknownCircuits++;
}
}

const totalBreakers = breakers.length;
const affectedCircuits = openCircuits + halfCircuits + degradedCircuits + unknownCircuits;
Comment on lines 470 to +490

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation performs four separate .filter() operations on the breakers array, calling normalizeBreakerState repeatedly for each element. Since this runs inside a useMemo hook that executes on every health update (every 5 seconds), we can optimize this to a single-pass loop. This avoids multiple array allocations and redundant string normalization operations, improving both performance and readability.

  const counts = useMemo(() => {
    let openCircuits = 0;
    let halfCircuits = 0;
    let degradedCircuits = 0;
    let unknownCircuits = 0;

    const knownBreakerStates = new Set(["CLOSED", "OPEN", "HALF_OPEN", "DEGRADED"]);

    for (const b of breakers) {
      const state = normalizeBreakerState(b.state);
      if (state === "OPEN") {
        openCircuits++;
      } else if (state === "HALF_OPEN") {
        halfCircuits++;
      } else if (state === "DEGRADED") {
        degradedCircuits++;
      } else if (!knownBreakerStates.has(state)) {
        unknownCircuits++;
      }
    }

    const totalBreakers = breakers.length;
    const affectedCircuits = openCircuits + halfCircuits + degradedCircuits + unknownCircuits;

Comment on lines 470 to +490
const sessions = health?.sessions?.activeCount ?? 0;
const lockouts = lockoutEntries.length;
const quota = health?.quotaMonitor;
Expand All @@ -443,6 +498,9 @@ export default function RuntimePageClient() {
stickyBound: health?.sessions?.stickyBoundCount ?? 0,
openCircuits,
halfCircuits,
degradedCircuits,
unknownCircuits,
affectedCircuits,
totalBreakers,
cooldowns: cooldowns.length,
lockouts,
Expand All @@ -456,7 +514,7 @@ export default function RuntimePageClient() {
return feed.filter((ev) => FEED_KIND_META[ev.kind].group === feedFilter);
}, [feed, feedFilter]);

const overallHealthy = counts.totalBreakers - counts.openCircuits - counts.halfCircuits;
const overallHealthy = counts.totalBreakers - counts.affectedCircuits;
const overallPercent =
counts.totalBreakers > 0 ? Math.round((overallHealthy / counts.totalBreakers) * 100) : 100;

Expand Down Expand Up @@ -519,14 +577,20 @@ export default function RuntimePageClient() {
label={t("kpiCircuits")}
value={`${counts.openCircuits} / ${counts.totalBreakers}`}
hint={
counts.halfCircuits > 0
? t("hintRecovering", { count: counts.halfCircuits })
counts.halfCircuits + counts.degradedCircuits + counts.unknownCircuits > 0
? t("hintRecovering", {
count: counts.halfCircuits + counts.degradedCircuits + counts.unknownCircuits,
})
: counts.openCircuits === 0
? t("hintAllHealthy")
: t("hintOpen")
}
tone={
counts.openCircuits > 0 ? "#ef4444" : counts.halfCircuits > 0 ? "#eab308" : "#22c55e"
counts.openCircuits > 0
? "#ef4444"
: counts.halfCircuits + counts.degradedCircuits + counts.unknownCircuits > 0
? "#eab308"
: "#22c55e"
}
onClick={() => setFeedFilter("circuits")}
active={feedFilter === "circuits"}
Expand Down Expand Up @@ -561,7 +625,9 @@ export default function RuntimePageClient() {
trailing={
<div className="flex items-center gap-3 text-[11px]">
<span className="text-green-500">✓ {overallHealthy}</span>
<span className="text-amber-500">⚠ {counts.halfCircuits}</span>
<span className="text-amber-500">
⚠ {counts.halfCircuits + counts.degradedCircuits + counts.unknownCircuits}
</span>
<span className="text-red-500">⛔ {counts.openCircuits}</span>
</div>
}
Expand All @@ -585,25 +651,30 @@ export default function RuntimePageClient() {
title={t("layer1Title")}
description={t("layer1Desc")}
badge={t("badgeAffectedOf", {
affected: counts.openCircuits + counts.halfCircuits,
affected: counts.affectedCircuits,
total: counts.totalBreakers,
})}
badgeTone={
counts.openCircuits > 0 ? "red" : counts.halfCircuits > 0 ? "amber" : "green"
counts.openCircuits > 0
? "red"
: counts.halfCircuits + counts.degradedCircuits + counts.unknownCircuits > 0
? "amber"
: "green"
}
>
{breakers.length === 0 ? (
<EmptyHint text={t("emptyCircuits")} />
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
{breakers.map((b) => {
const tone = BREAKER_TONE[b.state];
const state = normalizeBreakerState(b.state);
const tone = getBreakerTone(state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Passing state (which is already normalized via normalizeBreakerState(b.state)) to getBreakerTone causes redundant normalization because getBreakerTone calls normalizeBreakerState internally. Passing b.state directly to getBreakerTone avoids this double normalization.

Suggested change
const tone = getBreakerTone(state);
const tone = getBreakerTone(b.state);

return (
<div
key={b.provider}
className="rounded-md border px-2.5 py-2 flex flex-col gap-0.5"
style={{ borderColor: tone.ring, background: tone.bg }}
title={`${b.provider} · ${b.state} · failures ${b.failureCount}`}
title={`${b.provider} · ${state || "UNKNOWN"} · failures ${b.failureCount}`}
>
<div className="flex items-center gap-1.5 text-[11px] font-semibold text-text-main">
<ProviderIcon providerId={b.provider} size={14} />
Expand All @@ -613,7 +684,7 @@ export default function RuntimePageClient() {
</span>
</div>
<div className="text-[10px] text-text-muted tabular-nums">
{b.state === "OPEN"
{state === "OPEN"
? `retry ${fmtMs(b.retryAfterMs)}`
: `${b.failureCount} failures`}
</div>
Expand Down
Loading