ui: keep detail-page SSE swaps live after the first outerHTML replace
CI / Lint + build + test (push) Successful in 1m28s
Release / release (push) Successful in 6m29s

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.
This commit is contained in:
2026-04-18 17:03:39 -04:00
parent e73e31af92
commit cdd6cae3b0
5 changed files with 429 additions and 320 deletions
+26 -2
View File
@@ -280,11 +280,35 @@ templ Pipeline(nodes []PipelineNode) {
</div>
}
// 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.
templ PipelineSection(run *model.Run, nodes []PipelineNode) {
<section
id={ fmt.Sprintf("pipeline-%d", run.ID) }
class="detail-section"
sse-swap={ fmt.Sprintf("pipeline-%d", run.ID) }
hx-swap="outerHTML"
>
<h2>Pipeline</h2>
@Pipeline(nodes)
</section>
}
// 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.
// 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
_ = Pipeline(BuildPipeline(run, stages)).Render(context.Background(), &buf)
_ = PipelineSection(run, BuildPipeline(run, stages)).Render(context.Background(), &buf)
return buf.String()
}