#!/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//{*.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/ 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="${containers_dir}libreportal/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/ 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 }