diff --git a/board/common/rootfs/etc/default/webui b/board/common/rootfs/etc/default/webui index 765ba175..4b9f2800 100644 --- a/board/common/rootfs/etc/default/webui +++ b/board/common/rootfs/etc/default/webui @@ -1,2 +1,5 @@ RESTCONF_URL=https://127.0.0.1/restconf INSECURE_TLS=1 +# Spool firmware uploads (and any other temp files) to persistent storage +# instead of the RAM-backed /tmp to enable push-upgrades on low-memory systems. +TMPDIR=/var/tmp diff --git a/src/rauc-installation-status/rauc-installation-status.c b/src/rauc-installation-status/rauc-installation-status.c index d2832dcc..cecb70e5 100644 --- a/src/rauc-installation-status/rauc-installation-status.c +++ b/src/rauc-installation-status/rauc-installation-status.c @@ -47,10 +47,10 @@ int main(int argc, char **argv) json_object_set_new(json, "last-error", json_string(strval)); props = rauc_installer_get_progress(rauc); if(props) { - GVariant *val; + gint32 pct; progress = json_object(); - g_variant_get(props, "(@isi)", &val, &strval, NULL); - json_object_set_new(progress, "percentage", json_string(g_variant_print(val, FALSE))); + g_variant_get(props, "(isi)", &pct, &strval, NULL); + json_object_set_new(progress, "percentage", json_integer(pct)); json_object_set_new(progress, "message", json_string(strval)); json_object_set_new(json, "progress", progress); } diff --git a/src/webui/internal/server/server.go b/src/webui/internal/server/server.go index 5f6b96cc..90eee648 100644 --- a/src/webui/internal/server/server.go +++ b/src/webui/internal/server/server.go @@ -56,6 +56,14 @@ func New( if err != nil { return nil, err } + sysCtrlTmpl, err := template.ParseFS(templateFS, "layouts/*.html", "pages/system-control.html") + if err != nil { + return nil, err + } + backupTmpl, err := template.ParseFS(templateFS, "layouts/*.html", "pages/backup.html") + if err != nil { + return nil, err + } routingTmpl, err := template.ParseFS(templateFS, "layouts/*.html", "pages/routing.html") if err != nil { return nil, err @@ -155,8 +163,10 @@ func New( } sys := &handlers.SystemHandler{ - RC: rc, - Template: fwrTmpl, + RC: rc, + Template: fwrTmpl, + SysCtrlTmpl: sysCtrlTmpl, + BackupTmpl: backupTmpl, } routing := &handlers.RoutingHandler{Template: routingTmpl, RC: rc} @@ -203,10 +213,19 @@ func New( }) mux.HandleFunc("GET /firmware", sys.Firmware) mux.HandleFunc("GET /firmware/progress", sys.FirmwareProgress) - mux.HandleFunc("POST /firmware/install", sys.FirmwareInstall) - mux.HandleFunc("POST /reboot", sys.Reboot) - mux.HandleFunc("GET /device-status", sys.DeviceStatus) + mux.HandleFunc("POST /firmware/install", sys.FirmwareInstall) + mux.HandleFunc("POST /firmware/upload", sys.FirmwareUpload) + mux.HandleFunc("POST /firmware/boot-order", sys.SetBootOrder) + mux.HandleFunc("POST /reboot", sys.Reboot) // kept for firmware page "Reboot to activate" mux.HandleFunc("GET /config", sys.DownloadConfig) + mux.HandleFunc("GET /maintenance/backup", sys.Backup) + mux.HandleFunc("POST /maintenance/backup/restore", sys.RestoreConfig) + mux.HandleFunc("GET /maintenance/system", sys.SystemControl) + mux.HandleFunc("POST /maintenance/system/reboot", sys.Reboot) + mux.HandleFunc("POST /maintenance/system/shutdown", sys.Shutdown) + mux.HandleFunc("POST /maintenance/system/factory-default", sys.FactoryDefault) + mux.HandleFunc("POST /maintenance/system/factory-reset", sys.FactoryReset) + mux.HandleFunc("POST /maintenance/system/datetime", sys.SetDatetime) mux.HandleFunc("GET /routing", routing.Overview) mux.HandleFunc("GET /wifi", wifi.Overview) mux.HandleFunc("GET /vpn", vpn.Overview) diff --git a/src/webui/static/css/style.css b/src/webui/static/css/style.css index c0bb5982..79bdf0e0 100644 --- a/src/webui/static/css/style.css +++ b/src/webui/static/css/style.css @@ -42,6 +42,10 @@ --warning: var(--amber-500); --danger: var(--red-500); + --warning-bg: #fffbeb; + --warning-fg: #78350f; + --warning-border: #fcd34d; + /* Sidebar — Light mode */ --sidebar-bg: var(--slate-100); --sidebar-fg: var(--slate-700); @@ -84,6 +88,9 @@ --sidebar-hover-fg: #fff; --sidebar-border: rgba(255, 255, 255, 0.1); --dot-color: rgba(255, 255, 255, 0.045); + --warning-bg: #2d2007; + --warning-fg: #fde68a; + --warning-border: #92400e; } } @@ -104,6 +111,9 @@ --sidebar-hover-fg: #fff; --sidebar-border: rgba(255, 255, 255, 0.1); --dot-color: rgba(255, 255, 255, 0.045); + --warning-bg: #2d2007; + --warning-fg: #fde68a; + --warning-border: #92400e; } /* Force Light Mode (override system dark preference) */ @@ -122,6 +132,9 @@ --sidebar-hover-fg: var(--primary); --sidebar-border: rgba(0, 0, 0, 0.08); --dot-color: rgba(0, 0, 0, 0.055); + --warning-bg: #fffbeb; + --warning-fg: #78350f; + --warning-border: #fcd34d; } .light .alert-error { background: #fef2f2; color: #991b1b; border-color: #fecaca; } .light .alert-info { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; } @@ -742,9 +755,11 @@ details[open].nav-group-top > summary.nav-group-summary-top::before { padding: 0.5rem; font-size: 0.9rem; font-weight: 600; - position: sticky; - top: 0; - z-index: 50; + position: fixed; + top: var(--topbar-height); + left: 0; + right: 0; + z-index: 100; } .alert { @@ -1466,6 +1481,11 @@ details.fw-hint[open] summary::before { transform: rotate(90deg); } margin-right: 0.2rem; } +.fw-boot-slots { display: flex; gap: 0.3rem; align-items: center; flex: 1; } +.fw-boot-badge { cursor: grab; user-select: none; } +.fw-boot-badge.fw-boot-dragging { opacity: 0.35; } +.fw-boot-badge.fw-boot-drop-before { box-shadow: -3px 0 0 var(--primary); } + .fw-slot-list { display: flex; flex-direction: column; } .fw-slot-item { padding: 0.75rem 1.25rem; @@ -2012,24 +2032,12 @@ details.fw-hint[open] summary::before { transform: rotate(90deg); } align-items: center; gap: 0.6rem; padding: 0.5rem 1.25rem; - background: #fffbeb; - border-bottom: 1px solid #fcd34d; - color: #78350f; + background: var(--warning-bg); + border-bottom: 1px solid var(--warning-border); + color: var(--warning-fg); font-size: 0.875rem; flex-shrink: 0; } -.dark .cfg-unsaved-banner { - background: #2d2007; - border-color: #92400e; - color: #fde68a; -} -@media (prefers-color-scheme: dark) { - .cfg-unsaved-banner { - background: #2d2007; - border-color: #92400e; - color: #fde68a; - } -} /* cfgError inline feedback */ .cfg-save-status.error { color: var(--danger); cursor: pointer; } @@ -2177,6 +2185,66 @@ select.cfg-input { .dark .cfg-save-status.saved { color: #86efac; } .light .cfg-save-status.saved { color: #16a34a; } +/* ========================================================================== + System Control page + ========================================================================== */ + +.sc-action-body { + display: flex; + flex-direction: column; + justify-content: space-between; + flex: 1; + gap: 1rem; +} + +.sc-desc { + color: var(--fg-muted); + font-size: 0.875rem; + line-height: 1.5; + margin: 0; +} + +.sc-danger-card { border-color: var(--warning-border); } + +.sc-danger-body { display: flex; flex-direction: column; } + +.sc-danger-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1.25rem; + flex-wrap: wrap; +} + +.sc-danger-text { flex: 1; min-width: 0; } +.sc-danger-text strong { display: block; margin-bottom: 0.35rem; } + +.sc-danger-sep { height: 1px; background: var(--border); margin: 0 1.25rem; } + +.sc-fd-ok { color: var(--success); font-size: 0.875rem; } +.sc-fd-err { color: var(--danger); font-size: 0.875rem; } + +.sc-dt-row { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +.sc-restore-label { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.75rem; + cursor: pointer; +} +.sc-restore-warn-text { + color: var(--warning-fg); + font-size: 0.875rem; +} + /* ========================================================================== YANG Tree Navigation ========================================================================== */ diff --git a/src/webui/static/js/app.js b/src/webui/static/js/app.js index d95f4053..89b6540b 100644 --- a/src/webui/static/js/app.js +++ b/src/webui/static/js/app.js @@ -135,6 +135,12 @@ if (!slots || slots.dataset.dndInit) return; slots.dataset.dndInit = 'true'; + // Stash the page-load order so Reset can restore it without a page refresh. + // Slot names are a fixed enum (primary | secondary | net), so comma-joining is safe. + var initialOrder = []; + slots.querySelectorAll('.fw-boot-badge').forEach(function (b) { initialOrder.push(b.dataset.slot); }); + slots.dataset.originalOrder = initialOrder.join(','); + var dragging = null; var insertRef = undefined; // node to insertBefore; undefined = not set, null = append @@ -189,8 +195,28 @@ var saveBtn = document.getElementById('fw-boot-save-btn'); if (saveBtn) saveBtn.addEventListener('click', function () { window.fwBootSave(saveBtn); }); + + var resetBtn = document.getElementById('fw-boot-reset-btn'); + if (resetBtn) resetBtn.addEventListener('click', window.fwBootReset); } + // Restore the boot-order row to the page-load order — i.e. what RAUC + // reported as the current device boot order before any drag/drop. + // Set will push the displayed order to the device; Reset just undoes + // local rearrangement without a server round-trip. + window.fwBootReset = function () { + var slots = document.getElementById('fw-boot-slots'); + if (!slots) return; + var original = (slots.dataset.originalOrder || '').split(','); + var existing = {}; + slots.querySelectorAll('.fw-boot-badge').forEach(function (b) { + existing[b.dataset.slot] = b; + }); + original.forEach(function (slot) { + if (existing[slot]) slots.appendChild(existing[slot]); + }); + }; + window.fwBootSave = function (btn) { var badges = document.querySelectorAll('#fw-boot-slots .fw-boot-badge'); var params = new URLSearchParams(); @@ -213,6 +239,14 @@ body: params.toString(), }).then(function (r) { if (r.ok) { + // Device now holds the displayed order; refresh the Reset baseline + // so a follow-up \u21ba doesn't revert to a state the device no longer has. + var slots = document.getElementById('fw-boot-slots'); + if (slots) { + var saved = []; + badges.forEach(function (b) { saved.push(b.dataset.slot); }); + slots.dataset.originalOrder = saved.join(','); + } btnSet('\u2713 Set', true); setTimeout(function () { btnSet('Set', false); }, 2000); return; diff --git a/src/webui/templates/layouts/base.html b/src/webui/templates/layouts/base.html index c9d947ca..f2f52795 100644 --- a/src/webui/templates/layouts/base.html +++ b/src/webui/templates/layouts/base.html @@ -16,6 +16,7 @@ +
+ + + System Control + diff --git a/src/webui/templates/pages/backup.html b/src/webui/templates/pages/backup.html new file mode 100644 index 00000000..8b120165 --- /dev/null +++ b/src/webui/templates/pages/backup.html @@ -0,0 +1,48 @@ +{{define "backup.html"}} +{{template "base" .}} +{{end}} + +{{define "content"}} +
+ +
+
Backup
+
+

Download the current startup configuration as a JSON file. + The file format is

startup-config-$hostname-$date-$time.cfg
+

+
+ Download config +
+
+
+ +
+
Restore
+
+

Restore a previously backed-up configuration. By default + the restore operation is made to running config, this is the safe default.

+
+
+ +
+ +
+
+ +
+
+
+
+ +
+{{end}} diff --git a/src/webui/templates/pages/firmware.html b/src/webui/templates/pages/firmware.html index 04ad5dda..b10139aa 100644 --- a/src/webui/templates/pages/firmware.html +++ b/src/webui/templates/pages/firmware.html @@ -11,8 +11,9 @@
{{.Message}}
{{end}} -{{if .Installing}} {{$sseURL := "/firmware/progress"}}{{if .AutoReboot}}{{$sseURL = "/firmware/progress?auto-reboot=1"}}{{end}} + +{{if .Installing}}
{{template "fw-progress-body" .}}
@@ -25,7 +26,14 @@ {{if .BootOrder}}
Boot order - {{range .BootOrder}}{{.}}{{end}} + + {{range .BootOrder}}{{.}}{{end}} + + +
{{end}} {{if .Slots}} @@ -42,6 +50,8 @@
{{end}} + {{else if .Installing}} +

Slot details paused during install.

{{else}}

No software information available.

{{end}} @@ -84,25 +94,31 @@ python3 -m http.server 8080 -
-
Upload & Install Coming Soon
+
+
Upload & Install

- Direct file upload is not yet available. As a workaround, serve the - .pkg file from your computer using a local HTTP server (see the URL installer). + Select a .pkg firmware file from your computer to upload and install directly.

-
- - Drop .pkg file here or click to browse +
+ + +
+ +
+
-
+ {{end}} diff --git a/src/webui/templates/pages/system-control.html b/src/webui/templates/pages/system-control.html new file mode 100644 index 00000000..5ee02889 --- /dev/null +++ b/src/webui/templates/pages/system-control.html @@ -0,0 +1,98 @@ +{{define "system-control.html"}} +{{template "base" .}} +{{end}} + +{{define "content"}} +
+ +
+
Reboot
+
+

Restart the device. The running configuration is preserved. + Any unsaved changes (running config not yet copied to startup) will be lost on reboot.

+
+ +
+
+
+ +
+
Shutdown
+
+

Power off the device. Physical access is required to bring it back online.

+
+ +
+
+
+ +
+
Date & Time
+
+

+ {{if .CurrentDatetime}}Device clock: {{.CurrentDatetime}}.{{else}}Device clock unavailable.{{end}} +

+
+ +
+ + +
+
+
+ +
+
+
+
+ +
+
Factory Reset
+
+ +
+
+ Reset running config +

Replaces the running configuration with factory defaults. + No reboot — takes effect immediately. Use the persistent notification to save to startup.

+
+
+ +
+ +
+ +
+
+ Factory reset & reboot +

Wipes all configuration and non-volatile storage, then reboots. + The device may become unreachable on the network. This cannot be undone.

+
+ +
+ +
+
+ +
+{{end}}