ui: GitHub-Actions-style detail page, sub-steps, mini-tile run-view
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:
@@ -0,0 +1,81 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"vetting/internal/model"
|
||||
)
|
||||
|
||||
// subStepDuration formats a sub-step's elapsed time the same way
|
||||
// stageDuration does for pipeline nodes. Empty string when not started.
|
||||
func subStepDuration(ss model.SubStep) string {
|
||||
if ss.StartedAt == nil {
|
||||
return ""
|
||||
}
|
||||
end := time.Now()
|
||||
if ss.CompletedAt != nil {
|
||||
end = *ss.CompletedAt
|
||||
}
|
||||
d := end.Sub(*ss.StartedAt)
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
switch {
|
||||
case d < time.Second:
|
||||
return fmt.Sprintf("%dms", int(d/time.Millisecond))
|
||||
case d < 10*time.Second:
|
||||
return fmt.Sprintf("%.1fs", d.Seconds())
|
||||
case d < time.Minute:
|
||||
return fmt.Sprintf("%ds", int(d/time.Second))
|
||||
case d < time.Hour:
|
||||
return fmt.Sprintf("%dm", int(d/time.Minute))
|
||||
default:
|
||||
return fmt.Sprintf("%dh", int(d/time.Hour))
|
||||
}
|
||||
}
|
||||
|
||||
// subStepMarker mirrors stageMarker — a single-char glyph used inside the
|
||||
// state badge. StageState values reused verbatim for sub-steps.
|
||||
func subStepMarker(s model.StageState) string {
|
||||
switch s {
|
||||
case model.StagePassed:
|
||||
return "✓"
|
||||
case model.StageFailed:
|
||||
return "!"
|
||||
case model.StageRunning:
|
||||
return "●"
|
||||
case model.StageSkipped:
|
||||
return "–"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SubStepRow renders one sub-step entry for the expanded-step pane. The
|
||||
// outer <div> carries the sse-swap target keyed by (runID, stage,
|
||||
// ordinal) so the orchestrator's PublishSubStepUpdate can swap just this
|
||||
// row without touching the rest of the stage panel. hx-swap="outerHTML"
|
||||
// keeps the attributes intact across repeat swaps.
|
||||
templ SubStepRow(ss model.SubStep) {
|
||||
<div
|
||||
id={ fmt.Sprintf("substep-%d-%s-%d", ss.RunID, ss.StageName, ss.Ordinal) }
|
||||
class={ "substep", "substep-" + string(ss.State) }
|
||||
sse-swap={ fmt.Sprintf("substep-%d-%s-%d", ss.RunID, ss.StageName, ss.Ordinal) }
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<span class={ "substep-badge", "substep-badge-" + string(ss.State) }>{ subStepMarker(ss.State) }</span>
|
||||
<span class="substep-name">{ ss.Name }</span>
|
||||
<span class="substep-duration">{ subStepDuration(ss) }</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
// RenderSubStepRowString is the one-shot renderer the orchestrator
|
||||
// registers as SubStepRenderer so it can emit substep-* SSE payloads
|
||||
// without importing the templates package directly.
|
||||
func RenderSubStepRowString(ss model.SubStep) string {
|
||||
var buf bytes.Buffer
|
||||
_ = SubStepRow(ss).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
Reference in New Issue
Block a user