diff --git a/handler/routes.go b/handler/routes.go index 37b230c..853b5c1 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -29,6 +29,7 @@ import ( "github.com/ngoduykhanh/wireguard-ui/firewall" "github.com/ngoduykhanh/wireguard-ui/model" "github.com/ngoduykhanh/wireguard-ui/store" + "github.com/ngoduykhanh/wireguard-ui/system" "github.com/ngoduykhanh/wireguard-ui/telegram" "github.com/ngoduykhanh/wireguard-ui/util" ) @@ -1770,3 +1771,11 @@ func AboutPage() echo.HandlerFunc { }) } } + +// GetSystemUpdateStatus reports pending OS package updates (Debian/Ubuntu +// via apt), read-only - nothing is installed or upgraded. +func GetSystemUpdateStatus() echo.HandlerFunc { + return func(c echo.Context) error { + return c.JSON(http.StatusOK, system.CheckAptUpdates()) + } +} diff --git a/main.go b/main.go index 059af48..95d6308 100644 --- a/main.go +++ b/main.go @@ -236,6 +236,7 @@ func main() { app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) app.GET(util.BasePath+"/about", handler.AboutPage()) + app.GET(util.BasePath+"/system/update-status", handler.GetSystemUpdateStatus(), handler.ValidSession, handler.NeedsAdmin) app.GET(util.BasePath+"/_health", handler.Health()) app.GET(util.BasePath+"/favicon", handler.Favicon()) app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) diff --git a/system/updates.go b/system/updates.go new file mode 100644 index 0000000..8caac74 --- /dev/null +++ b/system/updates.go @@ -0,0 +1,63 @@ +// Package system checks the host OS for pending package updates. It never +// installs or upgrades anything - read-only status only, using the +// existing apt package index (no "apt update" is run automatically, since +// that touches network/package state on every page load). +package system + +import ( + "bufio" + "context" + "os" + "os/exec" + "strings" + "time" +) + +// UpdateStatus summarizes pending OS package updates on a Debian/Ubuntu host. +type UpdateStatus struct { + Supported bool `json:"supported"` // false if apt isn't available (e.g. non-Debian host) + UpgradableCount int `json:"upgradable_count"` + Packages []string `json:"packages"` + RebootRequired bool `json:"reboot_required"` + CheckedAt time.Time `json:"checked_at"` + Error string `json:"error,omitempty"` +} + +// CheckAptUpdates lists upgradable packages from the current apt index +// (`apt list --upgradable`) without refreshing it, and checks for the +// standard Debian/Ubuntu reboot-required marker file. +func CheckAptUpdates() UpdateStatus { + status := UpdateStatus{CheckedAt: time.Now().UTC()} + + if _, err := exec.LookPath("apt"); err != nil { + status.Supported = false + return status + } + status.Supported = true + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable") + out, err := cmd.Output() + if err != nil { + status.Error = "cannot list upgradable packages: " + err.Error() + return status + } + + scanner := bufio.NewScanner(strings.NewReader(string(out))) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "Listing...") { + continue + } + status.Packages = append(status.Packages, line) + } + status.UpgradableCount = len(status.Packages) + + if _, err := os.Stat("/var/run/reboot-required"); err == nil { + status.RebootRequired = true + } + + return status +} diff --git a/templates/about.html b/templates/about.html index a903068..66ce295 100644 --- a/templates/about.html +++ b/templates/about.html @@ -45,6 +45,25 @@ About + + +
Read-only check against the current apt package index. Nothing is installed automatically.
+