hub (unreleased): the System page answers 'is anything wrong?' and 'is anything waiting for me?' first
gates / gates (push) Successful in 6m17s

Needs attention, Waiting for you (one card per kernel/Docker/Proxmox set the button may approve), a
7-column Boxes table whose rows open to every old value, and a closed Details (release ids, cancelled
approvals, ring-0 counts, floors, crash guard and root files, Approve now - which now asks first).
Same data, buttons, routes and CSRF field. No deploy. Screenshots in audits/hub-system-page-2026-10-10/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-10 15:38:25 +02:00
parent ec5605d42c
commit cf6fec8d87
16 changed files with 4365 additions and 108 deletions
+54 -31
View File
@@ -1,6 +1,7 @@
package web
import (
"encoding/json"
"fmt"
"net/http"
"sort"
@@ -46,6 +47,10 @@ type systemRow struct {
Engine, Containerd, LiveRestore, DockerRelease cell
// last leg
LastLeg cell
// the narrow table and "Needs attention" (system_view.go)
Controller, Mark, Updates, KernelShort, LastNight cell
KernelNext string
Reasons []reason
}
// OSSystemView is what the System page needs beyond OSUpdateAdmin (implemented by *osupdates.Service).
@@ -58,8 +63,8 @@ type OSSystemView interface {
BundleThreshold() time.Duration
AgentThreshold() time.Duration
ApproveDocker() (string, error)
ApprovePVE() (string, error) // R-812 option A: the Proxmox package set
ApproveKernel() (string, error) // R-836: the kernel set
ApprovePVE() (string, error) // R-812 option A: the Proxmox package set
ApproveKernel() (string, error) // R-836: the kernel set
KernelLineFor(hostID string) osupdates.KernelLine // R-836: one box's kernel step and day-before mail
}
@@ -383,6 +388,7 @@ func buildSystemRows(lines []osupdates.FleetLine, facts map[string]sysfacts.Syst
} else if last.LastOutcome == "health_failed" || last.LastOutcome == "failed" || last.LastOutcome == "refused" {
r.LastLeg.Class = "warn"
}
r.LastNight = lastNightCell(l.Enabled, last.LastOutcome, last.LastAt, r.LastLeg, now)
rows = append(rows, r)
}
sort.Slice(rows, func(i, j int) bool { return rows[i].HostID < rows[j].HostID })
@@ -409,44 +415,61 @@ func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
return
}
hosts, _ := s.store.ListHosts()
facts, names := map[string]sysfacts.System{}, map[string]string{}
for _, h := range hosts {
names[h.HostID] = s.customerName(h.CustomerID)
if rj, _ := s.store.GetLatestHostReportJSON(h.CustomerID); rj != "" {
facts[h.HostID] = sysfacts.Parse(rj)
now := time.Now()
in := systemInput{Lines: lines, Facts: map[string]sysfacts.System{}, Names: map[string]string{}, Controllers: map[string]string{},
Agents: map[string]string{}, KernelLines: map[string]osupdates.KernelLine{}, BundleSince: map[string]time.Time{},
AgentSince: map[string]time.Time{}, BundleAfter: view.BundleThreshold(), AgentAfter: view.AgentThreshold(),
GlobalFloor: s.store.GetGlobalMinControllerVersion(), Releases: view.Releases(), Cancelled: view.CancelledReleases(),
Candidates: view.Candidates(), Now: now}
in.Stale, in.Reboot, in.NotCov = view.Thresholds()
ctrl := map[string]string{}
if cs, cerr := s.store.GetCustomers(); cerr != nil {
s.logger.Printf("[ERROR] system page: customers: %v", cerr)
} else {
for _, c := range cs {
ctrl[c.CustomerID] = c.ControllerVersion
}
}
stale, reboot, notCov := view.Thresholds()
rows := buildSystemRows(lines, facts, names, stale, reboot, notCov, time.Now())
man := s.store.GetArtifactManifest()
agents := map[string]string{}
for _, h := range hosts {
agents[h.HostID] = h.AgentVersion
in.Names[h.HostID] = s.customerName(h.CustomerID)
in.Agents[h.HostID] = h.AgentVersion
in.Controllers[h.HostID] = ctrl[h.CustomerID]
if rj, _ := s.store.GetLatestHostReportJSON(h.CustomerID); rj != "" {
in.Facts[h.HostID] = sysfacts.Parse(rj)
}
in.KernelLines[h.HostID] = view.KernelLineFor(h.HostID)
in.BundleSince[h.HostID] = s.store.BundleBehindSince(h.HostID)
in.AgentSince[h.HostID] = s.store.AgentBehindSince(h.HostID)
}
for i := range rows {
rows[i].KernelDefault, rows[i].KernelStep = kernelCells(facts[rows[i].HostID], view.KernelLineFor(rows[i].HostID), time.Now())
rows[i].Bundle = bundleCell(facts[rows[i].HostID], man.AgentVersion, man.BundleSHA256,
s.store.BundleBehindSince(rows[i].HostID), view.BundleThreshold(), time.Now())
rows[i].Agent = agentCell(agents[rows[i].HostID], man.AgentVersion,
s.store.AgentBehindSince(rows[i].HostID), view.AgentThreshold(), time.Now())
}
global := s.store.GetGlobalMinControllerVersion()
man := s.store.GetArtifactManifest()
in.VouchedAgent, in.VouchedBundle = man.AgentVersion, man.BundleSHA256
ovs, oerr := s.store.CustomerFloorOverrides()
if oerr != nil {
s.logger.Printf("[ERROR] system page: floor overrides: %v", oerr)
}
data := map[string]interface{}{
"Rows": rows,
"GlobalFloor": global,
"VouchedAgent": man.AgentVersion,
"Floors": buildFloorRows(ovs, global, time.Now()),
"Releases": view.Releases(),
"Cancelled": view.CancelledReleases(),
"Candidates": view.Candidates(),
"Flash": r.URL.Query().Get("flash"),
"FlashErr": r.URL.Query().Get("err"),
"CSRFToken": s.getCSRFToken(r),
in.Floors = ovs
in.Packages = func(fp string) []osupdates.Package {
var list []osupdates.Package
if pj, _ := s.store.OSCandidatePackages(fp); pj != "" {
_ = json.Unmarshal([]byte(pj), &list)
}
return list
}
// A healthy night run of the layer since the set was first seen: the same count the approval rule makes.
in.Nights = func(hostID, layer string, since time.Time) int {
reps, _ := s.store.OSReportsSince(hostID, layer, since)
n := 0
for _, rep := range reps {
if rep.Trigger == "night" && rep.Healthy && rep.Outcome != "failed" && rep.Outcome != "refused" && rep.Outcome != "health_failed" {
n++
}
}
return n
}
data := buildSystemPage(in)
data["Flash"] = r.URL.Query().Get("flash")
data["FlashErr"] = r.URL.Query().Get("err")
data["CSRFToken"] = s.getCSRFToken(r)
if err := s.templates.ExecuteTemplate(w, "system.html", data); err != nil {
s.logger.Printf("[ERROR] system.html template: %v", err)
}