ui: split /hosts/{id} into host page + /runs/{runID} run page
Host page owns host metadata, full runs table with per-row stage strip, in-flight banner, and empty-state CTA. Run page owns pipeline, active step, logs, sub-steps, spec diffs, and hold banner with a breadcrumb back to the host. Dashboard tile reverts to host-only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -33,30 +33,38 @@ func setupRunner(t *testing.T) (*orchestrator.Runner, *store.Hosts, *store.Runs,
|
||||
// grep the published fragments without parsing HTML.
|
||||
prevTile := orchestrator.TileRenderer
|
||||
prevPipe := orchestrator.PipelineRenderer
|
||||
prevDetail := orchestrator.HostDetailRenderer
|
||||
prevHost := orchestrator.HostPageRenderer
|
||||
prevRun := orchestrator.RunPageRenderer
|
||||
orchestrator.TileRenderer = func(_ context.Context, host model.Host, _ *model.Run) string {
|
||||
return fmt.Sprintf(`<article id="host-%d">tile</article>`, host.ID)
|
||||
}
|
||||
orchestrator.PipelineRenderer = func(run *model.Run, _ []model.Stage) string {
|
||||
return fmt.Sprintf(`<section id="pipeline-%d">pipeline</section>`, run.ID)
|
||||
}
|
||||
orchestrator.HostDetailRenderer = func(_ context.Context, hostID int64) (orchestrator.HostDetailFragments, bool) {
|
||||
var runID int64
|
||||
orchestrator.HostPageRenderer = func(_ context.Context, hostID int64) (orchestrator.HostPageFragments, bool) {
|
||||
rows := map[int64]string{}
|
||||
if latest, err := runs.LatestForHost(context.Background(), hostID); err == nil && latest != nil {
|
||||
runID = latest.ID
|
||||
rows[latest.ID] = fmt.Sprintf(`<tr id="runrow-%d">row</tr>`, latest.ID)
|
||||
}
|
||||
return orchestrator.HostDetailFragments{
|
||||
Summary: fmt.Sprintf(`<header id="detail-summary-%d">summary</header>`, hostID),
|
||||
Actions: fmt.Sprintf(`<section id="detail-actions-%d">actions</section>`, hostID),
|
||||
SpecDiffs: fmt.Sprintf(`<section id="detail-specdiffs-%d">diffs</section>`, runID),
|
||||
Hold: fmt.Sprintf(`<section id="detail-hold-%d">hold</section>`, runID),
|
||||
LatestRunID: runID,
|
||||
return orchestrator.HostPageFragments{
|
||||
Summary: fmt.Sprintf(`<header id="detail-summary-%d">summary</header>`, hostID),
|
||||
Actions: fmt.Sprintf(`<section id="detail-actions-%d">actions</section>`, hostID),
|
||||
InFlightBanner: fmt.Sprintf(`<section id="detail-inflight-%d">inflight</section>`, hostID),
|
||||
RunRows: rows,
|
||||
}, true
|
||||
}
|
||||
orchestrator.RunPageRenderer = func(_ context.Context, runID int64) (orchestrator.RunPageFragments, bool) {
|
||||
return orchestrator.RunPageFragments{
|
||||
Header: fmt.Sprintf(`<header id="run-header-%d">header</header>`, runID),
|
||||
Hold: fmt.Sprintf(`<section id="detail-hold-%d">hold</section>`, runID),
|
||||
SpecDiffs: fmt.Sprintf(`<section id="detail-specdiffs-%d">diffs</section>`, runID),
|
||||
}, true
|
||||
}
|
||||
cleanup := func() {
|
||||
orchestrator.TileRenderer = prevTile
|
||||
orchestrator.PipelineRenderer = prevPipe
|
||||
orchestrator.HostDetailRenderer = prevDetail
|
||||
orchestrator.HostPageRenderer = prevHost
|
||||
orchestrator.RunPageRenderer = prevRun
|
||||
_ = conn.Close()
|
||||
}
|
||||
return runner, hosts, runs, hub, cleanup
|
||||
@@ -128,11 +136,12 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
// TestPublishesHostDetailFragments asserts that every state-change
|
||||
// publish site also emits the four detail-page SSE events (summary,
|
||||
// actions, specdiffs, hold). Without this, the host detail page
|
||||
// stays frozen on the state at page-load time.
|
||||
func TestPublishesHostDetailFragments(t *testing.T) {
|
||||
// TestPublishesHostPageAndRunPageFragments asserts that every state-
|
||||
// change publish site emits the full set of host-page SSE events
|
||||
// (summary, actions, in-flight banner, runrow) *and* the run-page
|
||||
// events (header, hold, specdiffs). Without this, neither /hosts/{id}
|
||||
// nor /runs/{runID} update live.
|
||||
func TestPublishesHostPageAndRunPageFragments(t *testing.T) {
|
||||
runner, hosts, runs, hub, cleanup := setupRunner(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
@@ -160,10 +169,13 @@ func TestPublishesHostDetailFragments(t *testing.T) {
|
||||
}
|
||||
|
||||
want := map[string]bool{
|
||||
fmt.Sprintf("detail-summary-%d", hostID): false,
|
||||
fmt.Sprintf("detail-actions-%d", hostID): false,
|
||||
fmt.Sprintf("detail-specdiffs-%d", runID): false,
|
||||
fmt.Sprintf("detail-hold-%d", runID): false,
|
||||
fmt.Sprintf("detail-summary-%d", hostID): false,
|
||||
fmt.Sprintf("detail-actions-%d", hostID): false,
|
||||
fmt.Sprintf("detail-inflight-%d", hostID): false,
|
||||
fmt.Sprintf("runrow-%d", runID): false,
|
||||
fmt.Sprintf("run-header-%d", runID): false,
|
||||
fmt.Sprintf("detail-hold-%d", runID): false,
|
||||
fmt.Sprintf("detail-specdiffs-%d", runID): false,
|
||||
}
|
||||
deadline := time.After(500 * time.Millisecond)
|
||||
for {
|
||||
|
||||
Reference in New Issue
Block a user