LibrePortal/containers/dashy/scripts/dashy_update_conf.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

289 lines
12 KiB
Bash
Executable File

#!/bin/bash
# Build dashy's conf.yml from the same source of truth the WebUI uses
# for its URL buttons: apps-services.json. Each row in that file is a
# single (service, link) pair; the user picks which URLs to surface via
# CFG_DASHY_SHORTCUTS (CSV of "<service.name>:<link_index>" IDs, set by
# the Manage Shortcuts tool). Items are grouped by their app's category
# (read from the app's `# Category :` header).
appDashyUpdateConf()
{
local conf_file="${containers_dir}dashy/etc/conf.yml"
local services_json="$(webuiDir)/frontend/data/apps/generated/apps-services.json"
local icons_src_dir="$(webuiDir)/frontend/icons/apps"
# Don't check the apps DB here — appUpdateSpecifics calls us at
# install step 6, BEFORE databaseInstallApp (step 8) writes the
# row, so dockerCheckAppInstalled would say not_installed and bail.
# Look at the actual docker container instead — if the container
# exists or the install dir is present, generate the conf.
if ! runFileOp docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qE '^(dashy|dashy-service)$' \
&& [[ ! -d "${containers_dir}dashy" ]]; then
return 0
fi
isHeader "Dashy Config Generation"
# Always seed a minimal conf so dashy never starts against an
# empty user-data dir (which would 500 on / with "Configuration
# could not be loaded"). Both branches below — the no-services
# bootstrap and the full render — will overwrite this.
_dashyWriteSkeleton() {
local install_name="${CFG_INSTALL_NAME:-LibrePortal}"
runFileOp mkdir -p "$(dirname "$conf_file")"
runFileWrite "$conf_file" <<EOF
---
pageInfo:
title: Dashy - LibrePortal - ${install_name}
description: Welcome to your LibrePortal Dashy dashboard!
navLinks:
- title: Dashy GitHub
path: http://github.com/Lissy93/dashy
- title: Dashy Documentation
path: http://dashy.to/docs
appConfig:
theme: Nord-Frost
sections: []
EOF
runFileOp chown "$docker_install_user:$docker_install_user" "$conf_file" 2>/dev/null || true
}
if ! command -v jq >/dev/null 2>&1; then
isError "jq is required for dashy config generation but is not installed."
# Still seed a skeleton so dashy can boot.
[[ -f "$conf_file" ]] || _dashyWriteSkeleton
return 1
fi
if [[ ! -f "$services_json" ]]; then
isNotice "$services_json not found yet — emitting minimal dashy conf so the container can boot. Run the Manage Shortcuts tool to populate it."
_dashyWriteSkeleton
return 0
fi
local original_md5=""
[[ -f "$conf_file" ]] && original_md5=$(runFileOp md5sum "$conf_file" 2>/dev/null | awk '{print $1}')
runFileOp mkdir -p "$(dirname "$conf_file")"
# Build the selected-id set (empty CFG = include every URL).
local _selected_set=""
if [[ -n "${CFG_DASHY_SHORTCUTS:-}" ]]; then
_selected_set="|${CFG_DASHY_SHORTCUTS//,/|}|"
fi
_isSelected() {
[[ -z "$_selected_set" ]] && return 0
[[ "$_selected_set" == *"|$1|"* ]]
}
# Resolve an app's category from its install script header, with a
# cache so we don't re-read the same file once per URL.
declare -A _cat_cache=()
_appCategory() {
local app="$1"
if [[ -n "${_cat_cache[$app]+x}" ]]; then
printf '%s' "${_cat_cache[$app]}"
return
fi
local cat=""
local sh="${install_containers_dir}/${app}/${app}.sh"
if [[ -f "$sh" ]]; then
cat=$(awk -F ': ' '/# Category :/{print $2; exit}' "$sh" \
| tr -d '\r' | sed 's/[[:space:]]*$//')
fi
[[ -z "$cat" ]] && cat="Other"
_cat_cache[$app]="$cat"
printf '%s' "$cat"
}
# Resolve display title + description from the app's .config so the
# dashboard tile reads "Jellyfin / Media server" instead of the raw
# service slug.
declare -A _title_cache=() _desc_cache=()
_appMeta() {
local app="$1"
if [[ -z "${_title_cache[$app]+x}" ]]; then
local cfg="${install_containers_dir}/${app}/${app}.config"
local upper="${app^^}"
local t="" d=""
if [[ -f "$cfg" ]]; then
t=$(grep -E "^CFG_${upper}_TITLE=" "$cfg" | cut -d= -f2- | sed 's/^"//; s/"$//')
d=$(grep -E "^CFG_${upper}_DESCRIPTION=" "$cfg" | cut -d= -f2- | sed 's/^"//; s/"$//')
fi
[[ -z "$t" ]] && t="$app"
_title_cache[$app]="$t"
_desc_cache[$app]="$d"
fi
}
# Stream every (service, link) pair as TSV:
# id <tab> app <tab> label <tab> url <tab> traefik <tab> login
# Mirrors window.expandServiceLinks(): use links[] when present;
# otherwise fall back to the service-level externalURL (or build
# one from serverIP+externalPort).
local rows
rows=$(jq -r '
.services
| map(select(.buttonEnabled != false))
| map(
. as $s
| (
if (.links | type) == "array" and (.links | length) > 0
then .links | to_entries | map(
{
id: ($s.name + ":" + (.key|tostring)),
app: $s.app,
label: (.value.label // $s.buttonText // $s.name),
url: (.value.externalURL // .value.internalURL // ""),
traefik: ($s.traefikManaged == true),
login: ($s.loginRequired == true)
})
else [{
id: ($s.name + ":0"),
app: $s.app,
label: ($s.buttonText // $s.name),
url: ($s.externalURL // ("http://" + ($s.serverIP // "localhost") + ":" + ($s.externalPort|tostring))),
traefik: ($s.traefikManaged == true),
login: ($s.loginRequired == true)
}]
end
)
)
| flatten
| map(select(.url != "" and .url != null))
| .[]
| [.id, .app, .label, .url, (.traefik|tostring), (.login|tostring)] | @tsv
' "$services_json")
# Group selected rows by category in a stable order.
declare -A _cat_buckets=()
declare -a _cat_order=()
local _id _app _label _url _traefik _login
local IFS_BAK="$IFS"
while IFS=$'\t' read -r _id _app _label _url _traefik _login; do
[[ -z "$_id" ]] && continue
_isSelected "$_id" || continue
# Skip dashy URLs themselves to avoid a recursive shortcut.
[[ "$_app" == "dashy" ]] && continue
local cat
cat=$(_appCategory "$_app")
if [[ -z "${_cat_buckets[$cat]+x}" ]]; then
_cat_buckets[$cat]=""
_cat_order+=("$cat")
fi
# Prefix items under their parent app with an icon name dashy
# understands (`hl-<app>`). YAML-escape via printf %q-style: we
# only need to handle quotes in label/desc; URLs are URI-safe.
local _label_safe="${_label//\"/\\\"}"
_cat_buckets[$cat]+=$'\t'"${_app}"$'\x01'"${_label_safe}"$'\x01'"${_url}"$'\x01'"${_traefik}"$'\x01'"${_login}"$'\n'
done <<< "$rows"
IFS="$IFS_BAK"
# Header.
local install_name="${CFG_INSTALL_NAME:-LibrePortal}"
local theme="${CFG_DASHY_THEME:-Nord-Frost}"
local page_title="${CFG_DASHY_PAGE_TITLE:-}"
local page_desc="${CFG_DASHY_PAGE_DESCRIPTION:-}"
local layout="${CFG_DASHY_LAYOUT:-auto}"
local icon_size="${CFG_DASHY_ICON_SIZE:-medium}"
local open_target="${CFG_DASHY_OPEN_TARGET:-newtab}"
local status_check="${CFG_DASHY_STATUS_CHECK:-false}"
[[ -z "$page_title" ]] && page_title="Dashy - LibrePortal - ${install_name}"
[[ -z "$page_desc" ]] && page_desc="Welcome to your LibrePortal Dashy dashboard!"
runFileWrite "$conf_file" <<EOF
---
pageInfo:
title: ${page_title}
description: ${page_desc}
navLinks:
- title: Dashy GitHub
path: http://github.com/Lissy93/dashy
- title: Dashy Documentation
path: http://dashy.to/docs
appConfig:
theme: ${theme}
layout: ${layout}
iconSize: ${icon_size}
defaultOpeningMethod: ${open_target}
statusCheck: ${status_check}
sections:
EOF
# Reference icons via absolute URLs back to the LibrePortal WebUI's
# public icon server (the same URL that renders these icons inside
# the WebUI itself). This sidesteps two problems with the previous
# local-mirror approach:
# 1. Dashy's Vue icon resolver doesn't treat bare `/path.svg`
# as a fetchable image — it routes through its own icon
# pipeline which expects `item-icons/foo.svg` or `hl-foo`.
# 2. The `./etc:/app/user-data` mount only serves files via
# dashy's express middleware, not via its icon pipeline.
# Pulling ed_base from apps-services.json keeps it in sync with
# whatever IP/port LibrePortal is currently on.
local ed_base
ed_base=$(jq -r '.services[] | select(.app == "libreportal") | .externalURL' "$services_json" 2>/dev/null | head -1)
[[ -z "$ed_base" || "$ed_base" == "null" ]] && ed_base="http://localhost:3719"
local _total_items=0
local cat
for cat in "${_cat_order[@]}"; do
printf -- "- name: %s\n icon: fas fa-cube\n items:\n" "$cat" \
| runFileWrite -a "$conf_file"
local IFS_BAK="$IFS"
local entry
while IFS= read -r entry; do
entry="${entry#$'\t'}"
[[ -z "$entry" ]] && continue
local app label url traefik login
IFS=$'\x01' read -r app label url traefik login <<< "$entry"
[[ -z "$url" ]] && continue
_appMeta "$app"
local title="${_title_cache[$app]}"
local desc="${_desc_cache[$app]}"
# If the app exposes multiple URLs, prefix the per-URL
# label so users can tell them apart on the tile.
local tile_title="$title"
if [[ -n "$label" && "$label" != "$title" ]]; then
tile_title="${title}${label}"
fi
local tile_desc="$desc"
[[ -z "$tile_desc" ]] && tile_desc="$label"
# Pick an icon: prefer the synced local SVG (always works
# for our installed apps), fall back to dashy's homelab
# bundle for anything we couldn't sync.
# Absolute URL to the LibrePortal WebUI's icon endpoint.
# Falls back to dashy's bundled `hl-<app>` set if the SVG
# isn't present (covers third-party apps not shipped with
# an LibrePortal icon).
local icon_ref
if [[ -f "${icons_src_dir}/${app}.svg" ]]; then
icon_ref="${ed_base}/icons/apps/${app}.svg"
else
icon_ref="hl-${app}"
fi
printf -- " - title: %s\n description: %s\n icon: %s\n url: %s\n statusCheck: %s\n target: %s\n" \
"$tile_title" "$tile_desc" "$icon_ref" "$url" "$status_check" "$open_target" \
| runFileWrite -a "$conf_file"
_total_items=$((_total_items + 1))
done <<< "${_cat_buckets[$cat]}"
IFS="$IFS_BAK"
done
runFileOp chown "$docker_install_user:$docker_install_user" "$conf_file" 2>/dev/null || true
local updated_md5=$(runFileOp md5sum "$conf_file" 2>/dev/null | awk '{print $1}')
if [[ "$original_md5" != "$updated_md5" ]]; then
isNotice "Dashy config changed — restarting container..."
runFileOp docker restart dashy-service >/dev/null 2>&1 || runFileOp docker restart dashy >/dev/null 2>&1 || true
local _cat_label="categories"
[[ ${#_cat_order[@]} -eq 1 ]] && _cat_label="category"
isSuccessful "Restarted dashy (${#_cat_order[@]} ${_cat_label}, ${_total_items} URL(s))."
else
isSuccessful "No changes to dashy config (${_total_items} URL(s) selected)."
fi
}