Files
bee/audit/internal/webui/page_scenario.go
mchusandClaude Sonnet 5 8a91f0f783 fix(webui): repair broken scenario Run button onclick, dedupe build.sh overlay staging
- page_scenario.go: onclick built via JSON.stringify() embedded raw double
  quotes inside a double-quoted HTML attribute, truncating the attribute so
  the click handler never compiled; pass the name through an escaped
  data-scenario-name attribute instead.
- build.sh: overlay staging rsyncs (OVERLAY_DIR->stage, stage->includes.chroot)
  ran without --delete, so a scenario removed from the repo (a9924b0) stayed
  baked into every ISO built from the persistent stage cache since — the
  "second script" in the Scenario page's list.
- blackbox: rewritten around a deterministic local zip + incremental
  patch-the-changed-suffix onto removable media, instead of walking/copying
  ~90 files through a synchronous ntfs-3g FUSE mount every cycle. journalctl
  captures are now "--since last sync" (were "--since boot", growing with
  uptime) and metrics.db is excluded (was copied whole every cycle).
- scenario: nvbandwidth-acs-ab now escalates GPU count (same-socket pair,
  other socket's pair, one cross-socket pair, all GPUs) under each ACS state
  instead of always running all 6 GPUs at once, using a new `bee
  gpu-bandwidth-groups` subcommand that discovers socket layout from
  `nvidia-smi topo -m` at runtime — gpu_indices is host-specific, so this
  can't be baked into the scenario file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 18:11:24 +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" data-scenario-name="' + escapeHTML(f.name) + '" onclick="scenarioRun(this.dataset.scenarioName, 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>`
}