diff --git a/internal/admin/dashboard/static/css/dashboard.css b/internal/admin/dashboard/static/css/dashboard.css index dc77fcbe..8129a121 100644 --- a/internal/admin/dashboard/static/css/dashboard.css +++ b/internal/admin/dashboard/static/css/dashboard.css @@ -4202,6 +4202,34 @@ body.conversation-drawer-open { color: var(--success); } +.workflow-node-warning { + border-color: color-mix(in srgb, var(--warning) 52%, var(--border)); + background: color-mix(in srgb, var(--warning) 9%, var(--bg-surface)); +} + +.workflow-node-warning .workflow-node-icon { + background: color-mix(in srgb, var(--warning) 14%, var(--bg)); + color: var(--warning); +} + +.workflow-node-warning .workflow-node-icon-endpoint { + color: var(--warning); +} + +.workflow-node-warning .workflow-node-label { + color: color-mix(in srgb, var(--warning) 85%, var(--text)); +} + +.workflow-node-warning .workflow-node-sub { + color: color-mix(in srgb, var(--warning) 72%, var(--text-muted)); +} + +.workflow-node-warning .workflow-node-badge { + background: color-mix(in srgb, var(--warning) 14%, var(--bg)); + border-color: color-mix(in srgb, var(--warning) 38%, var(--border)); + color: var(--warning); +} + .workflow-node-error { border-color: color-mix(in srgb, var(--danger) 52%, var(--border)); background: color-mix(in srgb, var(--danger) 9%, var(--bg-surface)); @@ -4224,6 +4252,34 @@ body.conversation-drawer-open { color: color-mix(in srgb, var(--danger) 72%, var(--text-muted)); } +.workflow-node-neutral { + border-color: color-mix(in srgb, var(--text-muted) 40%, var(--border)); + background: color-mix(in srgb, var(--text-muted) 8%, var(--bg-surface)); +} + +.workflow-node-neutral .workflow-node-icon { + background: color-mix(in srgb, var(--text-muted) 12%, var(--bg)); + color: var(--text-muted); +} + +.workflow-node-neutral .workflow-node-icon-endpoint { + color: var(--text-muted); +} + +.workflow-node-neutral .workflow-node-label { + color: var(--text-muted); +} + +.workflow-node-neutral .workflow-node-sub { + color: color-mix(in srgb, var(--text-muted) 84%, var(--border)); +} + +.workflow-node-neutral .workflow-node-badge { + background: color-mix(in srgb, var(--text-muted) 10%, var(--bg)); + border-color: color-mix(in srgb, var(--text-muted) 28%, var(--border)); + color: var(--text-muted); +} + .workflow-node-skipped { position: relative; opacity: 0.28; diff --git a/internal/admin/dashboard/static/js/modules/workflows.js b/internal/admin/dashboard/static/js/modules/workflows.js index aa62bfcb..9131ab68 100644 --- a/internal/admin/dashboard/static/js/modules/workflows.js +++ b/internal/admin/dashboard/static/js/modules/workflows.js @@ -1234,6 +1234,7 @@ aiNodeClass: this.workflowAiNodeClass(runtime), responseConnClass: this.workflowResponseConnClass(runtime), responseNodeClass: this.workflowResponseNodeClass(runtime), + responseNodeSublabel: this.workflowResponseNodeSublabel(runtime), authNodeClass: this.workflowAuthNodeClass(runtime), authNodeSublabel: this.workflowAuthNodeSublabel(runtime), usageNodeClass: this.workflowAsyncNodeClass(showUsage, highlightAsyncPresent), @@ -1345,7 +1346,18 @@ workflowResponseNodeClass(runtime) { if (!runtime) return ''; - return runtime.responseSuccess ? 'workflow-node-success' : ''; + const statusCode = runtime.statusCode; + if (!Number.isFinite(statusCode)) return ''; + if (statusCode >= 500) return 'workflow-node-error'; + if (statusCode >= 400) return 'workflow-node-warning'; + if (statusCode >= 300) return 'workflow-node-neutral'; + if (statusCode >= 200) return 'workflow-node-success'; + return ''; + }, + + workflowResponseNodeSublabel(runtime) { + if (!runtime || !Number.isFinite(runtime.statusCode)) return null; + return String(runtime.statusCode); }, workflowAuthNodeClass(runtime) { diff --git a/internal/admin/dashboard/static/js/modules/workflows.test.js b/internal/admin/dashboard/static/js/modules/workflows.test.js index dd012e51..19240ca6 100644 --- a/internal/admin/dashboard/static/js/modules/workflows.test.js +++ b/internal/admin/dashboard/static/js/modules/workflows.test.js @@ -230,6 +230,7 @@ test('workflowChart returns the shared chart contract for workflow sources', () aiNodeClass: '', responseConnClass: '', responseNodeClass: '', + responseNodeSublabel: null, authNodeClass: '', authNodeSublabel: null, usageNodeClass: '', @@ -290,6 +291,7 @@ test('workflowChart masks globally disabled workflow features from persisted wor aiNodeClass: '', responseConnClass: '', responseNodeClass: '', + responseNodeSublabel: null, authNodeClass: '', authNodeSublabel: null, usageNodeClass: '', @@ -439,6 +441,7 @@ test('workflowAuditChart returns the shared chart contract for audit runtime ent aiNodeClass: 'workflow-node-skipped', responseConnClass: 'workflow-conn-dim', responseNodeClass: 'workflow-node-success', + responseNodeSublabel: '200', authNodeClass: '', authNodeSublabel: null, usageNodeClass: 'workflow-node-success', @@ -480,6 +483,7 @@ test('workflowAuditChart forces audit nodes even when the workflow version canno aiNodeClass: 'workflow-node-skipped', responseConnClass: 'workflow-conn-dim', responseNodeClass: 'workflow-node-success', + responseNodeSublabel: '200', authNodeClass: '', authNodeSublabel: null, usageNodeClass: '', @@ -550,6 +554,7 @@ test('workflowAuditChart prefers request-time workflow features over current wor aiNodeClass: 'workflow-node-success', responseConnClass: '', responseNodeClass: 'workflow-node-success', + responseNodeSublabel: '200', authNodeClass: '', authNodeSublabel: null, usageNodeClass: '', @@ -621,6 +626,7 @@ test('workflowAuditChart highlights configured failover redirects and exposes th aiNodeClass: 'workflow-node-success', responseConnClass: '', responseNodeClass: 'workflow-node-success', + responseNodeSublabel: '200', authNodeClass: '', authNodeSublabel: null, usageNodeClass: 'workflow-node-success', @@ -1900,6 +1906,7 @@ test('audit runtime uses explicit cache-hit labels and highlights the uncached 2 assert.equal(module.workflowAiNodeClass(semanticHit), 'workflow-node-skipped'); assert.equal(module.workflowResponseConnClass(semanticHit), 'workflow-conn-dim'); assert.equal(module.workflowResponseNodeClass(semanticHit), 'workflow-node-success'); + assert.equal(module.workflowResponseNodeSublabel(semanticHit), '200'); const uncachedSuccess = module.workflowRuntimeFromEntry({ provider: 'openai', @@ -1913,6 +1920,39 @@ test('audit runtime uses explicit cache-hit labels and highlights the uncached 2 assert.equal(module.workflowAiNodeClass(uncachedSuccess), 'workflow-node-success'); assert.equal(module.workflowResponseConnClass(uncachedSuccess), ''); assert.equal(module.workflowResponseNodeClass(uncachedSuccess), 'workflow-node-success'); + assert.equal(module.workflowResponseNodeSublabel(uncachedSuccess), '200'); +}); + +test('response runtime maps 3xx and 4xx statuses to neutral and warning chart colors', () => { + const module = createWorkflowsModule(); + + const redirect = module.workflowRuntimeFromEntry({ + provider: 'openai', + model: 'gpt-5', + status_code: 304 + }); + assert.equal(module.workflowResponseNodeClass(redirect), 'workflow-node-neutral'); + assert.equal(module.workflowResponseNodeSublabel(redirect), '304'); + + const clientError = module.workflowRuntimeFromEntry({ + provider: 'openai', + model: 'gpt-5', + status_code: 429 + }); + assert.equal(module.workflowResponseNodeClass(clientError), 'workflow-node-warning'); + assert.equal(module.workflowResponseNodeSublabel(clientError), '429'); +}); + +test('response runtime maps 5xx statuses to the error chart color', () => { + const module = createWorkflowsModule(); + + const serverError = module.workflowRuntimeFromEntry({ + provider: 'openai', + model: 'gpt-5', + status_code: 503 + }); + assert.equal(module.workflowResponseNodeClass(serverError), 'workflow-node-error'); + assert.equal(module.workflowResponseNodeSublabel(serverError), '503'); }); test('workflowRuntimeFromEntry treats any uncached 2xx status as a successful AI and response path', () => { diff --git a/internal/admin/dashboard/templates/workflow-chart.html b/internal/admin/dashboard/templates/workflow-chart.html index 4f29a844..b27ce992 100644 --- a/internal/admin/dashboard/templates/workflow-chart.html +++ b/internal/admin/dashboard/templates/workflow-chart.html @@ -74,6 +74,7 @@ Response +