package templates import ( "fmt" "time" "vetting/internal/model" ) // ActiveStepData is the per-stage payload for the expanded step panel. // The handler builds one per stage in DefaultStageOrder and hands it to // ActiveStep so the template stays free of any slicing logic. type ActiveStepData struct { RunID int64 Stage model.Stage SubSteps []model.SubStep LogReplay string Open bool } // ActiveStep renders one stage's expanded panel: the header summary // (state badge, stage name, duration), any sub-step rows, a per-step // search box, and a live log pane scoped to that stage's SSE topic. // Uses
so the server-picked default stage // opens automatically on page load; app.js takes over after that for // SSE-driven auto-advance. templ ActiveStep(d ActiveStepData) {
{ stageMarker(string(d.Stage.State)) } { d.Stage.Name } { stageDurationFromStage(d.Stage) }
if len(d.SubSteps) > 0 {
    for _, ss := range d.SubSteps { @SubStepRow(ss) }
}
@templ.Raw(d.LogReplay)
} // SubStepsForStage filters a flat []SubStep to just the entries for one // stage. Used by host_detail when wiring ActiveStepData — keeps the // filtering logic testable and off the template surface. func SubStepsForStage(all []model.SubStep, stageName string) []model.SubStep { out := make([]model.SubStep, 0, len(all)) for _, ss := range all { if ss.StageName == stageName { out = append(out, ss) } } return out } // stageDurationFromStage is stageDuration adapted to a model.Stage — same // formatting rules, different input shape. func stageDurationFromStage(s model.Stage) string { if s.StartedAt == nil { return "" } end := time.Now() if s.CompletedAt != nil { end = *s.CompletedAt } d := end.Sub(*s.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)) } }