web: added update notification on web-ui

Signed-off-by: Ejub Sabic <ejub1946@outlook.com>
This commit is contained in:
Ejub Sabic
2026-06-18 10:59:03 +02:00
parent 67ea7a74c1
commit be6d287b19
6 changed files with 126 additions and 35 deletions
@@ -1,6 +1,6 @@
#!/bin/sh
# Check for available firmware updates and notify on login if one exists.
# Called by the scheduler when predefined-action infix-schedule:check-update fires.
# Check for available software updates and notify on login if one exists.
# Called by the scheduler.
NOTIFY_FILE=/run/infix-update
TAG=infix-update
@@ -46,7 +46,7 @@ newer() {
if [ "$IS_RELEASE" = false ] || newer "$LATEST" "$VERSION"; then
RELEASE_URL="${UPDATE_URL}/releases/${LATEST_TAG}"
MSG="Firmware update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})"
MSG="Software update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})"
logger -t "$TAG" "$MSG"
printf '%s\n' "$MSG" > "$NOTIFY_FILE"
else
@@ -95,7 +95,7 @@ submodule infix-system-software {
container check-update {
description
"Policy for automatic firmware update checks.
"Policy for automatic software update checks.
When 'enabled' and 'schedule' references a schedule, the system
checks the configured URL for a newer release on each occurrence
+69 -30
View File
@@ -11,7 +11,9 @@ import (
"math"
"net"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
@@ -199,15 +201,15 @@ type hardwareWrapper struct {
}
type hwComponentJSON struct {
Name string `json:"name"`
Class string `json:"class"`
Description string `json:"description"`
Parent string `json:"parent"`
MfgName string `json:"mfg-name"`
ModelName string `json:"model-name"`
SerialNum string `json:"serial-num"`
HardwareRev string `json:"hardware-rev"`
PhysAddress string `json:"infix-hardware:phys-address"`
Name string `json:"name"`
Class string `json:"class"`
Description string `json:"description"`
Parent string `json:"parent"`
MfgName string `json:"mfg-name"`
ModelName string `json:"model-name"`
SerialNum string `json:"serial-num"`
HardwareRev string `json:"hardware-rev"`
PhysAddress string `json:"infix-hardware:phys-address"`
WiFiRadio *wifiRadioHWJSON `json:"infix-hardware:wifi-radio"`
GPSReceiver *struct{} `json:"infix-hardware:gps-receiver"`
SensorData *struct {
@@ -226,26 +228,26 @@ type hwComponentJSON struct {
type dashboardData struct {
PageData
Hostname string
Contact string
Location string
OSName string
OSVersion string
Machine string
CurrentTime string
Software string
Uptime string
MemTotal int64
MemUsed int64
MemPercent int
MemClass string
Load1 string
Load5 string
Load15 string
CPUClass string
Disks []diskEntry
Board boardInfo
KeyVitals []sensorEntry // Overview's at-a-glance subset: CPU/SoC + wifi-radio temperatures and fan RPMs. Status > Hardware has the full inventory.
Hostname string
Contact string
Location string
OSName string
OSVersion string
Machine string
CurrentTime string
Software string
Uptime string
MemTotal int64
MemUsed int64
MemPercent int
MemClass string
Load1 string
Load5 string
Load15 string
CPUClass string
Disks []diskEntry
Board boardInfo
KeyVitals []sensorEntry // Overview's at-a-glance subset: CPU/SoC + wifi-radio temperatures and fan RPMs. Status > Hardware has the full inventory.
// Connectivity card.
Gateways []gatewayEntry
InternetProbe string // address pinged for the Internet reachability row
@@ -254,7 +256,11 @@ type dashboardData struct {
NTPSync string // "" / the selected NTP source address
// Addresses card.
Addresses []ifaceAddrEntry
Error string
// Software-update banner — shown only when an update is available.
UpdateAvailable bool
UpdateMessage string // verbatim CLI/login-banner notice
UpdateURL string // release URL extracted from the notice
Error string
}
// gatewayEntry is a default route's next-hop.
@@ -305,6 +311,31 @@ type diskEntry struct {
// reachability row — a well-known, stable anycast resolver.
const internetProbe = "1.1.1.1"
// updateNoticeFile holds the software-update notice written by
// /usr/sbin/infix-check-update — the same text the CLI shows at login.
// A package var so tests can point it elsewhere.
var updateNoticeFile = "/run/infix-update"
var updateURLRe = regexp.MustCompile(`https?://\S+`)
// readUpdateNotice returns the software-update message verbatim (as shown in
// the CLI login banner) and the release URL extracted from it. Both are empty
// when no update is pending or the notice file is absent/empty.
func readUpdateNotice() (msg, url string) {
b, err := os.ReadFile(updateNoticeFile)
if err != nil {
return "", ""
}
msg = strings.TrimSpace(string(b))
if msg == "" {
return "", ""
}
if u := updateURLRe.FindString(msg); u != "" {
url = strings.TrimRight(u, ").,;")
}
return msg, url
}
// DashboardHandler serves the main dashboard page.
type DashboardHandler struct {
Template *template.Template
@@ -501,6 +532,14 @@ func (h *DashboardHandler) Index(w http.ResponseWriter, r *http.Request) {
}
data.Addresses = ifaceAddresses(ifaces)
// Software-update banner: surfaces the same notice as the CLI login
// banner (written by infix-check-update to the notice file).
if msg, url := readUpdateNotice(); msg != "" {
data.UpdateAvailable = true
data.UpdateMessage = msg
data.UpdateURL = url
}
tmplName := "dashboard.html"
if r.Header.Get("HX-Request") == "true" {
tmplName = "content"
+37
View File
@@ -579,6 +579,43 @@ details[open].nav-group-top > summary.nav-group-summary-top::before {
padding: 1.25rem;
}
/* Dashboard software-update banner — amber "attention" treatment, bright
and theme-aware, but built from the same card primitives as everything
else. Spans the full info-grid width and sits at the top. */
.update-card {
border-color: var(--warning-border);
background: var(--warning-bg);
box-shadow: 0 0 0 1px var(--warning-border), var(--shadow-sm);
}
.update-card .update-card-header {
background: var(--warning);
color: #fff;
border-bottom-color: var(--warning-border);
display: flex;
align-items: center;
gap: 0.5rem;
}
.update-card .update-card-icon {
font-size: 1rem;
line-height: 1;
}
.update-card .update-card-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem 1.5rem;
flex-wrap: wrap;
}
.update-card .update-card-msg {
margin: 0;
color: var(--warning-fg);
font-size: 0.95rem;
font-weight: 500;
}
.update-card .update-card-action {
flex: none;
}
/* ==========================================================================
Tables
+15
View File
@@ -8,6 +8,21 @@
{{end}}
<section class="info-grid">
{{if .UpdateAvailable}}
<div class="info-card info-grid-full update-card">
<div class="card-header update-card-header">
<span class="update-card-icon" aria-hidden="true">&#x2B06;</span> Software Update Available
</div>
<div class="card-body update-card-body">
<p class="update-card-msg">{{.UpdateMessage}}</p>
{{if .UpdateURL}}
<a href="{{.UpdateURL}}" target="_blank" rel="noopener noreferrer"
class="btn btn-primary update-card-action">View release &rarr;</a>
{{end}}
</div>
</div>
{{end}}
<div class="info-card">
<div class="card-header">System Information</div>
<div class="card-body" style="padding:0">
+1 -1
View File
@@ -12,7 +12,7 @@ with infamy.Test() as test:
env = infamy.Env()
target = env.attach("target", "mgmt", "netconf")
with test.step("Define a schedule and point scheduled-reboot at it"):
with test.step("Schedule a reboot"):
target.put_config_dicts({
"ietf-system": {
"system": {