package templates import ( "bytes" "context" "fmt" "time" "vetting/internal/model" ) // HostTile renders a single dashboard card. The whole tile is a link // to /hosts/{id} (via a CSS-overlay ) — 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) {
{ t.Host.Name }
{ lastSeenLabel(t.LastSeenAt) }
{ tileStatus(t.Latest) }
if canStart(t.Latest) {
} else 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))) }