Files
bee/audit/internal/webui/page_scenario.go
T
Mikhail ChusavitinandClaude Sonnet 5 d108df7fe9 platform/webui: add scenario description, show it in the Scenario page's list
ScenarioSpec gains an optional "description" field. Listing (both
ListLocalScenarioFiles and ListScenarioFilesOnRemovableMedia, via the new
scenarioDescription helper) reads it out of each file without requiring
full ParseScenarioJSON validation to succeed, so a listing never hides a
scenario over an unrelated validation issue. The webui Scenario page now
renders Name/Description/Found-on/Run instead of just Name/Found-on — a
bare filename rarely tells anyone but the author what a scenario actually
does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 19:28:07 +03:00

92 lines
3.8 KiB
Go

package webui
// renderScenario renders the "6. Scenario" page: every scenarios/*.json
// runnable via platform.System.ReadScenario — the ones shipped with the
// image (platform.LocalScenariosDir) plus anything found on mounted
// removable media (e.g. the blackbox USB stick) — each runnable with one
// click. A run is enqueued as a normal Task (target "scenario") —
// progress/logs live in Tasks like every other SAT pack, no separate
// live-output UI needed here.
//
// See audit/internal/platform/scenario.go for the scenario file format and
// scenarios/README.md for how to add one (shipped in the image, or dropped
// on the USB stick for a one-off/air-gapped run).
func renderScenario(opts HandlerOptions) string {
return `<p style="color:var(--muted);font-size:13px;margin-bottom:16px">
Scripted, ad-hoc test scenarios — commands plus background samplers (IPMI
sensors, nvidia-smi, ...), described in a small JSON file instead of a
hardcoded test. The list below includes scenarios shipped with this image
as well as any <code>&lt;name&gt;.json</code> dropped under
<code>scenarios/</code> on a mounted removable drive (e.g. the same USB
stick already plugged in for blackbox) — no rebuild needed for the
latter. Runs are enqueued like any other task; watch progress in
<a href="/tasks">Tasks</a>.
</p>
<div class="card">
<div class="card-head">
<span>Available scenarios</span>
<button class="btn btn-sm btn-secondary" type="button" onclick="scenarioRefresh()">&#8635; Refresh</button>
</div>
<div class="card-body">
<div id="scenario-list"><p style="color:var(--muted);font-size:13px">Loading...</p></div>
</div>
</div>
<script>
function scenarioRefresh() {
const list = document.getElementById('scenario-list');
list.innerHTML = '<p style="color:var(--muted);font-size:13px">Loading...</p>';
fetch('/api/scenario/list')
.then(r => r.json())
.then(files => {
if (!files || !files.length) {
list.innerHTML = '<p style="color:var(--muted);font-size:13px">No scenarios found — none shipped with this image and none under scenarios/ on mounted removable media.</p>';
return;
}
let html = '<table class="table"><thead><tr><th>Name</th><th>Description</th><th>Found on</th><th></th></tr></thead><tbody>';
for (const f of files) {
const desc = f.description ? escapeHTML(f.description) : '<span style="color:var(--muted)">—</span>';
html += '<tr><td style="white-space:nowrap">' + escapeHTML(f.name) + '</td>'
+ '<td style="font-size:12px;max-width:360px;color:var(--muted)">' + desc + '</td>'
+ '<td style="color:var(--muted);font-size:12px;white-space:nowrap">' + escapeHTML(f.device) + '</td>'
+ '<td style="white-space:nowrap"><button class="btn btn-primary" onclick="scenarioRun(' + JSON.stringify(f.name) + ', this)">&#9654; Run</button></td></tr>';
}
html += '</tbody></table>';
list.innerHTML = html;
})
.catch(err => {
list.innerHTML = '<p style="color:var(--crit-fg)">Failed to list scenarios: ' + escapeHTML(String(err)) + '</p>';
});
}
function escapeHTML(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function scenarioRun(name, btn) {
btn.disabled = true;
btn.textContent = 'Starting...';
fetch('/api/scenario/run', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name})
})
.then(r => {
if (!r.ok) return r.text().then(t => { throw new Error(t || r.statusText); });
return r.json();
})
.then(() => { window.location.href = '/tasks'; })
.catch(err => {
btn.disabled = false;
btn.textContent = 'Run';
alert('Failed to start scenario: ' + err.message);
});
}
scenarioRefresh();
</script>`
}