Merge claude/2

This commit is contained in:
librelad 2026-07-06 19:39:21 +01:00
commit b83f8c589d
8 changed files with 390 additions and 43 deletions

View File

@ -0,0 +1,145 @@
#!/bin/bash
#
# Catalog sources — the ordered list of app-catalog URLs the App Center browses.
# ---------------------------------------------------------------------------
# Source #1 is ALWAYS the official LibrePortal catalog, synthesized live from
# CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL so it can't be edited or removed and
# always tracks the release host. Extra (third-party) sources are stored as a
# small JSON array in their OWN file under the live configs dir and are treated
# as UNVERIFIED (the operator added the URL themselves).
#
# Priority = list order: the official catalog is first, then extras in the order
# they were added. When the same app slug is offered by more than one source the
# highest-priority source that has it is the default (the Add dialog can override).
#
# IMPORTANT SCOPE: this list governs APP BROWSE + APP ADD only. LibrePortal's own
# updates and hotfixes still resolve from the official CFG_RELEASE_BASE_URL alone
# (lpFetchIndex is untouched) — a third-party catalog can never become a channel
# for system updates.
# Its own file, next to the other live config (manager-owned control plane).
catalogSourcesFile() { echo "${configs_dir%/}/catalog/sources.json"; }
# The extra (non-official) sources as a JSON array; [] when none/absent/invalid.
catalogExtraSourcesJson() {
local f; f="$(catalogSourcesFile)"
command -v jq >/dev/null 2>&1 || { echo '[]'; return 0; }
if [[ -s "$f" ]] && jq empty "$f" 2>/dev/null; then
jq -c 'if type=="array" then . else [] end' "$f" 2>/dev/null || echo '[]'
else
echo '[]'
fi
}
# The full ordered source list as a JSON array. Each entry:
# { idx, name, base, channel, enabled, official, trust }
# idx 1 = official (synthesized from CFG); 2..N = extras in file order.
catalogSourcesJson() {
local off_base off_channel extras
off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}"
off_channel="$(lpReleaseChannel)"
extras="$(catalogExtraSourcesJson)"
jq -cn --arg base "$off_base" --arg channel "$off_channel" --argjson extras "$extras" '
[ { idx: 1, name: "LibrePortal Official", base: $base, channel: $channel,
enabled: true, official: true, trust: "official" } ]
+ ( $extras | to_entries | map(
{ idx: (.key + 2),
name: (.value.name // .value.base),
base: (.value.base // ""),
channel: (.value.channel // "stable"),
enabled: (if (.value | has("enabled")) then (.value.enabled == true) else true end),
official: false,
trust: "community" } ) )'
}
# One ENABLED source per line (compact JSON), in priority order — for `while read`.
catalogEnabledSources() { catalogSourcesJson | jq -c '.[] | select(.enabled)'; }
# Human-readable listing for the CLI.
catalogSourceList() {
catalogSourcesJson | jq -r '.[] |
"[\(.idx)] \(.name) — \(.base)/\(.channel) " +
"(\(if .official then "official ✓" else "unverified" end), " +
"\(if .enabled then "on" else "off" end))"'
}
# Fetch a THIRD-PARTY catalog index. Deliberately does NOT verify against the
# official key and does NOT touch the shared anti-rollback serial — community
# sources are UNVERIFIED (their payloads are still sha256-pinned at add time by
# the pin carried in this very index). Echoes the index JSON; non-zero on failure.
catalogFetchCommunityIndex() {
local base="$1" channel="$2" tmp idx json
base="${base%/}"
[[ -n "$base" && -n "$channel" ]] || return 1
command -v jq >/dev/null 2>&1 || return 1
[[ -n "$(_lpFetchTool)" ]] || return 1
tmp="$(mktemp -d)"; idx="$tmp/index.json"
if ! _lpDownload "$base/$channel/index.json" "$idx" 2>/dev/null; then rm -rf "$tmp"; return 1; fi
json="$(cat "$idx")"; rm -rf "$tmp"
jq empty <<<"$json" 2>/dev/null || return 1
printf '%s' "$json"
}
# --- Mutations (manager-owned file; write via the manager funnel) -----------
# catalogSourceAdd <url> [name] [channel]
catalogSourceAdd() {
local url="$1" name="$2" channel="${3:-stable}"
if [[ ! "$url" =~ ^https?://[^[:space:]/]+ ]]; then
isError "catalog: source URL must be http(s)://…"; return 1
fi
url="${url%/}"
[[ -z "$name" ]] && name="$url"
# Refuse a duplicate base+channel (including the official one).
if catalogSourcesJson | jq -e --arg b "$url" --arg c "$channel" 'any(.[]; .base==$b and .channel==$c)' >/dev/null 2>&1; then
isNotice "catalog: '$url' ($channel) is already a source."; return 0
fi
local f dir extras new
f="$(catalogSourcesFile)"; dir="$(dirname "$f")"
runInstallOp mkdir -p "$dir" 2>/dev/null || true
extras="$(catalogExtraSourcesJson)"
new="$(jq -c --arg n "$name" --arg b "$url" --arg c "$channel" \
'. + [ { name: $n, base: $b, channel: $c, enabled: true } ]' <<<"$extras")" || {
isError "catalog: failed to build the new source list."; return 1; }
printf '%s\n' "$new" | runInstallWrite "$f"
isSuccessful "catalog: added source '$name' ($url/$channel)."
}
# catalogSourceRemove <idx> (idx 1 = official, cannot be removed)
catalogSourceRemove() {
local idx="$1"
[[ "$idx" =~ ^[0-9]+$ ]] || { isError "catalog: <idx> must be a number."; return 1; }
(( idx == 1 )) && { isError "catalog: source 1 is the official catalog and can't be removed."; return 1; }
local f extras count pos new
f="$(catalogSourcesFile)"; extras="$(catalogExtraSourcesJson)"
count="$(jq 'length' <<<"$extras")"
pos=$(( idx - 2 ))
if (( pos < 0 || pos >= count )); then isError "catalog: no source with idx $idx."; return 1; fi
new="$(jq -c --argjson i "$pos" 'del(.[$i])' <<<"$extras")"
printf '%s\n' "$new" | runInstallWrite "$f"
isSuccessful "catalog: removed source $idx."
}
# catalogSourceToggle <idx> <true|false>
catalogSourceToggle() {
local idx="$1" on="$2"
[[ "$idx" =~ ^[0-9]+$ ]] || { isError "catalog: <idx> must be a number."; return 1; }
(( idx == 1 )) && { isError "catalog: the official catalog is always enabled."; return 1; }
[[ "$on" == "true" || "$on" == "false" ]] || { isError "catalog: toggle value must be true|false."; return 1; }
local f extras count pos new
f="$(catalogSourcesFile)"; extras="$(catalogExtraSourcesJson)"
count="$(jq 'length' <<<"$extras")"
pos=$(( idx - 2 ))
if (( pos < 0 || pos >= count )); then isError "catalog: no source with idx $idx."; return 1; fi
new="$(jq -c --argjson i "$pos" --argjson v "$on" '.[$i].enabled=$v' <<<"$extras")"
printf '%s\n' "$new" | runInstallWrite "$f"
isSuccessful "catalog: source $idx $([[ "$on" == "true" ]] && echo enabled || echo disabled)."
}
# Re-run the App Center browse scan (so a source change reflects immediately
# instead of waiting for the next `updater check`).
catalogRefresh() {
declare -F webuiRegistryCatalogScan >/dev/null 2>&1 || \
source "${install_scripts_dir}webui/data/generators/apps/webui_registry_scan.sh" 2>/dev/null
declare -F webuiRegistryCatalogScan >/dev/null 2>&1 && webuiRegistryCatalogScan
}

View File

@ -0,0 +1,70 @@
#!/bin/bash
cliHandleCatalogCommands()
{
local sub="$initial_command2" # source | refresh | help
local action="$initial_command3" # list | add | remove | enable | disable
local a1="$initial_command4"
local a2="$initial_command5"
local a3="$initial_command6"
# Read-side + model helpers (lazy loader may not have sourced these yet).
declare -F lpReleaseBaseUrl >/dev/null 2>&1 || source "$install_scripts_dir/source/fetch.sh" 2>/dev/null
declare -F catalogSourcesJson >/dev/null 2>&1 || source "$install_scripts_dir/catalog/catalog_sources.sh" 2>/dev/null
case "$sub" in
source|sources)
case "$action" in
""|list)
catalogSourceList
;;
add)
[[ -z "$a1" ]] && { isNotice "Usage: catalog source add <url> [name] [channel]"; return; }
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
catalogSourceAdd "$a1" "$a2" "$a3" && catalogRefresh
else
cliTaskRun "libreportal catalog source add $a1 $a2 $a3" "catalog"
fi
;;
remove)
[[ -z "$a1" ]] && { isNotice "Usage: catalog source remove <idx>"; return; }
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
catalogSourceRemove "$a1" && catalogRefresh
else
cliTaskRun "libreportal catalog source remove $a1" "catalog"
fi
;;
enable)
[[ -z "$a1" ]] && { isNotice "Usage: catalog source enable <idx>"; return; }
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
catalogSourceToggle "$a1" true && catalogRefresh
else
cliTaskRun "libreportal catalog source enable $a1" "catalog"
fi
;;
disable)
[[ -z "$a1" ]] && { isNotice "Usage: catalog source disable <idx>"; return; }
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
catalogSourceToggle "$a1" false && catalogRefresh
else
cliTaskRun "libreportal catalog source disable $a1" "catalog"
fi
;;
*)
isNotice "Invalid catalog source action: $action"
cliShowCatalogHelp
;;
esac
;;
refresh)
catalogRefresh
;;
""|help)
cliShowCatalogHelp
;;
*)
isNotice "Unknown catalog subcommand: $sub"
cliShowCatalogHelp
;;
esac
}

View File

@ -0,0 +1,24 @@
#!/bin/bash
cliShowCatalogHelp()
{
echo ""
echo "LibrePortal App Catalog — manage the sources the App Center browses."
echo ""
echo "catalog source list"
echo " List catalog sources in priority order (source 1 = official)."
echo ""
echo "catalog source add <url> [name] [channel]"
echo " Add a third-party catalog. channel defaults to 'stable'. Third-party"
echo " sources are UNVERIFIED — you are trusting that host."
echo ""
echo "catalog source remove <idx>"
echo " Remove a third-party catalog by index (the official source is fixed)."
echo ""
echo "catalog source enable <idx> | disable <idx>"
echo " Toggle a third-party catalog on/off without removing it."
echo ""
echo "catalog refresh"
echo " Re-scan all enabled sources and rebuild the App Center browse data."
echo ""
}

View File

@ -0,0 +1,9 @@
#!/bin/bash
# This file is auto-generated by generate_arrays.sh
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
catalog_scripts=(
"catalog/catalog_sources.sh"
)

View File

@ -15,6 +15,8 @@ cli_scripts=(
"cli/commands/artifact/cli_artifact_header.sh" "cli/commands/artifact/cli_artifact_header.sh"
"cli/commands/backup/cli_backup_commands.sh" "cli/commands/backup/cli_backup_commands.sh"
"cli/commands/backup/cli_backup_header.sh" "cli/commands/backup/cli_backup_header.sh"
"cli/commands/catalog/cli_catalog_commands.sh"
"cli/commands/catalog/cli_catalog_header.sh"
"cli/commands/config/cli_config_commands.sh" "cli/commands/config/cli_config_commands.sh"
"cli/commands/config/cli_config_header.sh" "cli/commands/config/cli_config_header.sh"
"cli/commands/debug/cli_debug_commands.sh" "cli/commands/debug/cli_debug_commands.sh"

View File

@ -8,6 +8,7 @@ source_scripts=(
"source/fetch.sh" "source/fetch.sh"
"source/files/arrays/files_app.sh" "source/files/arrays/files_app.sh"
"source/files/arrays/files_backup.sh" "source/files/arrays/files_backup.sh"
"source/files/arrays/files_catalog.sh"
"source/files/arrays/files_checks.sh" "source/files/arrays/files_checks.sh"
"source/files/arrays/files_cli.sh" "source/files/arrays/files_cli.sh"
"source/files/arrays/files_config.sh" "source/files/arrays/files_config.sh"

View File

@ -238,6 +238,17 @@ declare -gA LP_FN_MAP=(
[borgSnapshotListFiles]="backup/engine/borg_snapshots.sh" [borgSnapshotListFiles]="backup/engine/borg_snapshots.sh"
[borgSnapshotsJson]="backup/engine/borg_snapshots.sh" [borgSnapshotsJson]="backup/engine/borg_snapshots.sh"
[borgSystemSnapshotsJson]="backup/engine/borg_snapshots.sh" [borgSystemSnapshotsJson]="backup/engine/borg_snapshots.sh"
[catalogEnabledSources]="catalog/catalog_sources.sh"
[catalogExtraSourcesJson]="catalog/catalog_sources.sh"
[catalogFetchCommunityIndex]="catalog/catalog_sources.sh"
[catalogRefresh]="catalog/catalog_sources.sh"
[_catalogRowsFrom]="webui/data/generators/apps/webui_registry_scan.sh"
[catalogSourceAdd]="catalog/catalog_sources.sh"
[catalogSourceList]="catalog/catalog_sources.sh"
[catalogSourceRemove]="catalog/catalog_sources.sh"
[catalogSourcesFile]="catalog/catalog_sources.sh"
[catalogSourcesJson]="catalog/catalog_sources.sh"
[catalogSourceToggle]="catalog/catalog_sources.sh"
[changeRootOwnedFile]="function/permission/ownership/root_file.sh" [changeRootOwnedFile]="function/permission/ownership/root_file.sh"
[changeUserGroupOnFolder]="function/permission/ownership/folder_group.sh" [changeUserGroupOnFolder]="function/permission/ownership/folder_group.sh"
[checkApplicationsConfigFilesMissingVariables]="config/application/application_missing_variables.sh" [checkApplicationsConfigFilesMissingVariables]="config/application/application_missing_variables.sh"
@ -279,6 +290,7 @@ declare -gA LP_FN_MAP=(
[cliHandleAppCommands]="cli/commands/app/cli_app_commands.sh" [cliHandleAppCommands]="cli/commands/app/cli_app_commands.sh"
[cliHandleArtifactCommands]="cli/commands/artifact/cli_artifact_commands.sh" [cliHandleArtifactCommands]="cli/commands/artifact/cli_artifact_commands.sh"
[cliHandleBackupCommands]="cli/commands/backup/cli_backup_commands.sh" [cliHandleBackupCommands]="cli/commands/backup/cli_backup_commands.sh"
[cliHandleCatalogCommands]="cli/commands/catalog/cli_catalog_commands.sh"
[cliHandleConfigCommands]="cli/commands/config/cli_config_commands.sh" [cliHandleConfigCommands]="cli/commands/config/cli_config_commands.sh"
[cliHandleDebugCommands]="cli/commands/debug/cli_debug_commands.sh" [cliHandleDebugCommands]="cli/commands/debug/cli_debug_commands.sh"
[cliHandleDockertypeCommands]="cli/commands/dockertype/cli_dockertype_commands.sh" [cliHandleDockertypeCommands]="cli/commands/dockertype/cli_dockertype_commands.sh"
@ -304,6 +316,7 @@ declare -gA LP_FN_MAP=(
[cliShowAppHelp]="cli/commands/app/cli_app_header.sh" [cliShowAppHelp]="cli/commands/app/cli_app_header.sh"
[cliShowArtifactHelp]="cli/commands/artifact/cli_artifact_header.sh" [cliShowArtifactHelp]="cli/commands/artifact/cli_artifact_header.sh"
[cliShowBackupHelp]="cli/commands/backup/cli_backup_header.sh" [cliShowBackupHelp]="cli/commands/backup/cli_backup_header.sh"
[cliShowCatalogHelp]="cli/commands/catalog/cli_catalog_header.sh"
[cliShowConfigHelp]="cli/commands/config/cli_config_header.sh" [cliShowConfigHelp]="cli/commands/config/cli_config_header.sh"
[cliShowDebugHelp]="cli/commands/debug/cli_debug_header.sh" [cliShowDebugHelp]="cli/commands/debug/cli_debug_header.sh"
[cliShowDockertypeHelp]="cli/commands/dockertype/cli_dockertype_header.sh" [cliShowDockertypeHelp]="cli/commands/dockertype/cli_dockertype_header.sh"
@ -1205,6 +1218,17 @@ declare -gA LP_FN_ROOT=(
[borgSnapshotListFiles]="scripts" [borgSnapshotListFiles]="scripts"
[borgSnapshotsJson]="scripts" [borgSnapshotsJson]="scripts"
[borgSystemSnapshotsJson]="scripts" [borgSystemSnapshotsJson]="scripts"
[catalogEnabledSources]="scripts"
[catalogExtraSourcesJson]="scripts"
[catalogFetchCommunityIndex]="scripts"
[catalogRefresh]="scripts"
[_catalogRowsFrom]="scripts"
[catalogSourceAdd]="scripts"
[catalogSourceList]="scripts"
[catalogSourceRemove]="scripts"
[catalogSourcesFile]="scripts"
[catalogSourcesJson]="scripts"
[catalogSourceToggle]="scripts"
[changeRootOwnedFile]="scripts" [changeRootOwnedFile]="scripts"
[changeUserGroupOnFolder]="scripts" [changeUserGroupOnFolder]="scripts"
[checkApplicationsConfigFilesMissingVariables]="scripts" [checkApplicationsConfigFilesMissingVariables]="scripts"
@ -1246,6 +1270,7 @@ declare -gA LP_FN_ROOT=(
[cliHandleAppCommands]="scripts" [cliHandleAppCommands]="scripts"
[cliHandleArtifactCommands]="scripts" [cliHandleArtifactCommands]="scripts"
[cliHandleBackupCommands]="scripts" [cliHandleBackupCommands]="scripts"
[cliHandleCatalogCommands]="scripts"
[cliHandleConfigCommands]="scripts" [cliHandleConfigCommands]="scripts"
[cliHandleDebugCommands]="scripts" [cliHandleDebugCommands]="scripts"
[cliHandleDockertypeCommands]="scripts" [cliHandleDockertypeCommands]="scripts"
@ -1271,6 +1296,7 @@ declare -gA LP_FN_ROOT=(
[cliShowAppHelp]="scripts" [cliShowAppHelp]="scripts"
[cliShowArtifactHelp]="scripts" [cliShowArtifactHelp]="scripts"
[cliShowBackupHelp]="scripts" [cliShowBackupHelp]="scripts"
[cliShowCatalogHelp]="scripts"
[cliShowConfigHelp]="scripts" [cliShowConfigHelp]="scripts"
[cliShowDebugHelp]="scripts" [cliShowDebugHelp]="scripts"
[cliShowDockertypeHelp]="scripts" [cliShowDockertypeHelp]="scripts"
@ -2193,6 +2219,17 @@ borgSnapshotLatestId() { source "${install_scripts_dir}backup/engine/borg_snapsh
borgSnapshotListFiles() { source "${install_scripts_dir}backup/engine/borg_snapshots.sh"; borgSnapshotListFiles "$@"; } borgSnapshotListFiles() { source "${install_scripts_dir}backup/engine/borg_snapshots.sh"; borgSnapshotListFiles "$@"; }
borgSnapshotsJson() { source "${install_scripts_dir}backup/engine/borg_snapshots.sh"; borgSnapshotsJson "$@"; } borgSnapshotsJson() { source "${install_scripts_dir}backup/engine/borg_snapshots.sh"; borgSnapshotsJson "$@"; }
borgSystemSnapshotsJson() { source "${install_scripts_dir}backup/engine/borg_snapshots.sh"; borgSystemSnapshotsJson "$@"; } borgSystemSnapshotsJson() { source "${install_scripts_dir}backup/engine/borg_snapshots.sh"; borgSystemSnapshotsJson "$@"; }
catalogEnabledSources() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogEnabledSources "$@"; }
catalogExtraSourcesJson() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogExtraSourcesJson "$@"; }
catalogFetchCommunityIndex() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogFetchCommunityIndex "$@"; }
catalogRefresh() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogRefresh "$@"; }
_catalogRowsFrom() { source "${install_scripts_dir}webui/data/generators/apps/webui_registry_scan.sh"; _catalogRowsFrom "$@"; }
catalogSourceAdd() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogSourceAdd "$@"; }
catalogSourceList() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogSourceList "$@"; }
catalogSourceRemove() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogSourceRemove "$@"; }
catalogSourcesFile() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogSourcesFile "$@"; }
catalogSourcesJson() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogSourcesJson "$@"; }
catalogSourceToggle() { source "${install_scripts_dir}catalog/catalog_sources.sh"; catalogSourceToggle "$@"; }
changeRootOwnedFile() { source "${install_scripts_dir}function/permission/ownership/root_file.sh"; changeRootOwnedFile "$@"; } changeRootOwnedFile() { source "${install_scripts_dir}function/permission/ownership/root_file.sh"; changeRootOwnedFile "$@"; }
changeUserGroupOnFolder() { source "${install_scripts_dir}function/permission/ownership/folder_group.sh"; changeUserGroupOnFolder "$@"; } changeUserGroupOnFolder() { source "${install_scripts_dir}function/permission/ownership/folder_group.sh"; changeUserGroupOnFolder "$@"; }
checkApplicationsConfigFilesMissingVariables() { source "${install_scripts_dir}config/application/application_missing_variables.sh"; checkApplicationsConfigFilesMissingVariables "$@"; } checkApplicationsConfigFilesMissingVariables() { source "${install_scripts_dir}config/application/application_missing_variables.sh"; checkApplicationsConfigFilesMissingVariables "$@"; }
@ -2234,6 +2271,7 @@ cliFirewallHeader() { source "${install_scripts_dir}cli/commands/firewall/cli_fi
cliHandleAppCommands() { source "${install_scripts_dir}cli/commands/app/cli_app_commands.sh"; cliHandleAppCommands "$@"; } cliHandleAppCommands() { source "${install_scripts_dir}cli/commands/app/cli_app_commands.sh"; cliHandleAppCommands "$@"; }
cliHandleArtifactCommands() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_commands.sh"; cliHandleArtifactCommands "$@"; } cliHandleArtifactCommands() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_commands.sh"; cliHandleArtifactCommands "$@"; }
cliHandleBackupCommands() { source "${install_scripts_dir}cli/commands/backup/cli_backup_commands.sh"; cliHandleBackupCommands "$@"; } cliHandleBackupCommands() { source "${install_scripts_dir}cli/commands/backup/cli_backup_commands.sh"; cliHandleBackupCommands "$@"; }
cliHandleCatalogCommands() { source "${install_scripts_dir}cli/commands/catalog/cli_catalog_commands.sh"; cliHandleCatalogCommands "$@"; }
cliHandleConfigCommands() { source "${install_scripts_dir}cli/commands/config/cli_config_commands.sh"; cliHandleConfigCommands "$@"; } cliHandleConfigCommands() { source "${install_scripts_dir}cli/commands/config/cli_config_commands.sh"; cliHandleConfigCommands "$@"; }
cliHandleDebugCommands() { source "${install_scripts_dir}cli/commands/debug/cli_debug_commands.sh"; cliHandleDebugCommands "$@"; } cliHandleDebugCommands() { source "${install_scripts_dir}cli/commands/debug/cli_debug_commands.sh"; cliHandleDebugCommands "$@"; }
cliHandleDockertypeCommands() { source "${install_scripts_dir}cli/commands/dockertype/cli_dockertype_commands.sh"; cliHandleDockertypeCommands "$@"; } cliHandleDockertypeCommands() { source "${install_scripts_dir}cli/commands/dockertype/cli_dockertype_commands.sh"; cliHandleDockertypeCommands "$@"; }
@ -2259,6 +2297,7 @@ cliRunVerify() { source "${install_scripts_dir}cli/commands/verify/cli_verify_co
cliShowAppHelp() { source "${install_scripts_dir}cli/commands/app/cli_app_header.sh"; cliShowAppHelp "$@"; } cliShowAppHelp() { source "${install_scripts_dir}cli/commands/app/cli_app_header.sh"; cliShowAppHelp "$@"; }
cliShowArtifactHelp() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_header.sh"; cliShowArtifactHelp "$@"; } cliShowArtifactHelp() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_header.sh"; cliShowArtifactHelp "$@"; }
cliShowBackupHelp() { source "${install_scripts_dir}cli/commands/backup/cli_backup_header.sh"; cliShowBackupHelp "$@"; } cliShowBackupHelp() { source "${install_scripts_dir}cli/commands/backup/cli_backup_header.sh"; cliShowBackupHelp "$@"; }
cliShowCatalogHelp() { source "${install_scripts_dir}cli/commands/catalog/cli_catalog_header.sh"; cliShowCatalogHelp "$@"; }
cliShowConfigHelp() { source "${install_scripts_dir}cli/commands/config/cli_config_header.sh"; cliShowConfigHelp "$@"; } cliShowConfigHelp() { source "${install_scripts_dir}cli/commands/config/cli_config_header.sh"; cliShowConfigHelp "$@"; }
cliShowDebugHelp() { source "${install_scripts_dir}cli/commands/debug/cli_debug_header.sh"; cliShowDebugHelp "$@"; } cliShowDebugHelp() { source "${install_scripts_dir}cli/commands/debug/cli_debug_header.sh"; cliShowDebugHelp "$@"; }
cliShowDockertypeHelp() { source "${install_scripts_dir}cli/commands/dockertype/cli_dockertype_header.sh"; cliShowDockertypeHelp "$@"; } cliShowDockertypeHelp() { source "${install_scripts_dir}cli/commands/dockertype/cli_dockertype_header.sh"; cliShowDockertypeHelp "$@"; }

View File

@ -1,20 +1,46 @@
#!/bin/bash #!/bin/bash
# WebUI registry-catalog data generator (the marketplace browse data) # WebUI registry-catalog data generator (the App Center browse data)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Writes the read-only JSON the App Center merges into its grid as # Writes the read-only JSON the App Center merges into its grid as
# "Available — Add" cards: # "Available — Add" cards:
# frontend/data/apps/generated/registry_catalog.json # frontend/data/apps/generated/registry_catalog.json
# #
# It fetches + verifies the signed artifact index (lpFetchIndexInto), selects # MULTI-SOURCE: it walks the ordered catalog source list (catalogEnabledSources
# the type:"app" / payload.kind:"bundle" envelopes, annotates each with # — official #1, then any operator-added third-party catalogs). The OFFICIAL
# `defined` (a definition exists in the install tree) and `installed` (a live # source is fetched + signature-verified exactly as before (lpFetchIndexInto);
# dir exists), and mirrors each catalog icon into # third-party sources are fetched UNVERIFIED (catalogFetchCommunityIndex) and
# frontend/core/icons/apps/registry/<slug>.<ext> # every app they offer is tagged trust="community"/verified=false so the UI can
# only when it matches the sha256 pin in the signed index — the browser is # show an "unverified source" caution. Apps are merged by slug: an app offered
# never pointed at a remote host (same-origin only). Written ATOMICALLY # by several catalogs carries a `sources[]` array in priority order (highest
# (temp -> validate -> one runFileWrite); on ANY fetch/verify failure the # first = the default the Add dialog pre-selects).
# prior file is KEPT. Run by `libreportal updater check`. #
# Icons are mirrored same-origin ONLY from the official index (sha256-pinned, so
# a tampered/oversized icon is skipped) — third-party catalogs get the default
# icon for now (never point the browser at an untrusted remote host).
#
# Written ATOMICALLY (temp -> validate -> one runFileWrite); on a FAILED OFFICIAL
# fetch the prior file is KEPT untouched. Run by `libreportal updater check` and
# on demand by `catalogRefresh` after a source is added/removed.
# Rows (one object per app-offering) from a single source's verified index JSON.
# Trust/verified come from the SOURCE, never the artifact's self-claimed .trust —
# a community index can't promote itself to "official".
_catalogRowsFrom() {
local index="$1" sidx="$2" sname="$3" base="$4" channel="$5" strust="$6" verified="$7" icons="$8"
printf '%s' "$index" | jq -c \
--argjson sidx "$sidx" --arg sname "$sname" --arg base "$base" --arg channel "$channel" \
--arg strust "$strust" --arg verified "$verified" --argjson icons "$icons" '
[ .artifacts[]? | select(.type=="app" and .payload.kind=="bundle")
| (.applies_when.app // "") as $slug | select($slug != "")
| { app: $slug, id, version: (.version // 1), title: (.title // $slug), why: (.why // ""),
publisher: (.publisher // ""), trust: $strust,
category: (.meta.category // ""), description: (.meta.description // .why // ""),
long_description: (.meta.long_description // ""),
icon: ($icons[$slug] // null),
src_idx: $sidx, src_name: $sname, base: $base, channel: $channel,
verified: ($verified == "true") } ]' 2>/dev/null
}
webuiRegistryCatalogScan() { webuiRegistryCatalogScan() {
local out_dir="${containers_dir%/}/libreportal/frontend/data/apps/generated" local out_dir="${containers_dir%/}/libreportal/frontend/data/apps/generated"
@ -24,25 +50,31 @@ webuiRegistryCatalogScan() {
local max_icon=262144 local max_icon=262144
runFileOp mkdir -p "$out_dir" 2>/dev/null || true runFileOp mkdir -p "$out_dir" 2>/dev/null || true
# Lazy-loader gap: ensure the read primitives are present. # Lazy-loader gap: ensure the read primitives + source list are present.
if ! declare -F lpFetchIndex >/dev/null 2>&1; then if ! declare -F lpFetchIndex >/dev/null 2>&1; then
source "$install_scripts_dir/source/fetch.sh" 2>/dev/null source "$install_scripts_dir/source/fetch.sh" 2>/dev/null
source "$install_scripts_dir/source/artifacts.sh" 2>/dev/null source "$install_scripts_dir/source/artifacts.sh" 2>/dev/null
fi fi
declare -F catalogEnabledSources >/dev/null 2>&1 || \
source "$install_scripts_dir/catalog/catalog_sources.sh" 2>/dev/null
if ! command -v jq >/dev/null 2>&1; then if ! command -v jq >/dev/null 2>&1; then
isNotice "webuiRegistryCatalogScan: jq not available — keeping the prior registry_catalog.json." isNotice "webuiRegistryCatalogScan: jq not available — keeping the prior registry_catalog.json."
return 0 return 0
fi fi
# --- OFFICIAL index first (verified; its failure keeps the prior file) ---
local index local index
if ! lpFetchIndexInto index; then if ! lpFetchIndexInto index; then
isNotice "webuiRegistryCatalogScan: no verified index available — keeping the prior file." isNotice "webuiRegistryCatalogScan: no verified official index — keeping the prior file."
return 0 return 0
fi fi
local signed="false"; [[ "$LP_INDEX_SIGSTATE" == "verified" ]] && signed="true" local verified_official="false"; [[ "$LP_INDEX_SIGSTATE" == "verified" ]] && verified_official="true"
local serial now local signed="$verified_official"
local serial now off_base off_channel
serial="$(_lpJsonNum "$index" index_serial)" serial="$(_lpJsonNum "$index" index_serial)"
now="$(date -Iseconds 2>/dev/null || date)" now="$(date -Iseconds 2>/dev/null || date)"
off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}"
off_channel="$(lpReleaseChannel)"
# Defined apps = definition dirs in the install tree; installed = live dirs. # Defined apps = definition dirs in the install tree; installed = live dirs.
local defined="[]" installed="[]" d local defined="[]" installed="[]" d
@ -53,11 +85,9 @@ webuiRegistryCatalogScan() {
installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)" installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)"
fi fi
# Mirror the pinned catalog icons locally (slug -> local web path). An icon # Mirror the official catalog's pinned icons locally (slug -> web path). An
# is used only if its bytes match the sha256 the signed index pins — a # icon is used only if its bytes match the sha256 the SIGNED index pins.
# tampered or oversized icon is simply skipped, never served. local icons_map="{}" rows
local icons_map="{}" base rows
base="$(lpReleaseBaseUrl)"
rows="$(printf '%s' "$index" | jq -r ' rows="$(printf '%s' "$index" | jq -r '
.artifacts[]? | select(.type=="app" and .payload.kind=="bundle") .artifacts[]? | select(.type=="app" and .payload.kind=="bundle")
| select((.meta.icon // "") != "" and (.meta.icon_sha256 // "") != "") | select((.meta.icon // "") != "" and (.meta.icon_sha256 // "") != "")
@ -74,7 +104,7 @@ webuiRegistryCatalogScan() {
icons_map="$(jq -c --arg s "$slug" --arg p "$icon_web/$slug.$ext" '.[$s]=$p' <<<"$icons_map")" icons_map="$(jq -c --arg s "$slug" --arg p "$icon_web/$slug.$ext" '.[$s]=$p' <<<"$icons_map")"
continue continue
fi fi
url="$icon"; case "$url" in http*://*) : ;; *) url="$base/$url" ;; esac url="$icon"; case "$url" in http*://*) : ;; *) url="$off_base/$url" ;; esac
tmpf="$(mktemp)" tmpf="$(mktemp)"
if _lpDownload "$url" "$tmpf" 2>/dev/null; then if _lpDownload "$url" "$tmpf" 2>/dev/null; then
got="$(_lpSha256 "$tmpf")" got="$(_lpSha256 "$tmpf")"
@ -89,30 +119,57 @@ webuiRegistryCatalogScan() {
done <<<"$rows" done <<<"$rows"
fi fi
# --- Walk every enabled source, collect rows + per-source meta -----------
local all_rows="[]" sources_meta="[]" src
local s_idx s_name s_base s_chan s_official s_index s_verified s_trust s_icons s_rows
while IFS= read -r src; do
[[ -z "$src" ]] && continue
s_idx="$(jq -r '.idx' <<<"$src")"
s_name="$(jq -r '.name' <<<"$src")"
s_base="$(jq -r '.base' <<<"$src")"; s_base="${s_base%/}"
s_chan="$(jq -r '.channel' <<<"$src")"
s_official="$(jq -r '.official' <<<"$src")"
if [[ "$s_official" == "true" ]]; then
s_index="$index"; s_verified="$verified_official"; s_trust="official"; s_icons="$icons_map"
else
if ! s_index="$(catalogFetchCommunityIndex "$s_base" "$s_chan")"; then
isNotice "webuiRegistryCatalogScan: source '$s_name' ($s_base/$s_chan) unreachable — skipping."
continue
fi
s_verified="false"; s_trust="community"; s_icons="{}"
fi
s_rows="$(_catalogRowsFrom "$s_index" "$s_idx" "$s_name" "$s_base" "$s_chan" "$s_trust" "$s_verified" "$s_icons")"
[[ -z "$s_rows" ]] && s_rows="[]"
all_rows="$(jq -cn --argjson a "$all_rows" --argjson b "$s_rows" '$a + $b' 2>/dev/null || printf '%s' "$all_rows")"
sources_meta="$(jq -cn --argjson a "$sources_meta" --argjson s "$src" --arg v "$s_verified" \
'$a + [ $s + { verified: ($v == "true") } ]' 2>/dev/null || printf '%s' "$sources_meta")"
done < <(catalogEnabledSources)
# --- Merge rows by slug: one card per app, sources[] in priority order ---
local apps
apps="$(jq -cn --argjson rows "$all_rows" --argjson defined "$defined" --argjson installed "$installed" '
$rows | group_by(.app) | map(
.[0] as $top
| { id: $top.id, app: $top.app, version: $top.version, title: $top.title, why: $top.why,
publisher: $top.publisher, trust: $top.trust,
category: $top.category, description: $top.description, long_description: $top.long_description,
icon: $top.icon,
defined: (($defined | index($top.app)) != null),
installed: (($installed | index($top.app)) != null),
sources: ( map({ src_idx, src_name, id, version, trust, verified, base, channel }) ) } )' 2>/dev/null)"
[[ -z "$apps" ]] && apps="[]"
local tmp; tmp="$(mktemp)" local tmp; tmp="$(mktemp)"
printf '%s' "$index" | jq \ jq -n \
--arg now "$now" --arg signed "$signed" --arg serial "${serial:-0}" \ --arg now "$now" --arg signed "$signed" --arg serial "${serial:-0}" \
--arg src_base "$base" --arg src_channel "$(lpReleaseChannel)" \ --arg ob "$off_base" --arg oc "$off_channel" \
--argjson defined "$defined" --argjson installed "$installed" --argjson icons "$icons_map" ' --argjson sources "$sources_meta" --argjson apps "$apps" '
{ generated_at: $now, { generated_at: $now,
signed: ($signed=="true"), signed: ($signed == "true"),
serial: ($serial|tonumber? // 0), serial: ($serial | tonumber? // 0),
source: { base: $src_base, channel: $src_channel }, source: { base: $ob, channel: $oc },
apps: [ .artifacts[]? | select(.type=="app" and .payload.kind=="bundle") sources: $sources,
| (.applies_when.app // "") as $slug | select($slug != "") apps: $apps }' > "$tmp" 2>/dev/null
| { id,
app: $slug,
version: (.version // 1),
title: (.title // $slug),
why: (.why // ""),
publisher: (.publisher // ""),
trust: (.trust // "official"),
category: (.meta.category // ""),
description: (.meta.description // .why // ""),
long_description: (.meta.long_description // ""),
icon: ($icons[$slug] // null),
defined: (($defined | index($slug)) != null),
installed: (($installed | index($slug)) != null) } ] }' > "$tmp" 2>/dev/null
if ! jq empty "$tmp" 2>/dev/null; then if ! jq empty "$tmp" 2>/dev/null; then
isNotice "webuiRegistryCatalogScan: generated JSON was invalid — keeping the prior file." isNotice "webuiRegistryCatalogScan: generated JSON was invalid — keeping the prior file."
@ -121,6 +178,6 @@ webuiRegistryCatalogScan() {
runFileWrite "$out" < "$tmp" runFileWrite "$out" < "$tmp"
rm -f "$tmp" rm -f "$tmp"
runFileOp chown "$docker_install_user":"$docker_install_user" "$out" 2>/dev/null || true runFileOp chown "$docker_install_user":"$docker_install_user" "$out" 2>/dev/null || true
local n; n="$(jq '.apps | length' "$out" 2>/dev/null || echo '?')" local n ns; n="$(jq '.apps | length' "$out" 2>/dev/null || echo '?')"; ns="$(jq '.sources | length' "$out" 2>/dev/null || echo '?')"
isSuccessful "Registry catalog refreshed ($n app(s), serial=${serial:-?}, signed=$signed)." isSuccessful "Registry catalog refreshed ($n app(s) across $ns source(s), serial=${serial:-?}, signed=$signed)."
} }