LibrePortal/scripts/catalog/catalog_sources.sh
librelad 3766f73654 feat(catalog/webui): pin official source #1 (removable via warning+tickbox) + box config sections
- Catalog block now shows the official catalog as a pinned row #1 (verified
  badge + its URL from the generated data). Removing it opens the shared
  confirmation dialog with a REQUIRED tickbox (generalized to accept custom
  checkbox text) — it only hides official apps from browsing; system updates are
  unaffected. Backed by a new CFG_CATALOG_OFFICIAL_ENABLED flag the scan honors.
- Trimmed the info-banner wording per feedback.
- Config option sections: the body (after the divider, before the buttons) now
  sits in a darker boxed panel (bg + border), matching the content-box look used
  elsewhere. Applies to regular grids + the block editors; scoped to the main
  config editor.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:29:46 +01:00

130 lines
6.3 KiB
Bash

#!/bin/bash
#
# Catalog sources — the ordered list of app-catalog URLs the App Center browses.
# ---------------------------------------------------------------------------
# SOURCE OF TRUTH = the CFG_CATALOG_1..9 config vars in configs/general/
# general_catalogs (edited from the WebUI Catalog Sources block, like domains).
# registry_catalog.json is a GENERATED artifact derived from these + the official
# source — rebuilt by webuiRegistryCatalogScan on config save + `updater check`.
#
# Source #1 is ALWAYS the official catalog, synthesized live from
# CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL (pinned, NOT in general_catalogs).
# Each extra slot CFG_CATALOG_<N> maps to source idx <N+1> (a stable id the Add
# dialog / `app add --source` can reference) and is UNVERIFIED third-party.
#
# Priority = official first, then slots 1..9 ascending; the highest-priority
# source offering an app is the default (Add dialog can pick another).
#
# SCOPE: browse + add only. System updates/hotfixes still resolve from the
# official CFG_RELEASE_BASE_URL alone (lpFetchIndex untouched) — a third-party
# catalog can never become a system-update channel.
CATALOG_MAX_SLOTS=9
# Extra (third-party) sources as a JSON array [{idx,name,base,channel}] in slot
# order, skipping empty slots. A slot value is "url" or "url|channel".
catalogExtraSourcesJson() {
command -v jq >/dev/null 2>&1 || { echo '[]'; return 0; }
local arr='[]' n var val url channel host
for (( n=1; n<=CATALOG_MAX_SLOTS; n++ )); do
var="CFG_CATALOG_${n}"; val="${!var:-}"
[[ -z "$val" ]] && continue
url="${val%%|*}"
channel="stable"; [[ "$val" == *"|"* ]] && channel="${val##*|}"
[[ "$url" =~ ^https?://[^[:space:]/]+ ]] || continue
url="${url%/}"
host="${url#*://}"; host="${host%%/*}"
arr="$(jq -c --argjson i "$(( n + 1 ))" --arg name "$host" --arg base "$url" --arg ch "$channel" \
'. + [ { idx:$i, name:$name, base:$base, channel:$ch } ]' <<<"$arr")"
done
printf '%s' "$arr"
}
# The full ordered source list as a JSON array. Each:
# {idx,name,base,channel,official,trust,enabled}. The official source can be
# switched off for browsing via CFG_CATALOG_OFFICIAL_ENABLED (default on) —
# extras are always enabled (an empty slot is simply absent).
catalogSourcesJson() {
local off_base off_channel extras off_enabled
off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}"
off_channel="$(lpReleaseChannel)"
off_enabled="true"; [[ "${CFG_CATALOG_OFFICIAL_ENABLED:-true}" == "false" ]] && off_enabled="false"
extras="$(catalogExtraSourcesJson)"
jq -cn --arg base "$off_base" --arg channel "$off_channel" --argjson oe "$off_enabled" --argjson extras "$extras" '
[ { idx: 1, name: "LibrePortal Official", base: $base, channel: $channel,
official: true, trust: "official", enabled: $oe } ]
+ ( $extras | map( . + { official: false, trust: "community", enabled: true } ) )'
}
# One ENABLED source per line (compact JSON), 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 "" else ", off" end))"'
}
# Fetch a THIRD-PARTY catalog index. Deliberately NOT verified against the
# official key and does NOT touch the anti-rollback serial — community sources
# are UNVERIFIED (their payloads are still sha256-pinned at add time by the pin
# carried in this 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: write the CFG_CATALOG_<N> vars via the config system ---------
# catalogSourceAdd <url> [channel] — into the first free slot.
catalogSourceAdd() {
local url="$1" channel="$2"
if [[ ! "$url" =~ ^https?://[^[:space:]/]+ ]]; then
isError "catalog: source URL must be http(s)://…"; return 1
fi
url="${url%/}"
local stored="$url"; [[ -n "$channel" && "$channel" != "stable" ]] && stored="$url|$channel"
if catalogSourcesJson | jq -e --arg b "$url" 'any(.[]; .base==$b)' >/dev/null 2>&1; then
isNotice "catalog: '$url' is already a source."; return 0
fi
local n var
for (( n=1; n<=CATALOG_MAX_SLOTS; n++ )); do
var="CFG_CATALOG_${n}"
if [[ -z "${!var:-}" ]]; then
updateConfigOption "$var" "$stored" >/dev/null || { isError "catalog: failed to write $var."; return 1; }
isSuccessful "catalog: added source '$url' (slot $n)."
catalogRefresh
return 0
fi
done
isError "catalog: all $CATALOG_MAX_SLOTS catalog slots are full — remove one first."; return 1
}
# catalogSourceRemove <idx> — idx 1 = official (fixed); idx N+1 = CFG_CATALOG_N.
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 slot=$(( idx - 1 )) var="CFG_CATALOG_$(( idx - 1 ))"
(( slot < 1 || slot > CATALOG_MAX_SLOTS )) && { isError "catalog: no source with idx $idx."; return 1; }
[[ -z "${!var:-}" ]] && { isError "catalog: no source with idx $idx."; return 1; }
updateConfigOption "$var" "" >/dev/null || { isError "catalog: failed to clear $var."; return 1; }
isSuccessful "catalog: removed source $idx."
catalogRefresh
}
# Re-run the App Center browse scan so a source change reflects immediately.
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
}