mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: refactor maintenance section
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
========================================================================== */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<body hx-boost="true">
|
||||
<div id="page-progress" class="page-progress" aria-hidden="true"></div>
|
||||
<div id="conn-banner" class="conn-banner" hidden>Device unreachable</div>
|
||||
<div hidden hx-get="/device-status" hx-trigger="every 5s" hx-swap="none"></div>
|
||||
<div class="topbar">
|
||||
<button id="hamburger-btn" class="hamburger-btn" aria-label="Open menu" aria-expanded="false">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
@@ -218,20 +218,24 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/config" class="nav-link" hx-boost="false" download>
|
||||
<a href="/maintenance/backup"
|
||||
hx-get="/maintenance/backup"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "backup"}}active{{end}}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
Download Config
|
||||
Backup & Restore
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="nav-link nav-link-btn nav-link-danger"
|
||||
hx-post="/reboot"
|
||||
hx-confirm="Reboot the device?"
|
||||
hx-target="#content"
|
||||
hx-swap="innerHTML">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
||||
Reboot
|
||||
</button>
|
||||
<a href="/maintenance/system"
|
||||
hx-get="/maintenance/system"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "system-control"}}active{{end}}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
System Control
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{{define "backup.html"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="info-grid">
|
||||
|
||||
<section class="info-card">
|
||||
<div class="card-header">Backup</div>
|
||||
<div class="card-body sc-action-body">
|
||||
<p class="sc-desc">Download the current startup configuration as a JSON file.
|
||||
The file format is <pre>startup-config-$hostname-$date-$time.cfg</pre>
|
||||
</p>
|
||||
<div>
|
||||
<a href="/config" hx-boost="false" class="btn btn-primary">Download config</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card">
|
||||
<div class="card-header">Restore</div>
|
||||
<div class="card-body sc-action-body">
|
||||
<p class="sc-desc">Restore a previously backed-up configuration. By default
|
||||
the restore operation is made to running config, this is the safe default.</p>
|
||||
<form id="restore-form"
|
||||
hx-post="/maintenance/backup/restore"
|
||||
hx-target="#restore-result"
|
||||
hx-swap="innerHTML"
|
||||
hx-encoding="multipart/form-data"
|
||||
hx-confirm="Apply this configuration to the running system?"
|
||||
hx-headers='{"X-CSRF-Token": "{{.CsrfToken}}"}'>
|
||||
<div class="sc-dt-row">
|
||||
<input type="file" name="config" accept=".json" required class="cfg-input">
|
||||
</div>
|
||||
<label class="sc-restore-label">
|
||||
<input type="checkbox" name="save-to-startup" id="restore-startup-cb">
|
||||
<span class="sc-restore-warn-text">Save to startup — may break system boot!</span>
|
||||
</label>
|
||||
<div id="restore-result"></div>
|
||||
<div style="margin-top:0.75rem">
|
||||
<button type="submit" class="btn btn-primary">Restore</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -11,8 +11,9 @@
|
||||
<div class="alert alert-info">{{.Message}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Installing}}
|
||||
{{$sseURL := "/firmware/progress"}}{{if .AutoReboot}}{{$sseURL = "/firmware/progress?auto-reboot=1"}}{{end}}
|
||||
|
||||
{{if .Installing}}
|
||||
<section id="fw-progress-card" class="info-card" data-sse-src="{{$sseURL}}">
|
||||
{{template "fw-progress-body" .}}
|
||||
</section>
|
||||
@@ -25,7 +26,14 @@
|
||||
{{if .BootOrder}}
|
||||
<div class="fw-boot-order-row">
|
||||
<span class="fw-boot-order-label">Boot order</span>
|
||||
{{range .BootOrder}}<span class="badge badge-neutral">{{.}}</span>{{end}}
|
||||
<span id="fw-boot-slots" class="fw-boot-slots">
|
||||
{{range .BootOrder}}<span class="badge badge-neutral fw-boot-badge" draggable="true" data-slot="{{.}}">{{.}}</span>{{end}}
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline"
|
||||
id="fw-boot-save-btn">Set</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm"
|
||||
id="fw-boot-reset-btn"
|
||||
title="Restore current device boot order">↺</button>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Slots}}
|
||||
@@ -42,6 +50,8 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .Installing}}
|
||||
<p class="empty-message">Slot details paused during install.</p>
|
||||
{{else}}
|
||||
<p class="empty-message">No software information available.</p>
|
||||
{{end}}
|
||||
@@ -84,25 +94,31 @@ python3 -m http.server 8080</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card fw-card-muted">
|
||||
<div class="card-header">Upload & Install <span class="badge badge-neutral">Coming Soon</span></div>
|
||||
<section class="info-card">
|
||||
<div class="card-header">Upload & Install</div>
|
||||
<div class="card-body">
|
||||
<p class="fw-help-text">
|
||||
Direct file upload is not yet available. As a workaround, serve the
|
||||
<code>.pkg</code> file from your computer using a local HTTP server (see the URL installer).
|
||||
Select a <code>.pkg</code> firmware file from your computer to upload and install directly.
|
||||
</p>
|
||||
<div class="fw-upload-placeholder">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
<span>Drop .pkg file here or click to browse</span>
|
||||
<div class="form-group">
|
||||
<label for="fw-file">Firmware Package (.pkg)</label>
|
||||
<input type="file" id="fw-file" accept=".pkg" class="cfg-input" required>
|
||||
</div>
|
||||
<label class="fw-checkbox-row">
|
||||
<input type="checkbox" id="fw-upload-auto-reboot">
|
||||
Reboot after successful firmware upgrade
|
||||
</label>
|
||||
<div style="margin-top:0.75rem">
|
||||
<button type="button" class="btn btn-primary" id="fw-upload-btn"
|
||||
data-sse-url="{{$sseURL}}"
|
||||
{{if and .Installer .Installer.Active}}disabled{{end}}>
|
||||
Upload & Install
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" disabled>Upload & Install</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
{{define "system-control.html"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="info-grid">
|
||||
|
||||
<section class="info-card">
|
||||
<div class="card-header">Reboot</div>
|
||||
<div class="card-body sc-action-body">
|
||||
<p class="sc-desc">Restart the device. The running configuration is preserved.
|
||||
Any unsaved changes (running config not yet copied to startup) will be lost on reboot.</p>
|
||||
<div>
|
||||
<button class="btn btn-primary"
|
||||
hx-post="/maintenance/system/reboot"
|
||||
hx-confirm="Reboot the device?"
|
||||
hx-target="#content"
|
||||
hx-swap="innerHTML">Reboot</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card">
|
||||
<div class="card-header">Shutdown</div>
|
||||
<div class="card-body sc-action-body">
|
||||
<p class="sc-desc">Power off the device. Physical access is required to bring it back online.</p>
|
||||
<div>
|
||||
<button class="btn btn-secondary"
|
||||
hx-post="/maintenance/system/shutdown"
|
||||
hx-confirm="Shut down the device?"
|
||||
hx-target="#content"
|
||||
hx-swap="innerHTML">Shut down</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card">
|
||||
<div class="card-header">Date & Time</div>
|
||||
<div class="card-body sc-action-body">
|
||||
<p class="sc-desc">
|
||||
{{if .CurrentDatetime}}Device clock: <strong>{{.CurrentDatetime}}</strong>.{{else}}Device clock unavailable.{{end}}
|
||||
</p>
|
||||
<form hx-post="/maintenance/system/datetime"
|
||||
hx-target="#sc-dt-result"
|
||||
hx-swap="innerHTML">
|
||||
<input type="hidden" name="csrf" value="{{.CsrfToken}}">
|
||||
<div class="sc-dt-row">
|
||||
<input type="datetime-local" id="sc-dt-input" name="datetime"
|
||||
class="cfg-input" required>
|
||||
<button type="button" id="sc-sync-time-btn"
|
||||
class="btn btn-outline btn-sm">Browser time</button>
|
||||
</div>
|
||||
<div id="sc-dt-result"></div>
|
||||
<div style="margin-top:0.75rem">
|
||||
<button type="submit" class="btn btn-primary">Set time</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card info-grid-full sc-danger-card">
|
||||
<div class="card-header">Factory Reset</div>
|
||||
<div class="sc-danger-body">
|
||||
|
||||
<div class="sc-danger-item">
|
||||
<div class="sc-danger-text">
|
||||
<strong>Reset running config</strong>
|
||||
<p class="sc-desc">Replaces the running configuration with factory defaults.
|
||||
No reboot — takes effect immediately. Use the persistent notification to save to startup.</p>
|
||||
<div id="sc-fd-result"></div>
|
||||
</div>
|
||||
<button class="btn btn-danger-outline"
|
||||
hx-post="/maintenance/system/factory-default"
|
||||
hx-confirm="Reset running configuration to factory defaults? This cannot be undone."
|
||||
hx-target="#sc-fd-result"
|
||||
hx-swap="innerHTML">Reset running config</button>
|
||||
</div>
|
||||
|
||||
<div class="sc-danger-sep"></div>
|
||||
|
||||
<div class="sc-danger-item">
|
||||
<div class="sc-danger-text">
|
||||
<strong>Factory reset & reboot</strong>
|
||||
<p class="sc-desc">Wipes all configuration and non-volatile storage, then reboots.
|
||||
The device may become unreachable on the network. This cannot be undone.</p>
|
||||
</div>
|
||||
<button class="btn btn-danger-outline"
|
||||
hx-post="/maintenance/system/factory-reset"
|
||||
hx-confirm="Wipe all configuration and reboot? This cannot be undone."
|
||||
hx-target="#content"
|
||||
hx-swap="innerHTML">Factory reset</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user