`;
}
// Title block for the per-app Updates tab — emoji + title + description on the
- // left, Check/Update actions on the right. Mirrors .backup-title (.config-title
- // family) so every app-detail tab shares one header idiom. `a` may be null
- // (no data yet); the Update button only appears when an update is available.
+ // left, the Update action on the right. Mirrors .backup-title (.config-title
+ // family) so every app-detail tab shares one header idiom. There's no manual
+ // "Check" button: scans run automatically (the panel's auto-check line reports
+ // when). `a` may be null (no data yet); the Update button only appears when an
+ // update is genuinely available.
renderAppUpdaterHead(a) {
const esc = (s) => (this.appUpdater ? this.appUpdater.escape(s) : String(s == null ? '' : s));
const name = this.currentApp
@@ -616,7 +619,6 @@ class AppTabbedManager {
Version, security and recovery for ${esc(name)}.
-
${updBtn}
`;
diff --git a/containers/libreportal/frontend/components/apps/overview/css/overview.css b/containers/libreportal/frontend/components/apps/overview/css/overview.css
index 3882d7b..9f4805f 100644
--- a/containers/libreportal/frontend/components/apps/overview/css/overview.css
+++ b/containers/libreportal/frontend/components/apps/overview/css/overview.css
@@ -360,6 +360,32 @@
.updater-detail-container .updater-detail-section { padding: 16px 0; }
.updater-detail-container .updater-detail-section:first-child { padding-top: 0; }
.updater-detail-container .updater-detail-section:last-child { padding-bottom: 0; }
+
+/* "Checked automatically · last checked X · next check ~Y" — a calm status line
+ at the top of the panel that replaces the old manual "Check" button, since
+ scans run on a schedule. Green dot = automation live; muted when it's off. */
+.updater-autocheck {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ margin: 0 0 14px;
+ padding: 9px 12px;
+ border-radius: 8px;
+ background: rgba(var(--page-updater-rgb), .08);
+ border: 1px solid rgba(var(--page-updater-rgb), .18);
+ font-size: .82rem;
+ color: rgba(var(--text-rgb), .72);
+}
+.updater-autocheck strong { color: var(--text-primary, #fff); font-weight: 600; }
+.updater-autocheck-dot {
+ width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
+ background: #36d399; box-shadow: 0 0 8px rgba(54, 211, 153, .6);
+}
+.updater-autocheck.off {
+ background: rgba(var(--text-rgb), .04);
+ border-color: rgba(var(--text-rgb), .1);
+}
+.updater-autocheck.off .updater-autocheck-dot { background: rgba(var(--text-rgb), .3); box-shadow: none; }
.updater-detail-container .updater-detail-section + .updater-detail-section {
border-top: 1px solid var(--border-color);
}
diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js
index e7bd2af..0f03fcc 100644
--- a/containers/libreportal/frontend/components/updater/js/updater-page.js
+++ b/containers/libreportal/frontend/components/updater/js/updater-page.js
@@ -545,6 +545,38 @@ class UpdaterPage {
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
return `${Math.floor(s / 86400)}d ago`;
}
+
+ // Approximate "when will the next automatic check run" — the auto-scan fires on
+ // the task processor's idle poll once updates.json is older than the interval,
+ // so this is an earliest-estimate, hence the "~".
+ fmtRelFuture(t) {
+ const s = (t - Date.now()) / 1000;
+ if (!t || isNaN(t) || s <= 0) return 'due now';
+ if (s < 90) return 'in ~1m';
+ if (s < 3600) return `in ~${Math.round(s / 60)}m`;
+ if (s < 86400) return `in ~${Math.round(s / 3600)}h`;
+ return `in ~${Math.round(s / 86400)}d`;
+ }
+
+ // "Checked automatically · last checked X · next check ~Y" — LibrePortal scans
+ // for updates on a schedule (CFG_UPDATER_SCAN_INTERVAL, stamped into
+ // updates.json), so a manual "Check" button is redundant; this line says when
+ // the last scan ran and when the next is due instead. interval 0 = auto off.
+ renderAutoCheckLine() {
+ const gen = this.updates && this.updates.generated_at;
+ const iv = this.updates ? Number(this.updates.scan_interval_minutes) : NaN;
+ if (!gen && !(iv >= 0)) return '';
+ const last = gen ? this.fmtRel(gen) : 'not yet';
+ let nextBit = '', off = '';
+ if (iv === 0) {
+ off = ' off';
+ nextBit = ' · automatic checks are off';
+ } else if (gen && iv > 0) {
+ nextBit = ` · next check ${this.fmtRelFuture(Date.parse(gen) + iv * 60000)}`;
+ }
+ return `
` +
+ `Checked automatically · last checked ${last}${nextBit}
`;
+ }
}
window.UpdaterPage = UpdaterPage;
diff --git a/scripts/webui/data/generators/updater/webui_updater_scan.sh b/scripts/webui/data/generators/updater/webui_updater_scan.sh
index 37efbbc..776bd42 100644
--- a/scripts/webui/data/generators/updater/webui_updater_scan.sh
+++ b/scripts/webui/data/generators/updater/webui_updater_scan.sh
@@ -225,12 +225,17 @@ webuiUpdaterScan() {
done
[ "$do_registry" = "1" ] && touch "$reg_stamp" 2>/dev/null || true
+ # The WebUI shows "checked automatically · next check ~N" from this stamp +
+ # the auto-scan cadence, so the display needs no separate config fetch.
+ local scan_interval="${CFG_UPDATER_SCAN_INTERVAL:-30}"
+ [[ "$scan_interval" =~ ^[0-9]+$ ]] || scan_interval=30
+
local tmp; tmp="$(mktemp)"
if [ "$have_jq" = "1" ]; then
- jq -s --arg now "$now" '{generated_at:$now, apps:.}' "$objs" > "$tmp" 2>/dev/null \
- || printf '{ "generated_at": "%s", "apps": [] }\n' "$now" > "$tmp"
+ jq -s --arg now "$now" --argjson iv "$scan_interval" '{generated_at:$now, scan_interval_minutes:$iv, apps:.}' "$objs" > "$tmp" 2>/dev/null \
+ || printf '{ "generated_at": "%s", "scan_interval_minutes": %s, "apps": [] }\n' "$now" "$scan_interval" > "$tmp"
else
- { printf '{ "generated_at": "%s", "apps": [' "$now"
+ { printf '{ "generated_at": "%s", "scan_interval_minutes": %s, "apps": [' "$now" "$scan_interval"
paste -sd, "$objs"; printf '] }\n'; } > "$tmp"
fi
rm -f "$objs"