Add dynamic DNS and show the running kernel on the overview

router.ddns keeps provider records on the uplink's addresses with ddclient,
run by a timer and by a watcher that starts it when an address changes. The
Dynamic DNS page edits it; the secret is a private file on the router. The
overview shows the running kernel and says when the current system boots a
different one. PLAN.md records why the A/B image approach was not pursued.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-30 13:35:57 +01:00
co-authored by Claude Sonnet 5.5
parent c198ffbe37
commit a36cebc827
19 changed files with 732 additions and 8 deletions
+21
View File
@@ -16,6 +16,7 @@ import (
"net/netip"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
@@ -67,6 +68,8 @@ type UplinkStatus struct {
type SystemStatus struct {
Uptime time.Duration
Load string
Kernel string // the running kernel
KernelNext string // the kernel of the current system when it differs: a reboot is due
CPU float64 // percent; NaN until the second sample
MemUsed uint64
MemTotal uint64
@@ -253,6 +256,12 @@ func systemStatus() SystemStatus {
s.Uptime = time.Duration(secs) * time.Second
}
}
if data, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil {
s.Kernel = strings.TrimSpace(string(data))
}
if dest, err := filepath.EvalSymlinks("/run/current-system/kernel"); err == nil {
s.KernelNext = pendingKernel(s.Kernel, dest)
}
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
if f := strings.Fields(string(data)); len(f) >= 3 {
s.Load = strings.Join(f[:3], " ")
@@ -282,6 +291,18 @@ func systemStatus() SystemStatus {
return s
}
var kernelDirRe = regexp.MustCompile(`-linux-([0-9][^/]*)/`)
// pendingKernel is the version of the kernel image the current system boots
// (a path like /nix/store/…-linux-6.12.5/bzImage) if it is not the running one.
func pendingKernel(running, image string) string {
m := kernelDirRe.FindStringSubmatch(image)
if m == nil || running == "" || m[1] == running || strings.HasPrefix(running, m[1]+"-") {
return ""
}
return m[1]
}
// Sensor is one temperature the machine reports.
type Sensor struct {
Key string // stable name of its series