Skip to content

Commit c682b57

Browse files
fix(audit): preserve historical workflow cache state
1 parent f5d1794 commit c682b57

9 files changed

Lines changed: 250 additions & 23 deletions

File tree

internal/admin/dashboard/static/css/dashboard.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2535,6 +2535,7 @@ body.conversation-drawer-open {
25352535
═══════════════════════════════════════════════════════════════ */
25362536

25372537
.exec-pipeline {
2538+
position: relative;
25382539
display: flex;
25392540
flex-direction: column;
25402541
gap: 0;
@@ -2545,6 +2546,35 @@ body.conversation-drawer-open {
25452546
background: var(--bg);
25462547
}
25472548

2549+
.exec-pipeline-has-meta {
2550+
padding-top: 42px;
2551+
}
2552+
2553+
.exec-pipeline-meta {
2554+
position: absolute;
2555+
top: 12px;
2556+
right: 14px;
2557+
display: flex;
2558+
align-items: center;
2559+
gap: 6px;
2560+
flex-wrap: wrap;
2561+
justify-content: flex-end;
2562+
max-width: calc(100% - 28px);
2563+
}
2564+
2565+
.exec-pipeline-meta-chip {
2566+
display: inline-flex;
2567+
align-items: center;
2568+
padding: 3px 8px;
2569+
border-radius: 999px;
2570+
border: 1px solid var(--border);
2571+
background: color-mix(in srgb, var(--bg-surface) 86%, transparent);
2572+
color: var(--text-muted);
2573+
font-size: 10px;
2574+
line-height: 1.2;
2575+
white-space: nowrap;
2576+
}
2577+
25482578
/* ─── Main pipeline row ─── */
25492579

25502580
.exec-pipeline-row {

internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ test('workflow editor renders a live preview card from the draft workflow state'
239239
);
240240
});
241241

242-
test('audit log pipeline always renders cache and binds runtime highlight classes across the full path', () => {
242+
test('audit log pipeline binds cache visibility and runtime highlight classes across the full path', () => {
243243
const template = readExecutionPlanTemplateSource();
244244
const css = readFixture('../../css/dashboard.css');
245245

@@ -251,6 +251,10 @@ test('audit log pipeline always renders cache and binds runtime highlight classe
251251
template,
252252
/:class="{{\.}}\.authNodeClass"[\s\S]*x-show="{{\.}}\.showGuardrails"[\s\S]*x-show="{{\.}}\.showUsage"[\s\S]*x-show="{{\.}}\.showAudit"/
253253
);
254+
assert.match(
255+
template,
256+
/<div class="exec-pipeline" :class="\{ 'exec-pipeline-has-meta': {{\.}}\.workflowID \}">[\s\S]*<div class="exec-pipeline-meta" x-show="{{\.}}\.workflowID">[\s\S]*x-text="'id: ' \+ {{\.}}\.workflowID"/
257+
);
254258
assert.match(
255259
template,
256260
/<div class="ep-conn ep-conn-grow" :class="{{\.}}\.aiConnClass"><\/div>[\s\S]*<div class="ep-node ep-node-ai" :class="{{\.}}\.aiNodeClass">/
@@ -272,6 +276,14 @@ test('audit log pipeline always renders cache and binds runtime highlight classe
272276
assert.match(semanticCacheRule, /border-color:\s*color-mix\(in srgb, var\(--success\) 52%, var\(--border\)\)/);
273277
assert.match(semanticCacheRule, /background:\s*color-mix\(in srgb, var\(--success\) 9%, var\(--bg-surface\)\)/);
274278

279+
const pipelineRule = readCSSRule(css, '.exec-pipeline');
280+
assert.match(pipelineRule, /position:\s*relative/);
281+
282+
const metaRule = readCSSRule(css, '.exec-pipeline-meta');
283+
assert.match(metaRule, /position:\s*absolute/);
284+
assert.match(metaRule, /top:\s*12px/);
285+
assert.match(metaRule, /right:\s*14px/);
286+
275287
const authSuccessRule = readCSSRule(css, '.ep-node-auth-success');
276288
assert.match(authSuccessRule, /border-color:\s*color-mix\(in srgb, var\(--success\) 52%, var\(--border\)\)/);
277289
assert.match(authSuccessRule, /background:\s*color-mix\(in srgb, var\(--success\) 9%, var\(--bg-surface\)\)/);

internal/admin/dashboard/static/js/modules/execution-plans.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,14 @@
399399
};
400400
},
401401

402+
executionPlanEntryFeatures(entry) {
403+
const raw = entry && entry.data && entry.data.execution_features;
404+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
405+
return null;
406+
}
407+
return this.executionPlanNormalizedFeatures(raw);
408+
},
409+
402410
executionPlanSourceGuardrails(source) {
403411
const raw = Array.isArray(source && source.plan_payload && source.plan_payload.guardrails)
404412
? source.plan_payload.guardrails
@@ -1058,22 +1066,35 @@
10581066
return provider || 'AI';
10591067
},
10601068

1061-
epAiSublabel(source, runtime) {
1062-
if (runtime && runtime.model) return runtime.model;
1063-
return source && source.scope && source.scope.scope_model || null;
1069+
epAiSublabel(source, runtime) {
1070+
if (runtime && runtime.model) return runtime.model;
1071+
return source && source.scope && source.scope.scope_model || null;
1072+
},
1073+
1074+
executionPlanChartWorkflowID(source, entry) {
1075+
const sourceID = String(source && source.id || '').trim();
1076+
if (sourceID) {
1077+
return sourceID;
1078+
}
1079+
const entryID = String(entry && entry.execution_plan_version_id || '').trim();
1080+
return entryID || null;
10641081
},
10651082

10661083
executionPlanChartModel(source, runtime, options) {
10671084
const config = options || {};
1085+
const features = config.features && typeof config.features === 'object' && !Array.isArray(config.features)
1086+
? this.executionPlanNormalizedFeatures(config.features)
1087+
: this.executionPlanSourceFeatures(source);
10681088
const forceAudit = !!config.forceAudit;
1069-
const showGuardrails = this.epHasGuardrails(source);
1070-
const showUsage = this.epHasUsage(source);
1071-
const showAudit = this.executionPlanAuditVisible() && (forceAudit || this.epHasAudit(source));
1072-
const showAsync = !!(showUsage || showAudit);
1089+
const showGuardrails = !!features.guardrails;
1090+
const showUsage = !!features.usage;
1091+
const showAudit = forceAudit || !!features.audit;
1092+
const showAsync = !!config.forceAsync || !!(showUsage || showAudit);
1093+
const workflowID = this.executionPlanChartWorkflowID(source, config.entry);
10731094
return {
10741095
showGuardrails,
10751096
guardrailLabel: showGuardrails ? this.epGuardrailLabel(source) : '',
1076-
showCache: !!config.forceCache || this.epShowCacheStep(source, runtime),
1097+
showCache: !!config.forceCache || !!features.cache || this.epRuntimeHasCache(runtime),
10771098
cacheNodeClass: this.epCacheNodeClass(runtime),
10781099
cacheConnClass: this.epCacheConnClass(runtime),
10791100
cacheStatusLabel: this.epCacheStatusLabel(runtime),
@@ -1087,7 +1108,8 @@
10871108
authNodeSublabel: this.epAuthNodeSublabel(runtime),
10881109
showAsync,
10891110
showUsage,
1090-
showAudit
1111+
showAudit,
1112+
workflowID
10911113
};
10921114
},
10931115

@@ -1098,8 +1120,10 @@
10981120
executionPlanAuditChart(entry) {
10991121
const source = this.auditEntryExecutionPlan(entry);
11001122
const runtime = this.epRuntimeFromEntry(entry);
1123+
const features = this.executionPlanEntryFeatures(entry) || this.executionPlanSourceFeatures(source);
11011124
return this.executionPlanChartModel(source, runtime, {
1102-
forceCache: true,
1125+
entry,
1126+
features,
11031127
forceAudit: true,
11041128
forceAsync: true
11051129
});

internal/admin/dashboard/static/js/modules/execution-plans.test.js

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ test('executionPlanWorkflowChart returns the shared chart contract for workflow
169169

170170
assert.equal(
171171
JSON.stringify(module.executionPlanWorkflowChart({
172+
id: 'workflow-openai-gpt-5-v7',
172173
scope: {
173174
scope_provider: 'openai',
174175
scope_model: 'gpt-5'
@@ -204,7 +205,8 @@ test('executionPlanWorkflowChart returns the shared chart contract for workflow
204205
authNodeSublabel: null,
205206
showAsync: true,
206207
showUsage: false,
207-
showAudit: true
208+
showAudit: true,
209+
workflowID: 'workflow-openai-gpt-5-v7'
208210
})
209211
);
210212
});
@@ -256,7 +258,8 @@ test('executionPlanWorkflowChart masks globally disabled workflow features from
256258
authNodeSublabel: null,
257259
showAsync: false,
258260
showUsage: false,
259-
showAudit: false
261+
showAudit: false,
262+
workflowID: null
260263
})
261264
);
262265
});
@@ -310,7 +313,8 @@ test('executionPlanAuditChart returns the shared chart contract for audit runtim
310313
authNodeSublabel: null,
311314
showAsync: true,
312315
showUsage: true,
313-
showAudit: true
316+
showAudit: true,
317+
workflowID: 'historical-v1'
314318
})
315319
);
316320
});
@@ -343,7 +347,71 @@ test('executionPlanAuditChart forces audit nodes even when the workflow version
343347
authNodeSublabel: null,
344348
showAsync: true,
345349
showUsage: false,
346-
showAudit: true
350+
showAudit: true,
351+
workflowID: 'missing-plan'
352+
})
353+
);
354+
});
355+
356+
test('executionPlanAuditChart prefers request-time execution features over current workflow state', () => {
357+
const module = createExecutionPlansModule();
358+
module.executionPlanVersionsByID = {
359+
'historical-v2': {
360+
id: 'historical-v2',
361+
scope: {
362+
scope_provider: 'openai',
363+
scope_model: 'gpt-5'
364+
},
365+
plan_payload: {
366+
features: {
367+
cache: true,
368+
audit: true,
369+
usage: true,
370+
guardrails: true,
371+
fallback: true
372+
},
373+
guardrails: [
374+
{ ref: 'policy-system', step: 10 }
375+
]
376+
}
377+
}
378+
};
379+
380+
assert.equal(
381+
JSON.stringify(module.executionPlanAuditChart({
382+
execution_plan_version_id: 'historical-v2',
383+
provider: 'openai',
384+
model: 'gpt-5',
385+
status_code: 200,
386+
data: {
387+
execution_features: {
388+
cache: false,
389+
audit: true,
390+
usage: false,
391+
guardrails: false,
392+
fallback: true
393+
}
394+
}
395+
})),
396+
JSON.stringify({
397+
showGuardrails: false,
398+
guardrailLabel: '',
399+
showCache: false,
400+
cacheNodeClass: '',
401+
cacheConnClass: '',
402+
cacheStatusLabel: null,
403+
aiLabel: 'openai',
404+
aiSublabel: 'gpt-5',
405+
aiConnClass: '',
406+
aiNodeClass: 'ep-node-ai-success',
407+
responseConnClass: '',
408+
responseNodeClass: 'ep-node-endpoint-success',
409+
authNodeClass: '',
410+
authNodeSublabel: null,
411+
showAsync: true,
412+
showUsage: false,
413+
showAudit: true,
414+
workflowID: 'historical-v2'
347415
})
348416
);
349417
});

internal/admin/dashboard/templates/execution-plan-chart.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{{define "execution-plan-chart"}}
2-
<div class="exec-pipeline">
2+
<div class="exec-pipeline" :class="{ 'exec-pipeline-has-meta': {{.}}.workflowID }">
3+
<div class="exec-pipeline-meta" x-show="{{.}}.workflowID">
4+
<span class="exec-pipeline-meta-chip mono" x-show="{{.}}.workflowID" x-text="'id: ' + {{.}}.workflowID"></span>
5+
</div>
36
<div class="exec-pipeline-row">
47
<div class="ep-left">
58
<div class="ep-node ep-node-endpoint">

internal/auditlog/auditlog.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ type LogEntry struct {
5656
StatusCode int `json:"status_code" bson:"status_code"`
5757

5858
// Extracted fields for efficient filtering (indexed in relational DBs)
59-
RequestID string `json:"request_id,omitempty" bson:"request_id,omitempty"`
59+
RequestID string `json:"request_id,omitempty" bson:"request_id,omitempty"`
6060
AuthKeyID string `json:"auth_key_id,omitempty" bson:"auth_key_id,omitempty"`
6161
AuthMethod string `json:"auth_method,omitempty" bson:"auth_method,omitempty"`
62-
ClientIP string `json:"client_ip,omitempty" bson:"client_ip,omitempty"`
63-
Method string `json:"method,omitempty" bson:"method,omitempty"`
64-
Path string `json:"path,omitempty" bson:"path,omitempty"`
65-
UserPath string `json:"user_path,omitempty" bson:"user_path,omitempty"`
66-
Stream bool `json:"stream,omitempty" bson:"stream,omitempty"`
67-
ErrorType string `json:"error_type,omitempty" bson:"error_type,omitempty"`
62+
ClientIP string `json:"client_ip,omitempty" bson:"client_ip,omitempty"`
63+
Method string `json:"method,omitempty" bson:"method,omitempty"`
64+
Path string `json:"path,omitempty" bson:"path,omitempty"`
65+
UserPath string `json:"user_path,omitempty" bson:"user_path,omitempty"`
66+
Stream bool `json:"stream,omitempty" bson:"stream,omitempty"`
67+
ErrorType string `json:"error_type,omitempty" bson:"error_type,omitempty"`
6868

6969
// Data contains flexible request/response information as JSON
7070
Data *LogData `json:"data,omitempty" bson:"data,omitempty"`
@@ -78,6 +78,11 @@ type LogData struct {
7878
UserAgent string `json:"user_agent,omitempty" bson:"user_agent,omitempty"`
7979
APIKeyHash string `json:"api_key_hash,omitempty" bson:"api_key_hash,omitempty"`
8080

81+
// ExecutionFeatures captures the request-time effective workflow features
82+
// after runtime caps were applied. This keeps audit views historically accurate
83+
// even if the active process config changes later.
84+
ExecutionFeatures *ExecutionFeaturesSnapshot `json:"execution_features,omitempty" bson:"execution_features,omitempty"`
85+
8186
// Request parameters
8287
Temperature *float64 `json:"temperature,omitempty" bson:"temperature,omitempty"`
8388
MaxTokens *int `json:"max_tokens,omitempty" bson:"max_tokens,omitempty"`
@@ -101,6 +106,17 @@ type LogData struct {
101106
ResponseBodyTooBigToHandle bool `json:"response_body_too_big_to_handle,omitempty" bson:"response_body_too_big_to_handle,omitempty"`
102107
}
103108

109+
// ExecutionFeaturesSnapshot stores the effective workflow feature state that
110+
// applied to one request. Fields intentionally do not use omitempty so "false"
111+
// remains explicit once the snapshot exists.
112+
type ExecutionFeaturesSnapshot struct {
113+
Cache bool `json:"cache" bson:"cache"`
114+
Audit bool `json:"audit" bson:"audit"`
115+
Usage bool `json:"usage" bson:"usage"`
116+
Guardrails bool `json:"guardrails" bson:"guardrails"`
117+
Fallback bool `json:"fallback" bson:"fallback"`
118+
}
119+
104120
// marshalLogData marshals the Data field to JSON for SQL storage.
105121
// Returns nil if data is nil, or "{}" if marshaling fails.
106122
// This is used by PostgreSQL and SQLite stores.

0 commit comments

Comments
 (0)