LibrePortal/scripts/webui/webui_regen.sh
librelad 2d24a764a8 refactor(storage): route elevation tests and the WebUI tree through paths.sh
Two mechanical sweeps, no behaviour change on a single-root install.

The 14 `[[ "$p" == "$containers_dir"* ]]` prefix tests that decide
manager-vs-container-user elevation become pathIsContainerData, so a file
on a second storage root is no longer misclassified as manager-owned —
which would have written it with the wrong owner and failed later, far
from the cause. The 65 references to the WebUI's own tree become
webuiDir(), which is pinned to the primary root by design.

Two traps found while doing it:

run_privileged.sh is sourced directly by init.sh without paths.sh, so it
needs a fallback. Defining one named pathIsContainerData was wrong:
generate_function_manifest.sh indexes top-level definitions, and the
resulting autoload stub would have shadowed the real multi-root
implementation with the primary-only fallback — silently classifying
every file on a second disk as manager-owned, which is exactly the bug
this sweep exists to prevent. Renamed to _runCfgIsContainerPath, which
delegates when the real one is loaded.

setup_lock.sh built its path in a top-level assignment, so it was
evaluated at source time and needed the file flagged eager. Made it a
function instead: the path resolves on call, and the file drops off
LP_EAGER_FILES entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 04:04:19 +01:00

131 lines
6.1 KiB
Bash

#!/bin/bash
# Unified regeneration front door.
#
# One entry point that rebuilds the file-derived artifacts whose SOURCES changed,
# so callers (the CLI, the task-processor poll, a deploy) don't have to know which
# generator owns what. Runs as the manager and writes only into the data dirs — no
# privilege needed.
#
# lpRegen [scope] [--force]
# scope: all (default) | webui | arrays
#
# Self-heal: a stage runs only when a source file is newer than its artifact (or
# the artifact is missing), unless --force. The check is a cheap `find -newer`
# mtime compare — there is no watcher/daemon. The natural triggers (install,
# config change, deploy, the periodic task-processor poll) call this; it no-ops
# when nothing is stale, so it is safe to call often.
#
# webui — apps.json / apps-tools.json etc. from containers/<app>/{*.config,tools/*.tools.json}
# arrays — the static files_*.sh source arrays from scripts/** (a dev/build concern;
# a normal install never adds core scripts, so this is a no-op there)
# Is $artifact stale relative to the given find expression? Returns 0 (stale) when
# the artifact is missing or any matched source file is newer than it.
_lpRegenStale() {
local artifact="$1"; shift
[[ -f "$artifact" ]] || return 0
find "$@" -newer "$artifact" -print -quit 2>/dev/null | grep -q .
}
# Does apps.json still list an app whose containers/<app> dir is gone?
#
# Deletions are invisible to the mtime check above: removing an app dir leaves
# no source file newer than the artifact, so a ghost entry (an uninstalled
# instance, a hand-deleted app folder) would sit in apps.json forever — still
# drawn as a pill in the app-detail Instances bar and still counted on the Apps
# grid. Compare the two sets instead of their timestamps. Returns 0 when a ghost
# is present. No jq → skip the check rather than guess.
_lpRegenOrphanedApp() {
local apps_json="$1" slug
[[ -f "$apps_json" ]] || return 1
command -v jq >/dev/null 2>&1 || return 1
while IFS= read -r slug; do
[[ -n "$slug" ]] || continue
[[ -d "${install_containers_dir%/}/$slug" ]] || return 0
done < <(jq -r '.apps[]? | (.command // "") | split(" ") | last' "$apps_json" 2>/dev/null)
return 1
}
lpRegenWebui() {
local force="$1"
local gen="$(webuiDir)/frontend/data/apps/generated"
local apps_json="$gen/apps.json"
local tools_json="$gen/apps-tools.json"
if [[ "$force" == "force" ]] \
|| _lpRegenStale "$apps_json" "$install_containers_dir" -maxdepth 2 -name '*.config' \
|| _lpRegenStale "$tools_json" "$install_containers_dir" -maxdepth 3 -path '*/tools/*.tools.json' \
|| _lpRegenOrphanedApp "$apps_json"; then
# Sources changed (e.g. an app folder was dropped in or removed) — do the
# full, debounced refresh so the app appears/disappears everywhere. Force
# past the updater's own debounce: there is established real work.
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
return $?
fi
return 0
}
lpRegenArrays() {
local force="$1"
local arrays_dir="${install_scripts_dir}source/files/arrays"
local gen_script="${install_scripts_dir}source/files/generate_arrays.sh"
local manifest_script="${install_scripts_dir}source/files/generate_function_manifest.sh"
local newest_array
newest_array="$(ls -t "$arrays_dir"/files_*.sh 2>/dev/null | head -1)"
# Staleness covers BOTH roots the manifest indexes. generate_function_manifest
# scans scripts/ AND containers/, so watching only scripts/ meant the one event
# that actually adds functions on a live box — a new containers/<app> dir, i.e.
# every instance clone — never registered as stale. Nothing but an explicit
# --force would rebuild, so an instance's hooks stayed invisible to lazy mode.
if [[ "$force" == "force" ]] || [[ -z "$newest_array" ]] \
|| find "$install_scripts_dir" -name '*.sh' -newer "$newest_array" -print -quit 2>/dev/null | grep -q . \
|| find "$install_containers_dir" -maxdepth 3 -name '*.sh' -newer "$newest_array" -print -quit 2>/dev/null | grep -q .; then
local rc=0
# Both generators run THROUGH bash rather than being executed, so a missing
# exec bit cannot silently disable regeneration. Not hypothetical:
# generate_function_manifest.sh shipped 0644, so every invocation died with
# rc=126 and the `|| true` below swallowed it. The manifest was therefore
# never rebuilt on any live system — it stayed byte-identical to the copy
# laid down at deploy — and lazy mode (the CLI default) could not see a
# single function belonging to an app created after that deploy.
if [[ -f "$gen_script" ]]; then
bash "$gen_script" run || rc=$?
else
rc=1
fi
# Function manifest tracks the same source set — keep them in sync.
# A failure still doesn't abort (eager mode is unaffected by a stale
# manifest) but it is no longer silent: lazy mode is what the CLI runs, so
# "the manifest is stale" is the difference between an app's hooks existing
# and not existing, which surfaces as absurd errors elsewhere ("app has no
# upgrade verifier" for an app whose verifier is sitting on disk).
if [[ -f "$manifest_script" ]] && ! bash "$manifest_script" run >/dev/null 2>&1; then
isNotice "Function manifest regeneration failed — lazy mode may not see recently added functions. Retry with: libreportal regen arrays --force"
fi
return $rc
fi
return 0
}
lpRegen() {
local scope="all" force=""
local a
for a in "$@"; do
case "$a" in
--force|-f|force) force="force" ;;
all|webui|arrays) scope="$a" ;;
*) isNotice "Unknown regen argument: $a (use: all|webui|arrays [--force])" ;;
esac
done
local rc=0
case "$scope" in
arrays) lpRegenArrays "$force" || rc=$? ;;
webui) lpRegenWebui "$force" || rc=$? ;;
all) lpRegenArrays "$force" || rc=$?; lpRegenWebui "$force" || rc=$? ;;
esac
return $rc
}