diff --git a/containers/libreportal/frontend/html/backup-content.html b/containers/libreportal/frontend/html/backup-content.html
index 624d52f..937ca7b 100644
--- a/containers/libreportal/frontend/html/backup-content.html
+++ b/containers/libreportal/frontend/html/backup-content.html
@@ -93,6 +93,9 @@
System config
Global settings, WebUI login & backup-location credentials
+
+ No backup yet
+
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
diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js
index bc90606..bb9c13e 100644
--- a/containers/libreportal/frontend/js/components/backup/backup-page.js
+++ b/containers/libreportal/frontend/js/components/backup/backup-page.js
@@ -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 = `
+
+ ${hasSys ? 'Last backed up ' + this.formatRelative(sys.latest_time) : 'No backup yet'}
+ `;
+ }
+
if (!locs.length) {
locSummary.innerHTML = `
No locations enabled.
`;
} else {
diff --git a/scripts/backup/engine/engine_dispatch.sh b/scripts/backup/engine/engine_dispatch.sh
index d4a8917..81a52d6 100644
--- a/scripts/backup/engine/engine_dispatch.sh
+++ b/scripts/backup/engine/engine_dispatch.sh
@@ -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" "$@"; }
diff --git a/scripts/backup/engine/restic_snapshots.sh b/scripts/backup/engine/restic_snapshots.sh
index f0d262d..503d919 100644
--- a/scripts/backup/engine/restic_snapshots.sh
+++ b/scripts/backup/engine/restic_snapshots.sh
@@ -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"
diff --git a/scripts/webui/data/generators/backup/webui_backup_dashboard.sh b/scripts/webui/data/generators/backup/webui_backup_dashboard.sh
index 65d532b..fe91b9e 100644
--- a/scripts/webui/data/generators/backup/webui_backup_dashboard.sh
+++ b/scripts/webui/data/generators/backup/webui_backup_dashboard.sh
@@ -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"