fix(code-queue): add active-run liveness diagnostics

This commit is contained in:
Codex
2026-05-19 03:48:17 +00:00
parent f36ea37548
commit 2a45bfe180
13 changed files with 561 additions and 17 deletions
+77 -2
View File
@@ -2169,6 +2169,81 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
.codex-trace-status-chip.service {
white-space: normal;
}
.codex-trace-status-chip.liveness.ok {
border-color: rgba(78, 183, 168, 0.50);
color: var(--accent-2);
}
.codex-trace-status-chip.liveness.warn {
border-color: rgba(215, 161, 58, 0.55);
color: #ffe0a2;
}
.codex-trace-status-chip.liveness.failed {
border-color: rgba(255, 98, 98, 0.58);
color: #ffb2b2;
}
.codex-liveness-panel .panel-body {
display: grid;
gap: 8px;
}
.codex-liveness-grid {
display: grid;
grid-template-columns: repeat(5, minmax(130px, 1fr));
gap: 8px;
min-width: 0;
}
.codex-liveness-metric {
display: grid;
gap: 4px;
min-width: 0;
padding: 8px;
border: 1px solid rgba(78, 183, 168, 0.22);
background:
linear-gradient(135deg, rgba(78, 183, 168, 0.08), rgba(255,255,255,0.015)),
rgba(0,0,0,0.16);
}
.codex-liveness-metric.warn {
border-color: rgba(215, 161, 58, 0.44);
}
.codex-liveness-metric.failed {
border-color: rgba(255, 98, 98, 0.46);
}
.codex-liveness-metric.ok {
border-color: rgba(78, 183, 168, 0.45);
}
.codex-liveness-metric span {
color: var(--muted);
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.codex-liveness-metric strong {
min-width: 0;
color: var(--text);
font-size: 15px;
font-weight: 700;
overflow-wrap: anywhere;
}
.codex-liveness-metric code {
min-width: 0;
color: var(--muted);
font-size: 11px;
overflow-wrap: anywhere;
}
.codex-liveness-reasons {
display: flex;
flex-wrap: wrap;
gap: 5px;
min-width: 0;
}
.codex-liveness-reasons span {
max-width: 100%;
padding: 4px 7px;
border: 1px solid rgba(215, 161, 58, 0.28);
color: #ffe0a2;
background: rgba(215, 161, 58, 0.07);
font-size: 11px;
overflow-wrap: anywhere;
}
.codex-mark-all-read-btn {
border-color: rgba(78, 183, 168, 0.40);
color: #bdece4;
@@ -5490,7 +5565,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
}
@media (max-width: 1120px) {
.metric-grid, .policy-grid, .security-board, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .performance-metric-stack, .codex-load-test-grid, .baidu-doc-grid, .filebrowser-target-grid, .oa-flow-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.metric-grid, .policy-grid, .security-board, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .performance-metric-stack, .codex-load-test-grid, .codex-liveness-grid, .baidu-doc-grid, .filebrowser-target-grid, .oa-flow-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pipeline-oa-guarantees { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dispatch-form, .schedule-form { grid-template-columns: 1fr 1fr; }
.dispatch-actions { align-items: center; }
@@ -5680,7 +5755,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
padding: 4px 9px;
white-space: nowrap;
}
.metric-grid, .policy-grid, .security-board, .dispatch-form, .schedule-form, .schedule-card-grid, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .gateway-record-grid, .met-detail-kv, .code-queue-metrics, .codex-stats-summary-grid, .codex-form-grid, .baidu-doc-grid { grid-template-columns: 1fr; }
.metric-grid, .policy-grid, .security-board, .dispatch-form, .schedule-form, .schedule-card-grid, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .gateway-record-grid, .met-detail-kv, .code-queue-metrics, .codex-stats-summary-grid, .codex-liveness-grid, .codex-form-grid, .baidu-doc-grid { grid-template-columns: 1fr; }
.compact-row, .heartbeat-row, .log-row, .endpoint-list article, .volume-route, .findjob-hero, .pipeline-hero, .code-queue-hero, .claudeqq-login-card, .baidu-login-card, .baidu-pathbar { grid-template-columns: 1fr; align-items: start; }
.codex-output-line { grid-template-columns: 1fr; }
.codex-transcript { min-height: 360px; }
@@ -219,6 +219,34 @@ function activeTaskIds(queue: any): string[] {
return Array.isArray(queue?.activeTaskIds) ? queue.activeTaskIds.map((id: any) => String(id || "")).filter(Boolean) : [String(queue?.activeTaskId || "")].filter(Boolean);
}
function stringArray(value: any): string[] {
return Array.isArray(value) ? value.map((item: any) => String(item || "")).filter(Boolean) : [];
}
function compactIdList(value: any, limit = 3): string {
const ids = stringArray(value);
if (ids.length === 0) return "--";
const visible = ids.slice(0, limit).join(" / ");
return ids.length > limit ? `${visible} +${ids.length - limit}` : visible;
}
function executionDiagnosticsFromQueue(queue: any, health: any): AnyRecord {
return objectRecord(queue?.executionDiagnostics)
|| objectRecord(health?.body?.queue?.executionDiagnostics)
|| objectRecord(health?.queue?.executionDiagnostics)
|| objectRecord(health?.body?.executionDiagnostics)
|| objectRecord(health?.executionDiagnostics)
|| {};
}
function diagnosticsTone(state: any): string {
const value = String(state || "unknown").toLowerCase();
if (value === "healthy") return "ok";
if (value === "split-brain" || value === "stale-active") return "failed";
if (value === "degraded") return "warn";
return "unknown";
}
const allQueuesId = "__all__";
const queueMobileMediaQuery = "(max-width: 760px)";
const queueDesktopMediaQuery = "(min-width: 761px)";
@@ -1536,6 +1564,48 @@ function CodexStatsIcon() {
);
}
function LivenessMetric({ label, value, hint, tone }: AnyRecord) {
return h("div", { className: `codex-liveness-metric ${tone || ""}` },
h("span", null, label),
h("strong", null, value ?? "--"),
hint ? h("code", null, hint) : null,
);
}
function CodeQueueLivenessPanel({ diagnostics, queue, onRaw }: AnyRecord) {
const state = String(diagnostics?.state || diagnostics?.health || "unknown");
const oaPublisher = objectRecord(diagnostics?.oaPublisher);
const reasons = stringArray(diagnostics?.reasons).slice(0, 3);
const tone = diagnosticsTone(state);
return h(Panel, {
title: "执行活性",
eyebrow: `${String(diagnostics?.executionStateSource || queue?.executionStateSource || "unknown")} / ${String(diagnostics?.controlPlane || "code-queue")}`,
summary: h("div", { className: "codex-trace-status" },
h("span", { className: `codex-trace-status-chip liveness ${tone}` }, h("b", null, "状态"), state),
h("span", { className: "codex-trace-status-chip" }, h("b", null, "DB active"), String(diagnostics?.databaseActiveTaskCount ?? queue?.databaseActiveTaskCount ?? 0)),
h("span", { className: "codex-trace-status-chip" }, h("b", null, "scheduler slots"), String(diagnostics?.schedulerActiveRunSlotCount ?? queue?.activeRunSlotCount ?? 0)),
h("span", { className: "codex-trace-status-chip" }, h("b", null, "heartbeat"), `${stringArray(diagnostics?.heartbeatFreshTaskIds).length} fresh / ${stringArray(diagnostics?.heartbeatExpiredTaskIds).length} expired`),
oaPublisher ? h("span", { className: "codex-trace-status-chip" }, h("b", null, "OA"), `${Number(oaPublisher.pending || 0)} pending${oaPublisher.lastError ? " / error" : ""}`) : null,
),
actions: h(RawButton, { title: "Code Queue Execution Diagnostics", data: diagnostics, onOpen: onRaw, testId: "raw-code-queue-execution-diagnostics" }),
className: "codex-liveness-panel",
},
h("div", { className: "codex-liveness-grid", "data-testid": "codex-liveness-diagnostics" },
h(LivenessMetric, { tone, label: "健康状态", value: state, hint: diagnostics?.splitBrain ? "split-brain" : diagnostics?.degraded ? "degraded" : "ready" }),
h(LivenessMetric, { label: "PostgreSQL active", value: String(diagnostics?.databaseActiveTaskCount ?? queue?.databaseActiveTaskCount ?? 0), hint: compactIdList(diagnostics?.databaseActiveTaskIds ?? queue?.databaseActiveTaskIds) }),
h(LivenessMetric, { label: "Scheduler active", value: String(diagnostics?.schedulerActiveRunSlotCount ?? queue?.activeRunSlotCount ?? 0), hint: compactIdList(diagnostics?.schedulerActiveTaskIds ?? queue?.activeTaskIds) }),
h(LivenessMetric, { label: "Fresh heartbeat", value: String(stringArray(diagnostics?.heartbeatFreshTaskIds).length), hint: compactIdList(diagnostics?.heartbeatFreshTaskIds) }),
h(LivenessMetric, { tone: stringArray(diagnostics?.traceGapNotStaleTaskIds).length > 0 ? "warn" : "", label: "Trace gap", value: String(stringArray(diagnostics?.traceGapTaskIds).length), hint: compactIdList(diagnostics?.traceGapNotStaleTaskIds) }),
h(LivenessMetric, { tone: stringArray(diagnostics?.staleRecoveryCandidateTaskIds).length > 0 ? "failed" : "", label: "Stale candidates", value: String(stringArray(diagnostics?.staleRecoveryCandidateTaskIds).length), hint: compactIdList(diagnostics?.staleRecoveryCandidateTaskIds) }),
h(LivenessMetric, { label: "Last scheduler heartbeat", value: fmtRelativeAge(diagnostics?.lastSchedulerHeartbeatAt), hint: String(diagnostics?.lastSchedulerHeartbeatAt || "--") }),
h(LivenessMetric, { label: "Last agent event", value: fmtRelativeAge(diagnostics?.lastObservedAgentEventAt), hint: String(diagnostics?.lastObservedAgentEventAt || "--") }),
h(LivenessMetric, { label: "Last trace persist", value: fmtRelativeAge(diagnostics?.lastPersistedTraceAt), hint: String(diagnostics?.lastPersistedTraceAt || "--") }),
h(LivenessMetric, { tone: oaPublisher?.lastError ? "warn" : "", label: "OA publisher", value: `${Number(oaPublisher?.pending || 0)} pending`, hint: oaPublisher?.lastError ? shortText(oaPublisher.lastError, 90) : "ok" }),
),
reasons.length > 0 ? h("div", { className: "codex-liveness-reasons" }, reasons.map((reason: string) => h("span", { key: reason }, reason))) : null,
);
}
function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord) {
const rows = taskStatisticsRows(stats);
const totals = taskStatisticsTotals(stats);
@@ -2112,6 +2182,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
const tasks = applyLocalReadStateToRows(taskRows(tasksData));
const loadedUnreadTerminalTasks = tasks.filter(taskIsUnreadTerminal);
const queue = tasksData?.queue || health?.body?.queue || health?.queue || {};
const executionDiagnostics = executionDiagnosticsFromQueue(queue, health);
const statistics = taskStatistics(tasksData, queue);
const pagination = taskPagination(tasksData);
const queueRows = knownQueueRows(queue, queueId);
@@ -3828,6 +3899,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
h(UniDeskErrorBanner, { error, wide: true }),
mergeDialog,
h("div", { className: "codex-session-stage codex-session-stage-top" },
h(CodeQueueLivenessPanel, { diagnostics: executionDiagnostics, queue, onRaw }),
sessionPanel,
),
h("div", { className: "code-queue-layout" },