- New runInstallOp helper for manager install-dir/template ops (rooted: sudo; rootless: run as the current manager user, which owns the tree). - adguard.sh, traefik.sh: container-config sed -> runFileOp. - crowdsec.sh: host crowdsec systemctl/apt-get -> runSystem. - dashy_update_conf.sh: conf-file mkdir/chown/md5sum/tee -> runFileOp/ runFileWrite; docker ps/restart -> dockerCommandRun. Deferred (cross-owner copy / temp-file across /tmp<->/docker, need rootless env to bridge correctly): owncloud_setup_config.sh, adguard_auth.sh. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
289 lines
12 KiB
Bash
Executable File
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="${containers_dir}libreportal/frontend/data/apps/generated/apps-services.json"
|
|
local icons_src_dir="${containers_dir}libreportal/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 ! dockerCommandRun "docker ps -a --format '{{.Names}}'" "sudo" 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..."
|
|
dockerCommandRun "docker restart dashy-service" "sudo" >/dev/null 2>&1 || dockerCommandRun "docker restart dashy" "sudo" >/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
|
|
}
|