feat(updater): auto-check status line on the app Updates tab, drop manual Check

The per-app Updates tab pushed a manual "Check" button (header + empty state)
even though scans run automatically on CFG_UPDATER_SCAN_INTERVAL — so an app with
nothing to update read like an empty/actionable page. Replace the manual Check
with a calm status line inside the panel: "Checked automatically · last checked X
· next check ~Y", backup-schedule style. The genuine Apply/Roll back actions stay
(applying is still manual and safe). No auto-apply.

- webui_updater_scan.sh: stamp scan_interval_minutes alongside generated_at in
  updates.json so the display needs no separate config fetch (0 = auto off).
- updater-page.js: renderAutoCheckLine() + fmtRelFuture().
- app-tabbed-manager.js: drop the header/empty-state Check buttons; render the
  auto-check line; friendlier no-data copy.
- overview.css: style .updater-autocheck (green dot live / muted when off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
librelad 2026-07-18 22:38:05 +01:00
parent dea4764bb6
commit 1de4d5d970
4 changed files with 74 additions and 9 deletions

View File

@ -591,17 +591,20 @@ class AppTabbedManager {
const app = (this.appUpdater.apps || []).find((x) => x.name === this.currentApp);
// Always lead with the title block + recessed dark container, so the tab
// matches the Config / Backups / Tasks tabs whether or not there's data.
const autoLine = this.appUpdater.renderAutoCheckLine();
const body = app
? this.appUpdater.renderAppDetail(app, { includeVersion: true })
: `<div class="updater-empty">No update data for this app yet. <button class="updater-btn updater-btn-primary" data-updater-action="check">Check now</button></div>`;
: `<div class="updater-empty">No scan data for this app yet — the first automatic check will fill this in shortly.</div>`;
section.innerHTML = this.renderAppUpdaterHead(app) +
`<div class="updater-detail-container">${body}</div>`;
`<div class="updater-detail-container">${autoLine}${body}</div>`;
}
// 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 {
<p>Version, security and recovery for ${esc(name)}.</p>
</div>
<div class="updater-title-actions">
<button class="updater-btn" data-updater-action="check"> Check</button>
${updBtn}
</div>
</div>`;

View File

@ -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);
}

View File

@ -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 `<div class="updater-autocheck${off}"><span class="updater-autocheck-dot"></span>` +
`<span>Checked automatically · last checked <strong>${last}</strong>${nextBit}</span></div>`;
}
}
window.UpdaterPage = UpdaterPage;

View File

@ -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"