// Code generated by templ - DO NOT EDIT. // templ: version: v0.3.1001 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( "bytes" "context" "fmt" "time" "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 // ; every deeper control lives on the host page or the run page. // It's the SSE-swap target for per-host tile refreshes (`tile-N`). func HostTile(t TileData) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { return templ_7745c5c3_CtxErr } templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { defer func() { templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) if templ_7745c5c3_Err == nil { templ_7745c5c3_Err = templ_7745c5c3_BufErr } }() } ctx = templ.InitializeContext(ctx) templ_7745c5c3_Var1 := templ.GetChildren(ctx) if templ_7745c5c3_Var1 == nil { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) var templ_7745c5c3_Var2 = []any{"tile", "tile-" + tileMood(t.Latest)} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(t.Host.Name) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 26, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var9 = []any{"tile-last-seen", lastSeenClass(t.LastSeenAt)} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lastSeenLabel(t.LastSeenAt)) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 28, Col: 95} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(tileStatus(t.Latest)) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 29, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if canStart(t) { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if canStartIfOnline(t.Latest) { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if canCancel(t.Latest) { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if hasReport(t.Latest) { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "View report") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) } 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 } // 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 // can walk away from it via Cancel (which reboots to local disk). // Every other terminal state is truly done, so no Cancel button. // The server-side CancelRun handler mirrors this predicate. func canCancel(r *model.Run) bool { if r == nil { return false } if !r.State.IsTerminal() { return true } return r.State == model.StateFailedHolding } func tileStatus(r *model.Run) string { if r == nil { return "Idle" } switch r.State { case model.StateWaitingReboot: return "Waiting for reboot" } 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, model.StateCancelled: 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))) } var _ = templruntime.GeneratedTemplate