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>
92 lines
3.8 KiB
Go
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><name>.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()">↻ 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)">▶ 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>`
|
|
}
|