feat(ui): slim dashboard tile to hostname + online/offline only
Run status, Start/Cancel/View controls, and non-destructive toggle all
live on /hosts/{id} — duplicating them on the dashboard tile clogged
the grid and wouldn't scale past a handful of hosts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,45 +9,22 @@ import (
|
||||
"vetting/internal/model"
|
||||
)
|
||||
|
||||
// HostTile renders a single dashboard card: hostname, heartbeat badge,
|
||||
// latest run status, and the primary action (Start / Cancel / View
|
||||
// report). The whole tile is a link to /hosts/{id} via a CSS-overlay
|
||||
// <a>; every deeper control lives on the host page or the run page.
|
||||
// HostTile renders a single dashboard card: hostname + heartbeat badge
|
||||
// only. Everything else (run status, controls, reports) lives on the
|
||||
// host page — the whole tile is a link there via a CSS-overlay <a>.
|
||||
// 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) }
|
||||
class={ "tile", "tile-" + tileMood(t.Latest) }
|
||||
class="tile"
|
||||
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">
|
||||
<span class={ "tile-last-seen", lastSeenClass(t.LastSeenAt) }>{ lastSeenLabel(t.LastSeenAt) }</span>
|
||||
<div class="tile-status">{ tileStatus(t.Latest) }</div>
|
||||
</div>
|
||||
<span class={ "tile-last-seen", lastSeenClass(t.LastSeenAt) }>{ lastSeenLabel(t.LastSeenAt) }</span>
|
||||
</header>
|
||||
<div class="tile-primary-action">
|
||||
if canStart(t) {
|
||||
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/start", t.Host.ID)) } class="inline tile-start-form">
|
||||
<label class="tile-nd-toggle">
|
||||
<input type="checkbox" name="non_destructive" value="1"/>
|
||||
Non-destructive
|
||||
</label>
|
||||
<button type="submit">Start vetting</button>
|
||||
</form>
|
||||
} else if canStartIfOnline(t.Latest) {
|
||||
<button type="button" disabled title="host is not heartbeating — install the reporter via /register/quick.sh on the target host">Start vetting</button>
|
||||
} else if canCancel(t.Latest) {
|
||||
<form method="post" action={ templ.SafeURL(fmt.Sprintf("/hosts/%d/cancel", t.Host.ID)) } class="inline tile-cancel-form" onsubmit="return confirm('Cancel run? Destructive stages may leave the host in an intermediate state requiring manual cleanup.');">
|
||||
<button type="submit" class="danger">Cancel run</button>
|
||||
</form>
|
||||
} 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>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
|
||||
@@ -65,30 +42,6 @@ func hasReport(r *model.Run) bool {
|
||||
return r != nil && r.State == model.StateCompleted
|
||||
}
|
||||
|
||||
// canStart gates the Start button on two things: the run is in a state
|
||||
// that accepts a fresh start, AND the host is currently heartbeating.
|
||||
// The heartbeat check mirrors the StartRun handler's preflight so the
|
||||
// button never offers a click that the server would reject with 409.
|
||||
func canStart(t TileData) bool {
|
||||
if !canStartIfOnline(t.Latest) {
|
||||
return false
|
||||
}
|
||||
if t.LastSeenAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Since(*t.LastSeenAt) <= 60*time.Second
|
||||
}
|
||||
|
||||
// canStartIfOnline is the run-state half of canStart, split out so the
|
||||
// template can distinguish "waiting on run to end" (no button) from
|
||||
// "run is done but host is offline" (disabled button with tooltip).
|
||||
func canStartIfOnline(r *model.Run) bool {
|
||||
if r == nil {
|
||||
return true
|
||||
}
|
||||
return r.State.IsTerminal()
|
||||
}
|
||||
|
||||
// canCancel is true for any non-terminal run, plus FailedHolding —
|
||||
// a held run technically classifies as terminal for the pipeline but
|
||||
// the host is still live on the SSH hold prompt, and the operator
|
||||
|
||||
Reference in New Issue
Block a user