#!/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 ":" 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 ! sudo 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}" sudo mkdir -p "$(dirname "$conf_file")" sudo tee "$conf_file" >/dev/null </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=$(sudo md5sum "$conf_file" 2>/dev/null | awk '{print $1}') sudo 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 app label url traefik 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-`). 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!" sudo tee "$conf_file" >/dev/null </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" \ | sudo tee -a "$conf_file" >/dev/null 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-` 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" \ | sudo tee -a "$conf_file" >/dev/null _total_items=$((_total_items + 1)) done <<< "${_cat_buckets[$cat]}" IFS="$IFS_BAK" done sudo chown "$docker_install_user:$docker_install_user" "$conf_file" 2>/dev/null || true local updated_md5=$(sudo md5sum "$conf_file" 2>/dev/null | awk '{print $1}') if [[ "$original_md5" != "$updated_md5" ]]; then isNotice "Dashy config changed — restarting container..." sudo docker restart dashy-service >/dev/null 2>&1 || sudo 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 }