cdd6cae3b0
Pipeline fragment payload was a bare <div class=pipeline>, but the sse-swap=pipeline-N wrapper lived only in the page shell. The first outerHTML swap destroyed the wrapper, so every subsequent pipeline event had nothing to target — forcing a manual refresh. RenderPipelineString now emits the full <section id=pipeline-N sse-swap=... hx-swap=outerHTML> wrapper, used from both the shell and the orchestrator publish path. Also drop the red-bar styling from the empty DetailHold placeholder: the wrapper's detail-hold class was painting an unconditional red band between Pipeline and Actions whenever no hold was active.
507 lines
18 KiB
Go
507 lines
18 KiB
Go
// Code generated by templ - DO NOT EDIT.
|
||
|
||
// templ: version: v0.3.1001
|
||
package templates
|
||
|
||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||
|
||
import "github.com/a-h/templ"
|
||
import templruntime "github.com/a-h/templ/runtime"
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"vetting/internal/model"
|
||
"vetting/internal/store"
|
||
)
|
||
|
||
// PipelineNode is one dot on the detail-page timeline. The template
|
||
// doesn't know stages from pre-stages — it just renders whatever the
|
||
// BuildPipeline helper produces, in order.
|
||
type PipelineNode struct {
|
||
Name string
|
||
State string // pending|running|passed|failed|skipped
|
||
StartedAt *time.Time
|
||
CompletedAt *time.Time
|
||
}
|
||
|
||
// preStageOrder are the nodes that show before the first real stage.
|
||
// Derived from run.State rather than stage rows since we don't persist
|
||
// pre-stage timestamps.
|
||
var preStageOrder = []model.RunState{
|
||
model.StateQueued,
|
||
model.StateWaitingReboot,
|
||
model.StateBooting,
|
||
}
|
||
|
||
// runStateRank returns how far along the state machine a run is, using
|
||
// a linear ranking across pre-stages, stage states, and terminals. Used
|
||
// by BuildPipeline to decide which pre-stage nodes are "past" (passed),
|
||
// "current" (running), or "pending".
|
||
func runStateRank(s model.RunState) int {
|
||
order := []model.RunState{
|
||
model.StateRegistered,
|
||
model.StateQueued,
|
||
model.StateWaitingReboot,
|
||
model.StateBooting,
|
||
model.StateInventoryCheck,
|
||
model.StateSpecValidate,
|
||
model.StateSMART,
|
||
model.StateCPUStress,
|
||
model.StateStorage,
|
||
model.StateNetwork,
|
||
model.StateGPU,
|
||
model.StatePSU,
|
||
model.StateReporting,
|
||
model.StateCompleted,
|
||
}
|
||
for i, v := range order {
|
||
if v == s {
|
||
return i
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
// BuildPipeline projects (run, stages) into a linear slice of nodes
|
||
// covering the whole lifecycle: pre-stage → all 9 stage nodes →
|
||
// Completed. Every stage in store.DefaultStageOrder always appears,
|
||
// even if its row hasn't been seeded yet — those show as "pending"
|
||
// ghosts. This way a run stuck in WaitingWoL (stages unseeded until
|
||
// /claim) still shows the full pipeline ahead of it.
|
||
//
|
||
// When run == nil we emit a ghost timeline (everything pending) so a
|
||
// never-run host still shows what's coming.
|
||
func BuildPipeline(run *model.Run, stages []model.Stage) []PipelineNode {
|
||
nodes := make([]PipelineNode, 0, len(preStageOrder)+len(store.DefaultStageOrder)+1)
|
||
|
||
// --- pre-stage nodes ---
|
||
for _, ps := range preStageOrder {
|
||
n := PipelineNode{Name: string(ps), State: "pending"}
|
||
if run != nil {
|
||
switch {
|
||
case run.State == model.StateFailedHolding || run.State == model.StateFailed:
|
||
// If we failed before reaching a stage, a pre-stage may
|
||
// still have been entered — keep the "past" rank logic.
|
||
if runStateRank(ps) < runStateRank(firstStageState(run)) {
|
||
n.State = "passed"
|
||
}
|
||
case run.State == ps:
|
||
n.State = "running"
|
||
case runStateRank(run.State) > runStateRank(ps):
|
||
n.State = "passed"
|
||
}
|
||
}
|
||
nodes = append(nodes, n)
|
||
}
|
||
|
||
// --- stage nodes ---
|
||
// Iterate DefaultStageOrder, not the stages slice, so the list is
|
||
// always the full 9 nodes. For each stage, prefer the persisted row
|
||
// if it exists; otherwise synthesize a ghost whose state is derived
|
||
// from run state (passed if we've advanced past this stage's
|
||
// RunState, running if we're in it, skipped if a prior stage failed,
|
||
// pending otherwise).
|
||
stageByName := make(map[string]model.Stage, len(stages))
|
||
for _, st := range stages {
|
||
stageByName[st.Name] = st
|
||
}
|
||
failedBefore := false
|
||
for _, name := range store.DefaultStageOrder {
|
||
n := PipelineNode{Name: name}
|
||
if st, ok := stageByName[name]; ok {
|
||
n.StartedAt = st.StartedAt
|
||
n.CompletedAt = st.CompletedAt
|
||
switch {
|
||
case failedBefore:
|
||
n.State = "skipped"
|
||
case st.State == model.StagePassed:
|
||
n.State = "passed"
|
||
case st.State == model.StageRunning:
|
||
n.State = "running"
|
||
case st.State == model.StageFailed:
|
||
n.State = "failed"
|
||
failedBefore = true
|
||
case st.State == model.StageSkipped:
|
||
n.State = "skipped"
|
||
default:
|
||
n.State = "pending"
|
||
}
|
||
} else {
|
||
// Ghost: no row seeded yet. Derive from run state.
|
||
n.State = ghostStageState(run, name, failedBefore)
|
||
}
|
||
nodes = append(nodes, n)
|
||
}
|
||
|
||
// --- terminal Completed node ---
|
||
term := PipelineNode{Name: "Completed", State: "pending"}
|
||
if run != nil && run.State == model.StateCompleted {
|
||
term.State = "passed"
|
||
term.CompletedAt = run.CompletedAt
|
||
}
|
||
nodes = append(nodes, term)
|
||
|
||
return nodes
|
||
}
|
||
|
||
// ghostStageState derives a pipeline-node state for a stage with no DB
|
||
// row — either the run hasn't reached /claim yet (pre-seed) or the stage
|
||
// is simply later than the run's current state. Mirrors the seeded-row
|
||
// logic so a ghost node transitions through the same visual states as a
|
||
// real one.
|
||
func ghostStageState(run *model.Run, name string, failedBefore bool) string {
|
||
if failedBefore {
|
||
return "skipped"
|
||
}
|
||
if run == nil {
|
||
return "pending"
|
||
}
|
||
// Failed/FailedHolding: anything past the failed stage is skipped.
|
||
if run.State == model.StateFailed || run.State == model.StateFailedHolding {
|
||
if run.FailedStage != "" {
|
||
failedRank, ok1 := stageRank(run.FailedStage)
|
||
myRank, ok2 := stageRank(name)
|
||
if ok1 && ok2 && myRank > failedRank {
|
||
return "skipped"
|
||
}
|
||
}
|
||
return "pending"
|
||
}
|
||
stageState, ok := stageStateByName(name)
|
||
if !ok {
|
||
return "pending"
|
||
}
|
||
switch {
|
||
case run.State == stageState:
|
||
return "running"
|
||
case runStateRank(run.State) > runStateRank(stageState):
|
||
return "passed"
|
||
}
|
||
return "pending"
|
||
}
|
||
|
||
// stageRank returns the ordinal of a stage within DefaultStageOrder,
|
||
// used to decide which stages are "after" a failed stage.
|
||
func stageRank(name string) (int, bool) {
|
||
for i, s := range store.DefaultStageOrder {
|
||
if s == name {
|
||
return i, true
|
||
}
|
||
}
|
||
return -1, false
|
||
}
|
||
|
||
// firstStageState returns the stage-state the run was in when it failed,
|
||
// or the current state for runs still in-flight. Used only by the
|
||
// pre-stage "past" check to decide if a Booting node should render
|
||
// "passed" even after the run failed further along.
|
||
func firstStageState(run *model.Run) model.RunState {
|
||
if run.FailedStage != "" {
|
||
if s, ok := stageStateByName(run.FailedStage); ok {
|
||
return s
|
||
}
|
||
}
|
||
return run.State
|
||
}
|
||
|
||
// stageStateByName mirrors orchestrator.StateForStage without the
|
||
// import (templates can't see orchestrator).
|
||
func stageStateByName(name string) (model.RunState, bool) {
|
||
m := map[string]model.RunState{
|
||
"Inventory": model.StateInventoryCheck,
|
||
"SpecValidate": model.StateSpecValidate,
|
||
"SMART": model.StateSMART,
|
||
"CPUStress": model.StateCPUStress,
|
||
"Storage": model.StateStorage,
|
||
"Network": model.StateNetwork,
|
||
"GPU": model.StateGPU,
|
||
"PSU": model.StatePSU,
|
||
"Reporting": model.StateReporting,
|
||
}
|
||
s, ok := m[name]
|
||
return s, ok
|
||
}
|
||
|
||
// stageDuration renders node timing as "1.2s" / "12s" / "4m". Empty
|
||
// string when the node hasn't started or hasn't finished.
|
||
func stageDuration(n PipelineNode) string {
|
||
if n.StartedAt == nil {
|
||
return ""
|
||
}
|
||
end := time.Now()
|
||
if n.CompletedAt != nil {
|
||
end = *n.CompletedAt
|
||
}
|
||
d := end.Sub(*n.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))
|
||
}
|
||
}
|
||
|
||
// stageMarker returns the single-char glyph shown in the node's dot.
|
||
// Dots stay colored-via-class; the glyph is redundant-but-helpful.
|
||
func stageMarker(state string) string {
|
||
switch state {
|
||
case "passed":
|
||
return "✓"
|
||
case "failed":
|
||
return "!"
|
||
case "running":
|
||
return "●"
|
||
case "skipped":
|
||
return "–"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// Pipeline renders the ordered dot-and-line timeline. The caller wraps
|
||
// it in a <section id="pipeline-{runID}" sse-swap=...> so the runner can
|
||
// re-emit the fragment as stages progress.
|
||
func Pipeline(nodes []PipelineNode) templ.Component {
|
||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var1 == nil {
|
||
templ_7745c5c3_Var1 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"pipeline\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for i, n := range nodes {
|
||
if i > 0 {
|
||
var templ_7745c5c3_Var2 = []any{"stage-connector", "stage-connector-" + nodes[i-1].State}
|
||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var3 string
|
||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 1, Col: 0}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var4 = []any{"stage-node", "stage-node-" + n.State}
|
||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var5 string
|
||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 1, Col: 0}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var6 = []any{"stage-dot", "stage-dot-" + n.State}
|
||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var7 string
|
||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 1, Col: 0}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var8 string
|
||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(stageMarker(n.State))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 275, Col: 77}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"stage-name\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var9 string
|
||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(n.Name)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 276, Col: 36}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div class=\"stage-duration\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var10 string
|
||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(stageDuration(n))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 277, Col: 50}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// PipelineSection wraps Pipeline in the same <section id=pipeline-N
|
||
// sse-swap=pipeline-N hx-swap=outerHTML> the runner targets. Used both
|
||
// from the initial detail-page shell and from RenderPipelineString so
|
||
// the wrapper is present on the wire and after every SSE swap — without
|
||
// this, the first outerHTML swap would replace the section with a bare
|
||
// <div class=pipeline>, wiping out the sse-swap attribute and freezing
|
||
// the pipeline until full page reload.
|
||
func PipelineSection(run *model.Run, nodes []PipelineNode) templ.Component {
|
||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var11 == nil {
|
||
templ_7745c5c3_Var11 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<section id=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var12 string
|
||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("pipeline-%d", run.ID))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 292, Col: 41}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"detail-section\" sse-swap=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var13 string
|
||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("pipeline-%d", run.ID))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/pipeline.templ`, Line: 294, Col: 47}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-swap=\"outerHTML\"><h2>Pipeline</h2>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Pipeline(nodes).Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// RenderPipelineString is the one-shot renderer the orchestrator
|
||
// registers at startup so it can publish pipeline fragments over SSE
|
||
// without pulling in the template package directly. Returns the full
|
||
// PipelineSection wrapper so repeat outerHTML swaps preserve the
|
||
// sse-swap target.
|
||
func RenderPipelineString(run *model.Run, stages []model.Stage) string {
|
||
if run == nil {
|
||
return ""
|
||
}
|
||
var buf bytes.Buffer
|
||
_ = PipelineSection(run, BuildPipeline(run, stages)).Render(context.Background(), &buf)
|
||
return buf.String()
|
||
}
|
||
|
||
var _ = templruntime.GeneratedTemplate
|