Skip to content

Commit d4c5343

Browse files
fix(dashboard): harden audit chart and shared controls
1 parent 86075d6 commit d4c5343

5 files changed

Lines changed: 117 additions & 10 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ test('usage and audit pages reuse a shared pagination template', () => {
9595

9696
assert.match(
9797
paginationTemplate,
98-
/{{define "pagination"}}[\s\S]*x-show="{{\.}}\.total > 0"[\s\S]*@click="{{\.}}PrevPage\(\)"[\s\S]*@click="{{\.}}NextPage\(\)"[\s\S]*{{end}}/
98+
/{{define "pagination"}}[\s\S]*x-show="{{\.}}\.total > 0"[\s\S]*type="button"[\s\S]*@click="{{\.}}PrevPage\(\)"[\s\S]*type="button"[\s\S]*@click="{{\.}}NextPage\(\)"[\s\S]*{{end}}/
9999
);
100100
assert.match(indexTemplate, /{{template "pagination" "usageLog"}}/);
101101
assert.match(indexTemplate, /{{template "pagination" "auditLog"}}/);
@@ -115,7 +115,7 @@ test('audit request and response sections reuse a shared audit pane template', (
115115

116116
assert.match(
117117
auditPaneTemplate,
118-
/{{define "audit-pane"}}[\s\S]*x-text="{{\.}}\.title"[\s\S]*copyAuditJSON\({{\.\}}\.copyBody, \$event\)[\s\S]*x-text="formatJSON\({{\.\}}\.headers\)"[\s\S]*renderBodyWithConversationHighlights\({{\.\}}\.entry, {{\.}}\.body\)[\s\S]*x-text="{{\.}}\.emptyMessage"[\s\S]*x-text="{{\.}}\.tooLargeMessage"[\s\S]*{{end}}/
118+
/{{define "audit-pane"}}[\s\S]*x-text="{{\.}}\.title"[\s\S]*type="button"[\s\S]*copyAuditJSON\({{\.\}}\.copyBody, \$event\)[\s\S]*x-text="formatJSON\({{\.\}}\.headers\)"[\s\S]*renderBodyWithConversationHighlights\({{\.\}}\.entry, {{\.}}\.body\)[\s\S]*x-text="{{\.}}\.emptyMessage"[\s\S]*x-text="{{\.}}\.tooLargeMessage"[\s\S]*{{end}}/
119119
);
120120
assert.match(indexTemplate, /{{template "audit-pane" "auditRequestPane\(entry\)"}}/);
121121
assert.match(indexTemplate, /{{template "audit-pane" "auditResponsePane\(entry\)"}}/);

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@
275275
ref: String(step && step.ref || '').trim(),
276276
step: this.parseExecutionPlanGuardrailStep(step && step.step)
277277
}))
278-
.filter((step) => Number.isFinite(step.step));
278+
.filter((step) => Number.isInteger(step.step) && step.step >= 0);
279279
},
280280

281281
canDeactivateExecutionPlan(plan) {
@@ -404,7 +404,16 @@
404404
const provider = String(form.scope_provider || '').trim();
405405
const model = provider ? String(form.scope_model || '').trim() : '';
406406
const features = form.features || {};
407-
const includeFallback = this.executionPlanFailoverVisible() || !!this.executionPlanFormHydrated;
407+
const hydratedScope = this.executionPlanHydratedScope || {
408+
scope_provider: '',
409+
scope_model: ''
410+
};
411+
const sameHydratedScope = String(hydratedScope.scope_provider || '').trim() === provider
412+
&& String(hydratedScope.scope_model || '').trim() === model;
413+
const includeFallback = this.executionPlanFailoverVisible()
414+
|| (!!this.executionPlanFormHydrated
415+
&& sameHydratedScope
416+
&& Object.prototype.hasOwnProperty.call(features, 'fallback'));
408417

409418
const guardrails = !!features.guardrails
410419
? (Array.isArray(form.guardrails) ? form.guardrails : []).map((step) => {
@@ -813,6 +822,8 @@
813822

814823
executionPlanChartModel(source, runtime, options) {
815824
const config = options || {};
825+
const forceAudit = !!config.forceAudit;
826+
const forceAsync = !!config.forceAsync || forceAudit;
816827
return {
817828
showGuardrails: this.epHasGuardrails(source),
818829
guardrailLabel: this.epGuardrailLabel(source),
@@ -826,9 +837,9 @@
826837
aiNodeClass: this.epAiNodeClass(runtime),
827838
responseConnClass: this.epResponseConnClass(runtime),
828839
responseNodeClass: this.epResponseNodeClass(runtime),
829-
showAsync: this.epHasAsync(source),
840+
showAsync: forceAsync || this.epHasAsync(source),
830841
showUsage: this.epHasUsage(source),
831-
showAudit: this.epHasAudit(source)
842+
showAudit: forceAudit || this.epHasAudit(source)
832843
};
833844
},
834845

@@ -839,7 +850,11 @@
839850
executionPlanAuditChart(entry) {
840851
const source = this.auditEntryExecutionPlan(entry);
841852
const runtime = this.epRuntimeFromEntry(entry);
842-
return this.executionPlanChartModel(source, runtime, { forceCache: true });
853+
return this.executionPlanChartModel(source, runtime, {
854+
forceCache: true,
855+
forceAudit: true,
856+
forceAsync: true
857+
});
843858
},
844859

845860
// runtime shape: {

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,37 @@ test('executionPlanAuditChart returns the shared chart contract for audit runtim
213213
);
214214
});
215215

216+
test('executionPlanAuditChart forces audit nodes even when the workflow version cannot be resolved', () => {
217+
const module = createExecutionPlansModule();
218+
219+
assert.equal(
220+
JSON.stringify(module.executionPlanAuditChart({
221+
execution_plan_version_id: 'missing-plan',
222+
cache_type: 'exact',
223+
provider: 'openai',
224+
model: 'gpt-5',
225+
status_code: 200
226+
})),
227+
JSON.stringify({
228+
showGuardrails: false,
229+
guardrailLabel: '',
230+
showCache: true,
231+
cacheNodeClass: 'ep-node-cache-hit',
232+
cacheConnClass: 'ep-conn-hit',
233+
cacheStatusLabel: 'Hit (Exact)',
234+
aiLabel: 'openai',
235+
aiSublabel: 'gpt-5',
236+
aiConnClass: 'ep-conn-dim',
237+
aiNodeClass: 'ep-node-ai-skipped',
238+
responseConnClass: 'ep-conn-dim',
239+
responseNodeClass: 'ep-node-endpoint-success',
240+
showAsync: true,
241+
showUsage: false,
242+
showAudit: true
243+
})
244+
);
245+
});
246+
216247
test('executionPlanSubmitMode switches to save when an active workflow already matches the selected scope', () => {
217248
const module = createExecutionPlansModule();
218249
module.executionPlans = [
@@ -384,6 +415,27 @@ test('openExecutionPlanCreate drops blank guardrail steps instead of hydrating t
384415
);
385416
});
386417

418+
test('executionPlanSourceGuardrails keeps step zero but drops negative and fractional steps from previews', () => {
419+
const module = createExecutionPlansModule();
420+
421+
assert.equal(
422+
JSON.stringify(module.executionPlanSourceGuardrails({
423+
plan_payload: {
424+
guardrails: [
425+
{ ref: 'zero-step', step: 0 },
426+
{ ref: 'fractional', step: 1.5 },
427+
{ ref: 'negative', step: -1 },
428+
{ ref: 'valid', step: 10 }
429+
]
430+
}
431+
})),
432+
JSON.stringify([
433+
{ ref: 'zero-step', step: 0 },
434+
{ ref: 'valid', step: 10 }
435+
])
436+
);
437+
});
438+
387439
test('editing a cloned workflow preserves retired provider and model options', () => {
388440
const module = createExecutionPlansModule();
389441
module.models = [
@@ -587,6 +639,10 @@ test('buildExecutionPlanRequest preserves fallback state for hydrated plans even
587639
FEATURE_FALLBACK_MODE: 'off'
588640
};
589641
module.executionPlanFormHydrated = true;
642+
module.executionPlanHydratedScope = {
643+
scope_provider: 'openai',
644+
scope_model: 'gpt-5'
645+
};
590646
module.executionPlanForm = {
591647
scope_provider: 'openai',
592648
scope_model: 'gpt-5',
@@ -614,6 +670,42 @@ test('buildExecutionPlanRequest preserves fallback state for hydrated plans even
614670
);
615671
});
616672

673+
test('buildExecutionPlanRequest omits hidden fallback when a hydrated workflow is retargeted to a new scope', () => {
674+
const module = createExecutionPlansModule();
675+
module.executionPlanRuntimeConfig = {
676+
FEATURE_FALLBACK_MODE: 'off'
677+
};
678+
module.executionPlanFormHydrated = true;
679+
module.executionPlanHydratedScope = {
680+
scope_provider: 'openai',
681+
scope_model: 'gpt-5'
682+
};
683+
module.executionPlanForm = {
684+
scope_provider: 'openai',
685+
scope_model: 'gpt-4o-mini',
686+
name: 'OpenAI GPT-4o mini',
687+
description: 'Retargeted hidden fallback should not carry over',
688+
features: {
689+
cache: true,
690+
audit: true,
691+
usage: true,
692+
guardrails: false,
693+
fallback: true
694+
},
695+
guardrails: []
696+
};
697+
698+
assert.equal(
699+
JSON.stringify(module.buildExecutionPlanRequest().plan_payload.features),
700+
JSON.stringify({
701+
cache: true,
702+
audit: true,
703+
usage: true,
704+
guardrails: false
705+
})
706+
);
707+
});
708+
617709
test('validateExecutionPlanRequest rejects negative guardrail step numbers', () => {
618710
const module = createExecutionPlansModule();
619711
const payload = {

internal/admin/dashboard/templates/audit-pane.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<section class="audit-pane">
33
<div class="audit-pane-head">
44
<h4 x-text="{{.}}.title"></h4>
5-
<button class="pagination-btn" @click.prevent="copyAuditJSON({{.}}.copyBody, $event)">Copy Body</button>
5+
<button type="button" class="pagination-btn" @click.prevent="copyAuditJSON({{.}}.copyBody, $event)">Copy Body</button>
66
</div>
77
<div class="audit-pane-block" x-show="{{.}}.showErrorMessage">
88
<h5>Error Message</h5>

internal/admin/dashboard/templates/pagination.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
<div class="pagination" x-show="{{.}}.total > 0">
33
<span class="pagination-info" x-text="'Showing ' + ({{.}}.offset + 1) + '-' + Math.min({{.}}.offset + {{.}}.limit, {{.}}.total) + ' of ' + {{.}}.total"></span>
44
<div class="pagination-buttons">
5-
<button class="pagination-btn" :disabled="{{.}}.offset === 0" @click="{{.}}PrevPage()">Prev</button>
6-
<button class="pagination-btn" :disabled="{{.}}.offset + {{.}}.limit >= {{.}}.total" @click="{{.}}NextPage()">Next</button>
5+
<button type="button" class="pagination-btn" :disabled="{{.}}.offset === 0" @click="{{.}}PrevPage()">Prev</button>
6+
<button type="button" class="pagination-btn" :disabled="{{.}}.offset + {{.}}.limit >= {{.}}.total" @click="{{.}}NextPage()">Next</button>
77
</div>
88
</div>
99
{{end}}

0 commit comments

Comments
 (0)