No scan data yet — the first automatic scan runs within a couple of minutes, or hit Check now.
`}`;
}
backupSummary() {
diff --git a/containers/libreportal/frontend/components/updater/index.js b/containers/libreportal/frontend/components/updater/index.js
index d174b76..b142d98 100644
--- a/containers/libreportal/frontend/components/updater/index.js
+++ b/containers/libreportal/frontend/components/updater/index.js
@@ -27,6 +27,9 @@ LP.features.register({
// doesn't repaint a torn-down page. The page self-guards via
// (window.updaterPage === this); nulling it neutralises any pending work.
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('updater'); } catch (_) {}
+ // Release the auto-refresh timer that keeps the page in step with the
+ // host-side auto-scan.
+ try { window.updaterPage && window.updaterPage.dispose && window.updaterPage.dispose(); } catch (_) {}
window.updaterPage = null;
},
});
diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js
index 82be798..f625788 100644
--- a/containers/libreportal/frontend/components/updater/js/updater-page.js
+++ b/containers/libreportal/frontend/components/updater/js/updater-page.js
@@ -18,6 +18,7 @@ class UpdaterPage {
this.apps = []; // merged per-app view rendered in the table
this._pushedAnyTab = false;
this._eventBound = false;
+ this._poll = null; // auto-refresh timer (startAutoRefresh/dispose)
}
// ---- lifecycle -----------------------------------------------------------
@@ -29,6 +30,42 @@ class UpdaterPage {
await this.refreshAll();
this.render();
this.updateHeader();
+ // Keep the open page in step with the host-side auto-scan (the task
+ // processor refreshes the generated JSON on its own schedule, with no task
+ // event to hook). Repaints only when the data actually changed.
+ this.startAutoRefresh(
+ () => this.render(),
+ () => window.updaterPage === this && !!document.getElementById('updater-page')
+ );
+ }
+
+ // The host auto-scan (`libreportal updater check auto`, run from the task
+ // processor's idle poll) rewrites the generated JSON without a task event,
+ // so an open page re-reads it on a slow timer: a few static-file GETs per
+ // minute, and onChange() only when a generated_at stamp moved. isActive lets
+ // the hosting surface skip ticks while it isn't visible; dispose() releases
+ // the timer (also called by the feature unmount).
+ startAutoRefresh(onChange, isActive) {
+ if (this._poll) return;
+ this._poll = setInterval(() => {
+ if (document.hidden || (isActive && !isActive())) return;
+ const before = this._dataStamp();
+ this.refreshAll().then(() => {
+ if (this._poll && this._dataStamp() !== before && onChange) onChange();
+ });
+ }, 60000);
+ }
+
+ dispose() {
+ if (this._poll) { clearInterval(this._poll); this._poll = null; }
+ }
+
+ // Cheap change detector for the auto-refresh: the generators stamp each file
+ // with generated_at; history has no stamp, so its newest entry stands in.
+ _dataStamp() {
+ const g = (d) => (d && d.generated_at) || '';
+ const h = this.history && this.history.entries && this.history.entries[0];
+ return [g(this.updates), g(this.cves), g(this.artifacts), (h && h.ts) || ''].join('|');
}
parseTabFromUrl() {
@@ -272,7 +309,7 @@ class UpdaterPage {
${card('system', c.apps, 'Apps tracked', `last scan: ${checked}`,
``)}
- ${this.updates ? '' : `
No scan data yet — showing your installed apps. Run Check now to fetch versions & vulnerabilities.
`}`;
+ ${this.updates ? '' : `
No scan data yet — showing your installed apps. The first automatic scan runs within a couple of minutes, or hit Check now.
`}`;
}
renderUpdates() {
@@ -305,7 +342,7 @@ class UpdaterPage {
// withToolbar=false lets an embedding surface (the fleet Overview tab) skip
// the inline Check button because it provides one in its own header.
renderImprovements(withToolbar = true) {
- if (!this.artifacts) return this.empty('No hotfix data yet. Run a check to fetch the signed improvements index.', true);
+ if (!this.artifacts) return this.empty('No hotfix data yet — the automatic scan fetches the signed improvements index within a couple of minutes.', true);
const list = Array.isArray(this.artifacts.artifacts) ? this.artifacts.artifacts : [];
const signed = !!this.artifacts.signed;
if (!list.length) return this.empty('No improvements available right now — you are all caught up. 🎉');
@@ -339,7 +376,7 @@ class UpdaterPage {
renderSecurity() {
const withCves = this.apps.filter(a => (a.cves || []).length);
- if (!this.cves) return this.empty('No vulnerability scan yet. Run a check to scan your app images for known CVEs.', true);
+ if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.', true);
if (!withCves.length) return this.empty('No known vulnerabilities in your installed apps. 🎉');
const blocks = withCves.map(a => {
const items = (a.cves || []).map(c => `
diff --git a/scripts/cli/commands/updater/cli_updater_commands.sh b/scripts/cli/commands/updater/cli_updater_commands.sh
index 33b4875..31dc7e5 100644
--- a/scripts/cli/commands/updater/cli_updater_commands.sh
+++ b/scripts/cli/commands/updater/cli_updater_commands.sh
@@ -22,6 +22,22 @@ cliHandleUpdaterCommands()
case "$sub" in
""|"check")
+ # `check auto` — the task processor's idle poll calls this every
+ # ~60s; self-throttle on the age of the generated updates.json so a
+ # full scan only happens once per CFG_UPDATER_SCAN_INTERVAL minutes
+ # (0 disables automatic scans). A manual check / post-update rescan
+ # rewrites updates.json, which resets this clock for free. A missing
+ # file means never scanned -> run now (first scan needs no click).
+ if [[ "$app" == "auto" ]]; then
+ local scan_interval="${CFG_UPDATER_SCAN_INTERVAL:-30}"
+ [[ "$scan_interval" =~ ^[0-9]+$ ]] || scan_interval=30
+ (( scan_interval == 0 )) && return 0
+ local scan_file="${containers_dir%/}/libreportal/frontend/data/updater/generated/updates.json"
+ if [[ -f "$scan_file" ]]; then
+ local _now _last; _now=$(date +%s); _last=$(stat -c '%Y' "$scan_file" 2>/dev/null || echo 0)
+ (( _now - _last < scan_interval * 60 )) && return 0
+ fi
+ fi
# Quick + safe — just regenerates the read-only data files. Source
# the generator explicitly if the lazy loader hasn't mapped it yet
# (new file; the array regen self-heals it on deploy, this covers
diff --git a/scripts/cli/commands/updater/cli_updater_header.sh b/scripts/cli/commands/updater/cli_updater_header.sh
index f7946e9..59d5bdd 100644
--- a/scripts/cli/commands/updater/cli_updater_header.sh
+++ b/scripts/cli/commands/updater/cli_updater_header.sh
@@ -8,7 +8,9 @@ cliShowUpdaterHelp()
echo ""
echo "Available App Updater Commands:"
echo ""
- echo " libreportal updater check - Refresh per-app version & vulnerability data"
+ echo " libreportal updater check [auto] - Refresh per-app version & vulnerability data"
+ echo " (auto = background mode: only scans when the data is"
+ echo " older than CFG_UPDATER_SCAN_INTERVAL minutes)"
echo " libreportal updater apply - Update one app (snapshots it first; auto-rollback on failure)"
echo " libreportal updater apply-all [a,b] - Update a comma-list of apps (each snapshotted first)"
echo " libreportal updater rollback - Restore an app's most recent pre-update snapshot"
diff --git a/scripts/task/crontab_task_processor.sh b/scripts/task/crontab_task_processor.sh
index 711eea2..ee7992b 100755
--- a/scripts/task/crontab_task_processor.sh
+++ b/scripts/task/crontab_task_processor.sh
@@ -459,6 +459,12 @@ maybeRegenPoll() {
# config file — so it runs every poll, self-throttled to its own interval
# (cheap no-op most ticks), writing network_status.json when a scan is due.
command -v libreportal >/dev/null 2>&1 && libreportal system network check >/dev/null 2>&1 || true
+ # App updater auto-scan (versions / CVEs / signed improvements). Same shape as
+ # the drift check: runs every poll, self-throttled inside the CLI to
+ # CFG_UPDATER_SCAN_INTERVAL minutes via the age of the generated updates.json,
+ # so almost every tick is a single stat() and an early return. Eligible signed
+ # hotfixes are enqueued as ordinary tasks by the scan, never applied inline.
+ command -v libreportal >/dev/null 2>&1 && libreportal updater check auto >/dev/null 2>&1 || true
}
# ============================================================================
diff --git a/scripts/webui/data/generators/config/webui_update_config.sh b/scripts/webui/data/generators/config/webui_update_config.sh
index 25e6487..d33ff8c 100755
--- a/scripts/webui/data/generators/config/webui_update_config.sh
+++ b/scripts/webui/data/generators/config/webui_update_config.sh
@@ -72,7 +72,7 @@ webuiValidateConfigValue() {
isError " Invalid crontab format for $var_name"
fi
;;
- CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_UPDATER_CHECK|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC)
+ CFG_BACKUP_KEEP_LAST|CFG_BACKUP_KEEP_DAILY|CFG_BACKUP_KEEP_WEEKLY|CFG_BACKUP_KEEP_MONTHLY|CFG_BACKUP_KEEP_YEARLY|CFG_BACKUP_VERIFY_DATA_PERCENT|CFG_UPDATER_CHECK|CFG_UPDATER_SCAN_INTERVAL|CFG_SWAPFILE_SIZE|CFG_GENERATED_PASS_LENGTH|CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES|CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC)
# Validate numeric values
if ! echo "$var_value" | grep -qE '^[0-9]+$'; then
isError " $var_name must be a positive integer"
diff --git a/scripts/webui/data/generators/updater/webui_updater_scan.sh b/scripts/webui/data/generators/updater/webui_updater_scan.sh
index b7af812..4d46633 100644
--- a/scripts/webui/data/generators/updater/webui_updater_scan.sh
+++ b/scripts/webui/data/generators/updater/webui_updater_scan.sh
@@ -7,10 +7,13 @@
# frontend/data/updater/generated/cves.json (per-app known CVEs)
# frontend/data/updater/generated/history.json (created/owned by `apply`)
#
-# Run on demand by `libreportal updater check` (NOT on every regen) so a slow
-# registry/scanner call never stalls the periodic WebUI refresh. The WebUI
-# degrades gracefully when these files are absent (it derives the app list from
-# the installed-apps data), so this generator is a bonus, never a dependency.
+# Run by `libreportal updater check` — manually, or automatically from the task
+# processor's idle poll via `updater check auto`, which only falls through to a
+# real scan when updates.json is older than CFG_UPDATER_SCAN_INTERVAL minutes.
+# Kept OFF the regen path so a slow registry/scanner call never stalls the
+# periodic WebUI refresh. The WebUI degrades gracefully when these files are
+# absent (it derives the app list from the installed-apps data), so this
+# generator is a bonus, never a dependency.
#
# Version + CVE discovery is intentionally pluggable: the loop below records
# each installed app's CURRENT image (from its compose file) and leaves clearly