Skip to content

Commit a4e2ab7

Browse files
committed
Unify worker job visibility
Made-with: Cursor
1 parent 62778e3 commit a4e2ab7

16 files changed

Lines changed: 763 additions & 195 deletions

File tree

frontend/src/app/(app)/admin/page.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function QueueSlotsCard() {
133133
<Input
134134
value={queueFilter}
135135
onChange={(event) => setQueueFilter(event.target.value)}
136-
placeholder="Filter queue keys..."
136+
placeholder="Filter queue keys or job kinds..."
137137
className="h-8 text-xs"
138138
/>
139139
<Accordion type="multiple" className="space-y-2">
@@ -262,18 +262,20 @@ function QueueHealthCard() {
262262
});
263263
const [queueFilter, setQueueFilter] = useState("");
264264

265+
const queueStatusEntries =
266+
qsData?.queues && qsData.queues.length > 0
267+
? qsData.queues
268+
: (qsData?.trial_queues ?? []);
265269
const queueKeys = new Set<string>();
266270
slotsData?.queue_keys.forEach((p) => queueKeys.add(p.queue_key));
267-
qsData?.trial_queues?.forEach((q) => queueKeys.add(q.queue_key));
271+
queueStatusEntries.forEach((q) => queueKeys.add(q.queue_key));
268272

269-
const queueRows = Array.from(queueKeys).map((queueKey) => {
273+
const queueRows = queueStatusEntries.map((entry) => {
274+
const queueKey = entry.queue_key;
270275
const slotSummary =
271276
slotsData?.queue_keys.find((p) => p.queue_key === queueKey) ?? null;
272-
const trialEntry = qsData?.trial_queues?.find(
273-
(q) => q.queue_key === queueKey,
274-
);
275-
const queued = trialEntry?.queued ?? 0;
276-
const running = trialEntry?.running ?? 0;
277+
const queued = entry.queued ?? 0;
278+
const running = entry.running ?? 0;
277279
const totalSlots = slotSummary?.total_slots ?? 0;
278280
const activeSlots = slotSummary?.active_slots ?? 0;
279281
const staleLocks =
@@ -295,6 +297,7 @@ function QueueHealthCard() {
295297
}
296298

297299
return {
300+
kind: entry.kind ?? "TRIAL",
298301
queueKey,
299302
queued,
300303
running,
@@ -303,11 +306,39 @@ function QueueHealthCard() {
303306
notes,
304307
};
305308
});
309+
for (const queueKey of queueKeys) {
310+
if (queueRows.some((row) => row.queueKey === queueKey)) continue;
311+
const slotSummary =
312+
slotsData?.queue_keys.find((p) => p.queue_key === queueKey) ?? null;
313+
if (!slotSummary) continue;
314+
const staleLocks =
315+
slotSummary.slots.filter((slot) => slot.locked_by && !slot.is_active)
316+
.length ?? 0;
317+
queueRows.push({
318+
kind: "SLOT",
319+
queueKey,
320+
queued: 0,
321+
running: 0,
322+
totalSlots: slotSummary.total_slots,
323+
activeSlots: slotSummary.active_slots,
324+
notes:
325+
staleLocks > 0
326+
? [`${staleLocks} stale lock${staleLocks > 1 ? "s" : ""}`]
327+
: [],
328+
});
329+
}
306330
const filteredRows = queueRows
307331
.filter((row) =>
308-
row.queueKey.toLowerCase().includes(queueFilter.toLowerCase().trim()),
332+
`${row.kind} ${row.queueKey}`
333+
.toLowerCase()
334+
.includes(queueFilter.toLowerCase().trim()),
335+
)
336+
.sort(
337+
(a, b) =>
338+
b.queued + b.running - (a.queued + a.running) ||
339+
a.kind.localeCompare(b.kind) ||
340+
a.queueKey.localeCompare(b.queueKey),
309341
)
310-
.sort((a, b) => b.queued + b.running - (a.queued + a.running))
311342
.slice(0, 30);
312343

313344
const totalQueued = queueRows.reduce((sum, row) => sum + row.queued, 0);
@@ -376,6 +407,7 @@ function QueueHealthCard() {
376407
<Table>
377408
<TableHeader>
378409
<TableRow>
410+
<TableHead>Kind</TableHead>
379411
<TableHead>Queue Key</TableHead>
380412
<TableHead className="text-right">Queued</TableHead>
381413
<TableHead className="text-right">Running</TableHead>
@@ -385,7 +417,15 @@ function QueueHealthCard() {
385417
</TableHeader>
386418
<TableBody>
387419
{filteredRows.map((row) => (
388-
<TableRow key={row.queueKey}>
420+
<TableRow key={`${row.kind}-${row.queueKey}`}>
421+
<TableCell>
422+
<Badge
423+
variant="outline"
424+
className="font-mono text-[10px]"
425+
>
426+
{row.kind}
427+
</Badge>
428+
</TableCell>
389429
<TableCell>
390430
<span className="inline-flex items-center gap-2">
391431
<QueueKeyIcon queueKey={row.queueKey} size={13} />

frontend/src/app/(app)/dashboard/dashboard-client.tsx

Lines changed: 113 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
DashboardExperiment,
3737
DashboardExperimentAuthor,
3838
DashboardResponse,
39+
JobUsage,
3940
ModelUsage,
4041
QueueStats,
4142
} from "@/lib/types";
@@ -131,6 +132,7 @@ function useDashboardUsage(
131132
queues: data?.queues ?? null,
132133
pipeline: data?.pipeline ?? null,
133134
modelUsage: data?.model_usage ?? [],
135+
jobUsage: data?.job_usage ?? [],
134136
swrKey,
135137
cached: data?.cached ?? false,
136138
error,
@@ -397,10 +399,8 @@ type UsageRow = {
397399
hasUsageMetrics: boolean;
398400
};
399401

400-
type PipelineKind = "TRIAL" | "ANALYSIS" | "VERDICT";
401-
402402
const PIPELINE_KIND_DISPLAY: Record<
403-
PipelineKind,
403+
string,
404404
{
405405
label: string;
406406
description: string;
@@ -432,6 +432,22 @@ const PIPELINE_KIND_DISPLAY: Record<
432432
},
433433
};
434434

435+
function getPipelineKindDisplay(kind: string) {
436+
return (
437+
PIPELINE_KIND_DISPLAY[kind] ?? {
438+
label: kind
439+
.toLowerCase()
440+
.split("_")
441+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
442+
.join(" "),
443+
description: "Worker job",
444+
Icon: Beaker,
445+
accentText: "text-slate-500 dark:text-slate-300",
446+
accentBorder: "border-slate-500/30",
447+
}
448+
);
449+
}
450+
435451
// Compact tooltip surface used on the Status header badges to drill
436452
// down into "N running" → TRIAL / ANALYSIS / VERDICT splits. The
437453
// aggregate numbers would otherwise collapse all three kinds into a
@@ -442,19 +458,19 @@ function KindBreakdownTooltip({
442458
metric,
443459
}: {
444460
pipeline: Record<
445-
PipelineKind,
461+
string,
446462
{ running: number; queued: number; retrying: number }
447463
>;
448464
metric: "running" | "queued" | "retrying";
449465
}) {
450-
const kinds = Object.keys(PIPELINE_KIND_DISPLAY) as PipelineKind[];
466+
const kinds = Object.keys(pipeline).sort();
451467
const total = kinds.reduce((sum, k) => sum + pipeline[k][metric], 0);
452468

453469
return (
454470
<div className="space-y-1 py-1 text-left text-[11px]">
455471
<div className="font-medium capitalize">{metric}</div>
456472
{kinds.map((kind) => {
457-
const display = PIPELINE_KIND_DISPLAY[kind];
473+
const display = getPipelineKindDisplay(kind);
458474
const Icon = display.Icon;
459475
const value = pipeline[kind][metric];
460476
return (
@@ -487,6 +503,7 @@ function KindBreakdownTooltip({
487503
function UsageOverviewCard({
488504
queues,
489505
modelUsage,
506+
jobUsage,
490507
error,
491508
isLoading,
492509
isRefreshing,
@@ -495,6 +512,7 @@ function UsageOverviewCard({
495512
}: {
496513
queues: QueueStats | null;
497514
modelUsage: ModelUsage[];
515+
jobUsage: JobUsage[];
498516
error: Error | undefined;
499517
isLoading: boolean;
500518
isRefreshing: boolean;
@@ -528,28 +546,96 @@ function UsageOverviewCard({
528546

529547
const usageRows = useMemo(() => {
530548
const mergedRows = new Map<string, UsageRow>();
549+
const jobUsageByQueue = new Map<
550+
string,
551+
{
552+
jobCount: number;
553+
running: number;
554+
queued: number;
555+
retrying: number;
556+
durationTotalS: number;
557+
durationCount: number;
558+
avgDurationS: number | null;
559+
}
560+
>();
561+
562+
for (const job of jobUsage) {
563+
const existing = jobUsageByQueue.get(job.queue_key) ?? {
564+
jobCount: 0,
565+
running: 0,
566+
queued: 0,
567+
retrying: 0,
568+
durationTotalS: 0,
569+
durationCount: 0,
570+
avgDurationS: null,
571+
};
572+
existing.jobCount += job.job_count;
573+
existing.running += job.running;
574+
existing.queued += job.queued;
575+
existing.retrying += job.retrying;
576+
if (job.avg_duration_s != null && job.job_count > 0) {
577+
existing.durationTotalS += job.avg_duration_s * job.job_count;
578+
existing.durationCount += job.job_count;
579+
existing.avgDurationS =
580+
existing.durationCount > 0
581+
? existing.durationTotalS / existing.durationCount
582+
: null;
583+
}
584+
jobUsageByQueue.set(job.queue_key, existing);
585+
}
531586

532587
for (const usage of modelUsage) {
533588
const queueKey = usage.model || usage.provider || "unknown";
589+
const jobsForQueue = jobUsageByQueue.get(queueKey);
534590
const queueStats = queues?.[queueKey];
535591
mergedRows.set(queueKey, {
536592
key: queueKey,
537593
queueKey,
538594
model: usage.model,
539595
provider: usage.provider,
540-
jobCount: getQueueTotalJobs(queueStats) || usage.trial_count,
596+
jobCount:
597+
jobsForQueue?.jobCount ||
598+
getQueueTotalJobs(queueStats) ||
599+
usage.trial_count,
541600
inputTokens: usage.input_tokens,
542601
outputTokens: usage.output_tokens,
543602
cacheTokens: usage.cache_tokens,
544603
costUsd: usage.cost_usd,
545-
running: queueStats ? Number(queueStats.running) || 0 : usage.running,
546-
queued: queueStats ? getQueueQueuedJobs(queueStats) : usage.queued,
547-
retrying: queueStats ? Number(queueStats.retrying) || 0 : 0,
548-
avgDurationS: usage.avg_duration_s,
604+
running:
605+
jobsForQueue?.running ??
606+
(queueStats ? Number(queueStats.running) || 0 : usage.running),
607+
queued:
608+
jobsForQueue?.queued ??
609+
(queueStats ? getQueueQueuedJobs(queueStats) : usage.queued),
610+
retrying:
611+
jobsForQueue?.retrying ??
612+
(queueStats ? Number(queueStats.retrying) || 0 : 0),
613+
avgDurationS: jobsForQueue?.avgDurationS ?? usage.avg_duration_s,
549614
hasUsageMetrics: true,
550615
});
551616
}
552617

618+
for (const [queueKey, jobsForQueue] of jobUsageByQueue) {
619+
if (mergedRows.has(queueKey)) continue;
620+
621+
mergedRows.set(queueKey, {
622+
key: queueKey,
623+
queueKey,
624+
model: queueKey,
625+
provider: inferProviderFromQueueKey(queueKey),
626+
jobCount: jobsForQueue.jobCount,
627+
inputTokens: 0,
628+
outputTokens: 0,
629+
cacheTokens: 0,
630+
costUsd: 0,
631+
running: jobsForQueue.running,
632+
queued: jobsForQueue.queued,
633+
retrying: jobsForQueue.retrying,
634+
avgDurationS: jobsForQueue.avgDurationS,
635+
hasUsageMetrics: false,
636+
});
637+
}
638+
553639
for (const [queueKey, queueStats] of Object.entries(queues ?? {})) {
554640
const totalJobs = getQueueTotalJobs(queueStats);
555641
if (mergedRows.has(queueKey) || totalJobs === 0) continue;
@@ -573,7 +659,7 @@ function UsageOverviewCard({
573659
}
574660

575661
return Array.from(mergedRows.values());
576-
}, [modelUsage, queues]);
662+
}, [jobUsage, modelUsage, queues]);
577663

578664
const sortedUsageRows = useMemo(
579665
() =>
@@ -618,35 +704,21 @@ function UsageOverviewCard({
618704
[usageRows],
619705
);
620706

621-
// Pipeline aggregation: group the usage rows into the three
622-
// ``worker_jobs`` kinds so users see TRIAL / ANALYSIS / VERDICT as
623-
// independently-queued agent jobs instead of lumping everything
624-
// under one "Active Now" number. ANALYSIS and VERDICT have fixed
625-
// queue keys (``analysis`` / ``verdict``); everything else is a
626-
// trial execution queue.
707+
// Pipeline aggregation: group active counts by actual worker_jobs kind.
627708
const pipelineByKind = useMemo(() => {
628709
const kinds: Record<
629-
"TRIAL" | "ANALYSIS" | "VERDICT",
710+
string,
630711
{ running: number; queued: number; retrying: number }
631-
> = {
632-
TRIAL: { running: 0, queued: 0, retrying: 0 },
633-
ANALYSIS: { running: 0, queued: 0, retrying: 0 },
634-
VERDICT: { running: 0, queued: 0, retrying: 0 },
635-
};
636-
for (const row of usageRows) {
637-
const key = row.queueKey.toLowerCase();
638-
const kind: "TRIAL" | "ANALYSIS" | "VERDICT" =
639-
key === "analysis"
640-
? "ANALYSIS"
641-
: key === "verdict"
642-
? "VERDICT"
643-
: "TRIAL";
644-
kinds[kind].running += row.running;
645-
kinds[kind].queued += row.queued;
646-
kinds[kind].retrying += row.retrying;
712+
> = {};
713+
for (const job of jobUsage) {
714+
const kind = job.kind;
715+
kinds[kind] ??= { running: 0, queued: 0, retrying: 0 };
716+
kinds[kind].running += job.running;
717+
kinds[kind].queued += job.queued;
718+
kinds[kind].retrying += job.retrying;
647719
}
648720
return kinds;
649-
}, [usageRows]);
721+
}, [jobUsage]);
650722

651723
const selectedWindowValue = timeRange.startsWith("custom:")
652724
? "custom"
@@ -853,7 +925,7 @@ function UsageOverviewCard({
853925
<AlertTitle>Dashboard unavailable</AlertTitle>
854926
<AlertDescription>Failed to load usage data.</AlertDescription>
855927
</Alert>
856-
) : isLoading && modelUsage.length === 0 ? (
928+
) : isLoading && modelUsage.length === 0 && jobUsage.length === 0 ? (
857929
<div className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
858930
<Loader2 className="h-4 w-4 animate-spin" />
859931
Loading usage data...
@@ -1010,8 +1082,8 @@ function UsageOverviewCard({
10101082
</div>
10111083
) : (
10121084
<div className="py-6 text-center text-sm text-muted-foreground">
1013-
No job usage data yet. Trial, analysis, and verdict jobs will
1014-
appear here as they run.
1085+
No job usage data yet. Worker jobs will appear here as they
1086+
run.
10151087
</div>
10161088
)}
10171089

@@ -1419,6 +1491,7 @@ export function DashboardClient({
14191491
const {
14201492
queues,
14211493
modelUsage,
1494+
jobUsage,
14221495
error: usageError,
14231496
isLoading: usageIsLoading,
14241497
isRefreshing: usageIsRefreshing,
@@ -1470,6 +1543,7 @@ export function DashboardClient({
14701543
<UsageOverviewCard
14711544
queues={queues}
14721545
modelUsage={modelUsage}
1546+
jobUsage={jobUsage}
14731547
error={usageError}
14741548
isLoading={usageIsLoading}
14751549
isRefreshing={usageIsRefreshing}

0 commit comments

Comments
 (0)