feat(webui): track system-config backup status on the dashboard

Make the system config a tracked backup, not just action buttons:

- engine: resticSystemSnapshotsJson (tag system=config) + engineSystemSnapshotsJson
  dispatcher — query the system snapshots the way per-app status is queried.
- webui_backup_dashboard.sh: emit a "system": { latest_snapshot, latest_time }
  object (latest system snapshot on the primary location), and exclude the
  libreportal WebUI app from the per-app grid (it's intentionally not backed up, so
  it no longer shows a perpetual "No backup yet" tile).
- backup dashboard card: a status line (dot + "Last backed up <relative>" / "No
  backup yet"), populated in renderDashboard from d.system — mirrors the app tiles.

Verified: shell + JS parse; dashboard content assembles to valid JSON with the
system key; engine query defined + dispatched; frontend reads d.system into the
#backup-system-status element.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-26 00:38:39 +01:00
parent 914185d42d
commit 3283b3f7a3
5 changed files with 47 additions and 1 deletions

View File

@ -93,6 +93,9 @@
<h2>System config</h2>
<span class="backup-card-hint">Global settings, WebUI login &amp; backup-location credentials</span>
</div>
<div class="backup-app-tile-meta" id="backup-system-status" style="margin:0 0 10px">
<span class="backup-status-dot none"></span><span>No backup yet</span>
</div>
<p class="backup-card-hint" style="margin:0 0 12px">
Snapshot the LibrePortal system config to every enabled location so a bare-metal
restore is self-sufficient — without it, the credentials needed to reach your own

View File

@ -550,6 +550,16 @@ class BackupPage {
appGrid.innerHTML = apps.map(app => this.renderAppTile(app)).join('');
}
const sysStatus = document.getElementById('backup-system-status');
if (sysStatus) {
const sys = d.system || {};
const hasSys = !!sys.latest_snapshot;
sysStatus.innerHTML = `
<span class="backup-status-dot ${hasSys ? 'ok' : 'none'}"></span>
<span>${hasSys ? 'Last backed up ' + this.formatRelative(sys.latest_time) : 'No backup yet'}</span>
`;
}
if (!locs.length) {
locSummary.innerHTML = `<div class="backup-empty-state">No locations enabled.</div>`;
} else {

View File

@ -53,6 +53,7 @@ engineRestoreSystemLatest() { local i="$1"; shift; engineDispatch "$(engineForL
engineRestoreSnapshot() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSnapshot" "$i" "$@"; }
engineSnapshotLatestId() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotLatestId" "$i" "$@"; }
engineSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotsJson" "$i" "$@"; }
engineSystemSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SystemSnapshotsJson" "$i" "$@"; }
engineSnapshotListFiles() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotListFiles" "$i" "$@"; }
engineForgetApp() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")ForgetApp" "$i" "$@"; }
engineCheckLocation() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")CheckLocation" "$i" "$@"; }

View File

@ -18,6 +18,22 @@ resticSnapshotsJson()
return $rc
}
resticSystemSnapshotsJson()
{
local idx="$1"
local host_filter="$2"
resticEnvExport "$idx" || return 1
local args=(snapshots --json --no-lock --tag "system=config")
[[ -n "$host_filter" ]] && args+=(--host "$host_filter")
runBackupOp restic "${args[@]}" 2>/dev/null
local rc=$?
resticEnvUnset
return $rc
}
resticSnapshotLatestId()
{
local idx="$1"

View File

@ -55,6 +55,10 @@ webuiGenerateBackupDashboard()
primary_idx=$(resticEnabledLocations | head -1)
while IFS= read -r app; do
[[ -z "$app" ]] && continue
# The WebUI app is reproducible and skipped by backupAllApps — its
# state rides in the system-config snapshot below, so don't show a
# perpetually-"No backup yet" tile for it.
[[ "$app" == "libreportal" ]] && continue
local latest_id="" latest_time=""
if [[ -n "$primary_idx" ]]; then
local snap_json
@ -69,6 +73,17 @@ webuiGenerateBackupDashboard()
fi
apps_json+="]"
# System-config backup status (tag system=config) — its own tracked item.
local system_id="" system_time=""
local sys_idx
sys_idx=$(resticEnabledLocations | head -1)
if [[ -n "$sys_idx" ]]; then
local sys_json
sys_json=$(engineSystemSnapshotsJson "$sys_idx" "$CFG_INSTALL_NAME" 2>/dev/null)
system_id=$(echo "$sys_json" | grep -o '"short_id":"[^"]*"' | tail -1 | cut -d'"' -f4)
system_time=$(echo "$sys_json" | grep -o '"time":"[^"]*"' | tail -1 | cut -d'"' -f4)
fi
local content="{"
content+="\"generated_at\":\"$generated_at\","
content+="\"install_name\":\"${CFG_INSTALL_NAME:-libreportal}\","
@ -76,7 +91,8 @@ webuiGenerateBackupDashboard()
content+="\"verify_after\":${CFG_BACKUP_VERIFY_AFTER:-false},"
content+="\"strategy\":\"${CFG_BACKUP_STRATEGY:-auto}\","
content+="\"locations\":$locations_json,"
content+="\"apps\":$apps_json"
content+="\"apps\":$apps_json,"
content+="\"system\":{\"latest_snapshot\":\"$system_id\",\"latest_time\":\"$system_time\"}"
content+="}"
echo "$content" | runFileWrite "$output_file"