Skip to content

Commit e3679c4

Browse files
authored
Label jobs that have not started as In flight, in the GPU-chart yellow (#141)
## Description Jobs that have not started yet are easier to read on **Overview**. - The card's time line reads **In flight since <time>** instead of "Waiting since". - Once the job has been sent to a node, the card says **Sent to <node>**. It used to say "Ran on <node>", which is wrong for a job that has not run. - A job that fails or is cancelled before it starts keeps **Sent to <node>**. The proxy keeps the node on the final event, so the card decides between "Sent to" and "Ran on" from `startedAt`, not from the state. `workloadNodeLabel` in `ui/utils/workload-labels.ts` holds that rule. - In-flight cards and their connection lines use the GPU chart's yellow (`#f9b400`). The card corner used the KUI `--color-yellow-500` (`#d73d00`, an orange), and the lines used `#F5A623`. One `GPU_YELLOW` constant feeds both the GPU palette and `WORKLOAD_COLOR_MAP`, and the card passes it to `sheen.css` as `--workload-in-flight-color`, so the corner and the line cannot drift apart. - The getting-started guide and the troubleshooting page say a job's card shows **Sent to** once it has been sent to a node, next to **Running on** and **Ran on**. This is the first of three pull requests that make job states accurate. This one changes copy and color only. The second fixes job identity on the desktop and event ordering in the broker and proxy. The third makes In flight and Running reflect engine slots. ## Release intent <!-- pair-release-intent:v1 --> ### Changelog title Clearer in-flight jobs ### Changelog body - Jobs that have not started yet are labeled In flight, and they and their connection lines use the same yellow as the GPU chart. - A job's card names the node it was sent to with "Sent to" until the job starts, including a job that fails or is cancelled before it starts, instead of "Ran on". ### Bumps - services: patch - nvpair-cluster-manager: none - nvpair-engine-manager: none - nvpair-errors: none - nvpair-job-scheduler: none - nvpair-manual-nodes: none - nvpair-node-info: none - nvpair-node-scanner: none - nvpair-node-settings: none - nvpair-proxy: none - nvpair-tui: none - nvpair-ui-broker: none - nvpair-workload-manager: none <!-- /pair-release-intent:v1 --> `services` is a patch for the user-facing note; no service binary changes, so every component is `none`. ## Scope Included: the job card's copy and the in-flight color. Unchanged: - **The state itself.** The wire and internal value is still `queued`: the JSON-RPC payloads, `WorkloadState`, the `workload-card-queued` class, the Go services, and the specs. - **When a job counts as in flight.** A non-streaming job still stays in flight until its response completes. The third pull request changes that. - **The terminal UI,** which still prints `queued` in its STATE column. - **The hidden `workload-badge` bar** on the card, which still gets a color but is never displayed. The getting-started screenshots need no update: neither shows an in-flight card. ## Validation From `desktop/`, all passing: - `npm run typecheck` - `npm run lint` - `npm run dead-code:check` - `npm run test:unit` (238 passed, 2 skipped) - `npm run service-contracts:check` `node scripts/spdx-headers.mjs` from the repository root also passes. `tests/modular/workload-card-labels.test.ts` covers the node label for every state, with and without `startedAt`. It also checks that every `WORKLOAD_COLOR_MAP` value is six-digit hex and that the in-flight color is in the GPU palette. Still to do: run the Inference Demo and confirm that an in-flight card shows a yellow corner and "In flight since <time>", that its connection line is yellow, and that the card reads "Sent to <node>" once the job is dispatched, then "Running on <node>", then "Ran on <node>". ## Risk Display only: no backend, IPC, or payload change. `WORKLOAD_COLOR_MAP` values must stay six-digit hex because the connection lines append a two-digit alpha; the new test enforces it. ## Checklist - [x] I have read the [Contributing Guidelines](https://github.com/NVIDIA/Personal-AI-Router/blob/main/CONTRIBUTING.md). - [x] Every commit is signed off (`git commit -s`), certifying the [Developer Certificate of Origin](https://developercertificate.org/). - [x] New or existing tests cover the change. - [x] Relevant documentation is updated. - [x] I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts. - [x] I recorded the validation commands and results above. - [x] I declared version bumps in the release-intent block above. `services/versions.json` is written by automation — do not edit it by hand. --------- Signed-off-by: Chris Kelsey <ckelsey@nvidia.com>
1 parent 937cf6c commit e3679c4

7 files changed

Lines changed: 107 additions & 22 deletions

File tree

‎desktop/src/ui/components/Workloads/WorkloadItemCard.tsx‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { useMemo, memo } from 'react'
4+
import { useMemo, memo, type CSSProperties } from 'react'
55
import { Card, Flex, Stack, Text } from '@nvidia/foundations-react-core'
66
import type { Workload } from '@/shared/types/workloads'
77
import { workloadExecutionNodeId } from '@/shared/utils/workloads'
88
import { useNodesStore } from '@/ui/stores/nodes.store'
9+
import { WORKLOAD_COLOR_MAP } from '@/ui/constants/colors'
910
import { formatModelDisplayName } from '@/ui/utils/format-model-display-name'
1011
import { getWorkloadColorBar } from '@/ui/utils/colors'
12+
import { workloadNodeLabel } from '@/ui/utils/workload-labels'
1113
import EngineIcon from '@/ui/components/EngineIcon'
1214

1315
const formatDate = (timestamp: number) => {
@@ -40,10 +42,17 @@ const formatDate = (timestamp: number) => {
4042
}
4143
}
4244

45+
// sheen.css paints the in-flight card's corner from this property, so the card
46+
// and its connection line share one color.
47+
const CARD_STYLE: CSSProperties & { '--workload-in-flight-color': string } = {
48+
direction: 'ltr',
49+
'--workload-in-flight-color': WORKLOAD_COLOR_MAP.yellow
50+
}
51+
4352
function WorkloadItemCard({ workload }: { workload: Workload }) {
4453
// Subscribe to only this workload's execution node name. Selecting the whole
4554
// nodes array re-rendered every job card on any node/metrics update.
46-
const ranOnNodeText = useNodesStore(state => {
55+
const executionNodeText = useNodesStore(state => {
4756
const executionNodeId = workloadExecutionNodeId(workload)
4857
if (!executionNodeId) return ''
4958
return state.nodes.get(executionNodeId)?.name ?? ''
@@ -54,7 +63,6 @@ function WorkloadItemCard({ workload }: { workload: Workload }) {
5463
if (!workload.originatedFrom) return ''
5564
return state.nodes.get(workload.originatedFrom)?.name ?? ''
5665
})
57-
const ranOnLabel = workload.state === 'running' ? 'Running on' : 'Ran on'
5866
const barColor = useMemo(() => getWorkloadColorBar(workload.state), [workload.state])
5967

6068
const subtext = useMemo(() => {
@@ -65,10 +73,11 @@ function WorkloadItemCard({ workload }: { workload: Workload }) {
6573
switch (state) {
6674
case 'queued':
6775
value = workload.createdAt
68-
// Not "Queued at": PAIR runs no queue of its own, and the
69-
// elapsed wait is the useful part for a job that has been
70-
// accepted but is not generating yet.
71-
label = 'Waiting since'
76+
// "In flight", not "Queued": PAIR runs no queue of its own, and
77+
// a job that has not started may be waiting for a node, waiting
78+
// in an engine's queue, or already being processed where PAIR
79+
// cannot see it.
80+
label = 'In flight since'
7281
break
7382
case 'running':
7483
value = workload.startedAt ?? 0
@@ -120,7 +129,7 @@ function WorkloadItemCard({ workload }: { workload: Workload }) {
120129
<Card
121130
className={className}
122131
density="compact"
123-
style={{ direction: 'ltr' }}
132+
style={CARD_STYLE}
124133
data-workload-id={workload.id}
125134
data-workload-origin={workload.originatedFrom ?? ''}
126135
attributes={{ CardContent: { className: 'workload-card-content' } }}
@@ -134,7 +143,7 @@ function WorkloadItemCard({ workload }: { workload: Workload }) {
134143
</Text>
135144
</Flex>
136145

137-
{(requestedFromNodeText || ranOnNodeText) && (
146+
{(requestedFromNodeText || executionNodeText) && (
138147
<Stack gap="0" className="mt-1">
139148
{requestedFromNodeText && (
140149
<Flex align="center" wrap="wrap" gap="1">
@@ -146,13 +155,13 @@ function WorkloadItemCard({ workload }: { workload: Workload }) {
146155
</Text>
147156
</Flex>
148157
)}
149-
{ranOnNodeText && (
158+
{executionNodeText && (
150159
<Flex align="center" wrap="wrap" gap="1">
151160
<Text kind="body/regular/sm" className="text-subtle-color">
152-
{ranOnLabel}
161+
{workloadNodeLabel(workload)}
153162
</Text>
154163
<Text kind="body/regular/sm" className="text-subtle-color">
155-
{ranOnNodeText}
164+
{executionNodeText}
156165
</Text>
157166
</Flex>
158167
)}

‎desktop/src/ui/constants/colors.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
import type { BadgeColor } from '@/ui/types/types'
55

6-
// Color mapping for CSS
7-
export const WORKLOAD_COLOR_MAP: Record<BadgeColor, string> = {
8-
yellow: '#F5A623',
6+
// The GPU chart's yellow, which in-flight jobs also use.
7+
const GPU_YELLOW = '#f9b400'
8+
9+
// Color mapping for CSS. Values must stay six-digit hex: connection lines
10+
// append a two-digit alpha.
11+
export const WORKLOAD_COLOR_MAP: Record<BadgeColor, `#${string}`> = {
12+
yellow: GPU_YELLOW,
913
blue: '#4A90E2',
1014
green: '#7ED321',
1115
gray: '#9B9B9B',
@@ -44,7 +48,7 @@ export const CHART_COLORS = {
4448
*/
4549
export const GPU_COLOR_PALETTE = [
4650
'#0074df', // GPU3 - Dark Blue
47-
'#f9b400', // GPU2 - Yellow
51+
GPU_YELLOW, // GPU2 - Yellow
4852
'#8689ff', // GPU1 - Blue
4953
'#ff8181' // GPU4 - Red
5054
] as const

‎desktop/src/ui/styles/sheen.css‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,12 @@
144144
);
145145
}
146146

147+
/* WorkloadItemCard sets --workload-in-flight-color from WORKLOAD_COLOR_MAP. */
147148
.pair-paper.workload-card-queued {
148149
background-image: radial-gradient(
149150
143% 109% at 0% 73%,
150151
transparent 62%,
151-
var(--color-yellow-500) 105%
152+
var(--workload-in-flight-color) 105%
152153
);
153154
}
154155

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { Workload } from '@/shared/types/workloads'
5+
6+
/**
7+
* The verb before a job card's execution node. A job that ends before it
8+
* starts keeps the node it was sent to, so whether it ran is decided by
9+
* `startedAt`, not by its state.
10+
*/
11+
export function workloadNodeLabel(workload: Pick<Workload, 'state' | 'startedAt'>): string {
12+
if (workload.state === 'running') return 'Running on'
13+
return workload.startedAt === null ? 'Sent to' : 'Ran on'
14+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { describe, expect, it } from 'vitest'
5+
import { GPU_COLOR_PALETTE, WORKLOAD_COLOR_MAP } from '@/ui/constants/colors'
6+
import { workloadNodeLabel } from '@/ui/utils/workload-labels'
7+
8+
const STARTED_AT = 1_700_000_000_000
9+
10+
describe('workloadNodeLabel', () => {
11+
it('says Sent to for an in-flight job', () => {
12+
expect(workloadNodeLabel({ state: 'queued', startedAt: null })).toBe('Sent to')
13+
})
14+
15+
it('says Running on for a running job', () => {
16+
expect(workloadNodeLabel({ state: 'running', startedAt: STARTED_AT })).toBe('Running on')
17+
})
18+
19+
it('says Ran on for a completed job', () => {
20+
expect(workloadNodeLabel({ state: 'completed', startedAt: STARTED_AT })).toBe('Ran on')
21+
})
22+
23+
// The proxy keeps scheduledOn on the terminal event, so a job that ends
24+
// before it starts still names the node it was sent to.
25+
it('says Sent to for a job that failed before it started', () => {
26+
expect(workloadNodeLabel({ state: 'failed', startedAt: null })).toBe('Sent to')
27+
})
28+
29+
it('says Sent to for a job that was cancelled before it started', () => {
30+
expect(workloadNodeLabel({ state: 'cancelled', startedAt: null })).toBe('Sent to')
31+
})
32+
33+
it('says Ran on for a job that failed after it started', () => {
34+
expect(workloadNodeLabel({ state: 'failed', startedAt: STARTED_AT })).toBe('Ran on')
35+
})
36+
37+
it('says Ran on for a job that was cancelled after it started', () => {
38+
expect(workloadNodeLabel({ state: 'cancelled', startedAt: STARTED_AT })).toBe('Ran on')
39+
})
40+
})
41+
42+
describe('WORKLOAD_COLOR_MAP', () => {
43+
// Connection lines append a two-digit alpha to these values.
44+
it('holds only six-digit hex colors', () => {
45+
for (const [name, color] of Object.entries(WORKLOAD_COLOR_MAP)) {
46+
expect(color, name).toMatch(/^#[0-9a-f]{6}$/i)
47+
}
48+
})
49+
50+
it('colors in-flight jobs with the GPU chart yellow', () => {
51+
expect(GPU_COLOR_PALETTE).toContain(WORKLOAD_COLOR_MAP.yellow)
52+
})
53+
})

‎docs/getting-started.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,9 @@ curl -N <PAIR_BASE_URL>/api/chat \
265265
```
266266

267267
Then confirm the request was routed: open **Overview**, use the **Jobs** filter in
268-
the left column, and read **Ran on** or **Running on** on the job card to identify
269-
the serving node.
268+
the left column, and read **Running on** or **Ran on** on the job card to identify
269+
the serving node. Once a job has been sent to a node, its card shows **Sent to**
270+
until the job starts, and keeps it if the job ends without starting.
270271

271272
![A job card under the Jobs filter naming the node that served the request.](assets/onboarding/getting-started/09-job-served-by-node.png)
272273

@@ -487,7 +488,8 @@ A reply alone does not prove routing. To identify the serving node:
487488

488489
1. Open **Overview**.
489490
2. Use the **Jobs** filter in the left column.
490-
3. Read **Ran on** or **Running on** on the job card.
491+
3. Read **Running on** or **Ran on** on the job card. Once a job has been sent
492+
to a node, its card shows **Sent to** until the job starts.
491493

492494
### 5. Verify Cluster Routing
493495

‎docs/troubleshooting.mdx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ Routing confirmation happens on **Overview**:
144144

145145
1. Use the **Jobs** filter in the left column to show active, completed, or
146146
failed requests.
147-
2. Open a job card and read **Ran on** or **Running on** to see which node
148-
served the request.
147+
2. Open a job card and read **Running on** or **Ran on** to see which node
148+
served the request. Once a job has been sent to a node, its card shows
149+
**Sent to** until the job starts, and keeps it if the job ends without
150+
starting.
149151

150152
Send several independent requests to observe multi-node routing. One request
151153
runs on one selected node.

0 commit comments

Comments
 (0)