f79fe0f0db
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>
82 lines
2.3 KiB
Plaintext
82 lines
2.3 KiB
Plaintext
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()
|
||
}
|