ui: GitHub-Actions-style detail page, sub-steps, mini-tile run-view
CI / Lint + build + test (push) Successful in 1m26s
Release / release (push) Successful in 6m47s

Reshapes the detail page into a run-view: hybrid horizontal pipeline
+ expanded active-step pane with sub-steps, a per-step log pane with
line-numbered permalinks and client-side search, and a runs-history
sidebar that navigates via ?run=N. Default step is server-picked
(running → failed → Reporting) so the operator lands on the thing
that's moving.

Adds a sub_steps table + SSE topic (substep-{run}-{stage}-{ordinal})
so per-disk and per-pass work (SMART, CPUStress CPU/RAM, Storage,
GPU) is visible in the UI instead of buried in stage summary JSON.
Agent emits sub-step reports from existing per-iteration loops.

Dashboard tiles become a mini run-view with a 9-dot step strip so
the operator reads run health across the whole grid at a glance.
Register page gets the same card shell + button styling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 19:00:11 -04:00
parent 5c00edd7b6
commit f79fe0f0db
38 changed files with 3972 additions and 936 deletions
+57 -23
View File
@@ -3,6 +3,7 @@ package tests
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os"
@@ -52,6 +53,7 @@ func CPUStress(ctx context.Context, d Deps) Outcome {
cores := runtime.NumCPU()
extras := map[string]any{"cores": cores}
var subs []SubStepReport
// Pass 1: CPU
cpu := runStressPass(ctx, d, "CPU", cpuPassDuration, []string{
@@ -62,12 +64,14 @@ func CPUStress(ctx context.Context, d Deps) Outcome {
"--verify",
})
extras["cpu_pass"] = cpu
subs = append(subs, subStepFromPass("CPU pass", cpu))
if !cpu.Passed {
return Outcome{
Passed: false,
Message: "CPU pass failed: " + cpu.Err,
Summary: fmt.Sprintf("CPU pass failed after %ds", cpu.ElapsedSecs),
Extras: extras,
Passed: false,
Message: "CPU pass failed: " + cpu.Err,
Summary: fmt.Sprintf("CPU pass failed after %ds", cpu.ElapsedSecs),
Extras: extras,
SubSteps: subs,
}
}
@@ -77,10 +81,11 @@ func CPUStress(ctx context.Context, d Deps) Outcome {
if err != nil {
d.Error("CPUStress: read MemAvailable: " + err.Error())
return Outcome{
Passed: false,
Message: "read MemAvailable: " + err.Error(),
Summary: "failed (meminfo unreadable)",
Extras: extras,
Passed: false,
Message: "read MemAvailable: " + err.Error(),
Summary: "failed (meminfo unreadable)",
Extras: extras,
SubSteps: subs,
}
}
cap := avail - memHeadroomBytes
@@ -92,10 +97,11 @@ func CPUStress(ctx context.Context, d Deps) Outcome {
avail, memFloorBytes, memHeadroomBytes)
d.Error("CPUStress: " + msg)
return Outcome{
Passed: false,
Message: msg,
Summary: "failed (insufficient free RAM for memory pass)",
Extras: extras,
Passed: false,
Message: msg,
Summary: "failed (insufficient free RAM for memory pass)",
Extras: extras,
SubSteps: subs,
}
}
mem := runStressPass(ctx, d, "memory", memPassDuration, []string{
@@ -107,12 +113,14 @@ func CPUStress(ctx context.Context, d Deps) Outcome {
"--verify",
})
extras["mem_pass"] = mem
subs = append(subs, subStepFromPass(fmt.Sprintf("Memory pass (cap %s)", humanBytes(cap)), mem))
if !mem.Passed {
return Outcome{
Passed: false,
Message: "memory pass failed: " + mem.Err,
Summary: fmt.Sprintf("memory pass failed after %ds", mem.ElapsedSecs),
Extras: extras,
Passed: false,
Message: "memory pass failed: " + mem.Err,
Summary: fmt.Sprintf("memory pass failed after %ds", mem.ElapsedSecs),
Extras: extras,
SubSteps: subs,
}
}
@@ -120,7 +128,26 @@ func CPUStress(ctx context.Context, d Deps) Outcome {
Passed: true,
Summary: fmt.Sprintf("CPU+RAM PASSED (%d cores, %s cap)",
cores, humanBytes(cap)),
Extras: extras,
Extras: extras,
SubSteps: subs,
}
}
// subStepFromPass projects a stressPass into a SubStepReport — shared by
// both passes and by the mid-stage early-return paths so the UI always
// sees exactly one row per pass, even on failure.
func subStepFromPass(name string, p stressPass) SubStepReport {
summary, _ := json.Marshal(map[string]any{
"elapsed_secs": p.ElapsedSecs,
"target_secs": p.TargetSecs,
"err": p.Err,
})
return SubStepReport{
Name: name,
Passed: p.Passed,
StartedAt: p.StartedAt,
CompletedAt: p.CompletedAt,
SummaryJSON: summary,
}
}
@@ -140,12 +167,16 @@ const (
// stressPass is the per-pass result embedded in CPUStress's Extras.
// Passed==true and Elapsed close to target is the only happy path.
// StartedAt/CompletedAt are not serialized (the summary already has
// ElapsedSecs) but are used by the caller to emit SubStepReport rows.
type stressPass struct {
Passed bool `json:"passed"`
Err string `json:"err,omitempty"`
ElapsedSecs int `json:"elapsed_secs"`
TargetSecs int `json:"target_secs"`
OutputTail string `json:"output_tail,omitempty"`
Passed bool `json:"passed"`
Err string `json:"err,omitempty"`
ElapsedSecs int `json:"elapsed_secs"`
TargetSecs int `json:"target_secs"`
OutputTail string `json:"output_tail,omitempty"`
StartedAt time.Time `json:"-"`
CompletedAt time.Time `json:"-"`
}
// runStressPass invokes stress-ng and validates both exit code and
@@ -159,12 +190,15 @@ func runStressPass(ctx context.Context, d Deps, label string, target time.Durati
cmd := exec.CommandContext(runCtx, "stress-ng", args...)
start := time.Now()
out, err := cmd.CombinedOutput()
elapsed := time.Since(start)
end := time.Now()
elapsed := end.Sub(start)
res := stressPass{
ElapsedSecs: int(elapsed.Round(time.Second).Seconds()),
TargetSecs: int(target.Round(time.Second).Seconds()),
OutputTail: tailLines(string(out), 20),
StartedAt: start,
CompletedAt: end,
}
if err != nil {
res.Err = err.Error()