Files
Vetting/internal/web/templates/host_tile.templ
T
josh 9bb4b09a04
CI / Lint + build + test (push) Has been cancelled
Initial commit: full Phases 1-6 implementation
Post-repair hardware validation pipeline for Proxmox cluster hosts.
Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq
PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
2026-04-17 21:32:10 -04:00

145 lines
3.9 KiB
Plaintext

package templates
import (
"bytes"
"context"
"fmt"
"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.
templ HostTile(t TileData) {
<article
id={ fmt.Sprintf("host-%d", t.Host.ID) }
class={ "tile", "tile-" + tileMood(t.Latest) }
sse-swap={ fmt.Sprintf("tile-%d", t.Host.ID) }
hx-swap="outerHTML"
>
<header class="tile-head">
<div class="tile-name">{ t.Host.Name }</div>
<div class="tile-status">{ tileStatus(t.Latest) }</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">
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) {
<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>
}
func canOverrideWipe(r *model.Run) bool {
if r == nil {
return false
}
return r.State == model.StateFailedHolding && r.FailedStage == "Storage"
}
// hasReport is true once the reporting stage has produced an HTML
// artifact. We cheat slightly: Completed runs always have one, and
// that's the only state in which the tile wants to surface a link.
func hasReport(r *model.Run) bool {
return r != nil && r.State == model.StateCompleted
}
func canStart(r *model.Run) bool {
if r == nil {
return true
}
switch r.State {
case model.StateCompleted, model.StateReleased, model.StateFailedHolding:
return true
}
return false
}
func tileStatus(r *model.Run) string {
if r == nil {
return "Idle"
}
return string(r.State)
}
func tileMood(r *model.Run) string {
if r == nil {
return "idle"
}
switch r.State {
case model.StateCompleted:
return "pass"
case model.StateFailed, model.StateFailedHolding:
return "fail"
case model.StateReleased:
return "idle"
}
return "active"
}
func sshInvocation(keyPath, ip string) string {
if keyPath == "" {
return "ssh root@" + ip + " (hold key not yet recorded)"
}
return fmt.Sprintf("ssh -i %s root@%s", keyPath, ip)
}
// RenderTileString renders a single tile fragment so the orchestrator
// can publish it over SSE without threading a context through every
// event publisher.
func RenderTileString(t TileData) string {
var buf bytes.Buffer
_ = HostTile(t).Render(context.Background(), &buf)
return buf.String()
}