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
+22
View File
@@ -1,6 +1,7 @@
package templates
import (
"strings"
"testing"
"vetting/internal/model"
@@ -200,3 +201,24 @@ func TestBuildPipeline_PreStageRunning_WaitingReboot(t *testing.T) {
t.Errorf("Booting = %q, want pending", nodes[idxBooting].State)
}
}
// TestRenderPipelineString_IncludesSection asserts the orchestrator-
// published fragment carries the <section id=pipeline-N sse-swap=...
// hx-swap=outerHTML> wrapper. Without this, the first outerHTML swap
// would replace the section with a bare <div class=pipeline>, wiping
// out the sse-swap attribute and freezing every subsequent pipeline
// event until page reload.
func TestRenderPipelineString_IncludesSection(t *testing.T) {
run := &model.Run{ID: 42, State: model.StateSMART}
html := RenderPipelineString(run, seedStages())
for _, want := range []string{
`id="pipeline-42"`,
`sse-swap="pipeline-42"`,
`hx-swap="outerHTML"`,
`<h2>Pipeline</h2>`,
} {
if !strings.Contains(html, want) {
t.Errorf("RenderPipelineString missing %q in:\n%s", want, html)
}
}
}