23c689aa5b
Ships all five phases of the deep-profile overhaul together. Runs now carry a profile (quick/deep/soak); every profile walks the same 11-stage order — Inventory → Firmware → SpecValidate → SMART → CPUStress → Storage → Network → Burn → GPU → PSU → Reporting — with only per-stage durations and concurrency scaled. Phase 1: profiles.ProfileRegistry loaded from vetting.yaml; runs.profile column + CreateWithProfile; threshold table + evaluator seeded per-run from the shared vetting.thresholds block; breach flips result at /sensor + /result. Phase 2: upgraded CPUStress (stress-ng --cpu-method=all --verify + EDAC/MCE poll), Storage (fio --verify=md5 + SMART start/end delta), Network (sustained iperf + /proc/net/dev deltas) with per-profile knobs from Deps. Phase 3: Burn super-stage with goroutine fan-out for CPU + memory + fio + iperf, PSU rails sampled across the Burn window, SensorMux (2 s flush, 500-sample cap) to absorb backpressure. Phase 4: Firmware stage + firmware_snapshots table; probes dmidecode (BIOS), ipmitool (BMC), ethtool -i (NIC), nvme (sysfs + id-ctrl), lspci (HBA), /proc/cpuinfo (microcode). spec.DiffFirmware folds into SpecValidate with pin-by-identifier and fan-out-across-component matching; mismatches park the run in FailedHolding. Phase 5: profile radio on the host start form, profile chip on the run header, Firmware section in the HTML report, coverage artifact uploaded from CI, agent/tests/fakes/ scaffold with Deps.LookPath seam + stress_ng and dmidecode example fakes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
190 lines
6.9 KiB
Plaintext
190 lines
6.9 KiB
Plaintext
package templates
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
|
|
"vetting/internal/model"
|
|
"vetting/internal/store"
|
|
)
|
|
|
|
// RunPageData is the full payload for /runs/{runID}. Host is resolved
|
|
// from Run.HostID so the breadcrumb + run actions (which post to
|
|
// /hosts/{hostID}/...) have the host context without a separate call.
|
|
// Stages/SubSteps/SpecDiffs drive the pipeline + active-step panels +
|
|
// diff list. DefaultStepStage is the stage name whose <details> opens
|
|
// on first render — running → failed → Reporting. HoldKeyPath is the
|
|
// on-disk path of the hold_key artifact, needed to print the ssh
|
|
// invocation in the hold banner. SpecDiffCritical is the count of
|
|
// unignored critical diffs shown in the header.
|
|
type RunPageData struct {
|
|
Host model.Host
|
|
Run model.Run
|
|
Stages []model.Stage
|
|
SubSteps []model.SubStep
|
|
SpecDiffs []model.SpecDiff
|
|
DefaultStepStage string
|
|
LogReplayByStage map[string]string
|
|
HoldKeyPath string
|
|
SpecDiffCritical int
|
|
}
|
|
|
|
// RunPage is the run-focused URL: pipeline + per-stage active-step panels
|
|
// + spec diffs + hold banner. Host metadata stays on /hosts/{id}; this
|
|
// page carries only run-scoped content so the operator can read one run
|
|
// without surrounding noise.
|
|
templ RunPage(d RunPageData) {
|
|
@Layout(fmt.Sprintf("%s — run #%d", d.Host.Name, d.Run.ID)) {
|
|
<section class="run-page" hx-ext="sse" sse-connect="/events">
|
|
<nav class="breadcrumb">
|
|
<a href="/">Dashboard</a>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<a href={ templ.SafeURL(fmt.Sprintf("/hosts/%d", d.Host.ID)) }>{ d.Host.Name }</a>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<span>{ fmt.Sprintf("run #%d", d.Run.ID) }</span>
|
|
</nav>
|
|
|
|
@RunHeader(d)
|
|
@HoldBanner(d)
|
|
@PipelineSection(&d.Run, BuildPipeline(&d.Run, d.Stages))
|
|
|
|
<div class="run-body">
|
|
<div class="active-step-pane">
|
|
for _, stageName := range store.DefaultStageOrder {
|
|
@ActiveStep(ActiveStepData{
|
|
RunID: d.Run.ID,
|
|
Stage: stageForName(d.Stages, stageName),
|
|
SubSteps: SubStepsForStage(d.SubSteps, stageName),
|
|
LogReplay: d.LogReplayByStage[stageName],
|
|
Open: stageName == d.DefaultStepStage,
|
|
})
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@RunSpecDiffs(d)
|
|
</section>
|
|
}
|
|
}
|
|
|
|
// RunHeader is the run-page header: run id, state badge, elapsed, and
|
|
// the primary action on the right (Cancel during a non-terminal run;
|
|
// Start-new-run + View-report after). Keyed on run ID so SSE updates
|
|
// don't collide with a newer run's header. Rendered as a section rather
|
|
// than a bare header so it composes with the breadcrumb strip above.
|
|
templ RunHeader(d RunPageData) {
|
|
<header
|
|
id={ fmt.Sprintf("run-header-%d", d.Run.ID) }
|
|
class={ "run-header", "tile-" + tileMood(&d.Run) }
|
|
sse-swap={ fmt.Sprintf("run-header-%d", d.Run.ID) }
|
|
hx-swap="outerHTML"
|
|
>
|
|
<div class="run-header-left">
|
|
<h1 class="run-header-name">{ fmt.Sprintf("Run #%d", d.Run.ID) }</h1>
|
|
<span class={ "run-status-badge", "run-status-" + tileMood(&d.Run) }>{ tileStatus(&d.Run) }</span>
|
|
<span class={ "run-profile-chip", "run-profile-" + profileChipValue(d.Run.Profile) }>{ profileChipValue(d.Run.Profile) }</span>
|
|
<span class="run-duration">{ runDuration(&d.Run) }</span>
|
|
if d.Run.FailedStage != "" {
|
|
<span class="run-failed-stage">failed at <strong>{ d.Run.FailedStage }</strong></span>
|
|
}
|
|
if d.SpecDiffCritical > 0 {
|
|
<span class="run-diffs bad">{ fmt.Sprintf("%d critical diff", d.SpecDiffCritical) }</span>
|
|
}
|
|
</div>
|
|
<div class="run-header-right">
|
|
if canCancel(&d.Run) {
|
|
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/cancel", d.Host.ID)) } class="inline" onsubmit="return confirm('Cancel run? Destructive stages may leave the host in an intermediate state requiring manual cleanup.');">
|
|
<button type="submit" class="btn-danger">Cancel run</button>
|
|
</form>
|
|
}
|
|
if canOverrideWipe(&d.Run) {
|
|
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/override-wipe", d.Host.ID)) } class="inline">
|
|
<button type="submit" class="btn-danger">Override wipe-probe</button>
|
|
</form>
|
|
}
|
|
if hasReport(&d.Run) {
|
|
<a class="button-like" href={ templ.SafeURL(fmt.Sprintf("/reports/%d", d.Run.ID)) } target="_blank" rel="noopener">View report</a>
|
|
}
|
|
if d.Run.State.IsTerminal() {
|
|
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/start", d.Host.ID)) } class="inline">
|
|
<button type="submit" class="btn-primary">Start new run</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
</header>
|
|
}
|
|
|
|
// HoldBanner is the "Host is holding — SSH available" strip when a run
|
|
// is FailedHolding with an IP recorded. Emits an empty placeholder
|
|
// otherwise so the first SSE push when a hold actually fires has a
|
|
// target to swap into.
|
|
templ HoldBanner(d RunPageData) {
|
|
if d.Run.State == model.StateFailedHolding && d.Run.HoldIP != "" {
|
|
<section
|
|
id={ fmt.Sprintf("detail-hold-%d", d.Run.ID) }
|
|
class="hold-banner"
|
|
sse-swap={ fmt.Sprintf("detail-hold-%d", d.Run.ID) }
|
|
hx-swap="outerHTML"
|
|
>
|
|
<span class="hold-banner-label">Host is holding — SSH available:</span>
|
|
<code class="hold-ssh">{ sshInvocation(d.HoldKeyPath, d.Run.HoldIP) }</code>
|
|
</section>
|
|
} else {
|
|
<section
|
|
id={ fmt.Sprintf("detail-hold-%d", d.Run.ID) }
|
|
class="detail-hold-placeholder"
|
|
sse-swap={ fmt.Sprintf("detail-hold-%d", d.Run.ID) }
|
|
hx-swap="outerHTML"
|
|
></section>
|
|
}
|
|
}
|
|
|
|
// RunSpecDiffs renders the "Spec diffs (N)" collapsible. The wrapper is
|
|
// always emitted (even when empty) so SpecValidate-time SSE pushes have
|
|
// a target; the <details> body only renders when diffs exist.
|
|
templ RunSpecDiffs(d RunPageData) {
|
|
<section
|
|
id={ fmt.Sprintf("detail-specdiffs-%d", d.Run.ID) }
|
|
class="detail-section detail-diffs"
|
|
sse-swap={ fmt.Sprintf("detail-specdiffs-%d", d.Run.ID) }
|
|
hx-swap="outerHTML"
|
|
>
|
|
if len(d.SpecDiffs) > 0 {
|
|
<details open?={ hasCriticalDiff(d.SpecDiffs) }>
|
|
<summary><h2>Spec diffs ({ fmt.Sprintf("%d", len(d.SpecDiffs)) })</h2></summary>
|
|
<ul class="diff-list">
|
|
for _, diff := range d.SpecDiffs {
|
|
<li class={ "diff-row", "diff-" + diff.Severity }>
|
|
<div class="diff-field">{ diff.Field }</div>
|
|
<div class="diff-expected">expected: <code>{ diff.Expected }</code></div>
|
|
<div class="diff-actual">actual: <code>{ diff.Actual }</code></div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</details>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
// RenderRunHeaderString, RenderHoldBannerString, and
|
|
// RenderRunSpecDiffsString render each region to a string for the
|
|
// orchestrator's SSE publish path. Matches the RenderTileString pattern.
|
|
func RenderRunHeaderString(d RunPageData) string {
|
|
var buf bytes.Buffer
|
|
_ = RunHeader(d).Render(context.Background(), &buf)
|
|
return buf.String()
|
|
}
|
|
|
|
func RenderHoldBannerString(d RunPageData) string {
|
|
var buf bytes.Buffer
|
|
_ = HoldBanner(d).Render(context.Background(), &buf)
|
|
return buf.String()
|
|
}
|
|
|
|
func RenderRunSpecDiffsString(d RunPageData) string {
|
|
var buf bytes.Buffer
|
|
_ = RunSpecDiffs(d).Render(context.Background(), &buf)
|
|
return buf.String()
|
|
}
|