hub (unreleased): the System page answers 'is anything wrong?' and 'is anything waiting for me?' first
gates / gates (push) Successful in 6m17s
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:
+54
-31
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user