ui: split /hosts/{id} into host page + /runs/{runID} run page
CI / Lint + build + test (push) Successful in 1m35s
Release / release (push) Successful in 23m47s

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:
2026-04-18 20:37:57 -04:00
parent 5c6bfa5ffa
commit 19608bef1b
23 changed files with 3173 additions and 2827 deletions
+31 -19
View File
@@ -78,7 +78,6 @@ func main() {
tiles := &api.TileEnricher{
Runs: runStore,
Stages: stageStore,
Artifacts: artifactStore,
SpecDiffs: specDiffStore,
}
@@ -113,28 +112,41 @@ func main() {
PublicURL: cfg.Server.PublicURL,
}
// Inject the host-detail fragment renderer. The closure reuses
// LoadHostDetailData so the SSE-pushed HTML matches an identical
// reload-rendered page byte-for-byte, then hands each region to
// its Render*String helper.
orchestrator.HostDetailRenderer = func(ctx context.Context, hostID int64) (orchestrator.HostDetailFragments, bool) {
// Orchestrator-side publishes always reference the latest run —
// SSE topics are keyed by runID, so a stale ?run=N bookmark
// doesn't affect what the server pushes.
d, err := ui.LoadHostDetailData(ctx, hostID, 0)
// Inject the host-page + run-page fragment renderers. Each reuses
// the matching LoadHostPageData / LoadRunPageData so SSE-pushed HTML
// matches an initial page load byte-for-byte, then hands each region
// to its Render*String helper.
orchestrator.HostPageRenderer = func(ctx context.Context, hostID int64) (orchestrator.HostPageFragments, bool) {
d, err := ui.LoadHostPageData(ctx, hostID)
if err != nil {
return orchestrator.HostDetailFragments{}, false
return orchestrator.HostPageFragments{}, false
}
f := orchestrator.HostDetailFragments{
Summary: templates.RenderDetailSummaryString(d),
Actions: templates.RenderDetailActionsString(d),
SpecDiffs: templates.RenderDetailSpecDiffsString(d),
Hold: templates.RenderDetailHoldString(d),
rows := make(map[int64]string, len(d.Runs))
for _, r := range d.Runs {
rows[r.ID] = templates.RenderRunRowString(templates.RunRowData{
Run: r,
Stages: d.RunStages[r.ID],
Live: d.ActiveRun != nil && d.ActiveRun.ID == r.ID,
})
}
if d.Tile.Latest != nil {
f.LatestRunID = d.Tile.Latest.ID
return orchestrator.HostPageFragments{
Summary: templates.RenderHostSummaryString(d),
Actions: templates.RenderHostActionsString(d),
InFlightBanner: templates.RenderInFlightBannerString(d),
RunRows: rows,
}, true
}
orchestrator.RunPageRenderer = func(ctx context.Context, runID int64) (orchestrator.RunPageFragments, bool) {
d, err := ui.LoadRunPageData(ctx, runID)
if err != nil {
return orchestrator.RunPageFragments{}, false
}
return f, true
return orchestrator.RunPageFragments{
Header: templates.RenderRunHeaderString(d),
Hold: templates.RenderHoldBannerString(d),
SpecDiffs: templates.RenderRunSpecDiffsString(d),
}, true
}
agentAPI := &api.Agent{