fix code queue stats panel

This commit is contained in:
Codex
2026-05-20 05:54:09 +00:00
parent bd334bab4a
commit 47350d283c
5 changed files with 572 additions and 102 deletions
+238 -13
View File
@@ -155,6 +155,8 @@ const FRONTEND_CHECK_NAMES = [
"frontend:code-queue-trace-full-load",
"frontend:code-queue-judge-wrap",
"frontend:code-queue-error-red-markers",
"frontend:code-queue-stats-visible",
"frontend:code-queue-stats-degraded-visible",
"frontend:code-queue-retry-attempt-trace-current",
"frontend:code-queue-step-missing-diagnostic",
"frontend:code-queue-judge-feedback-attempt-order",
@@ -184,6 +186,10 @@ const CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES = [
"frontend:code-queue-step-missing-diagnostic",
"frontend:code-queue-judge-feedback-attempt-order",
] as const;
const CODE_QUEUE_STATS_FIXTURE_CHECK_NAMES = [
"frontend:code-queue-stats-visible",
"frontend:code-queue-stats-degraded-visible",
] as const;
const ALL_E2E_CHECK_NAMES = [
...NETWORK_CHECK_NAMES,
@@ -511,8 +517,9 @@ function wantsPrefix(options: E2ERunOptions, prefix: string): boolean {
function publicUrls(config: UniDeskConfig): PublicUrls {
const frontendTarget = String(process.env.UNIDESK_E2E_FRONTEND || "prod").trim().toLowerCase();
const frontendPort = frontendTarget === "dev" ? config.network.devFrontend.port : config.network.frontend.port;
const frontendUrlOverride = String(process.env.UNIDESK_E2E_FRONTEND_URL || "").trim();
return {
frontendUrl: `http://${config.network.publicHost}:${frontendPort}`,
frontendUrl: frontendUrlOverride || `http://${config.network.publicHost}:${frontendPort}`,
providerIngressHealthUrl: `http://${config.network.publicHost}:${config.network.providerIngress.port}/health`,
providerIngressWsUrl: `ws://${config.network.publicHost}:${config.network.providerIngress.port}/ws/provider`,
blockedCoreUrl: `http://${config.network.publicHost}:${config.network.core.port}`,
@@ -1274,6 +1281,187 @@ async function runCodeQueueRetryTraceFixture(page: Page, frontendUrl: string): P
}
}
function codeQueueStatsFixtureOverview(statistics: any, taskId: string): any {
const now = "2026-05-20T05:20:00.000Z";
const task = {
id: taskId,
queueId: "issue13-stats-fixture",
status: "succeeded",
providerId: "D601-dev",
executionMode: "default",
model: "gpt-5.5",
cwd: "/workspace-dev",
createdAt: "2026-05-20T04:00:00.000Z",
startedAt: "2026-05-20T04:01:00.000Z",
finishedAt: "2026-05-20T04:08:00.000Z",
updatedAt: now,
displayPromptPreview: "Issue 13 stats fixture task",
currentAttempt: 1,
maxAttempts: 99,
attemptCount: 1,
stepCount: 2,
outputCount: 2,
terminalUnread: false,
summaryOnly: true,
lastJudge: { decision: "complete", confidence: 1, reason: "fixture", source: "fixture" },
timing: { durationMs: 420000, totalElapsedMs: 480000 },
};
return {
ok: true,
queue: {
activeTaskId: "",
activeTaskIds: [],
total: 1,
unreadTerminal: 0,
counts: { succeeded: 1, queued: 0, running: 0, retry_wait: 0, judging: 0 },
queues: [{ id: "issue13-stats-fixture", name: "Issue 13 Stats Fixture", total: 1, counts: { succeeded: 1 } }],
mainProviderId: "D601-dev",
defaultProviderId: "D601-dev",
defaultWorkdir: "/workspace-dev",
defaultWorkdirByProvider: { "D601-dev": "/workspace-dev" },
codeModels: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"],
executionProviders: [{ id: "D601-dev", label: "D601 dev", defaultWorkdir: "/workspace-dev" }],
executionModes: [{ id: "default", label: "默认容器/本机" }],
},
statistics,
tasks: [task],
selected: null,
pagination: { returned: 1, total: 1, hasMore: false },
};
}
async function runCodeQueueStatsFixture(page: Page, frontendUrl: string, mode: "visible" | "degraded"): Promise<any> {
const taskId = `codex_issue13_stats_${mode}`;
const visibleStats = {
generatedAt: "2026-05-20T05:20:00.000Z",
timezone: "Asia/Shanghai",
source: "fixture-dailyBuckets",
range: { startDate: "2026-05-18", endDate: "2026-05-20" },
totals: { executedTasks: 9, completedTasks: 7, retryAttempts: 3, avgDurationMs: 420000, durationSamples: 7 },
dailyBuckets: [
{ date: "2026-05-18", tasks: 2, terminalCount: 2, retries: 1, averageDurationMs: 300000, samples: 2 },
{ date: "2026-05-19", tasks: 3, terminalCount: 2, retries: 0, averageDurationMs: 360000, samples: 2 },
{ date: "2026-05-20", tasks: 4, terminalCount: 3, retries: 2, averageDurationMs: 540000, samples: 3 },
],
};
const degradedStats = {
generatedAt: "2026-05-20T05:20:00.000Z",
timezone: "Asia/Shanghai",
source: "fixture-stats-api",
degraded: true,
unavailable: true,
reason: "fixture stats API timeout: database projection unavailable",
range: { startDate: "2026-05-18", endDate: "2026-05-20" },
totals: { executedTasks: 0, completedTasks: 0, retryAttempts: 0, avgDurationMs: null, durationSamples: 0 },
daily: [],
};
const statistics = mode === "visible" ? visibleStats : degradedStats;
const overview = codeQueueStatsFixtureOverview(statistics, taskId);
const routePattern = "**/api/microservices/code-queue/proxy/api/**";
const requests: string[] = [];
const handler = async (route: any, request: any): Promise<void> => {
if (request.method() !== "GET") {
await route.continue();
return;
}
const url = new URL(request.url());
const path = url.pathname.replace(/^\/api\/microservices\/code-queue\/proxy/u, "");
requests.push(`${path}${url.search}`);
if (path === "/api/tasks/overview") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(overview) });
return;
}
if (path === "/api/tasks/stats") {
if (mode === "degraded") {
await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ ok: false, error: degradedStats.reason, statistics: degradedStats }) });
} else {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, statistics, queue: overview.queue }) });
}
return;
}
if (path === "/api/workdirs") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, workdirs: [{ providerId: "D601-dev", executionMode: "default", path: "/workspace-dev", source: "fixture" }] }) });
return;
}
if (path === "/api/queues") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, queues: overview.queue.queues, summary: overview.queue }) });
return;
}
if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-summary` || path === `/api/tasks/${taskId}/trace-summary`) {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, summary: { ...overview.tasks[0], finalResponse: "Fixture complete", attempts: [], prompt: {}, execution: { durationMs: 420000, traceLineCount: 2, stepCount: 2 } } }) });
return;
}
await route.continue();
};
await page.route(routePattern, handler);
try {
await page.goto(`${frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 });
await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000, state: "attached" });
await page.waitForSelector('[data-testid="codex-stats-chart"]', { timeout: 15000 });
if (mode === "visible") {
await page.waitForSelector('[data-testid="codex-stats-day-2026-05-20"]', { timeout: 15000 });
} else {
await page.waitForSelector('[data-testid="codex-stats-diagnostic"]', { timeout: 15000 });
}
const metrics = await page.evaluate(() => {
const hero = document.querySelector('[data-testid="codex-stats-panel"]') as HTMLElement | null;
const chart = document.querySelector('[data-testid="codex-stats-chart"]') as HTMLElement | null;
const list = document.querySelector('[data-testid="codex-stats-daily-list"]') as HTMLElement | null;
const diagnostic = document.querySelector('[data-testid="codex-stats-diagnostic"]') as HTMLElement | null;
const statsPanel = document.querySelector(".codex-stats-panel") as HTMLElement | null;
const svg = chart?.querySelector("svg") as SVGElement | null;
const chartRect = chart?.getBoundingClientRect();
return {
heroText: hero?.innerText || hero?.textContent || "",
state: hero?.getAttribute("data-stats-state") || "",
degraded: hero?.getAttribute("data-stats-degraded") || "",
chartVisible: Boolean(chart),
chartEmpty: chart?.getAttribute("data-empty") || "",
chartHeight: Math.round(chartRect?.height || 0),
polylineCount: svg?.querySelectorAll("polyline.stat-line").length ?? 0,
pointCount: svg?.querySelectorAll(".stat-point-group").length ?? 0,
dailyRows: list?.querySelectorAll('[data-testid^="codex-stats-day-"]:not([data-testid="codex-stats-day-empty"])').length ?? 0,
dailyText: list?.textContent || "",
diagnosticText: diagnostic?.textContent || "",
emptyText: statsPanel?.querySelector(".empty-state")?.textContent || "",
summaryText: statsPanel?.textContent || "",
};
});
return {
checked: true,
mode,
taskId,
fixture: {
statsKeys: Object.keys(statistics),
bucketKey: mode === "visible" ? "dailyBuckets" : "daily",
totals: statistics.totals,
},
requests,
...metrics,
ok: mode === "visible"
? metrics.state === "ready"
&& metrics.chartVisible === true
&& metrics.chartEmpty === "false"
&& metrics.dailyRows >= 3
&& metrics.pointCount >= 9
&& metrics.heroText.includes("9 tasks / 3 retries")
&& metrics.dailyText.includes("4 exec")
&& !metrics.emptyText.includes("暂无统计")
: metrics.state === "unavailable"
&& metrics.degraded === "true"
&& metrics.chartVisible === true
&& metrics.chartHeight >= 180
&& metrics.diagnosticText.includes("fixture stats API timeout")
&& metrics.summaryText.includes("统计不可用")
&& metrics.chartEmpty === "true"
&& metrics.heroText.includes("0 tasks / 0 retries"),
};
} finally {
await page.unroute(routePattern, handler).catch(() => undefined);
}
}
function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } {
const result = runCommand([
"docker",
@@ -2181,6 +2369,8 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
"frontend:code-queue-trace-full-load",
"frontend:code-queue-judge-wrap",
"frontend:code-queue-error-red-markers",
"frontend:code-queue-stats-visible",
"frontend:code-queue-stats-degraded-visible",
"frontend:code-queue-retry-attempt-trace-current",
"frontend:code-queue-step-missing-diagnostic",
"frontend:code-queue-judge-feedback-attempt-order",
@@ -2193,6 +2383,8 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
"frontend:code-queue-trace-full-load",
"frontend:code-queue-judge-wrap",
"frontend:code-queue-error-red-markers",
"frontend:code-queue-stats-visible",
"frontend:code-queue-stats-degraded-visible",
]);
const needCodeQueueRetryTraceFixture = wantsAny([
"frontend:code-queue-retry-attempt-trace-current",
@@ -2306,9 +2498,11 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
let codeQueueScrollbarMetrics: any = { transcriptThin: false, toolHorizontalHidden: true };
let codexInitialPromptFullMetrics: any = { candidateFound: false };
let codexTraceFullMetrics: any = { candidateFound: false };
let codexJudgeWrapMetrics: any = { checked: false };
let codeQueueErrorHighlightMetrics: any = { checked: false, candidateFound: false };
let codeQueueRetryTraceFixtureMetrics: any = { checked: false };
let codexJudgeWrapMetrics: any = { checked: false };
let codeQueueErrorHighlightMetrics: any = { checked: false, candidateFound: false };
let codeQueueStatsVisibleMetrics: any = { checked: false };
let codeQueueStatsDegradedMetrics: any = { checked: false };
let codeQueueRetryTraceFixtureMetrics: any = { checked: false };
let claudeqqText = "";
let routeDeepLinkText = "";
let routeInitialPath = "";
@@ -3223,8 +3417,14 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
};
}
}
if (wants("frontend:code-queue-stats-visible")) {
codeQueueStatsVisibleMetrics = await runCodeQueueStatsFixture(page, urls.frontendUrl, "visible");
}
if (wants("frontend:code-queue-stats-degraded-visible")) {
codeQueueStatsDegradedMetrics = await runCodeQueueStatsFixture(page, urls.frontendUrl, "degraded");
}
codeQueueOutputText = needCodeQueueFullSurface ? await page.locator('[data-testid="codex-output"]').innerText({ timeout: 5000 }) : "";
codeQueueText = await page.locator('[data-testid="code-queue-page"]').innerText({ timeout: 5000 });
codeQueueText = await page.locator('[data-testid="code-queue-page"]').innerText({ timeout: 5000 });
codeQueueHtmlGuard = await page.evaluate(async () => {
const root = document.getElementById("root");
const overviewAttr = root?.getAttribute("data-codex-overview") || "";
@@ -3969,10 +4169,18 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
&& codexTraceFullMetrics.loadPartial !== "true"
),
{ codexTraceFullMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap",
codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true,
{ codexJudgeWrapMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current",
addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap",
codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true,
{ codexJudgeWrapMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-stats-visible",
codeQueueStatsVisibleMetrics.checked === true
&& codeQueueStatsVisibleMetrics.ok === true,
{ codeQueueStatsVisibleMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-stats-degraded-visible",
codeQueueStatsDegradedMetrics.checked === true
&& codeQueueStatsDegradedMetrics.ok === true,
{ codeQueueStatsDegradedMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current",
codeQueueRetryTraceFixtureMetrics.checked === true
&& codeQueueRetryTraceFixtureMetrics.ok === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardAttemptVisible === true
@@ -4132,8 +4340,15 @@ export async function runE2E(
const onlyRetryTraceFixture = options.only.length > 0
&& selectedChecks.length > 0
&& selectedChecks.every((name) => (CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES as readonly string[]).includes(name));
const needFrontend = wantsPrefix(options, "frontend") && !onlyRetryTraceFixture;
const onlyCodeQueueBrowserFixtures = options.only.length > 0
&& selectedChecks.length > 0
&& selectedChecks.every((name) => [
...(CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES as readonly string[]),
...(CODE_QUEUE_STATS_FIXTURE_CHECK_NAMES as readonly string[]),
].includes(name));
const needFrontend = wantsPrefix(options, "frontend") && !onlyRetryTraceFixture && !onlyCodeQueueBrowserFixtures;
const needRetryTraceFixture = wantsAnyCheck(options, [...CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES]);
const needStatsFixture = wantsAnyCheck(options, [...CODE_QUEUE_STATS_FIXTURE_CHECK_NAMES]);
const needCodeQueueFixtures = wantsAnyCheck(options, [...CODE_QUEUE_FIXTURE_CHECK_NAMES]);
const executedSections: string[] = [];
@@ -4161,8 +4376,8 @@ export async function runE2E(
if (needFrontend) {
executedSections.push("frontend");
frontend = await frontendCheck(config, urls, checks, options);
} else if (needRetryTraceFixture) {
executedSections.push("frontend-retry-trace-fixture");
} else if (needRetryTraceFixture || needStatsFixture) {
executedSections.push("frontend-code-queue-browser-fixtures");
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 920 } });
@@ -4176,7 +4391,17 @@ export async function runE2E(
await page.fill('input[name="password"]', auth.password);
await page.click('button[type="submit"]');
await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 });
const codeQueueRetryTraceFixtureMetrics = await runCodeQueueRetryTraceFixture(page, urls.frontendUrl);
const codeQueueRetryTraceFixtureMetrics = needRetryTraceFixture ? await runCodeQueueRetryTraceFixture(page, urls.frontendUrl) : { checked: false };
const codeQueueStatsVisibleMetrics = wantsCheck(options, "frontend:code-queue-stats-visible") ? await runCodeQueueStatsFixture(page, urls.frontendUrl, "visible") : { checked: false };
const codeQueueStatsDegradedMetrics = wantsCheck(options, "frontend:code-queue-stats-degraded-visible") ? await runCodeQueueStatsFixture(page, urls.frontendUrl, "degraded") : { checked: false };
addSelectedCheck(checks, options, "frontend:code-queue-stats-visible",
codeQueueStatsVisibleMetrics.checked === true
&& codeQueueStatsVisibleMetrics.ok === true,
{ codeQueueStatsVisibleMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-stats-degraded-visible",
codeQueueStatsDegradedMetrics.checked === true
&& codeQueueStatsDegradedMetrics.ok === true,
{ codeQueueStatsDegradedMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current",
codeQueueRetryTraceFixtureMetrics.checked === true
&& codeQueueRetryTraceFixtureMetrics.ok === true
File diff suppressed because one or more lines are too long
+56
View File
@@ -2365,6 +2365,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
display: grid;
gap: 7px;
min-width: 0;
min-height: 260px;
padding: 10px;
border: 1px solid var(--line-soft);
background:
@@ -2378,6 +2379,49 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
height: clamp(180px, 24vw, 260px);
overflow: visible;
}
.codex-stats-chart.no-data svg {
opacity: 0.42;
}
.codex-stats-chart-empty {
position: absolute;
inset: 10px;
display: grid;
place-content: center;
gap: 5px;
padding: 14px;
color: var(--muted);
text-align: center;
pointer-events: none;
}
.codex-stats-chart-empty strong {
color: var(--text);
font-size: 14px;
}
.codex-stats-chart-empty span {
max-width: 360px;
font-size: 12px;
line-height: 1.45;
}
.codex-stats-diagnostic {
display: grid;
gap: 4px;
min-width: 0;
padding: 8px 10px;
border: 1px solid rgba(215, 161, 58, 0.36);
background: linear-gradient(135deg, rgba(215, 161, 58, 0.10), rgba(207, 106, 84, 0.06));
color: var(--muted);
font-size: 12px;
}
.codex-stats-diagnostic b {
color: var(--accent);
}
.codex-stats-diagnostic code {
width: max-content;
max-width: 100%;
overflow: hidden;
color: var(--accent-2);
text-overflow: ellipsis;
}
.codex-stats-chart .axis,
.codex-stats-chart .grid {
stroke: rgba(255,255,255,0.14);
@@ -2582,6 +2626,18 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
border-color: rgba(215, 161, 58, 0.34);
background: rgba(215, 161, 58, 0.07);
}
.codex-stats-daily-row.empty {
grid-template-columns: minmax(90px, auto) minmax(0, 1fr) auto auto;
}
.codex-stats-daily-row.empty b {
color: var(--muted);
font-weight: 500;
}
.codex-stats-daily-row.empty.unavailable,
.codex-stats-daily-row.empty.degraded {
border-color: rgba(215, 161, 58, 0.28);
background: rgba(215, 161, 58, 0.05);
}
.codex-task-move-control {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
+206 -17
View File
@@ -389,6 +389,13 @@ async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq =
), codexNoCacheOptions());
}
async function loadTaskStatistics(apiBaseUrl: string, queueId = allQueuesId): Promise<any> {
return requestJson(codexApi(
apiBaseUrl,
`/api/tasks/stats?days=14${queueQuerySuffix(queueId)}`,
), codexNoCacheOptions());
}
async function loadTaskPage(apiBaseUrl: string, queueId: string, beforeId: string, limit = codexMoreTaskLimit, searchQuery = ""): Promise<any> {
return requestJson(codexApi(
apiBaseUrl,
@@ -1304,12 +1311,150 @@ function taskStatistics(data: any, fallbackQueue: any): AnyRecord {
return objectRecord(data?.statistics) || objectRecord(fallbackQueue?.statistics) || {};
}
function taskStatisticsRows(stats: any): any[] {
return Array.isArray(stats?.daily) ? stats.daily : [];
function firstDefined(...values: any[]): any {
return values.find((value) => value !== undefined && value !== null);
}
function taskStatisticsTotals(stats: any): AnyRecord {
return objectRecord(stats?.totals) || {};
function statsNumber(value: any): number | null {
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? number : null;
}
function statsCountValue(...values: any[]): number {
for (const value of values) {
const number = statsNumber(value);
if (number !== null) return Math.floor(number);
}
return 0;
}
function statsDurationValue(...values: any[]): number | null {
for (const value of values) {
const number = statsNumber(value);
if (number !== null) return Math.round(number);
}
return null;
}
function taskStatisticsRawRows(stats: any): any[] {
for (const value of [
stats?.daily,
stats?.dailyBuckets,
stats?.daily_buckets,
stats?.buckets,
stats?.rows,
stats?.series,
]) {
if (Array.isArray(value)) return value;
}
return [];
}
function taskStatisticsRows(stats: any): any[] {
return taskStatisticsRawRows(stats)
.map((row: any, index: number) => {
const record = objectRecord(row) || {};
const date = String(firstDefined(record.date, record.day, record.bucketDate, record.bucket, `day-${index}`) || "");
return {
...record,
date,
executedTasks: statsCountValue(record.executedTasks, record.tasks, record.taskCount, record.startedTasks, record.started, record.executed, record.count),
completedTasks: statsCountValue(record.completedTasks, record.terminalTasks, record.terminalCount, record.finishedTasks, record.finished, record.completed),
retryAttempts: statsCountValue(record.retryAttempts, record.retries, record.retryCount, record.retryTasks),
succeededTasks: statsCountValue(record.succeededTasks, record.succeeded, record.success),
failedTasks: statsCountValue(record.failedTasks, record.failed, record.failures),
canceledTasks: statsCountValue(record.canceledTasks, record.cancelledTasks, record.canceled, record.cancelled),
totalDurationMs: statsCountValue(record.totalDurationMs, record.durationTotalMs, record.totalCompletionDurationMs),
durationSamples: statsCountValue(record.durationSamples, record.avgDurationSamples, record.samples, record.sampleCount),
avgDurationMs: statsDurationValue(record.avgDurationMs, record.averageDurationMs, record.avgCompletedDurationMs, record.averageCompletionMs, record.averageDuration, record.durationMs),
};
})
.filter((row: any) => row.date.length > 0);
}
function sumStatRows(rows: any[], key: string): number {
return rows.reduce((sum, row) => sum + countNumber(row?.[key]), 0);
}
function weightedAverageDuration(rows: any[]): number | null {
let totalDuration = 0;
let samples = 0;
for (const row of rows) {
const rowSamples = countNumber(row?.durationSamples);
const average = statsDurationValue(row?.avgDurationMs);
if (rowSamples > 0 && average !== null) {
totalDuration += average * rowSamples;
samples += rowSamples;
}
}
return samples > 0 ? Math.round(totalDuration / samples) : null;
}
function taskStatisticsTotals(stats: any, rows: any[] = taskStatisticsRows(stats)): AnyRecord {
const totals = objectRecord(stats?.totals) || objectRecord(stats?.summary) || {};
const durationSamples = statsCountValue(totals.durationSamples, totals.avgDurationSamples, totals.samples, stats?.durationSamples, stats?.samples, sumStatRows(rows, "durationSamples"));
const totalDurationMs = statsCountValue(totals.totalDurationMs, totals.durationTotalMs, stats?.totalDurationMs, sumStatRows(rows, "totalDurationMs"));
const avgDurationMs = statsDurationValue(
totals.avgDurationMs,
totals.averageDurationMs,
totals.avgCompletedDurationMs,
stats?.avgDurationMs,
stats?.averageDurationMs,
durationSamples > 0 && totalDurationMs > 0 ? totalDurationMs / durationSamples : null,
weightedAverageDuration(rows),
);
return {
...totals,
executedTasks: statsCountValue(totals.executedTasks, totals.tasks, totals.taskCount, stats?.executedTasks, stats?.tasks, stats?.taskCount, sumStatRows(rows, "executedTasks")),
completedTasks: statsCountValue(totals.completedTasks, totals.terminalTasks, totals.terminalCount, totals.finishedTasks, stats?.completedTasks, stats?.terminalTasks, stats?.terminalCount, sumStatRows(rows, "completedTasks")),
retryAttempts: statsCountValue(totals.retryAttempts, totals.retries, totals.retryCount, stats?.retryAttempts, stats?.retries, stats?.retryCount, sumStatRows(rows, "retryAttempts")),
succeededTasks: statsCountValue(totals.succeededTasks, stats?.succeededTasks, sumStatRows(rows, "succeededTasks")),
failedTasks: statsCountValue(totals.failedTasks, stats?.failedTasks, sumStatRows(rows, "failedTasks")),
canceledTasks: statsCountValue(totals.canceledTasks, totals.cancelledTasks, stats?.canceledTasks, stats?.cancelledTasks, sumStatRows(rows, "canceledTasks")),
durationSamples,
totalDurationMs,
avgDurationMs,
};
}
function taskStatisticsReason(stats: any): string {
const raw = firstDefined(stats?.reason, stats?.degradedReason, stats?.statsReason, stats?.error, stats?.message, stats?.detail);
if (typeof raw === "string" && raw.trim().length > 0) return shortText(raw, 180);
if (stats?.skipped === true) return "统计 API 返回 skipped,当前后端未提供 daily buckets。";
if (stats?.unavailable === true) return "统计 API 当前不可用。";
return "统计 API 未返回可用的 daily buckets。";
}
function taskStatisticsState(stats: any, rows: any[], totals: AnyRecord): AnyRecord {
const hasStatsObject = objectRecord(stats) !== null;
const explicitUnavailable = stats?.unavailable === true || stats?._unavailable === true || stats?.available === false;
const skipped = stats?.skipped === true;
const degraded = stats?.degraded === true || stats?.isDegraded === true || explicitUnavailable || skipped;
const totalActivity = countNumber(totals.executedTasks) + countNumber(totals.retryAttempts) + countNumber(totals.completedTasks);
if (!hasStatsObject) {
return { state: "loading", label: "统计同步中", reason: "正在读取 Code Queue stats API。" };
}
if (explicitUnavailable || skipped || (degraded && rows.length === 0)) {
return { state: "unavailable", label: "统计不可用", reason: taskStatisticsReason(stats), degraded: true };
}
if (degraded) {
return { state: "degraded", label: "统计降级", reason: taskStatisticsReason(stats), degraded: true };
}
if (rows.length === 0 && totalActivity === 0) {
return { state: "empty", label: "暂无统计", reason: "任务开始执行后会生成按天汇总的曲线。" };
}
return { state: "ready", label: "统计就绪", reason: "" };
}
function taskStatisticsUnavailable(error: unknown): AnyRecord {
return {
source: "frontend-stats-fetch",
unavailable: true,
degraded: true,
reason: errorText(error, "统计 API 不可用"),
daily: [],
totals: null,
};
}
function statMetric(row: any, key: string): number {
@@ -1779,13 +1924,19 @@ function CodeQueueLivenessPanel({ diagnostics, queue, onRaw }: AnyRecord) {
function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord) {
const rows = taskStatisticsRows(stats);
const totals = taskStatisticsTotals(stats);
const totals = taskStatisticsTotals(stats, rows);
const latest = rows.at(-1) || {};
const executedMax = statMetricMax(rows, "executedTasks");
const retryMax = statMetricMax(rows, "retryAttempts");
const durationMax = statMetricMax(rows, "avgDurationMs");
const hasRows = rows.length > 0;
const statsRange = objectRecord(stats?.range) || {};
const statsState = taskStatisticsState(stats, rows, totals);
const statsStateLabel = String(statsState.label || "");
const statsStateReason = String(statsState.reason || "");
const chartDescription = statsState.state === "ready" || statsState.state === "degraded"
? "将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"
: statsStateReason || "等待统计数据。";
const [hoveredStatPoint, setHoveredStatPoint] = useState(null);
const [pinnedStatPoint, setPinnedStatPoint] = useState(null);
const maxLabelParts: string[] = [];
@@ -1846,18 +1997,23 @@ function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord
return h(Panel, {
title: "统计曲线",
eyebrow: `Daily task stats / ${activeQueueName}`,
className: "codex-stats-panel",
summary: h("span", null, `${statDateLabel(statsRange.startDate)} -> ${statDateLabel(statsRange.endDate)} · ${stats?.timezone || "Asia/Shanghai"}`),
className: `codex-stats-panel ${statsState.state}`,
summary: h("span", null, `${statDateLabel(statsRange.startDate)} -> ${statDateLabel(statsRange.endDate)} · ${stats?.timezone || "Asia/Shanghai"} · ${statsStateLabel}`),
actions: objectRecord(stats) ? h(RawButton, { title: "Code Queue Stats", data: stats, onOpen: onRaw, testId: "raw-codex-stats" }) : null,
},
h("div", { className: "codex-stats-hero", "data-testid": "codex-stats-panel" },
h("div", { className: "codex-stats-hero", "data-testid": "codex-stats-panel", "data-stats-state": statsState.state, "data-stats-degraded": statsState.degraded === true ? "true" : "false" },
h(CodexStatsIcon),
h("div", null,
h("strong", null, `${countNumber(totals.executedTasks)} tasks / ${countNumber(totals.retryAttempts)} retries`),
h("span", null, `平均完成耗时 ${fmtDuration(totals.avgDurationMs ?? undefined)} · 终态 ${countNumber(totals.completedTasks)}`),
),
),
hasRows ? h("div", { className: "codex-stats-chart", "data-testid": "codex-stats-chart", onMouseLeave: () => setHoveredStatPoint(null) },
statsState.degraded === true ? h("div", { className: `codex-stats-diagnostic ${statsState.state}`, "data-testid": "codex-stats-diagnostic" },
h("b", null, statsStateLabel),
h("span", null, statsStateReason),
stats?.source ? h("code", null, `source=${String(stats.source)}`) : null,
) : null,
h("div", { className: `codex-stats-chart ${hasRows ? "has-data" : "no-data"}`, "data-testid": "codex-stats-chart", "data-empty": hasRows ? "false" : "true", onMouseLeave: () => setHoveredStatPoint(null) },
h("svg", { viewBox: `0 0 ${statChartWidth} ${statChartHeight}`, preserveAspectRatio: "none", role: "img", "aria-label": "Code Queue daily task statistics" },
h("line", { className: "axis", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartBaselineY, y2: statChartBaselineY }),
h("line", { className: "grid", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartTopY + statChartValueHeight / 2, y2: statChartTopY + statChartValueHeight / 2 }),
@@ -1874,7 +2030,11 @@ function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord
activeStatPoint ? h("div", { className: "codex-stats-tooltip active", style: activeTooltipStyle, "data-testid": "codex-stats-tooltip" },
h("b", null, statDateLabel(activeStatPoint.row?.date)),
h("span", null, `${activeStatPoint.label} · ${activeStatPoint.valueLabel}`),
h("code", null, `${countNumber(activeStatPoint.row?.executedTasks)} exec / ${countNumber(activeStatPoint.row?.retryAttempts)} retry / ${fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)}`),
h("code", null, `${countNumber(activeStatPoint.row?.executedTasks)} exec / ${countNumber(activeStatPoint.row?.retryAttempts)} retry / ${fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)}`),
) : null,
!hasRows ? h("div", { className: "codex-stats-chart-empty", "data-testid": "codex-stats-empty" },
h("strong", null, statsStateLabel),
h("span", null, chartDescription),
) : null,
h("div", { className: "codex-stats-legend" },
h("span", { className: "tasks" }, "执行任务"),
@@ -1897,16 +2057,21 @@ function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord
h("code", null, `${countNumber(activeStatPoint.row?.retryAttempts)} retry`),
h("code", null, fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)),
),
) : h("span", null, "将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"),
) : h("span", null, chartDescription),
),
) : h(EmptyState, { title: "暂无统计", text: "任务开始执行后会生成按天汇总的曲线。" }),
),
h("div", { className: "codex-stats-summary-grid" },
h("article", null, h("span", null, "今日执行"), h("strong", null, String(countNumber(latest.executedTasks))), h("code", null, statDateLabel(latest.date))),
h("article", null, h("span", null, "今日重试"), h("strong", null, String(countNumber(latest.retryAttempts))), h("code", null, `累计 ${countNumber(totals.retryAttempts)}`)),
h("article", null, h("span", null, "平均耗时"), h("strong", null, fmtDuration(totals.avgDurationMs ?? undefined)), h("code", null, `${countNumber(totals.durationSamples)} samples`)),
),
h("div", { className: "codex-stats-daily-list", "data-testid": "codex-stats-daily-list" },
rows.slice(-7).map((row: any) => h("div", { key: String(row?.date || ""), className: `codex-stats-daily-row ${activeDate === String(row?.date || "") ? "active" : ""}`, "data-testid": `codex-stats-day-${String(row?.date || "unknown")}` },
rows.length === 0 ? h("div", { className: `codex-stats-daily-row empty ${statsState.state}`, "data-testid": "codex-stats-day-empty" },
h("span", null, statsStateLabel),
h("b", null, statsStateReason),
h("b", null, "--"),
h("code", null, statsState.state === "empty" ? "true empty" : "diagnostic"),
) : rows.slice(-7).map((row: any) => h("div", { key: String(row?.date || ""), className: `codex-stats-daily-row ${activeDate === String(row?.date || "") ? "active" : ""}`, "data-testid": `codex-stats-day-${String(row?.date || "unknown")}` },
h("span", null, statDateLabel(row?.date)),
h("b", null, `${countNumber(row?.executedTasks)} exec`),
h("b", null, `${countNumber(row?.retryAttempts)} retry`),
@@ -2317,6 +2482,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
const tasksDataRef = useRef(initialTasksData);
const [health, setHealth] = useState(null);
const [tasksData, setTasksDataState] = useState(initialTasksData);
const [statisticsData, setStatisticsData] = useState(objectRecord(initialTasksData?.statistics) as AnyRecord | null);
const [selectedId, setSelectedId] = useState(initialSelectedId);
const [selectedTask, setSelectedTask] = useState(initialSelectedTask);
const [selectedDetailLoading, setSelectedDetailLoading] = useState(false);
@@ -2372,7 +2538,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
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 statistics = statisticsData || taskStatistics(tasksData, queue);
const pagination = taskPagination(tasksData);
const queueRows = knownQueueRows(queue, queueId);
const mergeTargetQueueId = String(queueId || "default").trim() || "default";
@@ -2408,7 +2574,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
const promptParts = useMemo(() => splitPromptTasks(prompt), [prompt]);
const enqueueItems = useMemo(() => {
const count = repeatCountValue(repeatCount);
return promptParts.flatMap((text) => Array.from({ length: count }, () => withReferenceHint(text, referenceTaskId)));
return promptParts.flatMap((text: string) => Array.from({ length: count }, () => withReferenceHint(text, referenceTaskId)));
}, [promptParts, repeatCount, referenceTaskId]);
const enqueueCount = enqueueItems.length;
const batchNeedsConfirmation = enqueueCount > 1 && !batchConfirmed;
@@ -2428,10 +2594,26 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
function setTasksData(nextOrUpdater: any): any {
const next = typeof nextOrUpdater === "function" ? nextOrUpdater(tasksDataRef.current) : nextOrUpdater;
tasksDataRef.current = next;
const nextStats = objectRecord(next?.statistics);
if (nextStats) setStatisticsData(nextStats);
setTasksDataState(next);
return next;
}
async function refreshStatistics(queueFilterId = selectedQueueId): Promise<void> {
if (!service) return;
try {
const result = await loadTaskStatistics(apiBaseUrl, queueFilterId);
const nextStats = objectRecord(result?.statistics) || objectRecord(result?.stats) || objectRecord(result);
if (nextStats) setStatisticsData(nextStats);
if (result?.queue) {
setTasksData((previous: any) => previous ? { ...previous, queue: result.queue } : { ok: true, queue: result.queue, tasks: [], pagination: { returned: 0, total: 0, hasMore: false } });
}
} catch (err) {
setStatisticsData(taskStatisticsUnavailable(err));
}
}
function rememberLocalReadState(taskIds: string[], readAt: string, hideFromSidebar = true): string[] {
const ids = Array.from(new Set(taskIds.map((id) => String(id || "")).filter(Boolean)));
for (const id of ids) {
@@ -3183,6 +3365,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
}
setRefreshedAt(new Date());
if (trackLoad) trackedLoadInFlightRef.current = false;
void refreshStatistics(queueFilterId).catch((err) => setStatisticsData(taskStatisticsUnavailable(err)));
}
async function loadMoreTasks(): Promise<void> {
@@ -3345,7 +3528,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
.filter(taskIsUnreadTerminal)
.map((task: any) => String(task?.id || ""))
.filter(Boolean),
...Array.from(sessionCacheRef.current.entries())
...(Array.from(sessionCacheRef.current.entries()) as [string, AnyRecord][])
.filter(([, value]) => taskIsUnreadTerminal(value?.task))
.map(([id]) => id),
]));
@@ -3359,7 +3542,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
.filter(taskIsUnreadTerminal)
.map((task: any) => String(task?.id || ""))
.filter(Boolean);
const cachedUnreadIds = Array.from(sessionCacheRef.current.entries())
const cachedUnreadIds = (Array.from(sessionCacheRef.current.entries()) as [string, AnyRecord][])
.filter(([, value]) => taskIsUnreadTerminal(value?.task))
.map(([id]) => id);
const readIds = Array.from(new Set([...optimisticReadIds, ...loadedUnreadIds, ...cachedUnreadIds]));
@@ -3900,6 +4083,12 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
return undefined;
}, [service?.id]);
useEffect(() => {
if (!service) return undefined;
void refreshStatistics(selectedQueueId);
return undefined;
}, [service?.id, apiBaseUrl, selectedQueueId]);
const taskListContent = sidebarTasks.length === 0 ? h(EmptyState, {
title: searchActive ? (searchLoading ? "搜索中" : "没有匹配任务") : "队列为空",
text: searchActive ? (searchLoading ? `正在搜索包含“${normalizedSearchQuery}”的 task...` : `未找到包含“${normalizedSearchQuery}”的 task;可换个关键词或切换 queue。`) : "提交一个任务后,Codex 会串行执行并保存输出。",
@@ -4,7 +4,7 @@ FROM ${CODE_QUEUE_BASE_IMAGE}
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV UNIDESK_SKILLS_PATH=/root/.agents/skills
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1) \
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1 && test -x "$PLAYWRIGHT_BROWSERS_PATH/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell") \
|| (apt-get update \
&& apt-get install -y --no-install-recommends \
bash \