Each PSU is its own card, coloured by its own status — a failed unit
goes red on its own instead of dragging a single grouped card down —
and shows input voltage + draw (measured output/input, else nameplate
rating). Cards click through to the PSU detail modal and carry data-psu
so the shared topoLiveScript refreshes their wattage from
/api/metrics/latest on the same 5s poll as the fan tiles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
Each fan tile carries data-fan / data-ceil / data-measured and a small
script polls /api/metrics/latest every 5s — the metrics collector's own
sampling period, served from memory with no extra BMC call — updating
each tile's spin rate, duty fill and tooltip in place. Polling faster
would only re-read identical numbers; sampling the BMC faster would
choke it under load. FanReading gains json tags for the endpoint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
It is a platform cooling test (CPU + memory always; GPU only as extra
heat when present), not a GPU test — so it belongs with the other
whole-box checks, not in the NVIDIA section. Still Load-page only, via
the new renderLoadOnlySATCard (inverse of renderCheckOnlySATCard).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
- GPU load switches from bee-gpu-burn (compute burn, ~88% TDP) to
dcgmproftester -t 1004 / targeted_power via
resolveBenchmarkPowerLoadCommand — the same engine Power/Thermal Fit
uses and the hottest sustained NVIDIA load we have, so fans are
actually pushed toward their ceiling.
- Sample loop is now IPMI-hang-proof: every ipmitool read is time-boxed
in an abandonable goroutine, and the poll interval backs off
geometrically (1s→30s) when reads are slow, tightening again on
recovery. A plateau is only trusted while telemetry is healthy;
degraded runs ride out to MaxLoadSec. Summary gains fan_samples /
telemetry_degraded. Drops the per-second nvidia-smi+power+cpu-temp
sampling from the hot loop.
- Dead code removed: FanStressRow, GPUStressMetric, sampleFanStressRow,
sampleGPUStressMetrics, WriteFanStressCSV/WriteFanSensorsCSV,
analyzeMaxTemp, sampleSystemPowerResolved.
Topology fan tiles:
- size encodes the fan's ceiling RPM (its class), not current speed;
coloured fill rising from the bottom encodes live duty cycle
(current / ceiling), shown only when the ceiling was measured.
- glyph spin rate now maps absolute RPM into a human-perceptible band
(fanSpinPeriodSec: 2.2s/turn at <=1000 RPM, 0.35s at >=13000).
- the "N fans · N OK · tile size ∝ …" caption line is gone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
Repurpose the previously-unwired RunFanStressTest into RunFanCheck, a
Load-tier SAT test that drives stressapptest (CPU+memory) and, when a GPU
is present, a GPU burn to 100% simultaneously, then watches every fan
until none has climbed for ~60s. The observed peak RPM per fan is the
"ceiling"; it is persisted through the existing fan-observation store.
MSI G4201 / AMI MegaRAC exposes no host-side fan force (every OEM IPMI
command returns 0xc1; Redfish Thermal is GET-only), so load-driven ramp
is the closest safe equivalent. See
bible-local/decisions/2026-09-04-fan-ceiling-check.md.
- platform.ResolveFanMaxRPM: per-fan max with fallback (persisted peak ->
peer peak -> current RPM), resolved in platform, not the view.
- platform.ErrTestNotApplicable: no load source or no fan sensors ->
task lands as cancelled ("not applicable"), never failed, so an
engineer never sees a false red. executeTaskWithOptions maps the
sentinel; finalizeTaskForResult honours a pre-set TaskCancelled.
- Verdict FAIL only for a fan at 0 RPM / IPMI cr-nr under load.
- /topo: one small spinning square per fan, sized by RPM / resolved max,
clickable through to a new "fan" component-detail type; per-fan status
recorded to the component-status DB from the fan SAT summary.
- Wiring: /api/sat/fan/run route, "fan" task target, Load-page card,
stress-mode Run All.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
Every GPU-selection picker (Load/SAT, Burn, Benchmark) now renders the
card serial: "GPU N — <model> · <mem> MiB · sn: <serial>". The serial is
monospace and the digits that differ across the listed GPUs (common
prefix/suffix stripped) are emphasised so operators can tell cards apart.
Row markup was duplicated across three pages (and twice within
page_validate.go). Consolidated into a single module,
internal/webui/gpu_picker.go: beeGpuPicker.render({...}) builds every row;
gpuPickerCSS/gpuPickerJS are injected once by layoutHead/renderPage. Pages
keep their own selection-note text and multi-GPU toggles but no longer
hand-build <label> markup.
ListNvidiaGPUs() adds the serial via nvidia-smi --query-gpu=...,serial;
N/A is normalised to empty. Serial flows to the client unchanged through
the existing /api/gpu/nvidia JSON response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AudE3Q2nd9kxxVuxPKcTho
Every sample was stamped with time.Now(), so a timezone switch or NTP
step punched a multi-hour gap into the series: old points collapsed to
the left edge, new points bunched at the right, joined by one diagonal.
Stamp rows from a monotonic seqClock instead — seeded once from the wall
clock (or the newest persisted row) and thereafter advanced only by the
monotonic elapsed time between writes. Rebase Downsample/Prune on the
newest sample rather than time.Now() so a clock step just before the
hourly compaction can't drop fresh data.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GofKhuF9xQHaz3UFfncR6D
The techdump — the only place the IPMI FRU / Elabel identity (Product Name
= server model, serials, part numbers) is recorded — only ran under LiveCD.
Run it on every audit so `bee audit` on an installed host captures it too.
Also add `ipmitool mc info` and `ipmitool lan print` to the techdump, and
pin `ipmitool fru print 0` to FRU device 0 to match the Export Tools
"FRU / Elabel" editor exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rework the /topo diagram from side-by-side stacked cards into one tall
vertical bar per CPU socket with everything attached to it branching off
sideways (socket 0 left/branches right, socket 1 right/branches left).
Disks are now parented under the storage controller they physically hang
off (SATA/AHCI, SAS HBA, RAID) — itself a NUMA-affine PCIe device under one
socket — instead of a synthetic catch-all node. The disk->controller link
is read from a new storage-controllers.txt techdump
(platform.StorageControllerMapScript, a /sys/block walk); disks with no
resolvable controller fall back to an "Other" bar. No board/root node.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYjrG6bVmeG1Z2Wmc8kg3o
The bundle README is the bootstrap for an agent that knows nothing about
the layout; it listed neither the pcie-link (whole-bus link-retrain) nor
the tpm (read-only) SAT target, so a reader had no pointer to
tasks/bee-sat/pcie-link-* / tpm-* or their summary keys. Add a Fastest-
answers entry for each, refresh the component-status key examples, and add
a Signatures line for the bridge-side link-retrain false positive (bridge
DEGRADED at before==after==max while its downstream endpoint reads OK =
link at the device's native max, not a fault).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mx6AvuXcJNT7jx3jjWw4hc
The downstream-capability adjustment lowered f.MaxSpeed/f.MaxWidth to the
rate the endpoint supports, but the Degraded verdict still compared the
retrained speed against the stale local maxSpeed holding the bridge's own
uncapped capability. A Gen5 root port feeding a Gen4 HBA (or a Gen2 NIC)
was flagged DEGRADED even though the link ran at the fastest rate the pair
supports, failing the PCIe link check on healthy hardware.
Keep the local maxSpeed in sync with the peer-capped f.MaxSpeed so both
the pre-retrain and post-retrain verdicts use the real target.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mx6AvuXcJNT7jx3jjWw4hc
pcie-link's retrain check compared post-retrain speed against a port's
max capability without checking whether anything was actually seated
downstream. A switch downstream port with no card plugged in retrains
to zero lanes but still reports a reset-state speed, so it was
misreported as "retrained to Gen1, capable of Gen5" — indistinguishable
from a real degraded link.
Now a port that was already empty before the retrain is skipped (same
treatment as a disabled device: legitimately unpopulated slots are
common and shouldn't fail the check), while a device that answered
before the retrain and disappeared right after it still fails, with an
honest "no device detected downstream" message instead of a fabricated
speed reading.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pcie-nvidia-link.txt, pcie-nvidia-link-under-load.txt, and
kernel-aer-nvidia.txt only ever lived in supportBundleCommands, which
exclusively runs inside the on-demand "Download Support Bundle" web UI
action. The blackbox USB auto-sync worker never calls that function -
it only mirrors whatever CaptureTechnicalDump already wrote into the
live export tree at boot. So these three files were structurally
unreachable from a blackbox pull no matter how fresh the build was;
earlier analysis of a real blackbox misattributed their absence to
build/version drift instead.
Move the underlying scripts into shared exported constants
(platform.PCIeNvidiaLinkScript, PCIeNvidiaLinkUnderLoadScript,
KernelAERNvidiaScript) and add them to techDumpNvidiaCommands, so
CaptureTechnicalDump captures all three once at boot (bee-audit is
oneshot, so the ~8s bee-gpu-burn cost for the under-load sample is a
one-time boot cost, not a per-sync-cycle one) alongside the existing
nvidia-smi-* dumps. supportBundleCommands still re-runs the same
scripts on demand for a fresher sample - that's intentional, not a
duplicate to clean up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NVIDIA GPUs deliberately downclock PCIe to Gen1 at idle for power
saving, and applyPCIeLinkSpeedWarning fired on every idle collector
pass regardless - since component-status DB records never downgrade
(Record() only ever raises severity), one boot-time idle sample
permanently pinned pcie:gpu:nvidia to Warning for the rest of the
session even after every load-bearing GPU SAT test passed clean. Two
prior fixes (nvidia-smi-sourced link speed, pcie_aspm=off boot flag)
didn't hold up against this hardware/driver combination - see
bible-local/decisions/2026-08-24-pcie-gpu-gen1-idle-warning.md for the
full history.
Rather than add a downgrade path, stop writing an unverified status in
the first place: parseLspciDevice no longer calls
applyPCIeLinkSpeedWarning on the idle path. LinkSpeed/MaxLinkSpeed stay
populated as plain descriptive fields; only a verified-under-load
caller may now turn them into a status verdict.
Two new SAT targets provide that verified signal:
- pcie-link (platform/pcie_link_check.go): forces every enabled PCIe
device - not just GPUs - to retrain via the PCIe spec's Link Control
"Retrain Link" bit, then compares the negotiated speed against the
device's max. Covers NICs/HBAs/switches that have no bee-gpu-burn
equivalent load tool. Classifies by PCI class code + vendor ID, not
name substrings. Routes gpu_nvidia/gpu_amd/other sub-verdicts into
their own component-status keys so a degraded NIC never reads as a
GPU fault.
- nvidia-pcie-bandwidth (platform/nvidia_pcie_bandwidth.go): drives
real host<->device traffic via dcgmi diag -r nvbandwidth and
resamples link speed immediately after, independent of nvbandwidth's
own pass/fail.
Both wired into the task queue/webui the same way as nvidia-config
(routes, dispatch, priority, Validate page cards, Run All Check SAT).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`ipmitool fru edit` frequently exits non-zero after a field resize
(shrinking/growing a string shifts every later offset in the FRU
record) even though the write itself landed correctly on the BMC -
the failure is in ipmitool's own post-write re-parse, not the write.
bee was treating that exit code as authoritative and marking the task
failed even when the field was actually written.
runIPMIFRUWriteTask now re-reads the FRU on a non-zero exit and
compares the target field's actual value before deciding the task
failed, logging which path it took either way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A CPU that had thermally throttled (sysfs thermal_throttle counter >
0) still reported status "OK" everywhere: dmidecode-derived CPU status
only distinguishes populated/enabled/disabled and never looked at the
throttle flag the collector already recorded next to it, and neither
SAT path meant to catch this actually could:
- The routine "cpu" SAT pack (RunCPUAcceptancePack) only checked
lscpu/sensors/stress-ng exit codes — stress-ng exits 0 whether or
not the CPU throttled while running it, so an 89°C/throttled CPU
right after a "successful" run still showed cpu:all as OK in
component-status.json.
- The more thorough platform-stress test already detected throttling
and fan-spindown correctly, but wrote its verdict as "Overall: FAIL
— ..." with no "=", which parseSATKV can't parse — so even a real
detected throttle event never reached the component-status DB.
Fixes:
- cpu_telemetry.go: escalate a CPU's status to Warning (only-escalate,
same severity ranking already used elsewhere) when Throttled is set.
- sat.go: add a before/after thermal-throttle-counter check job around
the "cpu" pack's stress-ng run, so a throttle event during the run
fails that job and (via the existing FAILED->Warning DB mapping)
flips cpu:all to Warning.
- platform_stress.go: emit a machine-readable overall_status= line
alongside the human-readable verdict so platform-stress results
actually reach ApplySATResultToDB.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dmesgSeverity classified a kernel log line as Critical via plain
strings.Contains, without word boundaries, so common words that merely
contain a keyword as a substring false-positive — "disabled by
default" was flagged Critical because "default" contains "fault"
(de-fault). Found in a support bundle where 140/238 event_logs entries
came back Critical, most of them harmless boot messages (module load
notices, "... is initialized", "disabled by default"), drowning out
genuinely critical entries (Xid, AER, ECC) in the same list.
Switch to the same \b-bounded regexes already used for the capture
patterns above it in this file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An NVMe SSD passed through a RAID/HBA as /dev/sdX (not /dev/nvmeX) gets
smartctl'd over the SCSI protocol, which reports its model in
scsi_model_name/scsi_vendor/scsi_product instead of the ATA/NVMe
model_name field. smartctlInfo only mapped model_name, so the drive's
model was silently dropped from reanimator.json even though smartctl
ran successfully and the serial number came through fine — caught via
a support bundle where lsblk showed "SSSTC CA6-8D1024" for a drive but
reanimator.json's storage entry had no model at all.
Add scsi_model_name/scsi_vendor/scsi_product to smartctlInfo and fall
back to them, in that order, when model_name is empty.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A wedged GPU (fallen off the bus, needs a physical power-cycle) makes
nvidia-smi/nvidia-bug-report.sh/dcgmi hang indefinitely, so building a
support bundle on such a host never finished and the "Download Support
Bundle" button spun on "Building..." forever with no way to recover
short of restarting the service.
Give every subprocess this file shells out to a bounded context.
nvidia-bug-report.sh gets a longer timeout since it legitimately takes
tens of seconds on multi-GPU boxes even when healthy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
statusSeverity was byte-for-byte duplicated in collector/contract.go and
app/sat_overlay.go (same switch, same comment). Export it as
collector.StatusSeverity and drop the app-package copy.
page_topo.go inlined the same GPU/NIC/RAID class switch as its own
pcieDeviceKind function in one spot — call the function instead.
page_validate.go had its own copy of pages.go's firstNonEmpty (all call
sites already pre-trim their inputs via validateTrimPtr, so the extra
TrimSpace was redundant) — drop the duplicate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
matchesGPUVendor (sat_overlay.go) matched any PCIe DeviceClass containing
the substring "Controller" instead of the actual GPU classes, so a
same-vendor non-GPU device (e.g. an NVIDIA-branded NIC/storage controller)
with a degraded PCIe link surfaced as a pcie:gpu:<vendor> alarm on the
Hardware Summary/webui.
isGPUDevice (app_format.go) had the same bug via a different path: an
"any NVIDIA-vendor device is a GPU" fallback that could inflate the
"GPU: N x <model>" audit summary line with non-GPU companion devices
(NVSwitch/NVLink bridges, etc).
Both now defer to collector.IsGPUClass (exported from the previously
unexported isGPUClass), the canonical exact-match classifier already used
by amdgpu.go — removing the duplicated/incorrect class checks instead of
reimplementing them. Also dedup two more locally-duplicated classifiers
within webui (which doesn't import collector by convention): NIC-class
matching (pages.go/page_topo.go) and GPU-class matching in
page_validate.go now share one copy per package instead of being
reimplemented per file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
buildNvidiaTaskSet only treated ParallelGPUs as a reason to keep all
selected GPUs in one task, so ramp-up (StaggerGPUStart) fell through
to per-model task splitting. When that split left a single GPU in a
task, resolveNvidiaRampPlan silently returned StaggerSeconds=0,
making the ramp-up toggle a no-op.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pcie:gpu:<vendor> in the component-status DB (what the Hardware
Summary/webui "check passed" status reads) was only ever written by SAT
diag results, and none of the nvidia/nvidia-config/nvidia-interconnect/
nvidia-bandwidth SAT jobs check PCIe link speed. So a real Gen1/Gen4
degradation the collector already flagged as Warning in the hardware
snapshot never reached the DB-backed status, and the audit kept
reporting "OK" despite GPUs training at Gen1 with BMC showing x16.
Add writePCIeGPUStatusesToDB, mirroring the existing PSU write-through,
so RunAudit pushes the collector's PCIe status into the DB alongside
SAT results.
Also add export/gpu/pcie-nvidia-link-under-load.txt to the support
bundle: NVIDIA drivers deliberately downclock PCIe at idle to save
power and re-train to full speed under load, so an idle Gen1 reading
alone can't distinguish real hardware/riser degradation from normal
power management. Resample the same sysfs link attributes while
bee-gpu-burn is actively loading the GPUs so both bundles ship
together.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds POST /api/system/time-sync so the dashboard's "Sync with this
browser" button can set the host's date, time, and IANA timezone from
the client's browser — useful for appliances with no NTP/network
access to keep their own clock straight. Disables NTP before writing
the manual time so it doesn't get immediately overwritten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nvidia-bug-report.sh appends .gz to --output-file when gzip is available,
which silently produced empty nvidia-bug-report.txt in support bundles
(cat looked for the uncompressed name that never existed).
Also: a GPU that falls off the PCIe/NVLink bus (Xid 79) or gets flagged
for Node Reboot Required (Xid 154) mid-SAT-run left every downstream test
failing with generic, unrelated-looking errors (CUDA "unknown error",
"unable to determine device handle") with no indication the GPU needed a
physical power-cycle to recover. Detect these codes from SAT run logs and
surface a plain-English "physical reboot required" message in the task's
failure detail, the persisted component-status DB, a dashboard banner on
the Hardware Summary card, and topology diagram GPU-node severity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
enrichPCIeWithNVIDIAData unconditionally overwrote dev.Status after
collectPCIe() had already flagged a Warning/Critical (e.g. PCIe link
speed degraded), so a clean ECC/remap/reset readout silently downgraded
that finding back to OK while leaving the stale ErrorDescription behind.
Add a severity-ordered merge (OK/Unknown < Warning < Critical) shared
via mergeDeviceStatus in contract.go, and route both the NVIDIA status
calculation and the driver-unavailable fallback through it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On HGX B200 (Kaytus KR9288-X3) NVSwitch is managed via HMC/ERoT and never
appears on host lspci as 10de:0680, so the old check always returned
"not present" and silently skipped fabricmanager — leaving the fabric
untrained and CUDA/NCCL/dcgmi failing with "system not yet initialized"
on otherwise-healthy hardware. bee-check-nvswitch now checks, in order,
nvidia-smi's GPU Fabric GUID, /proc/driver/nvidia-nvswitch/devices, and
finally the legacy host-PCIe scan for older HGX generations.
- 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>
`bee run <scenario>` called platform.System.RunScenario directly with an
empty base dir, which defaults to /var/log/bee-sat — a tree the blackbox
worker does not mirror (it mirrors DefaultExportDir, /appdata/bee/export).
So a scenario launched from the CLI wrote its logs where blackbox never
looked: the sync-bracket hooks fired and flushed the export dir, but the
scenario's own output never reached the USB stick. The web-UI path was
unaffected because it goes through App.RunScenario, which defaults the base
dir to DefaultSATBaseDir (under the export dir).
Route the CLI through App.RunScenario too — matching the doc comment that
already claimed both paths did — so `bee run` output lands under the mirrored
export tree and shows up in the blackbox capture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Scenario page's Run button enqueued a task with target "scenario" that
never launched any load: the queue runs each task in an external
bee-worker subprocess (RunPersistedTask -> executeTaskWithOptions in
task_runner.go), whose target switch had no "scenario" case, so the task
died with "unknown target: scenario". The case had only been added to
taskQueue.runTask's switch in tasks.go — a stale copy exercised solely by
unit tests, which is why the tests passed while the button did nothing.
Add the "scenario" case (ReadScenario -> ParseScenarioJSON -> RunScenario)
to executeTaskWithOptions, and collapse runTask into a thin delegate to it
so there is a single target dispatch. The old runTask switch had already
drifted into a subset (missing nvme-format, *-write, raid-*, nvidia-config,
...); removing it eliminates the divergence that hid this bug. Also drop the
now-orphaned taskQueue.statusDB helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
platform.LocalScenariosDir (/usr/share/bee/scenarios, populated from
iso/overlay/usr/share/bee/scenarios/ by build.sh's overlay rsync) is now
checked before removable media for both `bee run <name>` and the "6.
Scenario" page — a scenario shipped with the image works with no USB
stick required. ReadScenario/ListAvailableScenarios merge local + USB;
the removable-media-only functions from the previous commit are kept
as-is (still used directly where that's actually what's wanted) rather
than renamed out from under existing callers/tests.
iso/overlay/usr/share/bee/scenarios/nvbandwidth-all-gpu-power-watch.json
is a copy of scenarios/nvbandwidth-all-gpu-power-watch.json — the two
aren't auto-synced (documented in scenarios/README.md), so shipping a
scenario baked-in means checking it into both places.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Exposes the scenario engine (platform.System.RunScenario, added earlier)
in the web UI instead of only the `bee run` CLI: a new nav item lists every
scenarios/*.json found on mounted removable media (GET /api/scenario/list)
and runs one with a click (POST /api/scenario/run), enqueued as a normal
Task with target "scenario" — progress/logs live in Tasks like any other
SAT pack, no separate live-output UI needed.
- app.go: satRunner gains RunScenario, exportManager gains
ListScenarioFilesOnRemovableMedia/ReadScenarioFromRemovableMedia — both
already implemented on platform.System, just newly exposed through App.
- webui/tasks.go: taskParams.ScenarioName; runTask's "scenario" case reads
the file from removable media, parses it, and runs it.
- webui/page_scenario.go: the page itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Investigating the CG480-S6053 reboot needed a way to run an ad-hoc load
(nvbandwidth across a specific GPU set) while sampling IPMI/nvidia-smi
telemetry in the background — without hardcoding a one-off test into the
SAT pack code for a single investigation.
- audit/internal/platform/scenario.go: ScenarioSpec/ScenarioJob (JSON,
no new dependency) + System.RunScenario. "command" jobs run sequential
or parallel (per-job "parallel" flag); "sampler" jobs run concurrently
in the background on their own interval until every command job
finishes or the scenario's timeout elapses. "{{gpus}}" in a command's
cmd is substituted from that job's gpu_indices. Command jobs are wired
through the same satJobBoundaryHook/satSyncBracketHook seams the SAT
job runner uses, so a scenario run gets the same durability treatment
(evidence that a risky command started/finished reaches blackbox before
a possible crash, not just whatever streamed to the RAM-backed export
dir).
- export.go: ReadScenarioFromRemovableMedia mounts each removable target
looking for scenarios/<name>.json — an air-gapped engineer can author a
scenario elsewhere, drop it under scenarios/ on the same USB stick
already plugged in for blackbox, and run it with no network path onto
the host.
- cmd/bee: new `bee run <file.json|name>` (bare name = looked up on
removable media); `bee scenario run <arg>` kept as a longer alias.
- scenarios/nvbandwidth-all-gpu-power-watch.json: the scenario that
reproduced the actual reboot (full nvbandwidth across all GPUs, which
crashed, vs. clean per-socket passes), with IPMI sensor + GPU power/temp
sampling for a power-delivery correlation check.
Also: webui/page_topo.go — the /topo page's component-status-detail modal
(GET /api/component-detail/{type}) showed "No status data recorded yet"
for any component type ComponentStatusDB has no history for yet (e.g. GPU
before a SAT run this boot), even though the topology card for the same
component already showed "N OK" from the audit inventory snapshot.
inventoryFallbackRecords now synthesizes records from that same inventory
snapshot when StatusDB is empty, using the same device classifiers
(isGPUDeviceClass etc.) and severity mapping (classifyTopoSeverity) the
topology card itself uses, so the two views never disagree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
copyPath/copyPathFiltered treated any os.Stat/os.ReadDir/os.Open error on a
source entry as fatal, aborting the entire tree copy. A source path is read
from the live export dir while bee's own task runner concurrently renames
task directories (e.g. "_pending" -> "_done") — an entry present in the
parent's os.ReadDir a moment ago disappearing by the time it's individually
Stat'd/Open'd is an expected race, not a real failure.
Seen on a real crash bundle: blackbox got stuck in status "degraded" from
early in the run (first hit during the CPU pack, well before the GPU tests)
after exactly this race, and every syncBracket wait then timed out for the
rest of the run — the discovery/wait plumbing from the previous fix works,
but had nothing working under it to wait on. The target also accumulated
stale "_pending" copies alongside "_done" ones with no cleanup, though
fixing that dedup is left for a follow-up.
os.IsNotExist(err) now skips the vanished entry instead of propagating.
Added regression tests simulating the race directly (copy_path_race_test.go).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the flat export/ + techdump/ + system/ + systemd/ layout (two
same-named-but-different techdump/ dirs, disk reports duplicated between a
SAT run dir and techdump/) with one shared categorizeExportTree used by both
BuildSupportBundle and the blackbox USB mirror:
- export/{cpu,memory,storage,gpu,network,platform}/ — raw vendor-tool
output grouped by device type, plus export/reanimator.json ready to POST
to Reanimator's /ingest/hardware endpoint.
- status/ — computed diagnosis (component-status.json, runtime-health.*,
and metrics.db, previously missing from every bundle entirely).
- tasks/ — bee's own task-run bookkeeping (bee-sat/, bee-bench/, task
reports, service logs) where duplication with export/ is expected.
- livecd/{gui,host}/ — live-boot/kiosk-session-only diagnostics, kept out
of the hardware-facing tree.
Drops the redundant disk-report mirror write in RunStorageAcceptancePack
and the now-dead syncDirectoryTree/removeMissingPaths; adds a size+mtime
skip in copyPath so the blackbox mirror doesn't rewrite unchanged files
every cycle. README.md rewritten to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- gpuBandwidthSocketGroups: a single GPU whose NUMA node fails to resolve
no longer collapses the whole per-socket nvbandwidth split into one
fallback pass — it now folds into the last resolved group instead,
preserving isolation for the sockets that did resolve.
- blackbox discoverMarkedTargets: skip mounting/unmounting devices that
already have a running worker on every 2s discovery tick. This was
observed hammering the same USB target continuously (mount+unmount
every ~2s for the whole session) and contending with the worker's own
sync cycle, plausibly explaining multi-minute sync cycles seen on a
real crash bundle.
- syncFilesystem now calls syscall.Sync() directly instead of spawning
/bin/sync per copied file; blackbox mounts removable targets with
-o sync so writes are durable without relying on the app-level sync as
the primary mechanism.
- New platform.SetSyncBracketHook / satJob.syncBracket: blocks (with a
bounded timeout) on blackbox actually reaching removable media right
before and right after a diagnostic's real load step (nvbandwidth,
memtester, stress-ng, dcgmi diag, nccl, smartctl/nvme self-test...),
instead of only firing a fire-and-forget kick after the job's own log
file is written. A crash mid-load now has durable evidence the load
started, not just whatever streamed to the RAM-backed export dir before
blackbox's next scheduled cycle.
Found investigating a real support bundle where blackbox's last
successful sync (19:55:25) predated both the previous job finishing and
the crashing nvbandwidth job starting (19:56:57) — none of the crash
window ever reached durable media.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A crash mid-command (e.g. the nvbandwidth reboot) previously lost that
job's entire output: streamExecOutput only buffered stdout/stderr in
memory and the job's log file was written once, after the process
exited. It now also streams each line straight to that file as it
arrives, so whatever printed before a crash survives.
Root filesystem here is a tmpfs overlay (toram boot), so the only real
persistence boundary is blackbox's mirror to removable media, not the
local write itself. platform.SetJobBoundaryHook lets app wire a touch
of a small kick-file after each job's output is written; blackboxWorker
now polls that file's mtime alongside its normal adaptive timer and
syncs immediately on a kick instead of waiting out the current flush
period (up to 30s).
On multi-socket systems, run the NVIDIA bandwidth diagnostic once per
CPU socket before the all-GPU pass, so a crash confined to the all-GPU
run (with clean per-socket passes preceding it) isolates a cross-socket
peer-to-peer fault instead of leaving it conflated with a general
GPU/PCIe issue. Single-socket systems keep the original one-pass shape.
Also expand the support-bundle README with reference notes distilled
from a real analysis pass (BMC clock drift, "0/empty" tool output
meaning absent hardware rather than a fault, timestamp-matching before
assigning causality, and a normal-power-cycle SEL signature), plus a
step-by-step recipe for diagnosing an unexpected reboot/crash during a
specific test.
Memory used to render as one unattached row below the whole diagram
regardless of which socket it belonged to. schema.HardwareMemory has no
NUMANode field (unlike PCIe devices), so CPU affinity is instead read out of
the DIMM's own Locator string: either a CPU number encoded directly in it
("CPU0_DIMM_A1"), or — when the Locator has no CPU number of its own, e.g.
"DIMM000(A)" — a node number from Bank Locator ("_Node1_Channel0_Dimm0"),
read from the persisted dmidecode-type17.txt techdump the same way the
NVLink card already reads extra techdump for visualization only.
DIMMs that can't be attached to a column via either heuristic still fall
back to the old unattached "Memory" row so nothing silently disappears.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>