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:
librelad 2026-08-11 16:22:04 +01:00
parent 48075d0551
commit cdeb2d1658
45 changed files with 372 additions and 35 deletions

View File

@ -5,3 +5,4 @@
CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - Minutes between automatic app update/CVE/improvement scans. 0 disables. 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_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_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]

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=adguard
CFG_ADGUARD_BACKUP=true CFG_ADGUARD_BACKUP=true
CFG_ADGUARD_BACKUP_STRATEGY=auto CFG_ADGUARD_BACKUP_STRATEGY=auto
CFG_ADGUARD_UPDATE_TYPE=auto
CFG_ADGUARD_COMPOSE_FILE=default CFG_ADGUARD_COMPOSE_FILE=default
CFG_ADGUARD_HEALTHCHECK=true CFG_ADGUARD_HEALTHCHECK=true
CFG_ADGUARD_AUTHELIA=false CFG_ADGUARD_AUTHELIA=false

View File

@ -6,6 +6,7 @@
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh) # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_REQUIRES="domain,traefik"
CFG_AUTHELIA_BACKUP=true CFG_AUTHELIA_BACKUP=true
CFG_AUTHELIA_BACKUP_STRATEGY=auto CFG_AUTHELIA_BACKUP_STRATEGY=auto
CFG_AUTHELIA_UPDATE_TYPE=auto
CFG_AUTHELIA_COMPOSE_FILE=default CFG_AUTHELIA_COMPOSE_FILE=default
CFG_AUTHELIA_HEALTHCHECK=true CFG_AUTHELIA_HEALTHCHECK=true
CFG_AUTHELIA_AUTHELIA=false CFG_AUTHELIA_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_MULTI_INSTANCE=true
CFG_BOOKSTACK_BACKUP=true CFG_BOOKSTACK_BACKUP=true
CFG_BOOKSTACK_BACKUP_STRATEGY=auto CFG_BOOKSTACK_BACKUP_STRATEGY=auto
CFG_BOOKSTACK_UPDATE_TYPE=auto
CFG_BOOKSTACK_COMPOSE_FILE=default CFG_BOOKSTACK_COMPOSE_FILE=default
CFG_BOOKSTACK_HEALTHCHECK=true CFG_BOOKSTACK_HEALTHCHECK=true
CFG_BOOKSTACK_AUTHELIA=false CFG_BOOKSTACK_AUTHELIA=false

View File

@ -9,6 +9,7 @@
# HOST_SERVICES = all units; feeds the Services + Logs tabs # HOST_SERVICES = all units; feeds the Services + Logs tabs
# HOST_LOG_FILES = <unit>|<path>,... mapping for the log viewer # HOST_LOG_FILES = <unit>|<path>,... mapping for the log viewer
# BACKUP = include in backup operations # 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) # 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) # 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_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=true
CFG_CROWDSEC_BACKUP_STRATEGY=auto CFG_CROWDSEC_BACKUP_STRATEGY=auto
CFG_CROWDSEC_UPDATE_TYPE=auto
CFG_CROWDSEC_MONITORING=false CFG_CROWDSEC_MONITORING=false
CFG_CROWDSEC_PROMETHEUS_LISTEN=0.0.0.0:6060 CFG_CROWDSEC_PROMETHEUS_LISTEN=0.0.0.0:6060
# #

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=dashy
CFG_DASHY_BACKUP=true CFG_DASHY_BACKUP=true
CFG_DASHY_BACKUP_STRATEGY=auto CFG_DASHY_BACKUP_STRATEGY=auto
CFG_DASHY_UPDATE_TYPE=auto
CFG_DASHY_COMPOSE_FILE=default CFG_DASHY_COMPOSE_FILE=default
CFG_DASHY_HEALTHCHECK=true CFG_DASHY_HEALTHCHECK=true
CFG_DASHY_AUTHELIA=false CFG_DASHY_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=focalboard
CFG_FOCALBOARD_BACKUP=true CFG_FOCALBOARD_BACKUP=true
CFG_FOCALBOARD_BACKUP_STRATEGY=auto CFG_FOCALBOARD_BACKUP_STRATEGY=auto
CFG_FOCALBOARD_UPDATE_TYPE=auto
CFG_FOCALBOARD_COMPOSE_FILE=default CFG_FOCALBOARD_COMPOSE_FILE=default
CFG_FOCALBOARD_HEALTHCHECK=true CFG_FOCALBOARD_HEALTHCHECK=true
CFG_FOCALBOARD_AUTHELIA=false CFG_FOCALBOARD_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=gitea
CFG_GITEA_BACKUP=true CFG_GITEA_BACKUP=true
CFG_GITEA_BACKUP_STRATEGY=auto CFG_GITEA_BACKUP_STRATEGY=auto
CFG_GITEA_UPDATE_TYPE=auto
CFG_GITEA_COMPOSE_FILE=default CFG_GITEA_COMPOSE_FILE=default
CFG_GITEA_HEALTHCHECK=true CFG_GITEA_HEALTHCHECK=true
CFG_GITEA_AUTHELIA=false CFG_GITEA_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=gluetun
CFG_GLUETUN_BACKUP=true CFG_GLUETUN_BACKUP=true
CFG_GLUETUN_BACKUP_STRATEGY=auto CFG_GLUETUN_BACKUP_STRATEGY=auto
CFG_GLUETUN_UPDATE_TYPE=auto
CFG_GLUETUN_COMPOSE_FILE=default CFG_GLUETUN_COMPOSE_FILE=default
CFG_GLUETUN_HEALTHCHECK=true CFG_GLUETUN_HEALTHCHECK=true
CFG_GLUETUN_AUTHELIA=false CFG_GLUETUN_AUTHELIA=false

View File

@ -6,6 +6,7 @@
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh) # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_REQUIRES="prometheus"
CFG_GRAFANA_BACKUP=true CFG_GRAFANA_BACKUP=true
CFG_GRAFANA_BACKUP_STRATEGY=auto CFG_GRAFANA_BACKUP_STRATEGY=auto
CFG_GRAFANA_UPDATE_TYPE=auto
CFG_GRAFANA_COMPOSE_FILE=default CFG_GRAFANA_COMPOSE_FILE=default
CFG_GRAFANA_HEALTHCHECK=true CFG_GRAFANA_HEALTHCHECK=true
CFG_GRAFANA_AUTHELIA=false CFG_GRAFANA_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # 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 # 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) # 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_APP_NAME=headscale
CFG_HEADSCALE_BACKUP=true CFG_HEADSCALE_BACKUP=true
CFG_HEADSCALE_BACKUP_STRATEGY=auto CFG_HEADSCALE_BACKUP_STRATEGY=auto
CFG_HEADSCALE_UPDATE_TYPE=auto
CFG_HEADSCALE_COMPOSE_FILE=default CFG_HEADSCALE_COMPOSE_FILE=default
CFG_HEADSCALE_HEALTHCHECK=true CFG_HEADSCALE_HEALTHCHECK=true
CFG_HEADSCALE_BASIC_AUTH_PASS=RANDOMIZEDPASSWORD1 CFG_HEADSCALE_BASIC_AUTH_PASS=RANDOMIZEDPASSWORD1

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=invidious
CFG_INVIDIOUS_BACKUP=false CFG_INVIDIOUS_BACKUP=false
CFG_INVIDIOUS_BACKUP_STRATEGY=auto CFG_INVIDIOUS_BACKUP_STRATEGY=auto
CFG_INVIDIOUS_UPDATE_TYPE=auto
CFG_INVIDIOUS_COMPOSE_FILE=default CFG_INVIDIOUS_COMPOSE_FILE=default
CFG_INVIDIOUS_HEALTHCHECK=false CFG_INVIDIOUS_HEALTHCHECK=false
CFG_INVIDIOUS_AUTHELIA=false CFG_INVIDIOUS_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=ipinfo
CFG_IPINFO_BACKUP=false CFG_IPINFO_BACKUP=false
CFG_IPINFO_BACKUP_STRATEGY=auto CFG_IPINFO_BACKUP_STRATEGY=auto
CFG_IPINFO_UPDATE_TYPE=auto
CFG_IPINFO_COMPOSE_FILE=default CFG_IPINFO_COMPOSE_FILE=default
CFG_IPINFO_HEALTHCHECK=true CFG_IPINFO_HEALTHCHECK=true
CFG_IPINFO_AUTHELIA=false CFG_IPINFO_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=jellyfin
CFG_JELLYFIN_BACKUP=true CFG_JELLYFIN_BACKUP=true
CFG_JELLYFIN_BACKUP_STRATEGY=auto CFG_JELLYFIN_BACKUP_STRATEGY=auto
CFG_JELLYFIN_UPDATE_TYPE=auto
CFG_JELLYFIN_COMPOSE_FILE=default CFG_JELLYFIN_COMPOSE_FILE=default
CFG_JELLYFIN_HEALTHCHECK=true CFG_JELLYFIN_HEALTHCHECK=true
CFG_JELLYFIN_AUTHELIA=false CFG_JELLYFIN_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=jitsimeet
CFG_JITSIMEET_BACKUP=true CFG_JITSIMEET_BACKUP=true
CFG_JITSIMEET_BACKUP_STRATEGY=auto CFG_JITSIMEET_BACKUP_STRATEGY=auto
CFG_JITSIMEET_UPDATE_TYPE=auto
CFG_JITSIMEET_COMPOSE_FILE=default CFG_JITSIMEET_COMPOSE_FILE=default
CFG_JITSIMEET_HEALTHCHECK=true CFG_JITSIMEET_HEALTHCHECK=true
CFG_JITSIMEET_AUTHELIA=false CFG_JITSIMEET_AUTHELIA=false

View File

@ -401,15 +401,37 @@ class OverviewManager {
if (!scanned) { if (!scanned) {
rows.push({ hue: 'updates', icon: '⬆️', kind: 'none', text: 'Updates — not scanned yet' }); rows.push({ hue: 'updates', icon: '⬆️', kind: 'none', text: 'Updates — not scanned yet' });
} else if (pending.length) { } else if (pending.length) {
const names = pending.map((a) => a.displayName || a.name); // Only the apps set to manual actually want a decision — the rest install
const list = names.slice(0, 3).join(', ') + (names.length > 3 ? ` +${names.length - 3} more` : ''); // 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({ rows.push({
hue: 'updates', icon: '⬆️', kind: 'warn', hue: 'updates', icon: '⬆️', kind: 'warn',
text: `${pending.length} update${pending.length === 1 ? '' : 's'} available`, text: `${waiting.length} update${waiting.length === 1 ? '' : 's'} waiting for you`,
sub: `${esc(list)} — a recovery snapshot is taken before each update`, sub: `${esc(listOf(waiting))} — a recovery snapshot is taken before each update`
+ (selfing ? ` · ${selfing} more will install automatically` : ''),
actions: goto('updates', 'Review', 'updates') actions: goto('updates', 'Review', 'updates')
+ `<button class="updater-btn updater-btn-primary" data-updater-action="update-all">Update all</button>`, + `<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 { } else {
rows.push({ rows.push({
hue: 'updates', icon: '⬆️', kind: 'ok', hue: 'updates', icon: '⬆️', kind: 'ok',
@ -558,13 +580,19 @@ class OverviewManager {
? `<span class="ov-status ov-status-update">↑ Update available</span>` ? `<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>`); : (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>` : ''; 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 const updBtn = a.update_available
? `<button class="updater-btn updater-btn-primary" data-updater-action="update" data-app="${slug}">Update</button>` ? `<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}"> 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}"> <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'"> <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="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> <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> </div>

View File

@ -515,7 +515,7 @@ class UpdaterPage {
const rows = entries.map(e => ` const rows = entries.map(e => `
<div class="updater-row"> <div class="updater-row">
<div class="updater-row-main"><span class="updater-row-name">${this.escape(e.app)}</span> <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-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 class="updater-row-actions">${this.fmtRel(e.ts)}</div>
</div>`).join(''); </div>`).join('');
@ -539,8 +539,15 @@ class UpdaterPage {
const badge = a.update_available const badge = a.update_available
? `<span class="updater-badge updater-badge-update">update available</span>` ? `<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>`); : (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> 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 cves = a.cves || [];
const appLabel = this.escape((window.getAppDisplayName ? window.getAppDisplayName(a.name) : null) || a.displayName || a.name || 'the app'); 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 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) => ` 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.escape(e.from || '')}${e.to ? `${this.escape(e.to)}` : ''}</span>
<span class="updater-detail-meta">${this.fmtRel(e.ts)}</span></div>`).join('')}</div>` : ''; <span class="updater-detail-meta">${this.fmtRel(e.ts)}</span></div>`).join('')}</div>` : '';
return `<div class="updater-detail">${versionSection}${security}${recovery}${history}</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) { 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>`; 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) { } else if (gen && iv > 0) {
nextBit = ` · next check ${this.fmtRelFuture(Date.parse(gen) + iv * 60000)}`; 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>` + 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>`; `<button class="updater-btn updater-autocheck-btn" data-updater-action="check" title="Check for updates now">↻ Check now</button></div>`;
} }
} }

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=libreportal_catalog
CFG_LIBREPORTAL_CATALOG_BACKUP=true CFG_LIBREPORTAL_CATALOG_BACKUP=true
CFG_LIBREPORTAL_CATALOG_BACKUP_STRATEGY=auto CFG_LIBREPORTAL_CATALOG_BACKUP_STRATEGY=auto
CFG_LIBREPORTAL_CATALOG_UPDATE_TYPE=auto
CFG_LIBREPORTAL_CATALOG_COMPOSE_FILE=default CFG_LIBREPORTAL_CATALOG_COMPOSE_FILE=default
CFG_LIBREPORTAL_CATALOG_HEALTHCHECK=false CFG_LIBREPORTAL_CATALOG_HEALTHCHECK=false
CFG_LIBREPORTAL_CATALOG_AUTHELIA=false CFG_LIBREPORTAL_CATALOG_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=linkding
CFG_LINKDING_BACKUP=true CFG_LINKDING_BACKUP=true
CFG_LINKDING_BACKUP_STRATEGY=auto CFG_LINKDING_BACKUP_STRATEGY=auto
CFG_LINKDING_UPDATE_TYPE=auto
CFG_LINKDING_COMPOSE_FILE=default CFG_LINKDING_COMPOSE_FILE=default
CFG_LINKDING_HEALTHCHECK=true CFG_LINKDING_HEALTHCHECK=true
CFG_LINKDING_AUTHELIA=false CFG_LINKDING_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=mastodon
CFG_MASTODON_BACKUP=true CFG_MASTODON_BACKUP=true
CFG_MASTODON_BACKUP_STRATEGY=auto CFG_MASTODON_BACKUP_STRATEGY=auto
CFG_MASTODON_UPDATE_TYPE=auto
CFG_MASTODON_COMPOSE_FILE=default CFG_MASTODON_COMPOSE_FILE=default
CFG_MASTODON_HEALTHCHECK=true CFG_MASTODON_HEALTHCHECK=true
CFG_MASTODON_AUTHELIA=false CFG_MASTODON_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_REQUIRES=""
CFG_MONEYAPP_BACKUP=true CFG_MONEYAPP_BACKUP=true
CFG_MONEYAPP_BACKUP_STRATEGY=auto CFG_MONEYAPP_BACKUP_STRATEGY=auto
CFG_MONEYAPP_UPDATE_TYPE=auto
CFG_MONEYAPP_COMPOSE_FILE=default CFG_MONEYAPP_COMPOSE_FILE=default
CFG_MONEYAPP_HEALTHCHECK=true CFG_MONEYAPP_HEALTHCHECK=true
CFG_MONEYAPP_AUTHELIA=false CFG_MONEYAPP_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=navidrome
CFG_NAVIDROME_BACKUP=true CFG_NAVIDROME_BACKUP=true
CFG_NAVIDROME_BACKUP_STRATEGY=auto CFG_NAVIDROME_BACKUP_STRATEGY=auto
CFG_NAVIDROME_UPDATE_TYPE=auto
CFG_NAVIDROME_COMPOSE_FILE=default CFG_NAVIDROME_COMPOSE_FILE=default
CFG_NAVIDROME_HEALTHCHECK=true CFG_NAVIDROME_HEALTHCHECK=true
CFG_NAVIDROME_AUTHELIA=false CFG_NAVIDROME_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=nextcloud
CFG_NEXTCLOUD_BACKUP=true CFG_NEXTCLOUD_BACKUP=true
CFG_NEXTCLOUD_BACKUP_STRATEGY=auto CFG_NEXTCLOUD_BACKUP_STRATEGY=auto
CFG_NEXTCLOUD_UPDATE_TYPE=auto
CFG_NEXTCLOUD_COMPOSE_FILE=default CFG_NEXTCLOUD_COMPOSE_FILE=default
CFG_NEXTCLOUD_HEALTHCHECK=true CFG_NEXTCLOUD_HEALTHCHECK=true
CFG_NEXTCLOUD_AUTHELIA=false CFG_NEXTCLOUD_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=ollama
CFG_OLLAMA_BACKUP=true CFG_OLLAMA_BACKUP=true
CFG_OLLAMA_BACKUP_STRATEGY=auto CFG_OLLAMA_BACKUP_STRATEGY=auto
CFG_OLLAMA_UPDATE_TYPE=auto
CFG_OLLAMA_COMPOSE_FILE=default CFG_OLLAMA_COMPOSE_FILE=default
CFG_OLLAMA_HEALTHCHECK=true CFG_OLLAMA_HEALTHCHECK=true
CFG_OLLAMA_AUTHELIA=false CFG_OLLAMA_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=onlyoffice
CFG_ONLYOFFICE_BACKUP=true CFG_ONLYOFFICE_BACKUP=true
CFG_ONLYOFFICE_BACKUP_STRATEGY=auto CFG_ONLYOFFICE_BACKUP_STRATEGY=auto
CFG_ONLYOFFICE_UPDATE_TYPE=auto
CFG_ONLYOFFICE_COMPOSE_FILE=default CFG_ONLYOFFICE_COMPOSE_FILE=default
CFG_ONLYOFFICE_HEALTHCHECK=true CFG_ONLYOFFICE_HEALTHCHECK=true
CFG_ONLYOFFICE_AUTHELIA=false CFG_ONLYOFFICE_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=owncloud
CFG_OWNCLOUD_BACKUP=true CFG_OWNCLOUD_BACKUP=true
CFG_OWNCLOUD_BACKUP_STRATEGY=auto CFG_OWNCLOUD_BACKUP_STRATEGY=auto
CFG_OWNCLOUD_UPDATE_TYPE=auto
CFG_OWNCLOUD_COMPOSE_FILE=default CFG_OWNCLOUD_COMPOSE_FILE=default
CFG_OWNCLOUD_HEALTHCHECK=true CFG_OWNCLOUD_HEALTHCHECK=true
CFG_OWNCLOUD_AUTHELIA=false CFG_OWNCLOUD_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=pihole
CFG_PIHOLE_BACKUP=true CFG_PIHOLE_BACKUP=true
CFG_PIHOLE_BACKUP_STRATEGY=auto CFG_PIHOLE_BACKUP_STRATEGY=auto
CFG_PIHOLE_UPDATE_TYPE=auto
CFG_PIHOLE_COMPOSE_FILE=default CFG_PIHOLE_COMPOSE_FILE=default
CFG_PIHOLE_HEALTHCHECK=true CFG_PIHOLE_HEALTHCHECK=true
CFG_PIHOLE_AUTHELIA=false CFG_PIHOLE_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=prometheus
CFG_PROMETHEUS_BACKUP=true CFG_PROMETHEUS_BACKUP=true
CFG_PROMETHEUS_BACKUP_STRATEGY=auto CFG_PROMETHEUS_BACKUP_STRATEGY=auto
CFG_PROMETHEUS_UPDATE_TYPE=auto
CFG_PROMETHEUS_COMPOSE_FILE=default CFG_PROMETHEUS_COMPOSE_FILE=default
CFG_PROMETHEUS_HEALTHCHECK=true CFG_PROMETHEUS_HEALTHCHECK=true
CFG_PROMETHEUS_AUTHELIA=false CFG_PROMETHEUS_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=searxng
CFG_SEARXNG_BACKUP=false CFG_SEARXNG_BACKUP=false
CFG_SEARXNG_BACKUP_STRATEGY=auto CFG_SEARXNG_BACKUP_STRATEGY=auto
CFG_SEARXNG_UPDATE_TYPE=auto
CFG_SEARXNG_COMPOSE_FILE=default CFG_SEARXNG_COMPOSE_FILE=default
CFG_SEARXNG_HEALTHCHECK=true CFG_SEARXNG_HEALTHCHECK=true
CFG_SEARXNG_AUTHELIA=false CFG_SEARXNG_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=speedtest
CFG_SPEEDTEST_BACKUP=false CFG_SPEEDTEST_BACKUP=false
CFG_SPEEDTEST_BACKUP_STRATEGY=auto CFG_SPEEDTEST_BACKUP_STRATEGY=auto
CFG_SPEEDTEST_UPDATE_TYPE=auto
CFG_SPEEDTEST_COMPOSE_FILE=default CFG_SPEEDTEST_COMPOSE_FILE=default
CFG_SPEEDTEST_HEALTHCHECK=false CFG_SPEEDTEST_HEALTHCHECK=false
CFG_SPEEDTEST_AUTHELIA=false CFG_SPEEDTEST_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=traefik
CFG_TRAEFIK_BACKUP=true CFG_TRAEFIK_BACKUP=true
CFG_TRAEFIK_BACKUP_STRATEGY=auto CFG_TRAEFIK_BACKUP_STRATEGY=auto
CFG_TRAEFIK_UPDATE_TYPE=auto
CFG_TRAEFIK_COMPOSE_FILE=default CFG_TRAEFIK_COMPOSE_FILE=default
CFG_TRAEFIK_HEALTHCHECK=true CFG_TRAEFIK_HEALTHCHECK=true
CFG_TRAEFIK_AUTHELIA=false CFG_TRAEFIK_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=trilium
CFG_TRILIUM_BACKUP=true CFG_TRILIUM_BACKUP=true
CFG_TRILIUM_BACKUP_STRATEGY=auto CFG_TRILIUM_BACKUP_STRATEGY=auto
CFG_TRILIUM_UPDATE_TYPE=auto
CFG_TRILIUM_COMPOSE_FILE=default CFG_TRILIUM_COMPOSE_FILE=default
CFG_TRILIUM_HEALTHCHECK=true CFG_TRILIUM_HEALTHCHECK=true
CFG_TRILIUM_AUTHELIA=false CFG_TRILIUM_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=trivy
CFG_TRIVY_BACKUP=false CFG_TRIVY_BACKUP=false
CFG_TRIVY_BACKUP_STRATEGY=auto CFG_TRIVY_BACKUP_STRATEGY=auto
CFG_TRIVY_UPDATE_TYPE=auto
CFG_TRIVY_COMPOSE_FILE=default CFG_TRIVY_COMPOSE_FILE=default
CFG_TRIVY_HEALTHCHECK=true CFG_TRIVY_HEALTHCHECK=true
CFG_TRIVY_AUTHELIA=false CFG_TRIVY_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=unbound
CFG_UNBOUND_BACKUP=true CFG_UNBOUND_BACKUP=true
CFG_UNBOUND_BACKUP_STRATEGY=auto CFG_UNBOUND_BACKUP_STRATEGY=auto
CFG_UNBOUND_UPDATE_TYPE=auto
CFG_UNBOUND_COMPOSE_FILE=default CFG_UNBOUND_COMPOSE_FILE=default
CFG_UNBOUND_HEALTHCHECK=true CFG_UNBOUND_HEALTHCHECK=true
CFG_UNBOUND_AUTHELIA=false CFG_UNBOUND_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_APP_NAME=vaultwarden
CFG_VAULTWARDEN_BACKUP=true CFG_VAULTWARDEN_BACKUP=true
CFG_VAULTWARDEN_BACKUP_STRATEGY=auto CFG_VAULTWARDEN_BACKUP_STRATEGY=auto
CFG_VAULTWARDEN_UPDATE_TYPE=auto
CFG_VAULTWARDEN_COMPOSE_FILE=default CFG_VAULTWARDEN_COMPOSE_FILE=default
CFG_VAULTWARDEN_HEALTHCHECK=false CFG_VAULTWARDEN_HEALTHCHECK=false
CFG_VAULTWARDEN_AUTHELIA=false CFG_VAULTWARDEN_AUTHELIA=false

View File

@ -5,6 +5,7 @@
# APP_NAME = name of application for use in scripts # 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 # 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 # 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 # HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off. # AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote # 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_SUBNET=
CFG_WIREGUARD_BACKUP=true CFG_WIREGUARD_BACKUP=true
CFG_WIREGUARD_BACKUP_STRATEGY=auto CFG_WIREGUARD_BACKUP_STRATEGY=auto
CFG_WIREGUARD_UPDATE_TYPE=auto
CFG_WIREGUARD_COMPOSE_FILE=default CFG_WIREGUARD_COMPOSE_FILE=default
CFG_WIREGUARD_HEALTHCHECK=true CFG_WIREGUARD_HEALTHCHECK=true
CFG_WIREGUARD_AUTHELIA=false CFG_WIREGUARD_AUTHELIA=false

View File

@ -1,6 +1,6 @@
# LibrePortal — Per-App Version Updater & CVE Scanning (Roadmap / Design) # 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 — P0P3 (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: > **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. > 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 | | `history.json` (fail-closed from→to audit trail) | ✅ real |
| Signed hotfix channel (fetch/verify/apply/undo, `CFG_HOTFIX_AUTO`) | ✅ real | | Signed hotfix channel (fetch/verify/apply/undo, `CFG_HOTFIX_AUTO`) | ✅ real |
| Auto-scan scheduling (`updater check auto`, 30-min throttle) | ✅ 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 | | 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) | | **"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) | | **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. - **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. - **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) ## 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.)* 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. 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. 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) ## 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. - **~~`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*). - **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. - **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. - **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. - **A new top-level "Versions" area** — unnecessary; the fleet Updates tab + per-app Updates tab already are that surface, they just need real data.

View File

@ -846,7 +846,10 @@ artifactApplyAuto() {
# gates re-checked at apply time) # gates re-checked at apply time)
app="$(jq -r '.applies_when.app // empty' <<<"$art")" app="$(jq -r '.applies_when.app // empty' <<<"$art")"
[[ -n "$app" && ! -d "${containers_dir%/}/$app" ]] && continue [[ -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)) enqueued=$((enqueued + 1))
done <<< "$ids" done <<< "$ids"

View 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
}

View File

@ -4,10 +4,13 @@
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Dispatched automatically by cli_initialize.sh (category -> cliHandleUpdaterCommands). # Dispatched automatically by cli_initialize.sh (category -> cliHandleUpdaterCommands).
# Subcommands (the features/updater WebUI buttons route to these as tasks): # 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 # apply <app> update one app — DISASTER-RECOVERY FIRST: snapshot the app
# via the backup engine, then pull + recreate; on failure, # 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) # 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 # 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 source "$install_scripts_dir/cli/commands/artifact/cli_artifact_apply.sh" 2>/dev/null
fi fi
declare -F artifactApplyAuto >/dev/null 2>&1 && artifactApplyAuto 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") "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 if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
updaterApplyApp "$app" updaterApplyApp "$app" "$trigger"
else 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 fi
;; ;;
@ -143,10 +159,18 @@ updaterLastUpdateFrom()
updaterApplyApp() updaterApplyApp()
{ {
local app="$1" 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" local app_dir="$containers_dir/$app"
if [[ ! -d "$app_dir" ]]; then isError "App '$app' is not installed."; return 1; fi if [[ ! -d "$app_dir" ]]; then isError "App '$app' is not installed."; return 1; fi
if [[ "$trigger" == "auto" ]]; then
isHeader "Automatically updating $app (a recovery snapshot is taken first)"
else
isHeader "Updating $app (a recovery snapshot is taken first)" isHeader "Updating $app (a recovery snapshot is taken first)"
fi
# 1. DISASTER RECOVERY — snapshot before touching anything. Call the backup # 1. DISASTER RECOVERY — snapshot before touching anything. Call the backup
# function directly (we already run under LIBREPORTAL_TASK_EXEC): the CLI form # function directly (we already run under LIBREPORTAL_TASK_EXEC): the CLI form
@ -157,7 +181,7 @@ updaterApplyApp()
isNotice "Snapshotting $app before update…" isNotice "Snapshotting $app before update…"
if ! backupAppStart "$app" >/dev/null 2>&1; then if ! backupAppStart "$app" >/dev/null 2>&1; then
isNotice "Pre-update snapshot did not complete cleanly — continuing is risky; aborting $app update." 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 return 1
fi fi
@ -176,7 +200,7 @@ updaterApplyApp()
local after_ref; after_ref="$(updaterPrimaryImage "$app" "$app_dir/docker-compose.yml")"; after_ref="${after_ref%%@*}" 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_dig; after_dig="$(updaterRefDigest "$after_ref")"
local after="$after_ref${after_dig:+@$after_dig}" 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." isSuccessful "$app updated. Rollback point retained."
webuiUpdaterScan >/dev/null 2>&1 || true webuiUpdaterScan >/dev/null 2>&1 || true
return 0 return 0
@ -185,7 +209,7 @@ updaterApplyApp()
# 4. Failure -> automatic rollback. # 4. Failure -> automatic rollback.
isNotice "Update of $app failed — rolling back to the pre-update snapshot…" isNotice "Update of $app failed — rolling back to the pre-update snapshot…"
updaterRollbackApp "$app" "auto" updaterRollbackApp "$app" "auto"
updaterRecordHistory "$app" "update" "$before" "" "rolled-back" updaterRecordHistory "$app" "update" "$before" "" "rolled-back" "" "" "" "$trigger"
return 1 return 1
} }
@ -250,11 +274,12 @@ updaterComposePull()
# actually recording, so it is FAIL-CLOSED, not best-effort: with jq we prepend + # 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 # 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. # (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() updaterRecordHistory()
{ {
local app="$1" action="$2" from="$3" to="$4" result="$5" 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 f="$containers_dir/libreportal/frontend/data/updater/generated/history.json"
local ts; ts="$(date -Iseconds 2>/dev/null || date)" local ts; ts="$(date -Iseconds 2>/dev/null || date)"
[ -f "$f" ] || printf '{ "entries": [] }\n' | runFileWrite "$f" [ -f "$f" ] || printf '{ "entries": [] }\n' | runFileWrite "$f"
@ -263,7 +288,8 @@ updaterRecordHistory()
local tmp; tmp="$(mktemp)" local tmp; tmp="$(mktemp)"
if jq --arg ts "$ts" --arg app "$app" --arg action "$action" --arg from "$from" --arg to "$to" \ 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" \ --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 "$f" > "$tmp" 2>/dev/null; then
runFileWrite "$f" < "$tmp"; rm -f "$tmp"; return 0 runFileWrite "$f" < "$tmp"; rm -f "$tmp"; return 0
fi fi
@ -274,7 +300,7 @@ updaterRecordHistory()
# jq absent or failed — bash-native, brace-agnostic prepend. History entries # jq absent or failed — bash-native, brace-agnostic prepend. History entries
# are flat (scalar fields only), so splicing on the outer [ ... ] is safe. # are flat (scalar fields only), so splicing on the outer [ ... ] is safe.
local entry 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 local cur inner
cur="$(cat "$f" 2>/dev/null)" cur="$(cat "$f" 2>/dev/null)"
inner="${cur#*[}"; inner="${inner%]*}" inner="${cur#*[}"; inner="${inner%]*}"

View File

@ -19,4 +19,10 @@ cliShowUpdaterHelp()
echo "touching the app, so any update is reversible. These commands back the" 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 "WebUI Updates page (features/updater); actions run through the task system."
echo "" 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 ""
} }

View File

@ -49,6 +49,7 @@ cli_scripts=(
"cli/commands/system/cli_system_header.sh" "cli/commands/system/cli_system_header.sh"
"cli/commands/update/cli_update_commands.sh" "cli/commands/update/cli_update_commands.sh"
"cli/commands/update/cli_update_header.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_commands.sh"
"cli/commands/updater/cli_updater_header.sh" "cli/commands/updater/cli_updater_header.sh"
"cli/commands/validation/cli_validation_commands.sh" "cli/commands/validation/cli_validation_commands.sh"

View File

@ -916,6 +916,12 @@ declare -gA LP_FN_MAP=(
[updaterAllServiceImages]="webui/data/generators/updater/webui_updater_scan.sh" [updaterAllServiceImages]="webui/data/generators/updater/webui_updater_scan.sh"
[updaterApplyAll]="cli/commands/updater/cli_updater_commands.sh" [updaterApplyAll]="cli/commands/updater/cli_updater_commands.sh"
[updaterApplyApp]="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" [updaterClassifyTag]="webui/data/generators/updater/webui_updater_scan.sh"
[_updaterCleanImageRef]="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" [updaterComposePull]="cli/commands/updater/cli_updater_commands.sh"
@ -1922,6 +1928,12 @@ declare -gA LP_FN_ROOT=(
[updaterAllServiceImages]="scripts" [updaterAllServiceImages]="scripts"
[updaterApplyAll]="scripts" [updaterApplyAll]="scripts"
[updaterApplyApp]="scripts" [updaterApplyApp]="scripts"
[updaterApplyAuto]="scripts"
[updaterAppPolicy]="scripts"
[_updaterAutoDir]="scripts"
[_updaterAutoGenDir]="scripts"
[_updaterAutoStamp]="scripts"
[updaterAutoTaskPending]="scripts"
[updaterClassifyTag]="scripts" [updaterClassifyTag]="scripts"
[_updaterCleanImageRef]="scripts" [_updaterCleanImageRef]="scripts"
[updaterComposePull]="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 "$@"; } 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 "$@"; } 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 "$@"; } 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 "$@"; } 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 "$@"; } _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 "$@"; } updaterComposePull() { unset -f updaterComposePull; __lpAutoload "${install_scripts_dir}cli/commands/updater/cli_updater_commands.sh"; updaterComposePull "$@"; }

View File

@ -173,6 +173,17 @@ PORTEOF
], ],
"default": "auto" "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": { "MONITORING": {
"category": "features", "category": "features",
"label": "Export metrics to Grafana", "label": "Export metrics to Grafana",

View File

@ -201,6 +201,19 @@ webuiUpdaterScan() {
svcs="$(jq -cs '.' "$sobjs" 2>/dev/null || echo '[]')"; rm -f "$sobjs" svcs="$(jq -cs '.' "$sobjs" 2>/dev/null || echo '[]')"; rm -f "$sobjs"
fi 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 if [ "$have_jq" = "1" ]; then
jq -cn \ jq -cn \
--arg name "$app" --arg displayName "$app" --arg type "$vtype" \ --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_image "$anchor" --arg available_version "$avail_ver" \
--arg available_digest "$avail_dig" --argjson update_available "$update_available" \ --arg available_digest "$avail_dig" --argjson update_available "$update_available" \
--arg last_checked "$now" --argjson services "$svcs" \ --arg last_checked "$now" --argjson services "$svcs" \
--arg update_type "$policy" \
'{name:$name,displayName:$displayName,type:$type,channel:$channel, '{name:$name,displayName:$displayName,type:$type,channel:$channel,
current_image:$current_image,current_version:$current_version,current_digest:$current_digest, current_image:$current_image,current_version:$current_version,current_digest:$current_digest,
available_image:$available_image, available_image:$available_image,
available_version:(if $available_version=="" then null else $available_version end), available_version:(if $available_version=="" then null else $available_version end),
available_digest:$available_digest,update_available:$update_available, available_digest:$available_digest,update_available:$update_available,
update_type:$update_type,
scanned:true,last_checked:$last_checked,services:$services}' \ scanned:true,last_checked:$last_checked,services:$services}' \
>> "$objs" 2>/dev/null >> "$objs" 2>/dev/null
else else
# jq-less fallback: minimal but valid object # 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')" \ "$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 fi
done done
[ "$do_registry" = "1" ] && touch "$reg_stamp" 2>/dev/null || true [ "$do_registry" = "1" ] && touch "$reg_stamp" 2>/dev/null || true