ui: update chip + per-host button
- Surface UpdateAvailable + TargetVersion on the dashboard host row, the host_chrome header, and the JSON Host shape. - New host_update_chip partial renders an amber out-of-date pill next to the agent-version display when the host's agent trails the server. - Host detail right-rail gains an admin-only Update agent button (disabled when host is offline or already updating). - New .update-chip and .btn-amber CSS tokens; tailwind output refreshed.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
stdhttp "net/http"
|
||||
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/version"
|
||||
)
|
||||
|
||||
// hostView is the JSON projection of a Host row. Same shape as the
|
||||
@@ -27,6 +28,8 @@ type hostView struct {
|
||||
RepoSizeBytes int64 `json:"repo_size_bytes"`
|
||||
SnapshotCount int `json:"snapshot_count"`
|
||||
OpenAlertCount int `json:"open_alert_count"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
TargetVersion string `json:"target_version,omitempty"`
|
||||
}
|
||||
|
||||
// handleListHosts returns the full fleet as JSON. Authenticated; the
|
||||
@@ -85,6 +88,8 @@ func hostToView(h store.Host) hostView {
|
||||
RepoSizeBytes: h.RepoSizeBytes,
|
||||
SnapshotCount: h.SnapshotCount,
|
||||
OpenAlertCount: h.OpenAlertCount,
|
||||
TargetVersion: version.Version,
|
||||
UpdateAvailable: h.AgentVersion != "" && h.AgentVersion != version.Version,
|
||||
}
|
||||
if v.Tags == nil {
|
||||
v.Tags = []string{}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/server/ui"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/server/ws"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/version"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/web"
|
||||
)
|
||||
|
||||
@@ -155,6 +156,10 @@ type dashboardPage struct {
|
||||
// when it's already active). Pre-computed so the template stays
|
||||
// dumb.
|
||||
SortURL map[string]string
|
||||
// UpdatesBehind is the count of online hosts whose agent_version
|
||||
// trails the server. Surfaces as the dashboard "N hosts behind"
|
||||
// hero tile and links to ?updates=behind.
|
||||
UpdatesBehind int
|
||||
}
|
||||
|
||||
// dashboardFilter holds the parsed query-string filter state.
|
||||
@@ -165,6 +170,10 @@ type dashboardFilter struct {
|
||||
Tag string // mirrors ActiveTag for round-trip on links
|
||||
Sort string // column key (see sortDashboard)
|
||||
Dir string // "asc" | "desc"
|
||||
// Updates narrows to hosts whose agent is behind the server's
|
||||
// version. Only valid value today is "behind"; empty means no
|
||||
// filter.
|
||||
Updates string
|
||||
}
|
||||
|
||||
// dashboardHostRow carries a host plus the per-row Run-now decision
|
||||
@@ -180,6 +189,13 @@ type dashboardHostRow struct {
|
||||
// NextRun is the next-fire time of RunAllScheduleID (when set),
|
||||
// computed server-side from its cron. nil otherwise.
|
||||
NextRun *time.Time
|
||||
// UpdateAvailable is true when the host's agent has connected at
|
||||
// least once AND its agent_version differs from the server's. Used
|
||||
// by the host_row partial to render the update-available chip.
|
||||
UpdateAvailable bool
|
||||
// TargetVersion is the server's build version, surfaced in the
|
||||
// chip's tooltip and label.
|
||||
TargetVersion string
|
||||
}
|
||||
|
||||
// pickRunAllSchedule returns the ID of the single schedule whose
|
||||
@@ -255,7 +271,11 @@ func (s *Server) handleUIDashboard(w stdhttp.ResponseWriter, r *stdhttp.Request)
|
||||
// calls per host — fine at fleet sizes we care about.
|
||||
rows := make([]dashboardHostRow, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
row := dashboardHostRow{Host: h}
|
||||
row := dashboardHostRow{
|
||||
Host: h,
|
||||
TargetVersion: version.Version,
|
||||
UpdateAvailable: h.AgentVersion != "" && h.AgentVersion != version.Version,
|
||||
}
|
||||
groups, gerr := s.deps.Store.ListSourceGroupsByHost(r.Context(), h.ID)
|
||||
if gerr != nil {
|
||||
slog.Warn("ui dashboard: list source groups", "host_id", h.ID, "err", gerr)
|
||||
@@ -289,6 +309,13 @@ func (s *Server) handleUIDashboard(w stdhttp.ResponseWriter, r *stdhttp.Request)
|
||||
critOpenCount = len(crit)
|
||||
}
|
||||
|
||||
updatesBehind := 0
|
||||
for _, h := range allHosts {
|
||||
if h.Status == "online" && h.AgentVersion != "" && h.AgentVersion != version.Version {
|
||||
updatesBehind++
|
||||
}
|
||||
}
|
||||
|
||||
view := s.baseView(r, u)
|
||||
view.Page = dashboardPage{
|
||||
Hosts: rows,
|
||||
@@ -302,6 +329,7 @@ func (s *Server) handleUIDashboard(w stdhttp.ResponseWriter, r *stdhttp.Request)
|
||||
Filter: filter,
|
||||
RefreshURL: "/?" + filter.encode(),
|
||||
SortURL: buildDashboardSortURLs(filter),
|
||||
UpdatesBehind: updatesBehind,
|
||||
}
|
||||
if err := s.deps.UI.Render(w, "dashboard", view); err != nil {
|
||||
slog.Error("ui: render dashboard", "err", err)
|
||||
@@ -320,6 +348,7 @@ func parseDashboardFilter(q url.Values) dashboardFilter {
|
||||
Tag: q.Get("tag"),
|
||||
Sort: q.Get("sort"),
|
||||
Dir: q.Get("dir"),
|
||||
Updates: q.Get("updates"),
|
||||
}
|
||||
if f.Sort == "" {
|
||||
f.Sort = "name"
|
||||
@@ -352,6 +381,9 @@ func (f dashboardFilter) encode() string {
|
||||
if f.Dir != "" && f.Dir != "asc" {
|
||||
v.Set("dir", f.Dir)
|
||||
}
|
||||
if f.Updates != "" {
|
||||
v.Set("updates", f.Updates)
|
||||
}
|
||||
return v.Encode()
|
||||
}
|
||||
|
||||
@@ -402,6 +434,11 @@ func filterAndSortDashboardHosts(hosts []store.Host, f dashboardFilter) []store.
|
||||
continue
|
||||
}
|
||||
}
|
||||
if f.Updates == "behind" {
|
||||
if h.AgentVersion == "" || h.AgentVersion == version.Version {
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, h)
|
||||
}
|
||||
sortDashboardHosts(out, f.Sort, f.Dir)
|
||||
@@ -809,6 +846,20 @@ type hostChromeData struct {
|
||||
SourceGroupCount int
|
||||
ScheduleCount int
|
||||
ScheduleVersion int64 // host_schedule_version (latest desired)
|
||||
// UpdateAvailable + TargetVersion drive the agent-out-of-date chip
|
||||
// in the host detail header. UpdateAvailable is true iff the host
|
||||
// has connected at least once AND its agent_version != server's.
|
||||
UpdateAvailable bool
|
||||
TargetVersion string
|
||||
// Online + UpdateInProgress drive the per-host "Update agent"
|
||||
// button on host_detail. Online mirrors hub.Connected; pulled here
|
||||
// so the button can disable when the host is unreachable.
|
||||
Online bool
|
||||
UpdateInProgress bool
|
||||
// CanAdmin is true when the viewing user has admin role; used to
|
||||
// gate the "Update agent" button. Kept on the chrome struct so any
|
||||
// page reusing host_chrome already has it for free.
|
||||
CanAdmin bool
|
||||
// KnownTags is the union of tags already in use across the fleet,
|
||||
// used for autocomplete on the host-tags edit form. Cheap query.
|
||||
KnownTags []string
|
||||
@@ -834,6 +885,14 @@ type hostChromeData struct {
|
||||
// render the page with stale counts than 500 the whole tab.
|
||||
func (s *Server) loadHostChrome(r *stdhttp.Request, host store.Host, subtab, crumb string) hostChromeData {
|
||||
d := hostChromeData{Host: host, SubTab: subtab, Crumb: crumb}
|
||||
d.TargetVersion = version.Version
|
||||
d.UpdateAvailable = host.AgentVersion != "" && host.AgentVersion != version.Version
|
||||
if s.deps.Hub != nil {
|
||||
d.Online = s.deps.Hub.Connected(host.ID)
|
||||
}
|
||||
if existing, _ := s.deps.Store.RunningUpdateJobForHost(r.Context(), host.ID); existing != "" {
|
||||
d.UpdateInProgress = true
|
||||
}
|
||||
if groups, err := s.deps.Store.ListSourceGroupsByHost(r.Context(), host.ID); err == nil {
|
||||
d.SourceGroupCount = len(groups)
|
||||
} else {
|
||||
@@ -972,8 +1031,10 @@ func (s *Server) handleUIHostDetail(w stdhttp.ResponseWriter, r *stdhttp.Request
|
||||
|
||||
view := s.baseView(r, u)
|
||||
view.Title = host.Name + " · restic-manager"
|
||||
chrome := s.loadHostChrome(r, *host, "snapshots", "snapshots")
|
||||
chrome.CanAdmin = u.Role == string(store.RoleAdmin)
|
||||
view.Page = hostDetailPage{
|
||||
hostChromeData: s.loadHostChrome(r, *host, "snapshots", "snapshots"),
|
||||
hostChromeData: chrome,
|
||||
Snapshots: shown,
|
||||
SnapshotsShown: len(shown),
|
||||
LegacyRestic: !restic.Env{Version: host.ResticVersion}.AtLeastVersion(0, 17),
|
||||
|
||||
Reference in New Issue
Block a user