@@ -14,6 +14,7 @@ import {
1414} from "@/components/ui" ;
1515import { Spark , Donut } from "@/components/charts" ;
1616import { runModelLabel } from "@/lib/run-model" ;
17+ import { formatWaited } from "@/lib/waited" ;
1718import { spanColor } from "@/lib/theme" ;
1819import { useCockpit } from "@/components/cockpit/context" ;
1920import { WindowSelector } from "@/components/cockpit/controls" ;
@@ -29,15 +30,17 @@ import type {
2930 KpisResponse ,
3031 EvalHealthResponse ,
3132 LiveRunsResponse ,
33+ DispatchCapacityResponse ,
3234 RunsResponse ,
3335 WorkflowsResponse ,
3436} from "@shared/contracts" ;
3537
36- /** Bundle of the five server-fetched responses passed into the presentational Overview. */
38+ /** Bundle of the server-fetched responses passed into the presentational Overview. */
3739export interface OverviewScreenData {
3840 kpis : KpisResponse ;
3941 evalHealth : EvalHealthResponse ;
4042 liveRuns : LiveRunsResponse ;
43+ capacity : DispatchCapacityResponse ;
4144 recentRuns : RunsResponse ;
4245 workflows : WorkflowsResponse ;
4346}
@@ -120,27 +123,54 @@ function EvalHealthKPI({ data }: { data: EvalHealthResponse | undefined }) {
120123 ) ;
121124}
122125
123- /* Live "Now running" panel — currently executing runs only. */
124- function NowRunningPanel ( {
126+ /* Live "Now running" panel. Shows executing runs, plus the occupied-slot count
127+ * counted the way dispatch refuses (parked claims included, from
128+ * listCapacityConsumers) and the at-capacity waiting queue — so a full pool with
129+ * zero executing runs no longer looks idle. */
130+ export function NowRunningPanel ( {
125131 rows,
132+ capacity,
126133 onOpenRun,
127134} : {
128135 rows : Run [ ] ;
136+ capacity : DispatchCapacityResponse ;
129137 onOpenRun : ( run : Run ) => void ;
130138} ) {
131139 const running = rows . filter ( ( r ) => r . status === "running" ) ;
140+ const { occupiedSlots, maxSlots } = capacity ;
141+ // maxSlots === 0 is the worker-unavailable fallback: capacity is UNKNOWN, not
142+ // full, so never render the amber "full" style for it.
143+ const capacityUnknown = maxSlots === 0 ;
144+ const poolFull = ! capacityUnknown && occupiedSlots >= maxSlots ;
145+ // Defense in depth against a stale row: a ticket that is running (or otherwise
146+ // holds a live slot) is not waiting, so it must never show in both panels.
147+ const liveTickets = new Set ( rows . map ( ( r ) => r . ticket ) ) ;
148+ const queued = capacity . queued . filter ( ( q ) => ! liveTickets . has ( q . ticketKey ) ) ;
132149
133150 return (
134151 < CkCard
135152 eyebrow = "Vercel workflow · live"
136153 title = "Now running"
137154 action = {
138- < span className = "inline-flex items-center gap-1.5 font-mono text-[10px] text-mariner tracking-[0.04em] uppercase" >
139- < span className = "relative w-1.5 h-1.5" >
140- < span className = "absolute inset-0 rounded-full bg-mariner" />
141- < span className = "absolute -inset-[3px] rounded-full border border-mariner animate-ck-pulse" />
155+ < span className = "inline-flex items-center gap-2.5 font-mono text-[10px] tracking-[0.04em] uppercase" >
156+ < span className = "inline-flex items-center gap-1.5 text-mariner" >
157+ < span className = "relative w-1.5 h-1.5" >
158+ < span className = "absolute inset-0 rounded-full bg-mariner" />
159+ < span className = "absolute -inset-[3px] rounded-full border border-mariner animate-ck-pulse" />
160+ </ span >
161+ { running . length } executing
162+ </ span >
163+ { /* Slots in use, from the same count dispatch refuses against, so a
164+ pool full of parked claims is not mistaken for an idle one. */ }
165+ < span
166+ className = { `inline-flex items-center rounded-[3px] border px-1.5 py-[2px] ${
167+ poolFull
168+ ? "border-amber-300 bg-amber-50 text-amber-800"
169+ : "border-neutral-200 bg-off-white text-neutral-600"
170+ } `}
171+ >
172+ { capacityUnknown ? "slots —" : `${ occupiedSlots } /${ maxSlots } slots` }
142173 </ span >
143- { running . length } executing
144174 </ span >
145175 }
146176 pad = { 0 }
@@ -209,6 +239,26 @@ function NowRunningPanel({
209239 } ) }
210240 </ div >
211241 ) }
242+ { queued . length > 0 && (
243+ < div className = "border-t border-amber-200 bg-amber-50 px-5 py-3" >
244+ < div className = "font-mono text-[10px] uppercase tracking-[0.06em] text-amber-800 mb-2" >
245+ { queued . length } waiting for capacity
246+ </ div >
247+ < div className = "flex flex-col gap-1.5" >
248+ { queued . map ( ( q ) => (
249+ < div
250+ key = { q . ticketKey }
251+ className = "flex items-center justify-between gap-2 font-body text-xs text-amber-900"
252+ >
253+ < span className = "font-medium" > { q . ticketKey } </ span >
254+ < span className = "font-mono text-[11px] text-amber-700 whitespace-nowrap" >
255+ waiting { formatWaited ( q . queuedAt ) }
256+ </ span >
257+ </ div >
258+ ) ) }
259+ </ div >
260+ </ div >
261+ ) }
212262 </ CkCard >
213263 ) ;
214264}
@@ -472,7 +522,7 @@ export function OverviewScreen({
472522
473523 { /* Live row */ }
474524 < div className = "grid grid-cols-2 gap-3" >
475- < NowRunningPanel rows = { liveRows } onOpenRun = { openRun } />
525+ < NowRunningPanel rows = { liveRows } capacity = { data . capacity } onOpenRun = { openRun } />
476526 < AwaitingInputPanel rows = { liveRows } onOpenRun = { openRun } />
477527 </ div >
478528
0 commit comments