@@ -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-
402402const 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({
487503function 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