Click a tile to open /hosts/{id} — the canonical control surface per
host. Timeline renders every pre-stage, stage, and terminal node in
order, with the current one pulsing, failed ones flagged, and
downstream ones dimmed as skipped. Detail page shows summary, hold
card (when holding), all action buttons, spec diffs, a full-height
log pane, and a collapsed expected-spec YAML.
Tile slims to name, last-seen, status, and one primary action; a
CSS-overlay <a> makes the whole card clickable while buttons stay
receptive via z-index.
Runner.publishTileUpdate now also emits pipeline-{runID} fragments,
and CompleteStage wraps Stages.CompleteByName so stage completions
advance the timeline live — without this the dots only moved on
state transitions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,10 @@ import (
|
||||
"vetting/internal/model"
|
||||
)
|
||||
|
||||
// HostTile renders a single dashboard card. It's the SSE-swap target
|
||||
// for per-host tile refreshes (`tile-N`) and contains a per-run log
|
||||
// pane (`log-M`) whose live tail is appended by the events hub.
|
||||
// HostTile renders a single dashboard card. The whole tile is a link
|
||||
// to /hosts/{id} (via a CSS-overlay <a>) — every control beyond the one
|
||||
// primary action lives on the detail page. It's the SSE-swap target
|
||||
// for per-host tile refreshes (`tile-N`).
|
||||
templ HostTile(t TileData) {
|
||||
<article
|
||||
id={ fmt.Sprintf("host-%d", t.Host.ID) }
|
||||
@@ -18,6 +19,7 @@ templ HostTile(t TileData) {
|
||||
sse-swap={ fmt.Sprintf("tile-%d", t.Host.ID) }
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<a class="tile-link" href={ templ.SafeURL(fmt.Sprintf("/hosts/%d", t.Host.ID)) } aria-label={ "Open " + t.Host.Name }></a>
|
||||
<header class="tile-head">
|
||||
<div class="tile-name">{ t.Host.Name }</div>
|
||||
<div class="tile-header-right">
|
||||
@@ -25,61 +27,14 @@ templ HostTile(t TileData) {
|
||||
<div class="tile-status">{ tileStatus(t.Latest) }</div>
|
||||
</div>
|
||||
</header>
|
||||
<dl class="tile-meta">
|
||||
<div>
|
||||
<dt>MAC</dt>
|
||||
<dd>{ t.Host.MAC }</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>WoL</dt>
|
||||
<dd>{ fmt.Sprintf("%s:%d", t.Host.WoLBroadcastIP, t.Host.WoLPort) }</dd>
|
||||
</div>
|
||||
if t.Latest != nil && t.Latest.FailedStage != "" {
|
||||
<div>
|
||||
<dt>Failed at</dt>
|
||||
<dd>{ t.Latest.FailedStage }</dd>
|
||||
</div>
|
||||
}
|
||||
if t.SpecDiffCritical > 0 {
|
||||
<div>
|
||||
<dt>Spec diffs</dt>
|
||||
<dd class="bad">{ fmt.Sprintf("%d critical", t.SpecDiffCritical) }</dd>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
if t.Latest != nil && t.Latest.State == model.StateFailedHolding && t.Latest.HoldIP != "" {
|
||||
<div class="tile-hold">
|
||||
<div class="hold-title">Host is holding — SSH available</div>
|
||||
<code class="hold-ssh">{ sshInvocation(t.HoldKeyPath, t.Latest.HoldIP) }</code>
|
||||
</div>
|
||||
}
|
||||
if t.Latest != nil {
|
||||
<div
|
||||
class="tile-log"
|
||||
id={ fmt.Sprintf("log-%d", t.Latest.ID) }
|
||||
sse-swap={ fmt.Sprintf("log-%d", t.Latest.ID) }
|
||||
hx-swap="beforeend"
|
||||
></div>
|
||||
}
|
||||
<div class="tile-actions">
|
||||
<div class="tile-primary-action">
|
||||
if canStart(t.Latest) {
|
||||
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/start", t.Host.ID)) } class="inline">
|
||||
<button type="submit">Start vetting</button>
|
||||
</form>
|
||||
} else {
|
||||
<button type="button" disabled>Run in flight</button>
|
||||
}
|
||||
if canOverrideWipe(t.Latest) {
|
||||
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/override-wipe", t.Host.ID)) } class="inline">
|
||||
<button type="submit" class="danger">Override wipe-probe</button>
|
||||
</form>
|
||||
}
|
||||
if hasReport(t.Latest) {
|
||||
} else if hasReport(t.Latest) {
|
||||
<a class="button-like" href={ templ.SafeURL(fmt.Sprintf("/reports/%d", t.Latest.ID)) } target="_blank" rel="noopener">View report</a>
|
||||
}
|
||||
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/delete", t.Host.ID)) } class="inline">
|
||||
<button type="submit" class="danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user