From f10f09eeb114ddc265c107e44fd9f7232f957e38 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Fri, 12 Jun 2026 21:41:11 -0400 Subject: [PATCH] Bug fixes, arr cleanup API-driven scan, shared JS formatters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes: - fallback.sh: escape sed metacharacters (\ & |) in state_set values - common.sh: parse PID from lock file content correctly (handles pid:metadata format) - unraid_api_key_renew.sh: fix path depth (../../../) and sync registry key to conf when stale - stop.php: only clear pid/status if process is actually dead — D-state survives SIGKILL - array_started.sh: check ARRAY_START_SCRIPTS empty before printing launch header Arr cleanup: - radarr_cleanup.sh / sonarr_cleanup.sh: fetch root folders from arr API instead of reverse-looking up the path map — handles multi-root-folder setups correctly UI: - varaverk.js: extract shared formatters (_relTime, _fmtBytes, _sz, _uptime, _gb, _tb, _n) - arrs.php / partnership.php: use shared formatters, remove duplicates - arrs.php / fallback.php: show error message on fetch failure instead of silent empty - docker.php: disable rename input during request, restore original value on failure - setup.php: abort controller timeout on detect fetch - partnership.php: remove Re-run Phase 2 button opacity dimming --- Fallback/fallback.sh | 4 +- Media/radarr_cleanup.sh | 37 +++++++++-------- Media/sonarr_cleanup.sh | 37 +++++++++-------- Orchestrators/array_started.sh | 7 ++++ .../System_Essentials/unraid_api_key_renew.sh | 19 +++++++-- Plugin/unraid/api/stop.php | 13 +++--- Plugin/unraid/js/varaverk.js | 41 +++++++++++++++++++ Plugin/unraid/pages/arrs.php | 33 ++++----------- Plugin/unraid/pages/docker.php | 6 ++- Plugin/unraid/pages/fallback.php | 5 ++- Plugin/unraid/pages/partnership.php | 20 +-------- Plugin/unraid/pages/setup.php | 4 +- common.sh | 5 ++- 13 files changed, 137 insertions(+), 94 deletions(-) diff --git a/Fallback/fallback.sh b/Fallback/fallback.sh index 982ce6d..671d672 100755 --- a/Fallback/fallback.sh +++ b/Fallback/fallback.sh @@ -321,7 +321,9 @@ state_get() { state_set() { local key="$1" value="$2" if grep -q "^${key}=" "$FALLBACK_STATE_FILE" 2>/dev/null; then - sed -i "s|^${key}=.*|${key}=${value}|" "$FALLBACK_STATE_FILE" + # Escape sed replacement metacharacters: \ first, then & and | + local safe="${value//\\/\\\\}"; safe="${safe//&/\\&}"; safe="${safe//|/\\|}" + sed -i "s|^${key}=.*|${key}=${safe}|" "$FALLBACK_STATE_FILE" else echo "${key}=${value}" >> "$FALLBACK_STATE_FILE" fi diff --git a/Media/radarr_cleanup.sh b/Media/radarr_cleanup.sh index 6e030c1..e3b8c12 100755 --- a/Media/radarr_cleanup.sh +++ b/Media/radarr_cleanup.sh @@ -305,24 +305,25 @@ format_bytes() { echo "" echo "━━━ $ICON_SYNC Pre-flight: Radarr Import Scan ━━━" -# Reverse-lookup container path from path map so Radarr gets its own path, not the host path -RADARR_CONTAINER_ROOT="" -for _cp in "${!ARR_PATH_MAP[@]}"; do - if [[ "${ARR_PATH_MAP[$_cp]}" == "$RADARR_MOVIES_ROOT" ]]; then - RADARR_CONTAINER_ROOT="$_cp" - break - fi -done -unset _cp +# Fetch root folders from Radarr API and translate container paths to host paths +mapfile -t SCAN_ROOTS < <( + radarr_api "rootfolder" | \ + jq -r '.[].path' 2>/dev/null | \ + while IFS= read -r cp; do translate_path "$cp"; done +) -if [[ -n "$RADARR_CONTAINER_ROOT" ]]; then - info "Triggering DownloadedMoviesScan on: $RADARR_CONTAINER_ROOT" - SCAN_PAYLOAD="{\"name\": \"DownloadedMoviesScan\", \"path\": \"$RADARR_CONTAINER_ROOT\"}" -else - info "No path map match — triggering DownloadedMoviesScan (all root folders)" - SCAN_PAYLOAD='{"name": "DownloadedMoviesScan"}' +if [[ "${#SCAN_ROOTS[@]}" -eq 0 ]]; then + error "No root folders returned from Radarr API — aborting" + notify "Radarr cleanup aborted on $(hostname) — no root folders from API" \ + "Radarr Cleanup" "warning" + exit 1 fi +info "Scan targets (${#SCAN_ROOTS[@]}): ${SCAN_ROOTS[*]}" + +info "Triggering DownloadedMoviesScan (all root folders)" +SCAN_PAYLOAD='{"name": "DownloadedMoviesScan"}' + SCAN_RESPONSE=$(curl -sf --max-time 30 -X POST \ -H "X-Api-Key: $RADARR_API_KEY" \ -H "Content-Type: application/json" \ @@ -487,7 +488,7 @@ while IFS= read -r filepath; do fi done < <( - for host_path in "${ARR_PATH_MAP[@]}" "$RADARR_MOVIES_ROOT"; do + for host_path in "${SCAN_ROOTS[@]}"; do [[ -d "$host_path" ]] && find "$host_path" -type f 2>/dev/null done | sort -u ) @@ -532,13 +533,13 @@ if [[ "$DRY_RUN" == false ]]; then rm -f "$filepath" 2>/dev/null || error "Failed to delete: $filepath" done < <( - for host_path in "${ARR_PATH_MAP[@]}" "$RADARR_MOVIES_ROOT"; do + for host_path in "${SCAN_ROOTS[@]}"; do [[ -d "$host_path" ]] && find "$host_path" -type f 2>/dev/null done | sort -u ) info "Cleaning up empty folders..." - for host_path in "${ARR_PATH_MAP[@]}" "$RADARR_MOVIES_ROOT"; do + for host_path in "${SCAN_ROOTS[@]}"; do [[ -d "$host_path" ]] && \ find "$host_path" -mindepth 1 -type d -empty -delete 2>/dev/null done diff --git a/Media/sonarr_cleanup.sh b/Media/sonarr_cleanup.sh index cc61da4..507a57c 100755 --- a/Media/sonarr_cleanup.sh +++ b/Media/sonarr_cleanup.sh @@ -305,24 +305,25 @@ format_bytes() { echo "" echo "━━━ $ICON_SYNC Pre-flight: Sonarr Import Scan ━━━" -# Reverse-lookup container path from path map so Sonarr gets its own path, not the host path -SONARR_CONTAINER_ROOT="" -for _cp in "${!ARR_PATH_MAP[@]}"; do - if [[ "${ARR_PATH_MAP[$_cp]}" == "$SONARR_TV_ROOT" ]]; then - SONARR_CONTAINER_ROOT="$_cp" - break - fi -done -unset _cp +# Fetch root folders from Sonarr API and translate container paths to host paths +mapfile -t SCAN_ROOTS < <( + sonarr_api "rootfolder" | \ + jq -r '.[].path' 2>/dev/null | \ + while IFS= read -r cp; do translate_path "$cp"; done +) -if [[ -n "$SONARR_CONTAINER_ROOT" ]]; then - info "Triggering DownloadedEpisodesScan on: $SONARR_CONTAINER_ROOT" - SCAN_PAYLOAD="{\"name\": \"DownloadedEpisodesScan\", \"path\": \"$SONARR_CONTAINER_ROOT\"}" -else - info "No path map match — triggering DownloadedEpisodesScan (all root folders)" - SCAN_PAYLOAD='{"name": "DownloadedEpisodesScan"}' +if [[ "${#SCAN_ROOTS[@]}" -eq 0 ]]; then + error "No root folders returned from Sonarr API — aborting" + notify "Sonarr cleanup aborted on $(hostname) — no root folders from API" \ + "Sonarr Cleanup" "warning" + exit 1 fi +info "Scan targets (${#SCAN_ROOTS[@]}): ${SCAN_ROOTS[*]}" + +info "Triggering DownloadedEpisodesScan (all root folders)" +SCAN_PAYLOAD='{"name": "DownloadedEpisodesScan"}' + SCAN_RESPONSE=$(curl -sf --max-time 30 -X POST \ -H "X-Api-Key: $SONARR_API_KEY" \ -H "Content-Type: application/json" \ @@ -486,7 +487,7 @@ while IFS= read -r filepath; do fi done < <( - for host_path in "${ARR_PATH_MAP[@]}" "$SONARR_TV_ROOT"; do + for host_path in "${SCAN_ROOTS[@]}"; do [[ -d "$host_path" ]] && find "$host_path" -type f 2>/dev/null done | sort -u ) @@ -531,13 +532,13 @@ if [[ "$DRY_RUN" == false ]]; then rm -f "$filepath" 2>/dev/null || error "Failed to delete: $filepath" done < <( - for host_path in "${ARR_PATH_MAP[@]}" "$SONARR_TV_ROOT"; do + for host_path in "${SCAN_ROOTS[@]}"; do [[ -d "$host_path" ]] && find "$host_path" -type f 2>/dev/null done | sort -u ) info "Cleaning up empty folders..." - for host_path in "${ARR_PATH_MAP[@]}" "$SONARR_TV_ROOT"; do + for host_path in "${SCAN_ROOTS[@]}"; do [[ -d "$host_path" ]] && \ find "$host_path" -mindepth 1 -type d -empty -delete 2>/dev/null done diff --git a/Orchestrators/array_started.sh b/Orchestrators/array_started.sh index 4e4a317..2616f72 100755 --- a/Orchestrators/array_started.sh +++ b/Orchestrators/array_started.sh @@ -123,6 +123,13 @@ fi # ============================================================================================== # ━━━ Launch Scripts ━━━ # ============================================================================================== +if [[ ${#ARRAY_START_SCRIPTS[@]} -eq 0 ]]; then + notify "Array started on $LOCAL_SERVER_NAME ($MY_ID) but ARRAY_START_SCRIPTS is empty — boot sequence skipped. Check master.conf." \ + "Array Start" "alert" + error "ARRAY_START_SCRIPTS is empty — check master.conf" + exit 1 +fi + echo "" echo "━━━ $ICON_GEAR Array Start — $MY_ID — $(date '+%Y-%m-%d %H:%M:%S') ━━━" log "Ecosystem root: $ECOSYSTEM_ROOT" diff --git a/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh b/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh index dc55c5a..f60fead 100755 --- a/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh +++ b/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh @@ -37,7 +37,7 @@ acquire_lock detect_hosts # ────────────────────────────────────────────────────────────────────────────── -CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf" +CONF_FILE="$SCRIPT_DIR/../../../Configurations/${MY_ID,,}.conf" VAR_NAME="${MY_ID}_UNRAID_API_KEY" # Key name: "Varaverk " stripping any unraid- prefix @@ -69,8 +69,21 @@ KEY=$(echo "$EXISTING" | jq -r '.key // empty' 2>/dev/null) if [[ -n "$KEY" ]]; then PREVIEW="${KEY:0:8}...${KEY: -4}" - echo "API key valid ✅ — $VAR_NAME = $PREVIEW" - log "Key found in registry — no renewal needed" + # Always sync registry key → conf, even if the key was already there. + # Conf gets wiped on git pull / conf regeneration without touching the registry. + CONF_HAS_KEY=$(grep -oP "(?<=^\s*${VAR_NAME}=\")[^\"]*" "$CONF_FILE" 2>/dev/null || true) + if [[ "$CONF_HAS_KEY" == "$KEY" ]]; then + echo "API key valid ✅ — $VAR_NAME = $PREVIEW" + log "Key in registry and conf — no action needed" + exit 0 + fi + log "Key in registry but conf is stale — syncing..." + if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then + sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE" + else + echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE" + fi + echo "API key synced to conf ✅ — $VAR_NAME = $PREVIEW" exit 0 fi diff --git a/Plugin/unraid/api/stop.php b/Plugin/unraid/api/stop.php index f8882ca..4a58bdc 100644 --- a/Plugin/unraid/api/stop.php +++ b/Plugin/unraid/api/stop.php @@ -76,16 +76,17 @@ if (file_exists($namedLock)) { if (!in_array(basename($namedLock), $cleared)) $cleared[] = basename($namedLock); } -// Update stat file +// Update stat file — only clear pid if actually dead (D-state processes survive SIGKILL) $now = time(); -$stat['status'] = 'stopped'; -$stat['end'] = $now; -$stat['exit'] = -1; -unset($stat['pid']); +$stat['status'] = $dead ? 'stopped' : 'running'; +$stat['end'] = $dead ? $now : ($stat['end'] ?? null); +$stat['exit'] = $dead ? -1 : ($stat['exit'] ?? null); +if ($dead) unset($stat['pid']); file_put_contents($statFile, json_encode($stat)); echo json_encode([ - 'ok' => true, + 'ok' => $dead, 'killed' => $dead, 'locks' => $cleared, + 'error' => $dead ? null : 'Process still alive after SIGKILL (D-state) — lock may persist', ]); diff --git a/Plugin/unraid/js/varaverk.js b/Plugin/unraid/js/varaverk.js index d5ec74c..9a609ca 100644 --- a/Plugin/unraid/js/varaverk.js +++ b/Plugin/unraid/js/varaverk.js @@ -1,6 +1,47 @@ // Varaverk — shared JS utilities // Page-specific JS lives inline in each page partial. +// ── Shared formatters ──────────────────────────────────────────────────────── +const _GB = 1073741824, _MB = 1048576, _TB = 1099511627776; + +// Relative timestamp → "just now" / "5m ago" / "yesterday" / "3d ago" +function _relTime(ts) { + if (!ts) return '—'; + const d = Math.floor(Date.now() / 1000) - ts; + if (d < 60) return 'just now'; + if (d < 3600) return Math.floor(d / 60) + 'm ago'; + if (d < 86400) return Math.floor(d / 3600) + 'h ago'; + if (d < 172800) return 'yesterday'; + return Math.floor(d / 86400) + 'd ago'; +} + +// Bytes → human-readable size (GB / MB / KB) +function _fmtBytes(b) { + if (!b) return '—'; + if (b >= _GB) return (b / _GB).toFixed(1) + ' GB'; + if (b >= _MB) return (b / _MB).toFixed(0) + ' MB'; + return (b / 1024).toFixed(0) + ' KB'; +} + +// Bytes → auto-scale TB / GB / MB +function _sz(b) { + if (!b) return '—'; + if (b >= _TB) return (b / _TB).toFixed(1) + ' TB'; + if (b >= _GB) return (b / _GB).toFixed(1) + ' GB'; + return (b / _MB).toFixed(0) + ' MB'; +} + +function _gb(b) { return !b ? '—' : (b / _GB).toFixed(1) + ' GB'; } +function _tb(b) { return !b ? '—' : (b / _TB).toFixed(2) + ' TB'; } +function _n(v) { return v == null ? '—' : Number(v).toLocaleString(); } + +// Seconds → uptime string: "2d 3h 5m" / "3h 5m" / "5m" +function _uptime(sec) { + if (!sec) return '—'; + const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60); + return d > 0 ? `${d}d ${h}h ${m}m` : h > 0 ? `${h}h ${m}m` : `${m}m`; +} + // Flash a status element briefly then fade function vvFlashStatus(el, msg, ok) { el.textContent = msg; diff --git a/Plugin/unraid/pages/arrs.php b/Plugin/unraid/pages/arrs.php index 047c483..9272796 100644 --- a/Plugin/unraid/pages/arrs.php +++ b/Plugin/unraid/pages/arrs.php @@ -96,26 +96,6 @@