9bb4b09a04
CI / Lint + build + test (push) Has been cancelled
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.
145 lines
3.9 KiB
Plaintext
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()
|
|
}
|