Compare commits
2 Commits
86f84a62d3
...
5b437c0c52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b437c0c52 | ||
|
|
fa47e16cab |
@ -3,6 +3,7 @@
|
||||
# ================================================================================
|
||||
CFG_UPDATER_CHECK=60 # Update Check Interval - Hours between system update checks
|
||||
CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off]
|
||||
CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - Minutes between automatic app update/CVE/improvement scans (0 disables)
|
||||
CFG_SWAPFILE_SIZE=2G # Swap File Size - Size of swap file for memory management
|
||||
CFG_GENERATED_PASS_LENGTH=14 # Password Length - Length for auto generated passwords
|
||||
CFG_GENERATED_USER_LENGTH=8 # Username Length - Length for auto generated usernames
|
||||
|
||||
@ -123,6 +123,9 @@ LP.features.register({
|
||||
// overviewManager singleton + its DOM persist with the layout; its run()
|
||||
// self-guards, but unregistering is the clean release.
|
||||
try { ctx && ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('overview'); } catch (_) {}
|
||||
// Same for the headless updater's auto-refresh timer (re-armed on the next
|
||||
// Overview mount).
|
||||
try { window.overviewManager && window.overviewManager.updater && window.overviewManager.updater.dispose(); } catch (_) {}
|
||||
// The Backups tab embeds a BackupPage; release its document listeners +
|
||||
// task-refresh registration on the way out (the "stacks on revisit" bug).
|
||||
try { window.overviewBackupPage && window.overviewBackupPage.dispose && window.overviewBackupPage.dispose(); } catch (_) {}
|
||||
|
||||
@ -53,6 +53,20 @@ class OverviewManager {
|
||||
const initial = this.parseTabFromUrl() || 'overview';
|
||||
await this.refreshAll();
|
||||
this._applyTab(initial); // active + header + render, no history push
|
||||
// Keep an open Overview in step with the host-side auto-scan (no task event
|
||||
// fires for it). The headless updater owns the timer + change detection;
|
||||
// re-armed here on every mount, released by the apps feature's unmount.
|
||||
// isActive: the pane element persists hidden in the shared layout, so gate
|
||||
// on it actually being shown (offsetParent is null while display:none).
|
||||
if (this.updater && this.updater.startAutoRefresh) {
|
||||
this.updater.startAutoRefresh(
|
||||
() => this._applyTab(this.current || 'overview'),
|
||||
() => {
|
||||
const pane = window.overviewManager === this && document.getElementById('overview-view');
|
||||
return !!(pane && pane.offsetParent);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents(root) {
|
||||
@ -327,7 +341,7 @@ class OverviewManager {
|
||||
`<button class="updater-btn" data-overview-action="goto" data-tab="backups">Backups</button>`)}
|
||||
${card('system', u.apps, 'Apps tracked', `last scan: ${checked}`)}
|
||||
</div>
|
||||
${(this.updater && this.updater.updates) ? '' : `<div class="updater-hint">No scan data yet — run <strong>Check now</strong> to fetch versions, CVEs & improvements.</div>`}`;
|
||||
${(this.updater && this.updater.updates) ? '' : `<div class="updater-hint">No scan data yet — the first automatic scan runs within a couple of minutes, or hit <strong>Check now</strong>.</div>`}`;
|
||||
}
|
||||
|
||||
backupSummary() {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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}`,
|
||||
`<button class="updater-btn" data-updater-action="check">Check now</button>`)}
|
||||
</div>
|
||||
${this.updates ? '' : `<div class="updater-hint">No scan data yet — showing your installed apps. Run <strong>Check now</strong> to fetch versions & vulnerabilities.</div>`}`;
|
||||
${this.updates ? '' : `<div class="updater-hint">No scan data yet — showing your installed apps. The first automatic scan runs within a couple of minutes, or hit <strong>Check now</strong>.</div>`}`;
|
||||
}
|
||||
|
||||
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 => `
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <app> - 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 <app> - Restore an app's most recent pre-update snapshot"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user