package templates import ( "bytes" "context" "fmt" "time" "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) {
{ t.Host.Name }
{ lastSeenLabel(t.LastSeenAt) }
{ tileStatus(t.Latest) }
MAC
{ t.Host.MAC }
WoL
{ fmt.Sprintf("%s:%d", t.Host.WoLBroadcastIP, t.Host.WoLPort) }
if t.Latest != nil && t.Latest.FailedStage != "" {
Failed at
{ t.Latest.FailedStage }
} if t.SpecDiffCritical > 0 {
Spec diffs
{ fmt.Sprintf("%d critical", t.SpecDiffCritical) }
}
if t.Latest != nil && t.Latest.State == model.StateFailedHolding && t.Latest.HoldIP != "" {
Host is holding — SSH available
{ sshInvocation(t.HoldKeyPath, t.Latest.HoldIP) }
} if t.Latest != nil {
}
if canStart(t.Latest) {
} else { } if canOverrideWipe(t.Latest) {
} if hasReport(t.Latest) { View report }
} 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() } // lastSeenLabel renders the host-mode agent's liveness into a short // badge: "never" if the host has never heartbeated, "online" within // a 2×heartbeat grace window (60s, since agents heartbeat every 30s), // "Nm ago" / "Nh ago" / "Nd ago" otherwise. func lastSeenLabel(t *time.Time) string { if t == nil { return "never" } return humanAgoFrom(time.Now(), *t) } // lastSeenClass pairs with lastSeenLabel to drive the badge color // without the template having to carry its own logic. func lastSeenClass(t *time.Time) string { if t == nil { return "offline" } if time.Since(*t) < 60*time.Second { return "online" } return "stale" } // humanAgoFrom formats (now - t) as a short "Nm ago" style string. // Buckets: <60s -> "online", <60m -> minutes, <24h -> hours, else days. // Split on `now` so callers can hold time for tests. func humanAgoFrom(now time.Time, t time.Time) string { d := now.Sub(t) if d < 0 { d = 0 } if d < 60*time.Second { return "online" } if d < time.Hour { return fmt.Sprintf("%dm ago", int(d/time.Minute)) } if d < 24*time.Hour { return fmt.Sprintf("%dh ago", int(d/time.Hour)) } return fmt.Sprintf("%dd ago", int(d/(24*time.Hour))) }