#!/bin/bash # LibrePortal WebUI Main Updater # Coordinates all webui data updates including system info and app configurations webuiLibrePortalUpdate() { # Debounce: during a fresh install this function gets called back-to-back # by installLibrePortal (line 113) and then startScan (end of preinstall), # both within ~10 seconds. Skip if the last successful run is within the # debounce window. Callers that genuinely need a forced refresh can set # WEBUI_UPDATER_FORCE=1. local stamp_file="/tmp/libreportal_webui_updater_last" local debounce_seconds="${WEBUI_UPDATER_DEBOUNCE:-30}" if [[ -z "$WEBUI_UPDATER_FORCE" && -f "$stamp_file" ]]; then local _now=$(date +%s) local _last=$(stat -c '%Y' "$stamp_file" 2>/dev/null) if [[ -n "$_last" ]] && (( _now - _last < debounce_seconds )); then isNotice "WebUI updater ran $((_now - _last))s ago — skipping (debounced, within ${debounce_seconds}s)." return 0 fi fi local status=$(dockerCheckAppInstalled "libreportal" "docker") # Main process: generate config if app is installed if [ "$status" == "installed" ]; then isHeader "LibrePortal WebUI Updater" # Check for update lock file first. webuiCheckUpdateLock echoes its # verdict on stdout (and auto-clears a stale lock); capture it directly. # A $(...) subshell means any global the function sets never reaches us # here, so the verdict MUST come back via the echo, not a shared var. local lock_file_found; lock_file_found=$(webuiCheckUpdateLock) checkSuccess "Checked for update lock file." if [[ "$lock_file_found" == "true" ]]; then echo "" isNotice "Update already in progress. Please wait for current update to complete." isNotice "Status: Locked" isNotice "WebUI update skipped due to concurrent update" echo "" else # Create update lock file local result; result=$(webuiCreateUpdateLock) checkSuccess "Created update lock file..." # Update system information first local result; result=$(webuiSystemUpdate) checkSuccess "Updated system information..." # Ensure task system files exist (failsafe) local result; result=$(webuiEnsureTaskFiles) checkSuccess "Ensured task system files exist..." # Generate system configuration local result; result=$(webuiGenerateSystemConfigs) checkSuccess "Generated system configurations..." # Generate categories local result; result=$(webuiCreateCategories $containers_dir/libreportal/frontend/data) checkSuccess "Generated app and config categories..." # Generate LibrePortal app configuration local result; result=$(webuiGenerateLibrePortalConfig) checkSuccess "Generated LibrePortal app configuration..." # Generate apps-services.json local result; result=$(webuiGenerateAppsServicesConfig) checkSuccess "Generated apps-services.json..." # Generate apps-tools.json (aggregate of per-app *.tools.json) local result; result=$(webuiGenerateAppsToolsConfig) checkSuccess "Generated apps-tools.json..." # Per-app routine refresh hooks. An installed app may define # appWebuiRefresh_ (in containers//scripts/) for data it # wants refreshed on every WebUI update — e.g. gluetun's provider # snapshot. Gated on the app being installed (its live compose # exists, tested directly so it works without list perm on the # container-user-owned data dir), so non-users never pay for it. local _app _dir _hook for _dir in "${install_containers_dir}"*/; do _app="$(basename "$_dir")" [[ -f "${containers_dir}${_app}/docker-compose.yml" ]] || continue _hook="appWebuiRefresh_${_app}" declare -F "$_hook" >/dev/null 2>&1 || continue # Announce before running: a hook may reach upstream (e.g. # gluetun's provider list), and its own output is captured # below, so without this the update looks frozen mid-fetch. isNotice "Refreshing ${_app} WebUI data..." local result; result=$($_hook) checkSuccess "Refreshed ${_app} WebUI data..." done # Backup dashboard data, split by cost: # * local-only (engines/schema/passwords) — cheap file + config # reads, no remote I/O; always run so engine/password/config # edits surface on the next pass. # * remote (locations stats, dashboard, snapshots, app-status, # migrate) — each spawns restic against every enabled location # (over SSH for remote repos). Backups and location changes # already regen this live at the moment they happen # (backup_app_start.sh / location_*.sh), so this routine pass is # only a drift catch-up. Throttle it: pull at most once per # CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL minutes unless a backup # marked the data dirty since, or the caller forced a full # refresh (WEBUI_UPDATER_FORCE). 0 disables the throttle. local result result=$(webuiGenerateBackupEngines && webuiGenerateBackupSchema && webuiGenerateBackupPasswords) checkSuccess "Refreshed backup engine/schema/password data..." local backup_refresh_min="${CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL:-30}" local backup_stamp="/tmp/libreportal_webui_backup_refreshed" # Touched by backup_app_start.sh when a backup completes; a value # newer than the stamp forces the next routine pass to pull. local backup_dirty="/tmp/libreportal_webui_backup_dirty" local do_remote=1 if [[ -z "$WEBUI_UPDATER_FORCE" && "$backup_refresh_min" != "0" && -f "$backup_stamp" ]]; then local _bnow _blast _bdirty _bnow=$(date +%s) _blast=$(stat -c '%Y' "$backup_stamp" 2>/dev/null || echo 0) _bdirty=$(stat -c '%Y' "$backup_dirty" 2>/dev/null || echo 0) if (( _bnow - _blast < backup_refresh_min * 60 )) && (( _bdirty <= _blast )); then do_remote=0 fi fi if (( do_remote )); then # Announce before running: the chain reaches remote backup # locations (restic over SSH) and its output is captured below, # so without this the update looks frozen mid-fetch on slow links. isNotice "Refreshing backup data (may be slow on poor connections)..." # One unfiltered restic snapshots pull per location, shared across # the generators below instead of one each — see engineSnapshotsJson. export LP_SNAP_CACHE_DIR; LP_SNAP_CACHE_DIR="$(mktemp -d 2>/dev/null)" result=$(webuiGenerateBackupLocations && webuiGenerateBackupDashboard && webuiGenerateBackupSnapshots all && webuiGenerateBackupAppStatus && webuiGenerateBackupMigrate) checkSuccess "Refreshed backup dashboard data..." [[ -n "$LP_SNAP_CACHE_DIR" ]] && rm -rf "$LP_SNAP_CACHE_DIR" unset LP_SNAP_CACHE_DIR touch "$backup_stamp" 2>/dev/null || true else isNotice "Backup data current — remote refresh throttled (last pull <${backup_refresh_min}m ago)." fi # Peers (named other LibrePortal instances) — small, cheap; lives # in its own data/peers/generated/peers.json file consumed by # /peers and overlay-read by the migrate tab. local result; result=$(webuiGeneratePeers) checkSuccess "Refreshed peers data..." # SSH access snapshot (authorized keys + password-login state) local result; result=$(webuiGenerateSshAccess) checkSuccess "Refreshed SSH access data..." # Sync app icons local result; result=$(webuiSyncAppIcons) checkSuccess "Synced app icons..." # Generate log files for installed apps local result; result=$(webuiGenerateAppLogs) checkSuccess "Generated log files for installed apps..." # Remove update lock file local result; result=$(webuiRemoveUpdateLock) checkSuccess "Removed update lock file..." # Remove setup lock file local result; result=$(webuiRemoveSetupLock) checkSuccess "Removed setup lock file..." isSuccessful "WebUI update completed successfully!" touch "$stamp_file" 2>/dev/null || true fi else isNotice "LibrePortal not installed - skipping WebUI update" fi }