feat(updater): per-app UPDATE_TYPE, automatic by default
Adds the decision half of the app updater. Detection (P2) and the snapshot-first apply/revert (P3) were already real, but nothing ever pressed the button — every update waited for a click. CFG_<APP>_UPDATE_TYPE=auto|manual per app, default auto (33 templates) CFG_UPDATER_AUTO=true|false master switch, default true updaterAppPolicy resolves the two the way backupResolveStrategy already resolves backup strategy: the global switch can only make things more manual. updaterApplyAuto runs at the end of `updater check` and enqueues the ordinary updater_apply task for each auto app that has an update — never applies inline, so an automatic update is the same code path, task log, History entry and Roll back button as a manual one. Safety: each attempt stamps its target digest under generated/auto/, so a build that fails is rolled back and then left alone rather than retried on every scan; in-flight updater tasks are skipped so scans can't stack. Tracked end to end: updates.json carries each app's resolved update_type, History entries carry trigger=manual|auto. The WebUI says whether updates install themselves, chips only the apps that opted out, labels automatic history, and — since an auto app's pending update needs no decision — keeps it off the Overview board's "Needs action" view. Also fixes artifactApplyAuto enqueueing without --detach: it runs inside the single-threaded task processor's own poll, so following the new task in the foreground waits for a task that cannot start until it returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
48075d0551
commit
cdeb2d1658
@ -5,3 +5,4 @@
|
||||
CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - Minutes between automatic app update/CVE/improvement scans. 0 disables.
|
||||
CFG_UPDATER_REGISTRY_INTERVAL=360 # Registry Check Interval - Minutes between registry lookups for new image builds (the expensive step; the local scan still refreshes every scan). 0 = never (local-only).
|
||||
CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off]
|
||||
CFG_UPDATER_AUTO=true # Automatic App Updates - Master switch for per-app automatic updates. Each app's own Update Type decides individually; turning this off makes every app manual. Every update snapshots the app first and rolls back on failure. [true:On|false:Off]
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -15,6 +16,7 @@
|
||||
CFG_ADGUARD_APP_NAME=adguard
|
||||
CFG_ADGUARD_BACKUP=true
|
||||
CFG_ADGUARD_BACKUP_STRATEGY=auto
|
||||
CFG_ADGUARD_UPDATE_TYPE=auto
|
||||
CFG_ADGUARD_COMPOSE_FILE=default
|
||||
CFG_ADGUARD_HEALTHCHECK=true
|
||||
CFG_ADGUARD_AUTHELIA=false
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh)
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -18,6 +19,7 @@ CFG_AUTHELIA_APP_NAME=authelia
|
||||
CFG_AUTHELIA_REQUIRES="domain,traefik"
|
||||
CFG_AUTHELIA_BACKUP=true
|
||||
CFG_AUTHELIA_BACKUP_STRATEGY=auto
|
||||
CFG_AUTHELIA_UPDATE_TYPE=auto
|
||||
CFG_AUTHELIA_COMPOSE_FILE=default
|
||||
CFG_AUTHELIA_HEALTHCHECK=true
|
||||
CFG_AUTHELIA_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -19,6 +20,7 @@ CFG_BOOKSTACK_APP_NAME=bookstack
|
||||
CFG_BOOKSTACK_MULTI_INSTANCE=true
|
||||
CFG_BOOKSTACK_BACKUP=true
|
||||
CFG_BOOKSTACK_BACKUP_STRATEGY=auto
|
||||
CFG_BOOKSTACK_UPDATE_TYPE=auto
|
||||
CFG_BOOKSTACK_COMPOSE_FILE=default
|
||||
CFG_BOOKSTACK_HEALTHCHECK=true
|
||||
CFG_BOOKSTACK_AUTHELIA=false
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
# HOST_SERVICES = all units; feeds the Services + Logs tabs
|
||||
# HOST_LOG_FILES = <unit>|<path>,... mapping for the log viewer
|
||||
# BACKUP = include in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed; ships the official CrowdSec Grafana dashboards)
|
||||
# PROMETHEUS_LISTEN = address CrowdSec's metrics endpoint binds to; must be reachable from the Prometheus container (default: all interfaces, port 6060 — keep the :6060 port)
|
||||
#
|
||||
@ -20,6 +21,7 @@ CFG_CROWDSEC_HOST_SERVICES=crowdsec.service,crowdsec-firewall-bouncer.service
|
||||
CFG_CROWDSEC_HOST_LOG_FILES="crowdsec.service|/var/log/crowdsec.log,crowdsec-firewall-bouncer.service|/var/log/crowdsec-firewall-bouncer.log"
|
||||
CFG_CROWDSEC_BACKUP=true
|
||||
CFG_CROWDSEC_BACKUP_STRATEGY=auto
|
||||
CFG_CROWDSEC_UPDATE_TYPE=auto
|
||||
CFG_CROWDSEC_MONITORING=false
|
||||
CFG_CROWDSEC_PROMETHEUS_LISTEN=0.0.0.0:6060
|
||||
#
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_DASHY_APP_NAME=dashy
|
||||
CFG_DASHY_BACKUP=true
|
||||
CFG_DASHY_BACKUP_STRATEGY=auto
|
||||
CFG_DASHY_UPDATE_TYPE=auto
|
||||
CFG_DASHY_COMPOSE_FILE=default
|
||||
CFG_DASHY_HEALTHCHECK=true
|
||||
CFG_DASHY_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_FOCALBOARD_APP_NAME=focalboard
|
||||
CFG_FOCALBOARD_BACKUP=true
|
||||
CFG_FOCALBOARD_BACKUP_STRATEGY=auto
|
||||
CFG_FOCALBOARD_UPDATE_TYPE=auto
|
||||
CFG_FOCALBOARD_COMPOSE_FILE=default
|
||||
CFG_FOCALBOARD_HEALTHCHECK=true
|
||||
CFG_FOCALBOARD_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -14,6 +15,7 @@
|
||||
CFG_GITEA_APP_NAME=gitea
|
||||
CFG_GITEA_BACKUP=true
|
||||
CFG_GITEA_BACKUP_STRATEGY=auto
|
||||
CFG_GITEA_UPDATE_TYPE=auto
|
||||
CFG_GITEA_COMPOSE_FILE=default
|
||||
CFG_GITEA_HEALTHCHECK=true
|
||||
CFG_GITEA_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_GLUETUN_APP_NAME=gluetun
|
||||
CFG_GLUETUN_BACKUP=true
|
||||
CFG_GLUETUN_BACKUP_STRATEGY=auto
|
||||
CFG_GLUETUN_UPDATE_TYPE=auto
|
||||
CFG_GLUETUN_COMPOSE_FILE=default
|
||||
CFG_GLUETUN_HEALTHCHECK=true
|
||||
CFG_GLUETUN_AUTHELIA=false
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh)
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -15,6 +16,7 @@ CFG_GRAFANA_APP_NAME=grafana
|
||||
CFG_GRAFANA_REQUIRES="prometheus"
|
||||
CFG_GRAFANA_BACKUP=true
|
||||
CFG_GRAFANA_BACKUP_STRATEGY=auto
|
||||
CFG_GRAFANA_UPDATE_TYPE=auto
|
||||
CFG_GRAFANA_COMPOSE_FILE=default
|
||||
CFG_GRAFANA_HEALTHCHECK=true
|
||||
CFG_GRAFANA_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# BASIC_AUTH_PASS = password for the headscale-ui basic auth; auto-generated, fed to the compose via HEADSCALE_BASIC_AUTH_PASS_TAG
|
||||
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed)
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_HEADSCALE_APP_NAME=headscale
|
||||
CFG_HEADSCALE_BACKUP=true
|
||||
CFG_HEADSCALE_BACKUP_STRATEGY=auto
|
||||
CFG_HEADSCALE_UPDATE_TYPE=auto
|
||||
CFG_HEADSCALE_COMPOSE_FILE=default
|
||||
CFG_HEADSCALE_HEALTHCHECK=true
|
||||
CFG_HEADSCALE_BASIC_AUTH_PASS=RANDOMIZEDPASSWORD1
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_INVIDIOUS_APP_NAME=invidious
|
||||
CFG_INVIDIOUS_BACKUP=false
|
||||
CFG_INVIDIOUS_BACKUP_STRATEGY=auto
|
||||
CFG_INVIDIOUS_UPDATE_TYPE=auto
|
||||
CFG_INVIDIOUS_COMPOSE_FILE=default
|
||||
CFG_INVIDIOUS_HEALTHCHECK=false
|
||||
CFG_INVIDIOUS_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_IPINFO_APP_NAME=ipinfo
|
||||
CFG_IPINFO_BACKUP=false
|
||||
CFG_IPINFO_BACKUP_STRATEGY=auto
|
||||
CFG_IPINFO_UPDATE_TYPE=auto
|
||||
CFG_IPINFO_COMPOSE_FILE=default
|
||||
CFG_IPINFO_HEALTHCHECK=true
|
||||
CFG_IPINFO_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_JELLYFIN_APP_NAME=jellyfin
|
||||
CFG_JELLYFIN_BACKUP=true
|
||||
CFG_JELLYFIN_BACKUP_STRATEGY=auto
|
||||
CFG_JELLYFIN_UPDATE_TYPE=auto
|
||||
CFG_JELLYFIN_COMPOSE_FILE=default
|
||||
CFG_JELLYFIN_HEALTHCHECK=true
|
||||
CFG_JELLYFIN_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_JITSIMEET_APP_NAME=jitsimeet
|
||||
CFG_JITSIMEET_BACKUP=true
|
||||
CFG_JITSIMEET_BACKUP_STRATEGY=auto
|
||||
CFG_JITSIMEET_UPDATE_TYPE=auto
|
||||
CFG_JITSIMEET_COMPOSE_FILE=default
|
||||
CFG_JITSIMEET_HEALTHCHECK=true
|
||||
CFG_JITSIMEET_AUTHELIA=false
|
||||
|
||||
@ -401,15 +401,37 @@ class OverviewManager {
|
||||
if (!scanned) {
|
||||
rows.push({ hue: 'updates', icon: '⬆️', kind: 'none', text: 'Updates — not scanned yet' });
|
||||
} else if (pending.length) {
|
||||
const names = pending.map((a) => a.displayName || a.name);
|
||||
const list = names.slice(0, 3).join(', ') + (names.length > 3 ? ` +${names.length - 3} more` : '');
|
||||
rows.push({
|
||||
hue: 'updates', icon: '⬆️', kind: 'warn',
|
||||
text: `${pending.length} update${pending.length === 1 ? '' : 's'} available`,
|
||||
sub: `${esc(list)} — a recovery snapshot is taken before each update`,
|
||||
actions: goto('updates', 'Review', 'updates')
|
||||
+ `<button class="updater-btn updater-btn-primary" data-updater-action="update-all">Update all</button>`,
|
||||
});
|
||||
// Only the apps set to manual actually want a decision — the rest install
|
||||
// themselves on the next scan, so parking them on this board (and, since
|
||||
// the board opens on "Needs action", in the user's face) would be crying
|
||||
// wolf. They still get a quiet line so nothing is hidden.
|
||||
// With automatic scanning off nothing is found unattended, so an "auto"
|
||||
// app's update sits there until someone presses Check — that IS waiting
|
||||
// for the user, whatever the policy says.
|
||||
const autoScan = !(up.updates && Number(up.updates.scan_interval_minutes) === 0);
|
||||
const waiting = pending.filter((a) => !autoScan || a.update_type === 'manual');
|
||||
const selfing = pending.length - waiting.length;
|
||||
const listOf = (arr) => {
|
||||
const names = arr.map((a) => a.displayName || a.name);
|
||||
return names.slice(0, 3).join(', ') + (names.length > 3 ? ` +${names.length - 3} more` : '');
|
||||
};
|
||||
if (waiting.length) {
|
||||
rows.push({
|
||||
hue: 'updates', icon: '⬆️', kind: 'warn',
|
||||
text: `${waiting.length} update${waiting.length === 1 ? '' : 's'} waiting for you`,
|
||||
sub: `${esc(listOf(waiting))} — a recovery snapshot is taken before each update`
|
||||
+ (selfing ? ` · ${selfing} more will install automatically` : ''),
|
||||
actions: goto('updates', 'Review', 'updates')
|
||||
+ `<button class="updater-btn updater-btn-primary" data-updater-action="update-all">Update all</button>`,
|
||||
});
|
||||
} else {
|
||||
rows.push({
|
||||
hue: 'updates', icon: '⬆️', kind: 'ok',
|
||||
text: `${pending.length} update${pending.length === 1 ? '' : 's'} installing automatically`,
|
||||
sub: `${esc(listOf(pending))} — each is snapshotted first, and rolled back if it fails`,
|
||||
actions: goto('updates', 'Review', 'updates'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
rows.push({
|
||||
hue: 'updates', icon: '⬆️', kind: 'ok',
|
||||
@ -558,13 +580,19 @@ class OverviewManager {
|
||||
? `<span class="ov-status ov-status-update">↑ Update available</span>`
|
||||
: (a.scanned ? `<span class="ov-status ov-status-ok">✓ Up to date</span>` : `<span class="ov-status ov-status-unknown">• Unscanned</span>`);
|
||||
const sev = a.worstSeverity ? `<span class="updater-badge sev-${a.worstSeverity}">${a.worstSeverity}</span>` : '';
|
||||
// Automatic is the default, so only the apps that opt OUT carry a marker —
|
||||
// 30-odd "auto" chips would be wallpaper, one "manual" chip is information.
|
||||
// The fleet-level statement lives in the auto-check line above the list.
|
||||
const pol = a.update_type === 'manual'
|
||||
? `<span class="updater-badge updater-badge-unknown" title="This app updates only when you press Update">manual</span>`
|
||||
: '';
|
||||
const updBtn = a.update_available
|
||||
? `<button class="updater-btn updater-btn-primary" data-updater-action="update" data-app="${slug}">Update</button>`
|
||||
: '';
|
||||
return `<div class="updater-row ov-row" data-app="${slug}">
|
||||
<div class="updater-row-head ov-row-head" data-overview-action="toggle" data-app="${slug}" role="button" tabindex="0" aria-expanded="false" aria-controls="ov-detail-${slug}">
|
||||
<img class="ov-row-icon" src="/core/icons/apps/${slug}.svg" alt="" onerror="this.style.display='none'">
|
||||
<span class="updater-row-name">${name}</span> ${status} ${sev}
|
||||
<span class="updater-row-name">${name}</span> ${status} ${sev} ${pol}
|
||||
<span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span>
|
||||
<span class="ov-row-actions">${updBtn}<button class="ov-details-btn" data-overview-action="toggle" data-app="${slug}" tabindex="-1" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg><span>Details</span></button></span>
|
||||
</div>
|
||||
|
||||
@ -515,7 +515,7 @@ class UpdaterPage {
|
||||
const rows = entries.map(e => `
|
||||
<div class="updater-row">
|
||||
<div class="updater-row-main"><span class="updater-row-name">${this.escape(e.app)}</span>
|
||||
<span class="updater-badge ${e.result === 'ok' ? 'updater-badge-ok' : (e.result === 'rolled-back' ? 'updater-badge-update' : 'sev-high')}">${this.escape(e.action)}${e.result ? ' · ' + this.escape(e.result) : ''}</span></div>
|
||||
<span class="updater-badge ${e.result === 'ok' ? 'updater-badge-ok' : (e.result === 'rolled-back' ? 'updater-badge-update' : 'sev-high')}">${this.escape(e.action)}${e.result ? ' · ' + this.escape(e.result) : ''}</span>${this.triggerBadge(e)}</div>
|
||||
<div class="updater-row-ver">${this.escape(e.from || '')}${e.to ? ` <span class="updater-arrow">→</span> ${this.escape(e.to)}` : ''}</div>
|
||||
<div class="updater-row-actions">${this.fmtRel(e.ts)}</div>
|
||||
</div>`).join('');
|
||||
@ -539,8 +539,15 @@ class UpdaterPage {
|
||||
const badge = a.update_available
|
||||
? `<span class="updater-badge updater-badge-update">update available</span>`
|
||||
: (a.scanned ? `<span class="updater-badge updater-badge-ok">up to date</span>` : `<span class="updater-badge updater-badge-unknown">unscanned</span>`);
|
||||
// What happens next, in the app's own words — the Updates setting on this
|
||||
// app's Configure page decides, so say which way it is set rather than
|
||||
// leaving "update available" to imply someone must act.
|
||||
const policyLine = a.update_type === 'manual'
|
||||
? 'Set to <strong>manual</strong> — this app updates only when you press Update.'
|
||||
: 'Set to <strong>automatic</strong> — new builds install on their own, after a recovery snapshot.';
|
||||
versionSection = `<div class="updater-detail-section"><h4>Version</h4>
|
||||
<div class="updater-detail-row">${badge} <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span></div></div>`;
|
||||
<div class="updater-detail-row">${badge} <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span></div>
|
||||
<div class="updater-detail-row"><span class="updater-detail-meta">${policyLine}</span></div></div>`;
|
||||
}
|
||||
const cves = a.cves || [];
|
||||
const appLabel = this.escape((window.getAppDisplayName ? window.getAppDisplayName(a.name) : null) || a.displayName || a.name || 'the app');
|
||||
@ -580,13 +587,22 @@ class UpdaterPage {
|
||||
|
||||
const entries = ((this.history && this.history.entries) || []).filter((e) => e.app === a.name).slice(0, 8);
|
||||
const history = entries.length ? `<div class="updater-detail-section"><h4>History</h4>${entries.map((e) => `
|
||||
<div class="updater-detail-row"><span class="updater-badge ${e.result === 'ok' ? 'updater-badge-ok' : (e.result === 'rolled-back' ? 'updater-badge-update' : 'sev-high')}">${this.escape(e.action)}${e.result ? ' · ' + this.escape(e.result) : ''}</span>
|
||||
<div class="updater-detail-row"><span class="updater-badge ${e.result === 'ok' ? 'updater-badge-ok' : (e.result === 'rolled-back' ? 'updater-badge-update' : 'sev-high')}">${this.escape(e.action)}${e.result ? ' · ' + this.escape(e.result) : ''}</span>${this.triggerBadge(e)}
|
||||
<span class="updater-detail-meta">${this.escape(e.from || '')}${e.to ? ` → ${this.escape(e.to)}` : ''}</span>
|
||||
<span class="updater-detail-meta">${this.fmtRel(e.ts)}</span></div>`).join('')}</div>` : '';
|
||||
|
||||
return `<div class="updater-detail">${versionSection}${security}${recovery}${history}</div>`;
|
||||
}
|
||||
|
||||
// "automatic" marker for a History entry. Only automatic runs are labelled —
|
||||
// a hand-pressed update needs no explanation, and entries written before the
|
||||
// trigger field existed carry none, so silence is also the honest default.
|
||||
triggerBadge(e) {
|
||||
return e && e.trigger === 'auto'
|
||||
? ' <span class="updater-badge updater-badge-unknown">automatic</span>'
|
||||
: '';
|
||||
}
|
||||
|
||||
empty(msg, withCheck) {
|
||||
return `<div class="updater-empty">${this.escape(msg)}${withCheck ? `<div><button class="updater-btn updater-btn-primary" data-updater-action="check">Check now</button></div>` : ''}</div>`;
|
||||
}
|
||||
@ -636,8 +652,19 @@ class UpdaterPage {
|
||||
} else if (gen && iv > 0) {
|
||||
nextBit = ` · next check ${this.fmtRelFuture(Date.parse(gen) + iv * 60000)}`;
|
||||
}
|
||||
// Say plainly whether found updates install themselves. The scan carries each
|
||||
// app's resolved policy, so this counts what will actually happen rather than
|
||||
// restating a config value: all / some / none on automatic.
|
||||
// Nothing installs itself while the scan that finds updates is off, so with
|
||||
// iv === 0 the policy is moot and claiming otherwise would be a lie.
|
||||
const auto = this.apps.filter((a) => a.update_type !== 'manual').length;
|
||||
let autoBit = '';
|
||||
if (iv === 0 || !this.apps.length) autoBit = '';
|
||||
else if (auto === this.apps.length) autoBit = ' · updates install automatically';
|
||||
else if (auto) autoBit = ` · ${auto} of ${this.apps.length} apps install updates automatically`;
|
||||
else autoBit = ' · updates wait for you';
|
||||
return `<div class="updater-autocheck${off}"><span class="updater-autocheck-dot"></span>` +
|
||||
`<span class="updater-autocheck-text">Checked automatically · last checked <strong>${last}</strong>${nextBit}</span>` +
|
||||
`<span class="updater-autocheck-text">Checked automatically · last checked <strong>${last}</strong>${nextBit}${autoBit}</span>` +
|
||||
`<button class="updater-btn updater-autocheck-btn" data-updater-action="check" title="Check for updates now">↻ Check now</button></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_LIBREPORTAL_CATALOG_APP_NAME=libreportal_catalog
|
||||
CFG_LIBREPORTAL_CATALOG_BACKUP=true
|
||||
CFG_LIBREPORTAL_CATALOG_BACKUP_STRATEGY=auto
|
||||
CFG_LIBREPORTAL_CATALOG_UPDATE_TYPE=auto
|
||||
CFG_LIBREPORTAL_CATALOG_COMPOSE_FILE=default
|
||||
CFG_LIBREPORTAL_CATALOG_HEALTHCHECK=false
|
||||
CFG_LIBREPORTAL_CATALOG_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in the docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_LINKDING_APP_NAME=linkding
|
||||
CFG_LINKDING_BACKUP=true
|
||||
CFG_LINKDING_BACKUP_STRATEGY=auto
|
||||
CFG_LINKDING_UPDATE_TYPE=auto
|
||||
CFG_LINKDING_COMPOSE_FILE=default
|
||||
CFG_LINKDING_HEALTHCHECK=true
|
||||
CFG_LINKDING_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in the docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_MASTODON_APP_NAME=mastodon
|
||||
CFG_MASTODON_BACKUP=true
|
||||
CFG_MASTODON_BACKUP_STRATEGY=auto
|
||||
CFG_MASTODON_UPDATE_TYPE=auto
|
||||
CFG_MASTODON_COMPOSE_FILE=default
|
||||
CFG_MASTODON_HEALTHCHECK=true
|
||||
CFG_MASTODON_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -14,6 +15,7 @@ CFG_MONEYAPP_APP_NAME=moneyapp
|
||||
CFG_MONEYAPP_REQUIRES=""
|
||||
CFG_MONEYAPP_BACKUP=true
|
||||
CFG_MONEYAPP_BACKUP_STRATEGY=auto
|
||||
CFG_MONEYAPP_UPDATE_TYPE=auto
|
||||
CFG_MONEYAPP_COMPOSE_FILE=default
|
||||
CFG_MONEYAPP_HEALTHCHECK=true
|
||||
CFG_MONEYAPP_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_NAVIDROME_APP_NAME=navidrome
|
||||
CFG_NAVIDROME_BACKUP=true
|
||||
CFG_NAVIDROME_BACKUP_STRATEGY=auto
|
||||
CFG_NAVIDROME_UPDATE_TYPE=auto
|
||||
CFG_NAVIDROME_COMPOSE_FILE=default
|
||||
CFG_NAVIDROME_HEALTHCHECK=true
|
||||
CFG_NAVIDROME_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in the docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -16,6 +17,7 @@
|
||||
CFG_NEXTCLOUD_APP_NAME=nextcloud
|
||||
CFG_NEXTCLOUD_BACKUP=true
|
||||
CFG_NEXTCLOUD_BACKUP_STRATEGY=auto
|
||||
CFG_NEXTCLOUD_UPDATE_TYPE=auto
|
||||
CFG_NEXTCLOUD_COMPOSE_FILE=default
|
||||
CFG_NEXTCLOUD_HEALTHCHECK=true
|
||||
CFG_NEXTCLOUD_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in the docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_OLLAMA_APP_NAME=ollama
|
||||
CFG_OLLAMA_BACKUP=true
|
||||
CFG_OLLAMA_BACKUP_STRATEGY=auto
|
||||
CFG_OLLAMA_UPDATE_TYPE=auto
|
||||
CFG_OLLAMA_COMPOSE_FILE=default
|
||||
CFG_OLLAMA_HEALTHCHECK=true
|
||||
CFG_OLLAMA_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in the docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_ONLYOFFICE_APP_NAME=onlyoffice
|
||||
CFG_ONLYOFFICE_BACKUP=true
|
||||
CFG_ONLYOFFICE_BACKUP_STRATEGY=auto
|
||||
CFG_ONLYOFFICE_UPDATE_TYPE=auto
|
||||
CFG_ONLYOFFICE_COMPOSE_FILE=default
|
||||
CFG_ONLYOFFICE_HEALTHCHECK=true
|
||||
CFG_ONLYOFFICE_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in the docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_OWNCLOUD_APP_NAME=owncloud
|
||||
CFG_OWNCLOUD_BACKUP=true
|
||||
CFG_OWNCLOUD_BACKUP_STRATEGY=auto
|
||||
CFG_OWNCLOUD_UPDATE_TYPE=auto
|
||||
CFG_OWNCLOUD_COMPOSE_FILE=default
|
||||
CFG_OWNCLOUD_HEALTHCHECK=true
|
||||
CFG_OWNCLOUD_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_PIHOLE_APP_NAME=pihole
|
||||
CFG_PIHOLE_BACKUP=true
|
||||
CFG_PIHOLE_BACKUP_STRATEGY=auto
|
||||
CFG_PIHOLE_UPDATE_TYPE=auto
|
||||
CFG_PIHOLE_COMPOSE_FILE=default
|
||||
CFG_PIHOLE_HEALTHCHECK=true
|
||||
CFG_PIHOLE_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_PROMETHEUS_APP_NAME=prometheus
|
||||
CFG_PROMETHEUS_BACKUP=true
|
||||
CFG_PROMETHEUS_BACKUP_STRATEGY=auto
|
||||
CFG_PROMETHEUS_UPDATE_TYPE=auto
|
||||
CFG_PROMETHEUS_COMPOSE_FILE=default
|
||||
CFG_PROMETHEUS_HEALTHCHECK=true
|
||||
CFG_PROMETHEUS_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_SEARXNG_APP_NAME=searxng
|
||||
CFG_SEARXNG_BACKUP=false
|
||||
CFG_SEARXNG_BACKUP_STRATEGY=auto
|
||||
CFG_SEARXNG_UPDATE_TYPE=auto
|
||||
CFG_SEARXNG_COMPOSE_FILE=default
|
||||
CFG_SEARXNG_HEALTHCHECK=true
|
||||
CFG_SEARXNG_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_SPEEDTEST_APP_NAME=speedtest
|
||||
CFG_SPEEDTEST_BACKUP=false
|
||||
CFG_SPEEDTEST_BACKUP_STRATEGY=auto
|
||||
CFG_SPEEDTEST_UPDATE_TYPE=auto
|
||||
CFG_SPEEDTEST_COMPOSE_FILE=default
|
||||
CFG_SPEEDTEST_HEALTHCHECK=false
|
||||
CFG_SPEEDTEST_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_TRAEFIK_APP_NAME=traefik
|
||||
CFG_TRAEFIK_BACKUP=true
|
||||
CFG_TRAEFIK_BACKUP_STRATEGY=auto
|
||||
CFG_TRAEFIK_UPDATE_TYPE=auto
|
||||
CFG_TRAEFIK_COMPOSE_FILE=default
|
||||
CFG_TRAEFIK_HEALTHCHECK=true
|
||||
CFG_TRAEFIK_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_TRILIUM_APP_NAME=trilium
|
||||
CFG_TRILIUM_BACKUP=true
|
||||
CFG_TRILIUM_BACKUP_STRATEGY=auto
|
||||
CFG_TRILIUM_UPDATE_TYPE=auto
|
||||
CFG_TRILIUM_COMPOSE_FILE=default
|
||||
CFG_TRILIUM_HEALTHCHECK=true
|
||||
CFG_TRILIUM_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -12,6 +13,7 @@
|
||||
CFG_TRIVY_APP_NAME=trivy
|
||||
CFG_TRIVY_BACKUP=false
|
||||
CFG_TRIVY_BACKUP_STRATEGY=auto
|
||||
CFG_TRIVY_UPDATE_TYPE=auto
|
||||
CFG_TRIVY_COMPOSE_FILE=default
|
||||
CFG_TRIVY_HEALTHCHECK=true
|
||||
CFG_TRIVY_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_UNBOUND_APP_NAME=unbound
|
||||
CFG_UNBOUND_BACKUP=true
|
||||
CFG_UNBOUND_BACKUP_STRATEGY=auto
|
||||
CFG_UNBOUND_UPDATE_TYPE=auto
|
||||
CFG_UNBOUND_COMPOSE_FILE=default
|
||||
CFG_UNBOUND_HEALTHCHECK=true
|
||||
CFG_UNBOUND_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -13,6 +14,7 @@
|
||||
CFG_VAULTWARDEN_APP_NAME=vaultwarden
|
||||
CFG_VAULTWARDEN_BACKUP=true
|
||||
CFG_VAULTWARDEN_BACKUP_STRATEGY=auto
|
||||
CFG_VAULTWARDEN_UPDATE_TYPE=auto
|
||||
CFG_VAULTWARDEN_COMPOSE_FILE=default
|
||||
CFG_VAULTWARDEN_HEALTHCHECK=false
|
||||
CFG_VAULTWARDEN_AUTHELIA=false
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
@ -14,6 +15,7 @@ CFG_WIREGUARD_APP_NAME=wireguard
|
||||
CFG_WIREGUARD_SUBNET=
|
||||
CFG_WIREGUARD_BACKUP=true
|
||||
CFG_WIREGUARD_BACKUP_STRATEGY=auto
|
||||
CFG_WIREGUARD_UPDATE_TYPE=auto
|
||||
CFG_WIREGUARD_COMPOSE_FILE=default
|
||||
CFG_WIREGUARD_HEALTHCHECK=true
|
||||
CFG_WIREGUARD_AUTHELIA=false
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# LibrePortal — Per-App Version Updater & CVE Scanning (Roadmap / Design)
|
||||
|
||||
**Status:** Design — **not built** (except the Trivy/CVE half, shipped 2026-07-16). Fills the two deliberately-unwired hooks in `webui_updater_scan.sh`. · **Audience:** us, future-self · **Scope:** real "update available" detection, per-app version identity (pick/track/revert), and the vulnerability scanner behind `/apps/overview/updates` · **Origin:** "the updates system is currently not set up" conversation 2026-07-14. Companion to [updates-and-distribution.md](updates-and-distribution.md) — that doc owns the *signed artifact/hotfix pipe*; this one owns the *generators* it always treated as pre-existing.
|
||||
**Status:** Largely built — P0–P3 (2026-07-17), the Trivy/CVE half (2026-07-16), and the auto-update policy of §6 (2026-08-11). Fills the two deliberately-unwired hooks in `webui_updater_scan.sh`. · **Audience:** us, future-self · **Scope:** real "update available" detection, per-app version identity (pick/track/revert), and the vulnerability scanner behind `/apps/overview/updates` · **Origin:** "the updates system is currently not set up" conversation 2026-07-14. Companion to [updates-and-distribution.md](updates-and-distribution.md) — that doc owns the *signed artifact/hotfix pipe*; this one owns the *generators* it always treated as pre-existing.
|
||||
|
||||
> **Revised 2026-07-17 — config-first identity + version types + anchor fix.** Three changes from the original direction, after auditing the compose corpus (33 apps, 50 image lines) and the `#LIBREPORTAL|…|…` tagging system:
|
||||
> 1. **`CFG_<APP>_VERSION` is adopted, not rejected.** The tagging system (`scripts/config/tags/processors/tags_processor_app_config_values.sh`) already derives compose values from `CFG_<APP>_<KEY>` config — so a version config makes the config *the* source and the image tag *derived*, not a second source of truth. The original §8 rejection assumed no such mechanism. See revised §2 / §8.
|
||||
@ -21,6 +21,7 @@ Most of the updater is **already built and working**. What's missing is narrow:
|
||||
| `history.json` (fail-closed from→to audit trail) | ✅ real |
|
||||
| Signed hotfix channel (fetch/verify/apply/undo, `CFG_HOTFIX_AUTO`) | ✅ real |
|
||||
| Auto-scan scheduling (`updater check auto`, 30-min throttle) | ✅ real |
|
||||
| Auto-**apply** policy (`CFG_<APP>_UPDATE_TYPE`, default auto) | ✅ real (2026-08-11, §6) |
|
||||
| WebUI — fleet Updates tab, per-app Updates tab, Security/Recovery/History renderers, Update/Roll-back buttons wired to tasks | ✅ real, waiting on data |
|
||||
| **"Update available" detection** | ❌ stub — `update_available` is hard-coded `false` (`webui_updater_scan.sh:55-58` is a marked hook) |
|
||||
| **CVE data** (`cves.json`) | ❌ stub — written empty once, then never rewritten (the `[ ! -f ]` guard at `:82-89` must go) |
|
||||
@ -130,9 +131,19 @@ So: **trivy ships as a normal catalog app** (`containers/trivy/`), marked **reco
|
||||
- **Not alarmist** (house rule): the Security tab lists everything, but the per-app chip/badge only fires for **critical/high with a fix available** — "your box has 400 unfixable medium CVEs" red badges are noise, not signal.
|
||||
- **Tie-in with updates:** a CVE whose `fixed_in` is satisfied by the available build marks that update as a *security update* — the Security filter chip and severity sort already exist in the UI, they just start meaning something.
|
||||
|
||||
## 6. Auto-update policy (deliberately later)
|
||||
## 6. Auto-update policy — **BUILT 2026-08-11** (per-app, not severity-split)
|
||||
|
||||
Once detection + pinned apply are trustworthy, add `CFG_UPDATER_AUTO=off|security|all` (default **off**): `security` auto-enqueues `updater_apply` only for security updates (mirrors `CFG_HOTFIX_AUTO`'s severity-split precedent, and like `artifactApplyAuto` it only *enqueues tasks*). Not part of the initial build — auto-updating before the revert story is proven live would be backwards.
|
||||
Shipped a shape the original sketch didn't consider: the policy is **per app**, not one fleet-wide severity rule.
|
||||
|
||||
- **`CFG_<APP>_UPDATE_TYPE=auto|manual`, default `auto`** — in all 33 app config templates (not `libreportal`, whose own updates are a different subsystem). Reaches existing installs through the normal template reconcile, and shows up on the app's Configure page as *Updates* (field mapping `UPDATE_TYPE`, category general).
|
||||
- **`CFG_UPDATER_AUTO=true|false`, default `true`** — the master switch in `configs/webui/webui_updater`. It can only ever make things *more* manual, so "stop auto-updating everything" is one flip, not 33 edits. Precedence deliberately mirrors `backupResolveStrategy` (per-app override on top of a global default); `updaterAppPolicy` is the single resolver, used by both the enqueuer and the scan.
|
||||
- **`updaterApplyAuto`** (`cli/commands/updater/cli_updater_auto.sh`), called at the end of `updater check`, enqueues the ordinary `updater_apply` task for every update-available app whose policy is auto — never applies inline, exactly like `artifactApplyAuto`. So an automatic update is byte-for-byte the manual path: snapshot → pull → up → auto-rollback on failure, one task log, one History entry, same Roll back button.
|
||||
- **No retry storms.** Each attempt stamps the target digest under `generated/auto/<app>.digest`; the same digest is never auto-attempted twice. A broken upstream build is tried once, rolled back, then left alone (the Update button still offers a manual retry) until a *newer* build changes the digest. In-flight tasks are also skipped, so a scan landing mid-update can't stack a second one.
|
||||
- **Tracked, not silent.** `updates.json` carries each app's resolved `update_type`, and History entries carry `trigger: manual|auto`. The WebUI reads both: the fleet list says whether updates install themselves, rows that opted out carry a `manual` chip, and automatic History entries are labelled. The Overview board treats an auto app's pending update as a *quiet* line, not a "needs action" one — it doesn't need a decision.
|
||||
|
||||
Why not the sketched `off|security|all` severity split: severity is a property of the *CVE data*, which is only as complete as Trivy's per-image scan, whereas "do I let this app move on its own?" is a property of the *app* — nextcloud and a DNS blocker are not the same risk. Per-app also composes with the CVE work instead of competing with it: a future `security` value can be added per app without changing the master switch or the enqueuer's shape.
|
||||
|
||||
**Still to soak:** the honest gap below is unchanged — apply/revert has been exercised in helper-level tests, not end-to-end on a live install with a real pending update. Auto-update inherits exactly that risk, which is why every attempt still snapshots first and why the digest stamp makes a bad build a one-time event rather than a loop.
|
||||
|
||||
## 7. Build phases (each independently shippable)
|
||||
|
||||
@ -142,13 +153,13 @@ Once detection + pinned apply are trustworthy, add `CFG_UPDATER_AUTO=off|securit
|
||||
3. **P3 — Pinned apply/revert.** ✅ *done 2026-07-17.* `updaterApplyApp` records exact `repo:tag@sha256:…` from→to and un-pins before pulling; `updaterRollbackApp` re-pins the anchor to the pre-update build's digest before `up` (closes "new code on old data"). History carries digest refs. *(Helpers unit-tested; end-to-end apply/revert not yet exercised on a live install with a pending update.)*
|
||||
4. **P4 — CVE scanner.** The trivy app (catalog + recommended + first-install list) plus the updater's scan step through it; `cves.json` for real, guard dropped, totals + severity chips live, security-update tie-in. The app itself can ship ahead of the rest of P4.
|
||||
5. **P5 — Polish.** Per-app critical-CVE / update chip on the app header (pattern exists for improvements), remote version-label enrichment, "N services" aggregation in rows.
|
||||
6. **P6 — Auto-update policy** (§6), only after P3 has soaked on a real install.
|
||||
6. **P6 — Auto-update policy.** ✅ *done 2026-08-11.* Per-app `CFG_<APP>_UPDATE_TYPE` (default auto) under the `CFG_UPDATER_AUTO` master switch; `updaterApplyAuto` enqueues the normal apply task, one attempt per target digest, policy + trigger surfaced in `updates.json`/`history.json` and the WebUI. See the rewritten §6. *(Landed ahead of P3's live soak at the maintainer's call — the snapshot-first apply and the one-shot digest stamp are what make that acceptable.)*
|
||||
|
||||
## 8. Rejected alternatives (for the record)
|
||||
|
||||
- **~~`CFG_<APP>_VERSION` config vars~~ — ADOPTED 2026-07-17 (see §2).** Original objection: "second source of truth vs the compose file, collides with the `_TAG` namespace, doesn't solve floating `latest`." Reassessed: the tagging system makes the config *the* source (compose tag derived from it, like every other config value), and `CFG_<APP>_VERSION` → `<APP>_VERSION_TAG` fits the naming convention with no collision. The only surviving point — *doesn't detect floating `latest` alone* — is real and is exactly why the **digest fingerprint** rides alongside it; config = channel, digest = build.
|
||||
- **Pure "compare version numbers" (no digest)** — the intuitive model, but most upstreams don't publish a comparable version and there is no universal "latest version of X" API. Would require per-app curated version-source adapters (where to look, how to parse/compare) — permanent, fragile curation. The digest engine works for all apps automatically; version numbers are display enrichment on top (§2, *two types*).
|
||||
- **Pin semver tags across all templates** — permanent curation burden, inconsistent upstreams, still blind to republished tags.
|
||||
- **Watchtower-style auto-pull of latest** — maximum freshness, zero visibility, no revert; the exact opposite of "monitor, update deliberately, roll back."
|
||||
- **Watchtower-style auto-pull of latest** — maximum freshness, zero visibility, no revert; the exact opposite of "monitor, update deliberately, roll back." Note §6's auto default is *not* this: it is the same deliberate, snapshotted, revertible apply the button runs, scheduled — visible in Tasks and History, per-app switchable, and never retried blindly.
|
||||
- **Docker Scout / hosted scanners** — requires accounts / sends data off-box; against the ethos.
|
||||
- **A new top-level "Versions" area** — unnecessary; the fleet Updates tab + per-app Updates tab already are that surface, they just need real data.
|
||||
|
||||
@ -846,7 +846,10 @@ artifactApplyAuto() {
|
||||
# gates re-checked at apply time)
|
||||
app="$(jq -r '.applies_when.app // empty' <<<"$art")"
|
||||
[[ -n "$app" && ! -d "${containers_dir%/}/$app" ]] && continue
|
||||
cliTaskRun "libreportal artifact apply $id" "artifact_apply" "$id" ""
|
||||
# --detach: this runs inside the task processor's own poll, so following
|
||||
# the new task in the foreground would wait for a task the (single-
|
||||
# threaded) processor can't start until we return.
|
||||
cliTaskRun "libreportal artifact apply $id" "artifact_apply" "$id" "--detach"
|
||||
enqueued=$((enqueued + 1))
|
||||
done <<< "$ids"
|
||||
|
||||
|
||||
124
scripts/cli/commands/updater/cli_updater_auto.sh
Normal file
124
scripts/cli/commands/updater/cli_updater_auto.sh
Normal file
@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Automatic app updates — policy resolution + the auto-apply enqueuer.
|
||||
# ---------------------------------------------------------------------------
|
||||
# The decision half of the updater: `webui_updater_scan.sh` answers "is a new
|
||||
# build available?", `updaterApplyApp` answers "how do I install it safely?",
|
||||
# and this file answers "should I install it without being asked?".
|
||||
#
|
||||
# Policy is per app, with a fleet-wide master switch:
|
||||
# CFG_<APP>_UPDATE_TYPE = auto | manual (per app, default auto)
|
||||
# CFG_UPDATER_AUTO = true | false (master switch, default true)
|
||||
# Precedence mirrors the established backup-strategy shape (per-app override on
|
||||
# top of a global default): the master switch can only ever make things MORE
|
||||
# manual, so "turn auto-updates off" is one flip, not 33 config edits.
|
||||
#
|
||||
# What it deliberately does NOT do:
|
||||
# * It never applies anything inline. Like artifactApplyAuto, it only enqueues
|
||||
# the ordinary `updater_apply` task, so an automatic update is the exact
|
||||
# same code path (snapshot -> pull -> up -> auto-rollback on failure), with
|
||||
# the same task log, the same History entry, and the same Roll back button
|
||||
# as a hand-pressed Update. The only difference is who pressed it, which is
|
||||
# recorded as the history entry's `trigger`.
|
||||
# * It never retries a build that already failed. Each auto-attempt stamps the
|
||||
# target digest under generated/auto/<app>; the same digest is skipped
|
||||
# forever after. Without this a broken upstream build would be re-attempted
|
||||
# (and rolled back) on every scan, snapshotting the app each time. A newer
|
||||
# build changes the digest and is attempted normally; the Update button
|
||||
# stays available for a manual retry of the skipped one.
|
||||
|
||||
_updaterAutoGenDir() { echo "${containers_dir%/}/libreportal/frontend/data/updater/generated"; }
|
||||
_updaterAutoDir() { echo "$(_updaterAutoGenDir)/auto"; }
|
||||
_updaterAutoStamp() { echo "$(_updaterAutoDir)/$1.digest"; } # $1=app
|
||||
|
||||
# Effective update policy for one app -> "auto" | "manual".
|
||||
# 1. master switch off -> manual (everything, no exceptions)
|
||||
# 2. per-app CFG_<APP>_UPDATE_TYPE
|
||||
# 3. unset / unrecognised -> auto (the documented default)
|
||||
# App configs are sourced globally (sourceScanFiles app_configs), so the per-app
|
||||
# key is read by indirect expansion exactly like backupResolveStrategy does.
|
||||
updaterAppPolicy()
|
||||
{
|
||||
local app="$1"
|
||||
[[ -n "$app" ]] || { echo manual; return 0; }
|
||||
[[ "${CFG_UPDATER_AUTO:-true}" == "true" ]] || { echo manual; return 0; }
|
||||
local key="CFG_${app^^}_UPDATE_TYPE"
|
||||
case "$(printf '%s' "${!key-}" | tr 'A-Z' 'a-z')" in
|
||||
manual|off|false) echo manual ;;
|
||||
*) echo auto ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# True when an updater task for this app is already queued or in flight, so a
|
||||
# scan that lands while the previous update is still running doesn't stack a
|
||||
# second one. Cheap: one jq over the task files, and only for apps that got
|
||||
# this far (update available + policy auto).
|
||||
updaterAutoTaskPending()
|
||||
{
|
||||
local app="$1" dir="${containers_dir%/}/libreportal/frontend/data/tasks"
|
||||
command -v jq >/dev/null 2>&1 || return 1
|
||||
local files=( "$dir"/task_*.json )
|
||||
[[ -e "${files[0]}" ]] || return 1
|
||||
local n
|
||||
n="$(jq -rs --arg a "$app" '
|
||||
[ .[] | select((.app // "") == $a
|
||||
and ((.type // "") | startswith("updater_"))
|
||||
and ((.status // "") == "queued" or (.status // "") == "pending"
|
||||
or (.status // "") == "running")) ] | length
|
||||
' "${files[@]}" 2>/dev/null)"
|
||||
[[ "${n:-0}" -gt 0 ]]
|
||||
}
|
||||
|
||||
# updaterApplyAuto — enqueue an `updater apply` task for every app that has an
|
||||
# update available and is set to auto. Called from `updater check` right after
|
||||
# the scan that produced updates.json, so it always acts on fresh data.
|
||||
updaterApplyAuto()
|
||||
{
|
||||
if [[ "${CFG_UPDATER_AUTO:-true}" != "true" ]]; then
|
||||
isNotice "Automatic app updates are off (CFG_UPDATER_AUTO=false)."
|
||||
return 0
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
isNotice "Automatic app updates need jq to read the scan results — skipping (updates are still listed in the WebUI)."
|
||||
return 0
|
||||
fi
|
||||
local upd; upd="$(_updaterAutoGenDir)/updates.json"
|
||||
[[ -f "$upd" ]] || return 0
|
||||
|
||||
local auto_dir; auto_dir="$(_updaterAutoDir)"
|
||||
[[ -d "$auto_dir" ]] || runFileOp mkdir -p "$auto_dir" 2>/dev/null
|
||||
|
||||
local app dig stamp enqueued=0 skipped=0
|
||||
while IFS=$'\t' read -r app dig; do
|
||||
[[ -n "$app" ]] || continue
|
||||
[[ "$(updaterAppPolicy "$app")" == "auto" ]] || continue
|
||||
|
||||
# An update we already queued may still be waiting its turn — that is
|
||||
# in-flight, not stuck, so it must be tested BEFORE the attempted-digest
|
||||
# stamp (which the enqueue already wrote) or it would be miscounted as a
|
||||
# failure below.
|
||||
updaterAutoTaskPending "$app" && continue
|
||||
|
||||
# Already auto-attempted this exact build? (failed last time — see header)
|
||||
stamp="$(_updaterAutoStamp "$app")"
|
||||
if [[ -n "$dig" && -f "$stamp" ]] && [[ "$(cat "$stamp" 2>/dev/null)" == "$dig" ]]; then
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Stamp BEFORE enqueueing: if the update fails and rolls back, the stamp
|
||||
# is what stops the next scan from trying the same build again. A crash
|
||||
# between stamp and enqueue costs one skipped auto-update, never a loop.
|
||||
printf '%s' "$dig" | runFileWrite "$stamp" 2>/dev/null || true
|
||||
cliTaskRun "libreportal updater apply $app auto" "updater_apply" "$app" "--detach"
|
||||
enqueued=$((enqueued + 1))
|
||||
done < <(jq -r '.apps[]? | select(.update_available == true)
|
||||
| "\(.name)\t\(.available_digest // "")"' "$upd" 2>/dev/null)
|
||||
|
||||
if (( enqueued > 0 )); then
|
||||
isSuccessful "Queued $enqueued automatic app update(s) — each is snapshotted before it is applied."
|
||||
elif (( skipped > 0 )); then
|
||||
isNotice "$skipped app(s) have an update that a previous automatic attempt could not apply — update them from the WebUI to see why."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@ -4,10 +4,13 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatched automatically by cli_initialize.sh (category -> cliHandleUpdaterCommands).
|
||||
# Subcommands (the features/updater WebUI buttons route to these as tasks):
|
||||
# check refresh the version/CVE data (runs the WebUI generator)
|
||||
# check refresh the version/CVE data (runs the WebUI generator),
|
||||
# then enqueue the updates for apps set to UPDATE_TYPE=auto
|
||||
# (cli_updater_auto.sh owns that decision)
|
||||
# apply <app> update one app — DISASTER-RECOVERY FIRST: snapshot the app
|
||||
# via the backup engine, then pull + recreate; on failure,
|
||||
# roll back to the snapshot automatically
|
||||
# roll back to the snapshot automatically. A trailing `auto`
|
||||
# marks it as policy-driven for History; the work is identical.
|
||||
# apply-all [a,b] apply to a comma-list (or every update-available app)
|
||||
# rollback <app> restore the app's most recent pre-update snapshot
|
||||
#
|
||||
@ -62,14 +65,27 @@ cliHandleUpdaterCommands()
|
||||
source "$install_scripts_dir/cli/commands/artifact/cli_artifact_apply.sh" 2>/dev/null
|
||||
fi
|
||||
declare -F artifactApplyAuto >/dev/null 2>&1 && artifactApplyAuto
|
||||
# App images: enqueue the updates for apps set to UPDATE_TYPE=auto.
|
||||
# Runs LAST so it acts on the updates.json the scan above just wrote.
|
||||
if ! declare -F updaterApplyAuto >/dev/null 2>&1; then
|
||||
source "$install_scripts_dir/cli/commands/updater/cli_updater_auto.sh" 2>/dev/null
|
||||
fi
|
||||
declare -F updaterApplyAuto >/dev/null 2>&1 && updaterApplyAuto
|
||||
;;
|
||||
|
||||
"apply"|"now")
|
||||
if [[ -z "$app" ]]; then isError "Usage: libreportal updater apply <app>"; return 1; fi
|
||||
if [[ -z "$app" ]]; then isError "Usage: libreportal updater apply <app> [auto]"; return 1; fi
|
||||
# Optional 4th word marks an automatic (policy-driven) update, so
|
||||
# History can say who pressed the button. Anything else = manual.
|
||||
local trigger="manual"; [[ "$initial_command4" == "auto" ]] && trigger="auto"
|
||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
||||
updaterApplyApp "$app"
|
||||
updaterApplyApp "$app" "$trigger"
|
||||
else
|
||||
cliTaskRun "libreportal updater apply $app" "updater_apply" "$app" ""
|
||||
# Carry the marker into the queued command so the task that
|
||||
# actually runs still knows who asked for it.
|
||||
local apply_cmd="libreportal updater apply $app"
|
||||
[[ "$trigger" == "auto" ]] && apply_cmd="$apply_cmd auto"
|
||||
cliTaskRun "$apply_cmd" "updater_apply" "$app" ""
|
||||
fi
|
||||
;;
|
||||
|
||||
@ -143,10 +159,18 @@ updaterLastUpdateFrom()
|
||||
updaterApplyApp()
|
||||
{
|
||||
local app="$1"
|
||||
# "auto" when the updater's own policy enqueued this (CFG_<APP>_UPDATE_TYPE),
|
||||
# "manual" when a person pressed Update. Recorded in History; changes nothing
|
||||
# about how the update is applied — both take the snapshot, both can roll back.
|
||||
local trigger="${2:-manual}"
|
||||
local app_dir="$containers_dir/$app"
|
||||
if [[ ! -d "$app_dir" ]]; then isError "App '$app' is not installed."; return 1; fi
|
||||
|
||||
isHeader "Updating $app (a recovery snapshot is taken first)"
|
||||
if [[ "$trigger" == "auto" ]]; then
|
||||
isHeader "Automatically updating $app (a recovery snapshot is taken first)"
|
||||
else
|
||||
isHeader "Updating $app (a recovery snapshot is taken first)"
|
||||
fi
|
||||
|
||||
# 1. DISASTER RECOVERY — snapshot before touching anything. Call the backup
|
||||
# function directly (we already run under LIBREPORTAL_TASK_EXEC): the CLI form
|
||||
@ -157,7 +181,7 @@ updaterApplyApp()
|
||||
isNotice "Snapshotting $app before update…"
|
||||
if ! backupAppStart "$app" >/dev/null 2>&1; then
|
||||
isNotice "Pre-update snapshot did not complete cleanly — continuing is risky; aborting $app update."
|
||||
updaterRecordHistory "$app" "update" "" "" "aborted-no-snapshot"
|
||||
updaterRecordHistory "$app" "update" "" "" "aborted-no-snapshot" "" "" "" "$trigger"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@ -176,7 +200,7 @@ updaterApplyApp()
|
||||
local after_ref; after_ref="$(updaterPrimaryImage "$app" "$app_dir/docker-compose.yml")"; after_ref="${after_ref%%@*}"
|
||||
local after_dig; after_dig="$(updaterRefDigest "$after_ref")"
|
||||
local after="$after_ref${after_dig:+@$after_dig}"
|
||||
updaterRecordHistory "$app" "update" "$before" "$after" "ok"
|
||||
updaterRecordHistory "$app" "update" "$before" "$after" "ok" "" "" "" "$trigger"
|
||||
isSuccessful "$app updated. Rollback point retained."
|
||||
webuiUpdaterScan >/dev/null 2>&1 || true
|
||||
return 0
|
||||
@ -185,7 +209,7 @@ updaterApplyApp()
|
||||
# 4. Failure -> automatic rollback.
|
||||
isNotice "Update of $app failed — rolling back to the pre-update snapshot…"
|
||||
updaterRollbackApp "$app" "auto"
|
||||
updaterRecordHistory "$app" "update" "$before" "" "rolled-back"
|
||||
updaterRecordHistory "$app" "update" "$before" "" "rolled-back" "" "" "" "$trigger"
|
||||
return 1
|
||||
}
|
||||
|
||||
@ -250,11 +274,12 @@ updaterComposePull()
|
||||
# actually recording, so it is FAIL-CLOSED, not best-effort: with jq we prepend +
|
||||
# cap to 200; WITHOUT jq we fall back to a brace-agnostic bash-native prepend
|
||||
# (no 200-cap, the one thing jq bought) rather than silently dropping the entry.
|
||||
# Args 6-8 are optional and carry the artifact channel's metadata.
|
||||
# Args 6-8 are optional and carry the artifact channel's metadata; arg 9 records
|
||||
# whether a person or the auto-update policy started it (manual|auto).
|
||||
updaterRecordHistory()
|
||||
{
|
||||
local app="$1" action="$2" from="$3" to="$4" result="$5"
|
||||
local artifact_id="${6:-}" serial="${7:-}" undo_id="${8:-}"
|
||||
local artifact_id="${6:-}" serial="${7:-}" undo_id="${8:-}" trigger="${9:-manual}"
|
||||
local f="$containers_dir/libreportal/frontend/data/updater/generated/history.json"
|
||||
local ts; ts="$(date -Iseconds 2>/dev/null || date)"
|
||||
[ -f "$f" ] || printf '{ "entries": [] }\n' | runFileWrite "$f"
|
||||
@ -263,7 +288,8 @@ updaterRecordHistory()
|
||||
local tmp; tmp="$(mktemp)"
|
||||
if jq --arg ts "$ts" --arg app "$app" --arg action "$action" --arg from "$from" --arg to "$to" \
|
||||
--arg result "$result" --arg aid "$artifact_id" --arg serial "$serial" --arg undo "$undo_id" \
|
||||
'.entries = ([{ts:$ts, app:$app, action:$action, from:$from, to:$to, result:$result, artifact_id:$aid, serial:$serial, undo_id:$undo}] + (.entries // []))[0:200]' \
|
||||
--arg trigger "$trigger" \
|
||||
'.entries = ([{ts:$ts, app:$app, action:$action, from:$from, to:$to, result:$result, artifact_id:$aid, serial:$serial, undo_id:$undo, trigger:$trigger}] + (.entries // []))[0:200]' \
|
||||
"$f" > "$tmp" 2>/dev/null; then
|
||||
runFileWrite "$f" < "$tmp"; rm -f "$tmp"; return 0
|
||||
fi
|
||||
@ -274,7 +300,7 @@ updaterRecordHistory()
|
||||
# jq absent or failed — bash-native, brace-agnostic prepend. History entries
|
||||
# are flat (scalar fields only), so splicing on the outer [ ... ] is safe.
|
||||
local entry
|
||||
entry="{\"ts\":\"$(_lpJsonEsc "$ts")\",\"app\":\"$(_lpJsonEsc "$app")\",\"action\":\"$(_lpJsonEsc "$action")\",\"from\":\"$(_lpJsonEsc "$from")\",\"to\":\"$(_lpJsonEsc "$to")\",\"result\":\"$(_lpJsonEsc "$result")\",\"artifact_id\":\"$(_lpJsonEsc "$artifact_id")\",\"serial\":\"$(_lpJsonEsc "$serial")\",\"undo_id\":\"$(_lpJsonEsc "$undo_id")\"}"
|
||||
entry="{\"ts\":\"$(_lpJsonEsc "$ts")\",\"app\":\"$(_lpJsonEsc "$app")\",\"action\":\"$(_lpJsonEsc "$action")\",\"from\":\"$(_lpJsonEsc "$from")\",\"to\":\"$(_lpJsonEsc "$to")\",\"result\":\"$(_lpJsonEsc "$result")\",\"artifact_id\":\"$(_lpJsonEsc "$artifact_id")\",\"serial\":\"$(_lpJsonEsc "$serial")\",\"undo_id\":\"$(_lpJsonEsc "$undo_id")\",\"trigger\":\"$(_lpJsonEsc "$trigger")\"}"
|
||||
local cur inner
|
||||
cur="$(cat "$f" 2>/dev/null)"
|
||||
inner="${cur#*[}"; inner="${inner%]*}"
|
||||
|
||||
@ -19,4 +19,10 @@ cliShowUpdaterHelp()
|
||||
echo "touching the app, so any update is reversible. These commands back the"
|
||||
echo "WebUI Updates page (features/updater); actions run through the task system."
|
||||
echo ""
|
||||
echo "Automatic updates: each check enqueues the update for every app set to"
|
||||
echo "CFG_<APP>_UPDATE_TYPE=auto (the default) — the same snapshot-first apply"
|
||||
echo "as pressing Update, recorded in History as automatic. Set an app to"
|
||||
echo "'manual' on its Configure page to hold it back, or CFG_UPDATER_AUTO=false"
|
||||
echo "to hold back every app. A build that fails is never auto-retried."
|
||||
echo ""
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ cli_scripts=(
|
||||
"cli/commands/system/cli_system_header.sh"
|
||||
"cli/commands/update/cli_update_commands.sh"
|
||||
"cli/commands/update/cli_update_header.sh"
|
||||
"cli/commands/updater/cli_updater_auto.sh"
|
||||
"cli/commands/updater/cli_updater_commands.sh"
|
||||
"cli/commands/updater/cli_updater_header.sh"
|
||||
"cli/commands/validation/cli_validation_commands.sh"
|
||||
|
||||
@ -916,6 +916,12 @@ declare -gA LP_FN_MAP=(
|
||||
[updaterAllServiceImages]="webui/data/generators/updater/webui_updater_scan.sh"
|
||||
[updaterApplyAll]="cli/commands/updater/cli_updater_commands.sh"
|
||||
[updaterApplyApp]="cli/commands/updater/cli_updater_commands.sh"
|
||||
[updaterApplyAuto]="cli/commands/updater/cli_updater_auto.sh"
|
||||
[updaterAppPolicy]="cli/commands/updater/cli_updater_auto.sh"
|
||||
[_updaterAutoDir]="cli/commands/updater/cli_updater_auto.sh"
|
||||
[_updaterAutoGenDir]="cli/commands/updater/cli_updater_auto.sh"
|
||||
[_updaterAutoStamp]="cli/commands/updater/cli_updater_auto.sh"
|
||||
[updaterAutoTaskPending]="cli/commands/updater/cli_updater_auto.sh"
|
||||
[updaterClassifyTag]="webui/data/generators/updater/webui_updater_scan.sh"
|
||||
[_updaterCleanImageRef]="webui/data/generators/updater/webui_updater_scan.sh"
|
||||
[updaterComposePull]="cli/commands/updater/cli_updater_commands.sh"
|
||||
@ -1922,6 +1928,12 @@ declare -gA LP_FN_ROOT=(
|
||||
[updaterAllServiceImages]="scripts"
|
||||
[updaterApplyAll]="scripts"
|
||||
[updaterApplyApp]="scripts"
|
||||
[updaterApplyAuto]="scripts"
|
||||
[updaterAppPolicy]="scripts"
|
||||
[_updaterAutoDir]="scripts"
|
||||
[_updaterAutoGenDir]="scripts"
|
||||
[_updaterAutoStamp]="scripts"
|
||||
[updaterAutoTaskPending]="scripts"
|
||||
[updaterClassifyTag]="scripts"
|
||||
[_updaterCleanImageRef]="scripts"
|
||||
[updaterComposePull]="scripts"
|
||||
@ -2961,6 +2973,12 @@ updateHostIPToWhitelist() { unset -f updateHostIPToWhitelist; __lpAutoload "${in
|
||||
updaterAllServiceImages() { unset -f updaterAllServiceImages; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterAllServiceImages "$@"; }
|
||||
updaterApplyAll() { unset -f updaterApplyAll; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterApplyAll "$@"; }
|
||||
updaterApplyApp() { unset -f updaterApplyApp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterApplyApp "$@"; }
|
||||
updaterApplyAuto() { unset -f updaterApplyAuto; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; updaterApplyAuto "$@"; }
|
||||
updaterAppPolicy() { unset -f updaterAppPolicy; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; updaterAppPolicy "$@"; }
|
||||
_updaterAutoDir() { unset -f _updaterAutoDir; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoDir "$@"; }
|
||||
_updaterAutoGenDir() { unset -f _updaterAutoGenDir; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoGenDir "$@"; }
|
||||
_updaterAutoStamp() { unset -f _updaterAutoStamp; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; _updaterAutoStamp "$@"; }
|
||||
updaterAutoTaskPending() { unset -f updaterAutoTaskPending; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_auto.sh"; updaterAutoTaskPending "$@"; }
|
||||
updaterClassifyTag() { unset -f updaterClassifyTag; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; updaterClassifyTag "$@"; }
|
||||
_updaterCleanImageRef() { unset -f _updaterCleanImageRef; __lpAutoload "${install_scripts_dir}webui/data/generators/updater/webui_updater_scan.sh"; _updaterCleanImageRef "$@"; }
|
||||
updaterComposePull() { unset -f updaterComposePull; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterComposePull "$@"; }
|
||||
|
||||
@ -173,6 +173,17 @@ PORTEOF
|
||||
],
|
||||
"default": "auto"
|
||||
},
|
||||
"UPDATE_TYPE": {
|
||||
"category": "general",
|
||||
"label": "Updates",
|
||||
"type": "select",
|
||||
"tooltip": "Automatic installs a new image build as soon as one is published — the app is snapshotted first and rolled back automatically if the update fails. Manual leaves it listed under Updates until you press Update.",
|
||||
"options": [
|
||||
{"value": "auto", "label": "Automatic (recommended)"},
|
||||
{"value": "manual", "label": "Manual — I'll press Update"}
|
||||
],
|
||||
"default": "auto"
|
||||
},
|
||||
"MONITORING": {
|
||||
"category": "features",
|
||||
"label": "Export metrics to Grafana",
|
||||
|
||||
@ -201,6 +201,19 @@ webuiUpdaterScan() {
|
||||
svcs="$(jq -cs '.' "$sobjs" 2>/dev/null || echo '[]')"; rm -f "$sobjs"
|
||||
fi
|
||||
|
||||
# The app's effective update policy (auto|manual) travels WITH its version
|
||||
# state so the WebUI can label every row without a second data source —
|
||||
# and so what the UI shows is what the enqueuer will actually do (both
|
||||
# read updaterAppPolicy). Falls back to the documented default when the
|
||||
# decision file hasn't been loaded (jq-less/degraded paths never lie here).
|
||||
local policy="auto"
|
||||
if declare -F updaterAppPolicy >/dev/null 2>&1; then
|
||||
policy="$(updaterAppPolicy "$app")"
|
||||
elif [ -f "$install_scripts_dir/cli/commands/updater/cli_updater_auto.sh" ]; then
|
||||
source "$install_scripts_dir/cli/commands/updater/cli_updater_auto.sh" 2>/dev/null
|
||||
declare -F updaterAppPolicy >/dev/null 2>&1 && policy="$(updaterAppPolicy "$app")"
|
||||
fi
|
||||
|
||||
if [ "$have_jq" = "1" ]; then
|
||||
jq -cn \
|
||||
--arg name "$app" --arg displayName "$app" --arg type "$vtype" \
|
||||
@ -209,18 +222,20 @@ webuiUpdaterScan() {
|
||||
--arg available_image "$anchor" --arg available_version "$avail_ver" \
|
||||
--arg available_digest "$avail_dig" --argjson update_available "$update_available" \
|
||||
--arg last_checked "$now" --argjson services "$svcs" \
|
||||
--arg update_type "$policy" \
|
||||
'{name:$name,displayName:$displayName,type:$type,channel:$channel,
|
||||
current_image:$current_image,current_version:$current_version,current_digest:$current_digest,
|
||||
available_image:$available_image,
|
||||
available_version:(if $available_version=="" then null else $available_version end),
|
||||
available_digest:$available_digest,update_available:$update_available,
|
||||
update_type:$update_type,
|
||||
scanned:true,last_checked:$last_checked,services:$services}' \
|
||||
>> "$objs" 2>/dev/null
|
||||
else
|
||||
# jq-less fallback: minimal but valid object
|
||||
printf '{"name":"%s","displayName":"%s","current_image":"%s","current_version":"%s","update_available":false,"scanned":true,"last_checked":"%s"}\n' \
|
||||
printf '{"name":"%s","displayName":"%s","current_image":"%s","current_version":"%s","update_available":false,"update_type":"%s","scanned":true,"last_checked":"%s"}\n' \
|
||||
"$app" "$app" "$(printf '%s' "$anchor" | sed 's/"/\\"/g')" \
|
||||
"$(printf '%s' "$cur_ver" | sed 's/"/\\"/g')" "$now" >> "$objs"
|
||||
"$(printf '%s' "$cur_ver" | sed 's/"/\\"/g')" "$policy" "$now" >> "$objs"
|
||||
fi
|
||||
done
|
||||
[ "$do_registry" = "1" ] && touch "$reg_stamp" 2>/dev/null || true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user