195	    "$EMBY_CONTAINER" 2>/dev/null)
196
197	case "$STATUS" in
198	    true)
199	        EMBY_WAS_RUNNING=true
200	        warn "Stopping $EMBY_CONTAINER — active sessions will be interrupted"
201	        if [[ "$DRY_RUN" == false ]]; then
202	            if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
203	                log "$EMBY_CONTAINER stopped ✅"
204	                sleep 3  # let file handles release
205	            else
206	                error "Failed to stop $EMBY_CONTAINER — aborting"
207	                exit 1
208	            fi
209	        else
210	            warn "DRY RUN — would stop $EMBY_CONTAINER"
211	        fi
212	        ;;
213	    false)
214	        log "$EMBY_CONTAINER is not running — proceeding with checks"
215	        ;;
216	    "")
217	        error "$EMBY_CONTAINER not found — check container name"
218	        exit 1
219	        ;;
220	    *)
221	        warn "$EMBY_CONTAINER status: $STATUS — proceeding with caution"
222	        ;;
223	esac
224
225	# ==============================================================================================
226	# ━━━ Database Integrity Check ━━━
227	# ==============================================================================================
228	echo ""
229	echo "━━━ $ICON_HEALTH Database Integrity Check ━━━"
230
231	START=$(date +%s)
232
233	DB_FILES=(
234	    "data/library.db"
235	    "data/library.db-wal"
236	    "data/librarydb.db"
237	    "data/users.db"
238	    "data/authentication.db"
239	    "data/activity.db"
240	)
241
242	PASS_DBS=()
243	FAIL_DBS=()
244	MISSING_DBS=()
245
246	for db_rel in "${DB_FILES[@]}"; do
247	    db_path="${EMBY_CONFIG_HOST}/${db_rel}"
248	    db_name=$(basename "$db_rel")
249
250	    if [[ ! -f "$db_path" ]]; then
251	        log "$db_name — not found, skipping"
252	        MISSING_DBS+=("$db_name")
253	        continue
254	    fi
255
256	    DB_SIZE=$(du -sh "$db_path" 2>/dev/null | cut -f1)
257	    log "Checking $db_name ($DB_SIZE)..."
258
259	    # WAL file — different check (not a full SQLite database)
260	    if [[ "$db_name" == *.wal ]]; then
261	        if [[ -s "$db_path" ]]; then
262	            warn "$db_name exists and is non-empty (${DB_SIZE})"
263	            warn "Uncommitted WAL data — will be merged when Emby next starts cleanly"
264	            PASS_DBS+=("$db_name (WAL — see warning)")
265	        else
266	            log "$db_name exists but is empty — no pending transactions ✅"
267	            PASS_DBS+=("$db_name")
268	        fi
269	        continue
270	    fi
271
272	    if [[ "$DRY_RUN" == true ]]; then
273	        warn "DRY RUN — would check: $db_path"
274	        continue
275	    fi
276
277	    # Full integrity check
278	    RESULT=$(sqlite3 "$db_path" "PRAGMA integrity_check;" 2>/dev/null)
279	    EXIT_CODE=$?
280
281	    if [[ "$EXIT_CODE" -ne 0 ]]; then
282	        error "$db_name — sqlite3 could not open database (locked or corrupt)"
283	        FAIL_DBS+=("$db_name")
284	    elif [[ "$RESULT" == "ok" ]]; then
285	        log "$db_name — integrity check passed ✅"
286	        PASS_DBS+=("$db_name")
287	    else
288	        error "$db_name — CORRUPTION DETECTED"
289	        echo "$RESULT" | head -10 | while IFS= read -r line; do
290	            error "  $line"
291	        done
292	        FAIL_DBS+=("$db_name")
293	    fi
294	done
295
296	END=$(date +%s)
297
298	# ==============================================================================================
299	# ━━━ Restart Emby ━━━
300	# ==============================================================================================
301	echo ""
302	echo "━━━ $ICON_START Restart Emby ━━━"
303
304	RESTART_OK=false
305
306	if [[ "$EMBY_WAS_RUNNING" == true ]]; then
307	    if [[ "$DRY_RUN" == false ]]; then
308	        log "Restarting $EMBY_CONTAINER..."
309	        if timeout "$DOCKER_TIMEOUT" docker start "$EMBY_CONTAINER" >/dev/null 2>&1; then
310	            sleep 5  # Emby takes longer to initialise than most containers
311	            POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \
312	                '{{.State.Running}}' "$EMBY_CONTAINER" 2>/dev/null)
313	            if [[ "$POST_STATUS" == "true" ]]; then
314	                log "$EMBY_CONTAINER restarted and running ✅"
315	                RESTART_OK=true
316	            else
317	                error "$EMBY_CONTAINER started but crashed — database may be corrupt"
318	                error "Check Docker logs: docker logs $EMBY_CONTAINER"
319	                notify "$EMBY_CONTAINER crashed on restart — possible database corruption on $(hostname)" \
320	                    "Emby DB Repair" "warning"
321	            fi
322	        else
323	            error "Failed to restart $EMBY_CONTAINER — start it manually"
324	            notify "$EMBY_CONTAINER failed to restart after integrity check on $(hostname)" \
325	                "Emby DB Repair" "warning"
326	        fi
327	    else
328	        warn "DRY RUN — would restart $EMBY_CONTAINER"
329	        RESTART_OK=true
330	    fi
331	else
332	    log "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"
333	    RESTART_OK=true
334	fi
335
336	# Clear EXIT trap — clean exit
337	trap - EXIT
338
339	# ==============================================================================================
340	# ━━━ Summary ━━━
341	# ==============================================================================================
342	echo ""
343	echo "━━━━━ $ICON_SUMMARY EMBY DATABASE REPAIR SUMMARY ━━━━━"
344	echo "$ICON_HOST    Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
345	echo "$ICON_EMBY    Container:  $EMBY_CONTAINER"
346	echo "$ICON_EMBY    Config:     $EMBY_CONFIG_HOST"
347	echo "$ICON_TIME    Duration:   $(format_duration $(( END - START )))"
348	echo ""
349	echo "  $ICON_SUCCESS Passed:  ${#PASS_DBS[@]}"
350	[[ ${#FAIL_DBS[@]} -gt 0 ]]    && echo "  $ICON_ERROR  Failed:  ${#FAIL_DBS[@]}"
351	[[ ${#MISSING_DBS[@]} -gt 0 ]] && echo "  Skipped: ${#MISSING_DBS[@]} (not found)"
352	echo ""
353
354	[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do log "  $ICON_SUCCESS $db"; done
355	[[ ${#FAIL_DBS[@]} -gt 0 ]] && for db in "${FAIL_DBS[@]}"; do echo "  $ICON_ERROR  $db"; done
356
357	echo ""
358
359	if [[ "$DRY_RUN" == true ]]; then

    "$EMBY_CONTAINER" 2>/dev/null)

case "$STATUS" in
    true)
        EMBY_WAS_RUNNING=true
        warn "Stopping $EMBY_CONTAINER — active sessions will be interrupted"
        if [[ "$DRY_RUN" == false ]]; then
            if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
                log "$EMBY_CONTAINER stopped ✅"
                sleep 3  # let file handles release
            else
                error "Failed to stop $EMBY_CONTAINER — aborting"
                exit 1
            fi
        else
            warn "DRY RUN — would stop $EMBY_CONTAINER"
        fi
        ;;
    false)
        log "$EMBY_CONTAINER is not running — proceeding with checks"
        ;;
    "")
        error "$EMBY_CONTAINER not found — check container name"
        exit 1
        ;;
    *)
        warn "$EMBY_CONTAINER status: $STATUS — proceeding with caution"
        ;;
esac

# ==============================================================================================
# ━━━ Database Integrity Check ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_HEALTH Database Integrity Check ━━━"

START=$(date +%s)

DB_FILES=(
    "data/library.db"
    "data/library.db-wal"
    "data/librarydb.db"
    "data/users.db"
    "data/authentication.db"
    "data/activity.db"
)

PASS_DBS=()
FAIL_DBS=()
MISSING_DBS=()

for db_rel in "${DB_FILES[@]}"; do
    db_path="${EMBY_CONFIG_HOST}/${db_rel}"
    db_name=$(basename "$db_rel")

    if [[ ! -f "$db_path" ]]; then
        log "$db_name — not found, skipping"
        MISSING_DBS+=("$db_name")
        continue
    fi

    DB_SIZE=$(du -sh "$db_path" 2>/dev/null | cut -f1)
    log "Checking $db_name ($DB_SIZE)..."

    # WAL file — different check (not a full SQLite database)
    if [[ "$db_name" == *.wal ]]; then
        if [[ -s "$db_path" ]]; then
            warn "$db_name exists and is non-empty (${DB_SIZE})"
            warn "Uncommitted WAL data — will be merged when Emby next starts cleanly"
            PASS_DBS+=("$db_name (WAL — see warning)")
        else
            log "$db_name exists but is empty — no pending transactions ✅"
            PASS_DBS+=("$db_name")
        fi
        continue
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would check: $db_path"
        continue
    fi

    # Full integrity check
    RESULT=$(sqlite3 "$db_path" "PRAGMA integrity_check;" 2>/dev/null)
    EXIT_CODE=$?

    if [[ "$EXIT_CODE" -ne 0 ]]; then
        error "$db_name — sqlite3 could not open database (locked or corrupt)"
        FAIL_DBS+=("$db_name")
    elif [[ "$RESULT" == "ok" ]]; then
        log "$db_name — integrity check passed ✅"
        PASS_DBS+=("$db_name")
    else
        error "$db_name — CORRUPTION DETECTED"
        echo "$RESULT" | head -10 | while IFS= read -r line; do
            error "  $line"
        done
        FAIL_DBS+=("$db_name")
    fi
done

END=$(date +%s)

# ==============================================================================================
# ━━━ Restart Emby ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_START Restart Emby ━━━"

RESTART_OK=false

if [[ "$EMBY_WAS_RUNNING" == true ]]; then
    if [[ "$DRY_RUN" == false ]]; then
        log "Restarting $EMBY_CONTAINER..."
        if timeout "$DOCKER_TIMEOUT" docker start "$EMBY_CONTAINER" >/dev/null 2>&1; then
            sleep 5  # Emby takes longer to initialise than most containers
            POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \
                '{{.State.Running}}' "$EMBY_CONTAINER" 2>/dev/null)
            if [[ "$POST_STATUS" == "true" ]]; then
                log "$EMBY_CONTAINER restarted and running ✅"
                RESTART_OK=true
            else
                error "$EMBY_CONTAINER started but crashed — database may be corrupt"
                error "Check Docker logs: docker logs $EMBY_CONTAINER"
                notify "$EMBY_CONTAINER crashed on restart — possible database corruption on $(hostname)" \
                    "Emby DB Repair" "warning"
            fi
        else
            error "Failed to restart $EMBY_CONTAINER — start it manually"
            notify "$EMBY_CONTAINER failed to restart after integrity check on $(hostname)" \
                "Emby DB Repair" "warning"
        fi
    else
        warn "DRY RUN — would restart $EMBY_CONTAINER"
        RESTART_OK=true
    fi
else
    log "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"
    RESTART_OK=true
fi

# Clear EXIT trap — clean exit
trap - EXIT

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY EMBY DATABASE REPAIR SUMMARY ━━━━━"
echo "$ICON_HOST    Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_EMBY    Container:  $EMBY_CONTAINER"
echo "$ICON_EMBY    Config:     $EMBY_CONFIG_HOST"
echo "$ICON_TIME    Duration:   $(format_duration $(( END - START )))"
echo ""
echo "  $ICON_SUCCESS Passed:  ${#PASS_DBS[@]}"
[[ ${#FAIL_DBS[@]} -gt 0 ]]    && echo "  $ICON_ERROR  Failed:  ${#FAIL_DBS[@]}"
[[ ${#MISSING_DBS[@]} -gt 0 ]] && echo "  Skipped: ${#MISSING_DBS[@]} (not found)"
echo ""

[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do log "  $ICON_SUCCESS $db"; done
[[ ${#FAIL_DBS[@]} -gt 0 ]] && for db in "${FAIL_DBS[@]}"; do echo "  $ICON_ERROR  $db"; done

echo ""

if [[ "$DRY_RUN" == true ]]; then


180
181	    # Apply file permissions — no execute bit on media files
182	    log "Applying file permissions: ${PERMISSIONS_FILE_MODE:-664}..."
183	    find "$share_path" -type f \
184	        -exec chmod "${PERMISSIONS_FILE_MODE:-664}" {} + 2>/dev/null || CHMOD_FILE_OK=false
185
186	    if [[ "$CHOWN_OK" == true && "$CHMOD_DIR_OK" == true && "$CHMOD_FILE_OK" == true ]]; then
187	        log "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"
188	        PASS+=("$(basename "$share_path")")
189	    else
190	        error "$(basename "$share_path") — repair failed"
191	        error "  chown: $CHOWN_OK  chmod dirs: $CHMOD_DIR_OK  chmod files: $CHMOD_FILE_OK"
192	        FAIL+=("$(basename "$share_path")")
193	    fi
194	done
195
196	END=$(date +%s)
197
198	# ==============================================================================================
199	# ━━━ Summary ━━━

    # Apply file permissions — no execute bit on media files
    log "Applying file permissions: ${PERMISSIONS_FILE_MODE:-664}..."
    find "$share_path" -type f \
        -exec chmod "${PERMISSIONS_FILE_MODE:-664}" {} + 2>/dev/null || CHMOD_FILE_OK=false

    if [[ "$CHOWN_OK" == true && "$CHMOD_DIR_OK" == true && "$CHMOD_FILE_OK" == true ]]; then
        log "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"
        PASS+=("$(basename "$share_path")")
    else
        error "$(basename "$share_path") — repair failed"
        error "  chown: $CHOWN_OK  chmod dirs: $CHMOD_DIR_OK  chmod files: $CHMOD_FILE_OK"
        FAIL+=("$(basename "$share_path")")
    fi
done

END=$(date +%s)

# ==============================================================================================
# ━━━ Summary ━━━


105	TOTAL_RECLAIMED=0
106
107	# ── Step 1 (--all only): remove stopped containers ────────────────────────────
108	if [[ "$ALL_MODE" == true ]]; then
109	    STOPPED_IDS=$(docker ps -a --filter "status=exited" --filter "status=created" -q 2>/dev/null)
110	    STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)
111
112	    if [[ "$STOPPED_COUNT" -eq 0 ]]; then
113	        log "No stopped containers"
114	    elif [[ "$DRY_RUN" == true ]]; then
115	        warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
116	        docker ps -a --filter "status=exited" --filter "status=created" \
117	            --format "  {{.Names}}   {{.Image}}   {{.Status}}" 2>/dev/null
118	    else
119	        log "Removing $STOPPED_COUNT stopped container(s)..."
120	        docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
121	        success "Removed $STOPPED_COUNT stopped container(s)"
122	    fi
123	    echo ""
124	fi
125
126	# ── Step 2: prune images ───────────────────────────────────────────────────────
127	PRUNE_FLAGS=$([[ "$ALL_MODE" == true ]] && echo "-a" || echo "")
128	PRUNE_FILTER=$([[ "$ALL_MODE" == true ]] && echo "" || echo '-f "dangling=true"')
129

TOTAL_RECLAIMED=0

# ── Step 1 (--all only): remove stopped containers ────────────────────────────
if [[ "$ALL_MODE" == true ]]; then
    STOPPED_IDS=$(docker ps -a --filter "status=exited" --filter "status=created" -q 2>/dev/null)
    STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)

    if [[ "$STOPPED_COUNT" -eq 0 ]]; then
        log "No stopped containers"
    elif [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
        docker ps -a --filter "status=exited" --filter "status=created" \
            --format "  {{.Names}}   {{.Image}}   {{.Status}}" 2>/dev/null
    else
        log "Removing $STOPPED_COUNT stopped container(s)..."
        docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
        success "Removed $STOPPED_COUNT stopped container(s)"
    fi
    echo ""
fi

# ── Step 2: prune images ───────────────────────────────────────────────────────
PRUNE_FLAGS=$([[ "$ALL_MODE" == true ]] && echo "-a" || echo "")
PRUNE_FILTER=$([[ "$ALL_MODE" == true ]] && echo "" || echo '-f "dangling=true"')


65	    mkdir -p "$CACHE_DIR"
66	fi
67
68	# ── Copy own conf into local cache ───────────────────────────────────────────
69	if [[ "$PUSH_ONLY" == false ]] && [[ "$PULL_ONLY" == false ]]; then
70	    if [[ -f "$MY_CONF" ]]; then
71	        if [[ "$DRY_RUN" == true ]]; then
72	            warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/"
73	        else
74	            cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \
75	                log "Own conf cached ✅" || warn "Failed to cache own conf"
76	        fi
77	    else
78	        warn "Own conf not found: $MY_CONF"
79	    fi
80	fi
81
82	# ── Per-partner sync ──────────────────────────────────────────────────────────
83	PUSHED=0
84	PULLED=0
85	FAILED=0
86
87	for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
88	    partner_host="${!host_var}"
89	    [[ -z "$partner_host" ]] && continue
90	    [[ "${host_var,,}" == "${MY_ID,,}" ]] && continue
91
92	    partner_slot="${host_var,,}"   # e.g. host2
93	    partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)
94
95	    if [[ -z "$partner_ip" ]]; then
96	        warn "$partner_host — cannot resolve Tailscale IP, skipping"
97	        (( FAILED++ ))
98	        continue
99	    fi
100
101	    # ── Pull: grab partner's conf from their disk → our local cache ──────────
102	    if [[ "$PUSH_ONLY" == false ]]; then
103	        remote_conf="${SCRIPTS_DIR}/Configurations/${partner_slot}.conf"
104	        if [[ "$DRY_RUN" == true ]]; then
105	            warn "DRY RUN — would pull $partner_host:$remote_conf → $CACHE_DIR/${partner_slot}.conf"
106	        elif timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
107	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
108	            "root@${partner_ip}:${remote_conf}" \
109	            "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then
110	            log "Pulled ${partner_slot}.conf from $partner_host ✅"
111	            (( PULLED++ ))
112	        else
113	            warn "Could not pull ${partner_slot}.conf from $partner_host"
114	            (( FAILED++ ))
115	        fi
116	    fi
117
118	    # ── Push: send own conf to partner's /tmp/.vv/ cache ────────────────────
119	    if [[ "$PULL_ONLY" == true ]]; then
120	        continue
121	    fi
122
123	    if [[ "$DRY_RUN" == true ]]; then
124	        warn "DRY RUN — would push ${MY_ID,,}.conf → $partner_host:/tmp/.vv/config/cached/.confs/"
125	        continue
126	    fi
127
128	    # Ensure partner's cache dir exists, then SCP own conf into it
129	    timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
130	        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
131	        "root@${partner_ip}" "mkdir -p '$CACHE_DIR'" 2>/dev/null
132
133	    if timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
134	        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
135	        "$MY_CONF" \
136	        "root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then
137	        log "Pushed ${MY_ID,,}.conf to $partner_host ✅"
138	        (( PUSHED++ ))
139	    else
140	        warn "Could not push to $partner_host"
141	        (( FAILED++ ))
142	    fi
143	done
144
145	# ── Summary ───────────────────────────────────────────────────────────────────
146	if [[ "$PUSH_ONLY" == true ]]; then
147	    info "Conf push complete — pushed to $PUSHED host(s)${FAILED:+, $FAILED failed}"
148	elif [[ "$PULL_ONLY" == true ]]; then
149	    info "Conf pull complete — pulled $PULLED partner conf(s)${FAILED:+, $FAILED failed}"

    mkdir -p "$CACHE_DIR"
fi

# ── Copy own conf into local cache ───────────────────────────────────────────
if [[ "$PUSH_ONLY" == false ]] && [[ "$PULL_ONLY" == false ]]; then
    if [[ -f "$MY_CONF" ]]; then
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/"
        else
            cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \
                log "Own conf cached ✅" || warn "Failed to cache own conf"
        fi
    else
        warn "Own conf not found: $MY_CONF"
    fi
fi

# ── Per-partner sync ──────────────────────────────────────────────────────────
PUSHED=0
PULLED=0
FAILED=0

for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
    partner_host="${!host_var}"
    [[ -z "$partner_host" ]] && continue
    [[ "${host_var,,}" == "${MY_ID,,}" ]] && continue

    partner_slot="${host_var,,}"   # e.g. host2
    partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)

    if [[ -z "$partner_ip" ]]; then
        warn "$partner_host — cannot resolve Tailscale IP, skipping"
        (( FAILED++ ))
        continue
    fi

    # ── Pull: grab partner's conf from their disk → our local cache ──────────
    if [[ "$PUSH_ONLY" == false ]]; then
        remote_conf="${SCRIPTS_DIR}/Configurations/${partner_slot}.conf"
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would pull $partner_host:$remote_conf → $CACHE_DIR/${partner_slot}.conf"
        elif timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
            "root@${partner_ip}:${remote_conf}" \
            "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then
            log "Pulled ${partner_slot}.conf from $partner_host ✅"
            (( PULLED++ ))
        else
            warn "Could not pull ${partner_slot}.conf from $partner_host"
            (( FAILED++ ))
        fi
    fi

    # ── Push: send own conf to partner's /tmp/.vv/ cache ────────────────────
    if [[ "$PULL_ONLY" == true ]]; then
        continue
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would push ${MY_ID,,}.conf → $partner_host:/tmp/.vv/config/cached/.confs/"
        continue
    fi

    # Ensure partner's cache dir exists, then SCP own conf into it
    timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "root@${partner_ip}" "mkdir -p '$CACHE_DIR'" 2>/dev/null

    if timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "$MY_CONF" \
        "root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then
        log "Pushed ${MY_ID,,}.conf to $partner_host ✅"
        (( PUSHED++ ))
    else
        warn "Could not push to $partner_host"
        (( FAILED++ ))
    fi
done

# ── Summary ───────────────────────────────────────────────────────────────────
if [[ "$PUSH_ONLY" == true ]]; then
    info "Conf push complete — pushed to $PUSHED host(s)${FAILED:+, $FAILED failed}"
elif [[ "$PULL_ONLY" == true ]]; then
    info "Conf pull complete — pulled $PULLED partner conf(s)${FAILED:+, $FAILED failed}"


243
244	if [[ ! -f "$ARRAY_STOP_SCRIPT" ]]; then
245	    warn "array_stopping.sh not found — skipping orchestrated stop"
246	elif [[ "$DRY_RUN" == true ]]; then
247	    bash "$ARRAY_STOP_SCRIPT" --dry-run
248	else
249	    if bash "$ARRAY_STOP_SCRIPT"; then
250	        log "Array stop complete ✅"
251	    else
252	        warn "array_stopping.sh reported failures — proceeding with reboot"
253	    fi
254	fi
255
256	# ==============================================================================================
257	# ━━━ Graceful VM Shutdown ━━━
258	# ==============================================================================================
259	if is_vm_manager_enabled && command -v virsh >/dev/null 2>&1; then
260	    VM_LIST=$(virsh list --name 2>/dev/null | grep -v "^$" || true)
261	    if [[ -n "$VM_LIST" ]]; then
262	        echo ""
263	        echo "━━━ $ICON_GEAR Graceful VM Shutdown ━━━"
264	        while IFS= read -r vm; do
265	            [[ -z "$vm" ]] && continue
266	            warn "Sending ACPI shutdown to VM: $vm"
267	            if [[ "$DRY_RUN" == false ]]; then
268	                virsh shutdown "$vm" >/dev/null 2>&1 || true
269	            else
270	                warn "DRY RUN — would virsh shutdown $vm"
271	            fi
272	        done <<< "$VM_LIST"
273
274	        if [[ "$DRY_RUN" == false ]]; then
275	            VM_WAIT="${REBOOT_VM_WAIT:-30}"
276	            log "Waiting ${VM_WAIT}s for VMs to shut down..."
277	            sleep "$VM_WAIT"
278	        fi
279	    else
280	        log "VM Manager enabled but no VMs running — skipping shutdown"
281	    fi
282	else
283	    log "VM Manager not enabled — skipping VM shutdown"
284	fi
285
286	# ==============================================================================================
287	# ━━━ Stop VM Manager ━━━
288	# ==============================================================================================
289	echo ""
290	echo "━━━ $ICON_GEAR Stop VM Manager ━━━"
291	if ! is_vm_manager_enabled; then
292	    log "VM Manager not enabled — skipping"
293	elif [[ "$DRY_RUN" == true ]]; then
294	    warn "DRY RUN — would stop VM Manager (libvirt)"
295	else
296	    if platform_stop_service libvirt; then
297	        warn "VM Manager stopped ✅"
298	    else
299	        warn "VM Manager stop returned non-zero — may already be stopped"
300	    fi
301	fi
302
303	# ==============================================================================================
304	# ━━━ Sync Disks ━━━
305	# ==============================================================================================
306	echo ""
307	echo "━━━ $ICON_DISK Sync Disks ━━━"
308	if [[ "$DRY_RUN" == true ]]; then
309	    warn "DRY RUN — would sync filesystem buffers"
310	else
311	    sync
312	    log "Filesystem buffers flushed ✅"
313	fi
314
315	# ==============================================================================================
316	# ━━━ Reboot ━━━
317	# ==============================================================================================
318	END=$(date +%s)
319
320	echo ""
321	echo "━━━━━ $ICON_SUMMARY REBOOT SUMMARY ━━━━━"
322	echo "$ICON_HOST    Identity: $MY_ID ($LOCAL_SERVER_NAME)"

if [[ ! -f "$ARRAY_STOP_SCRIPT" ]]; then
    warn "array_stopping.sh not found — skipping orchestrated stop"
elif [[ "$DRY_RUN" == true ]]; then
    bash "$ARRAY_STOP_SCRIPT" --dry-run
else
    if bash "$ARRAY_STOP_SCRIPT"; then
        log "Array stop complete ✅"
    else
        warn "array_stopping.sh reported failures — proceeding with reboot"
    fi
fi

# ==============================================================================================
# ━━━ Graceful VM Shutdown ━━━
# ==============================================================================================
if is_vm_manager_enabled && command -v virsh >/dev/null 2>&1; then
    VM_LIST=$(virsh list --name 2>/dev/null | grep -v "^$" || true)
    if [[ -n "$VM_LIST" ]]; then
        echo ""
        echo "━━━ $ICON_GEAR Graceful VM Shutdown ━━━"
        while IFS= read -r vm; do
            [[ -z "$vm" ]] && continue
            warn "Sending ACPI shutdown to VM: $vm"
            if [[ "$DRY_RUN" == false ]]; then
                virsh shutdown "$vm" >/dev/null 2>&1 || true
            else
                warn "DRY RUN — would virsh shutdown $vm"
            fi
        done <<< "$VM_LIST"

        if [[ "$DRY_RUN" == false ]]; then
            VM_WAIT="${REBOOT_VM_WAIT:-30}"
            log "Waiting ${VM_WAIT}s for VMs to shut down..."
            sleep "$VM_WAIT"
        fi
    else
        log "VM Manager enabled but no VMs running — skipping shutdown"
    fi
else
    log "VM Manager not enabled — skipping VM shutdown"
fi

# ==============================================================================================
# ━━━ Stop VM Manager ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Stop VM Manager ━━━"
if ! is_vm_manager_enabled; then
    log "VM Manager not enabled — skipping"
elif [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would stop VM Manager (libvirt)"
else
    if platform_stop_service libvirt; then
        warn "VM Manager stopped ✅"
    else
        warn "VM Manager stop returned non-zero — may already be stopped"
    fi
fi

# ==============================================================================================
# ━━━ Sync Disks ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_DISK Sync Disks ━━━"
if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would sync filesystem buffers"
else
    sync
    log "Filesystem buffers flushed ✅"
fi

# ==============================================================================================
# ━━━ Reboot ━━━
# ==============================================================================================
END=$(date +%s)

echo ""
echo "━━━━━ $ICON_SUMMARY REBOOT SUMMARY ━━━━━"
echo "$ICON_HOST    Identity: $MY_ID ($LOCAL_SERVER_NAME)"


127	# Soft IP resolution — rsync_stop continues local-only if remote unreachable
128	REMOTE_REACHABLE=false
129	REMOTE_SERVER=$(resolve_tailscale_ip "$REMOTE_SERVER_NAME")
130	if [[ -z "$REMOTE_SERVER" ]]; then
131	    warn "$REMOTE_SERVER_NAME — cannot resolve Tailscale IP, remote operations will be skipped"
132	elif timeout "$SSH_TIMEOUT" ping -c1 -W3 "$REMOTE_SERVER" &>/dev/null; then
133	    REMOTE_REACHABLE=true
134	    log "$REMOTE_SERVER_NAME reachable ✅"
135	else
136	    warn "$REMOTE_SERVER_NAME unreachable — remote operations will be skipped"
137	fi
138
139	[[ "$DRY_RUN"   == true ]] && warn "DRY RUN — no changes will be made"
140	[[ "$FULL_STOP" == true ]] && warn "FULL STOP mode — orchestrator + rsync will be killed"
141
142	# ==============================================================================================
143	# ━━━ Status ━━━
144	# ==============================================================================================
145	if [[ "$SHOW_STATUS" == true ]]; then
146	    echo ""

# Soft IP resolution — rsync_stop continues local-only if remote unreachable
REMOTE_REACHABLE=false
REMOTE_SERVER=$(resolve_tailscale_ip "$REMOTE_SERVER_NAME")
if [[ -z "$REMOTE_SERVER" ]]; then
    warn "$REMOTE_SERVER_NAME — cannot resolve Tailscale IP, remote operations will be skipped"
elif timeout "$SSH_TIMEOUT" ping -c1 -W3 "$REMOTE_SERVER" &>/dev/null; then
    REMOTE_REACHABLE=true
    log "$REMOTE_SERVER_NAME reachable ✅"
else
    warn "$REMOTE_SERVER_NAME unreachable — remote operations will be skipped"
fi

[[ "$DRY_RUN"   == true ]] && warn "DRY RUN — no changes will be made"
[[ "$FULL_STOP" == true ]] && warn "FULL STOP mode — orchestrator + rsync will be killed"

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""


ok, thats what we were doing last week. we were going through all scripts correcting for verbose...... lets go through and check echo vs log... goal clean informative no verbose, and all details verbo…


Run docker network script during onboarding


192	if ! sed -i "s/^pm\.max_children.*/pm.max_children = $PHP_MAX_CHILDREN/" "$PHP_CONF"; then
193	    error "Failed to update $PHP_CONF"
194	    notify "PHP-FPM config update failed on $(hostname) ($MY_ID)" \
195	        "PHP-FPM" "warning"
196	    exit 1
197	fi
198
199	log "Config updated"
200
201	# ── Restart PHP-FPM ──────────────────────────────────────────────────────────────────────────
202	log "Restarting PHP-FPM..."
203	if ! platform_restart_service php-fpm; then
204	    error "PHP-FPM restart command failed"
205	    notify "PHP-FPM restart failed on $(hostname) ($MY_ID)" \
206	        "PHP-FPM" "warning"
207	    exit 1
208	fi
209
210	sleep 3  # Allow PHP-FPM workers to initialise
211
212	# ── Verify process running ────────────────────────────────────────────────────────────────────
213	if ! pgrep -f "php-fpm" >/dev/null 2>&1; then
214	    error "PHP-FPM not running after restart — WebGUI may be broken"
215	    notify "PHP-FPM failed to start after config update on $(hostname) ($MY_ID)" \
216	        "PHP-FPM" "warning"
217	    exit 1
218	fi
219
220	# ── Verify config reflects target ────────────────────────────────────────────────────────────
221	APPLIED_VAL=$(grep -E "^pm\.max_children" "$PHP_CONF" 2>/dev/null | awk '{print $NF}')
222	if [[ "${APPLIED_VAL:-0}" -ne "$PHP_MAX_CHILDREN" ]]; then
223	    warn "Config reads pm.max_children = ${APPLIED_VAL:-unknown} — expected $PHP_MAX_CHILDREN"
224	    warn "Check $PHP_CONF manually"
225	else
226	    log "Verified: pm.max_children = $APPLIED_VAL ✅"
227	fi
228
229	FPM_WORKERS=$(pgrep -fc "php-fpm" 2>/dev/null || echo "?")
230	log "$ICON_PHP Workers running: $FPM_WORKERS"
231

if ! sed -i "s/^pm\.max_children.*/pm.max_children = $PHP_MAX_CHILDREN/" "$PHP_CONF"; then
    error "Failed to update $PHP_CONF"
    notify "PHP-FPM config update failed on $(hostname) ($MY_ID)" \
        "PHP-FPM" "warning"
    exit 1
fi

log "Config updated"

# ── Restart PHP-FPM ──────────────────────────────────────────────────────────────────────────
log "Restarting PHP-FPM..."
if ! platform_restart_service php-fpm; then
    error "PHP-FPM restart command failed"
    notify "PHP-FPM restart failed on $(hostname) ($MY_ID)" \
        "PHP-FPM" "warning"
    exit 1
fi

sleep 3  # Allow PHP-FPM workers to initialise

# ── Verify process running ────────────────────────────────────────────────────────────────────
if ! pgrep -f "php-fpm" >/dev/null 2>&1; then
    error "PHP-FPM not running after restart — WebGUI may be broken"
    notify "PHP-FPM failed to start after config update on $(hostname) ($MY_ID)" \
        "PHP-FPM" "warning"
    exit 1
fi

# ── Verify config reflects target ────────────────────────────────────────────────────────────
APPLIED_VAL=$(grep -E "^pm\.max_children" "$PHP_CONF" 2>/dev/null | awk '{print $NF}')
if [[ "${APPLIED_VAL:-0}" -ne "$PHP_MAX_CHILDREN" ]]; then
    warn "Config reads pm.max_children = ${APPLIED_VAL:-unknown} — expected $PHP_MAX_CHILDREN"
    warn "Check $PHP_CONF manually"
else
    log "Verified: pm.max_children = $APPLIED_VAL ✅"
fi

FPM_WORKERS=$(pgrep -fc "php-fpm" 2>/dev/null || echo "?")
log "$ICON_PHP Workers running: $FPM_WORKERS"


150	echo ok
151	PUSHSCRIPT
152
153	    if timeout 10 scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
154	        -o StrictHostKeyChecking=no "$tmp" "root@${partner_ip}:${remote}" 2>/dev/null; then
155	        if timeout 10 ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
156	            -o StrictHostKeyChecking=no "root@${partner_ip}" \
157	            "bash '${remote}'; rc=\$?; rm -f '${remote}'; exit \$rc" 2>/dev/null | grep -q ok; then
158	            log "Key pushed to $partner_host ✅"
159	        else
160	            warn "Key push to $partner_host failed — they can create their own copy"
161	        fi
162	    else
163	        warn "SCP to $partner_host failed — skipping"
164	    fi
165	    rm -f "$tmp"
166	done
167

echo ok
PUSHSCRIPT

    if timeout 10 scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
        -o StrictHostKeyChecking=no "$tmp" "root@${partner_ip}:${remote}" 2>/dev/null; then
        if timeout 10 ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
            -o StrictHostKeyChecking=no "root@${partner_ip}" \
            "bash '${remote}'; rc=\$?; rm -f '${remote}'; exit \$rc" 2>/dev/null | grep -q ok; then
            log "Key pushed to $partner_host ✅"
        else
            warn "Key push to $partner_host failed — they can create their own copy"
        fi
    else
        warn "SCP to $partner_host failed — skipping"
    fi
    rm -f "$tmp"
done


150	if ! mountpoint -q /mnt/user; then
151	    error "Array is not started — /mnt/user is not mounted"
152	    warn "Start the array in the unRAID UI before running this script"
153	    notify "Recreate shares failed on $(hostname) — array is not started" \
154	        "Recreate Shares" "warning"
155	    exit 1
156	fi
157	log "Array is started — /mnt/user is mounted ✅"
158
159	# Check share cfg directory exists and has files
160	if [[ ! -d "$SHARE_CFG_DIR" ]]; then
161	    error "Share config directory not found: $SHARE_CFG_DIR"
162	    error "Is /boot mounted? Is this the correct server?"
163	    exit 1
164	fi
165
166	CFG_FILES=("$SHARE_CFG_DIR"/*.cfg)
167	if [[ ! -f "${CFG_FILES[0]}" ]]; then
168	    warn "No share .cfg files found in $SHARE_CFG_DIR"
169	    warn "Nothing to recreate — are share configs present on /boot?"
170	    exit 0
171	fi
172
173	log "Found ${#CFG_FILES[@]} share .cfg file(s) in $SHARE_CFG_DIR"
174
175	# ==============================================================================================
176	# ━━━ Recreate Shares ━━━
177	# ==============================================================================================
178	echo ""
179	echo "━━━ $ICON_DISK Recreate Shares — $MY_ID ━━━"
180	echo ""
181
182	for cfg in "${CFG_FILES[@]}"; do
183	    [[ ! -f "$cfg" ]] && continue
184	    SHARE_NAME=$(basename "$cfg" .cfg)
185	    INCLUDE=$(grep '^shareInclude=' "$cfg" 2>/dev/null | cut -d'"' -f2)
186
187	    echo "━━━ $ICON_DISK $SHARE_NAME ━━━"
188
189	    if [[ -z "$INCLUDE" ]]; then
190	        warn "$SHARE_NAME — no shareInclude in .cfg — skipping"
191	        SKIPPED+=("$SHARE_NAME")
192	        echo ""
193	        continue
194	    fi
195
196	    log "$SHARE_NAME — disks: $INCLUDE"
197
198	    SHARE_OK=true
199	    DIRS_CREATED=0
200	    DIRS_EXISTED=0
201
202	    # Create directory on each listed disk
203	    IFS=',' read -ra DISKS <<< "$INCLUDE"
204	    for disk in "${DISKS[@]}"; do
205	        disk="${disk// /}"    # trim whitespace
206	        [[ -z "$disk" ]] && continue
207
208	        DISK_MOUNT="/mnt/${disk}"
209	        DISK_PATH="${DISK_MOUNT}/${SHARE_NAME}"
210
211	        # Verify disk is mounted
212	        if ! mountpoint -q "$DISK_MOUNT" 2>/dev/null; then
213	            warn "$disk not mounted — skipping $DISK_PATH"
214	            continue
215	        fi
216
217	        if [[ -d "$DISK_PATH" ]]; then
218	            log "$disk/$SHARE_NAME already exists — skipping"
219	            (( DIRS_EXISTED++ ))
220	        else
221	            if [[ "$DRY_RUN" == true ]]; then
222	                warn "DRY RUN — would create: $DISK_PATH"
223	                (( DIRS_CREATED++ ))
224	            elif mkdir -p "$DISK_PATH"; then
225	                log "Created: $DISK_PATH ✅"
226	                (( DIRS_CREATED++ ))
227	            else
228	                error "Failed to create: $DISK_PATH"
229	                SHARE_OK=false
230	            fi
231	        fi
232	    done
233
234	    # Place .recovery marker via /mnt/user (union filesystem)
235	    MARKER_PATH="/mnt/user/${SHARE_NAME}/${MARKER_FILE}"
236
237	    if [[ "$SHARE_OK" == true ]]; then
238	        if [[ -f "$MARKER_PATH" ]]; then
239	            log ".recovery marker already exists in $SHARE_NAME"
240	            CREATED+=("$SHARE_NAME")
241	        elif [[ "$DRY_RUN" == true ]]; then
242	            warn "DRY RUN — would place marker: $MARKER_PATH"
243	            CREATED+=("$SHARE_NAME")
244	        elif touch "$MARKER_PATH" 2>/dev/null; then
245	            log "Marker placed: $MARKER_PATH ✅"
246	            CREATED+=("$SHARE_NAME")
247	        else
248	            warn "$SHARE_NAME — could not place .recovery marker"
249	            warn "Share directory may not be visible via /mnt/user yet"
250	            warn "Try: touch /mnt/user/${SHARE_NAME}/.recovery manually after verifying share"
251	            SKIPPED+=("$SHARE_NAME")
252	        fi
253	    else
254	        FAILED+=("$SHARE_NAME")

if ! mountpoint -q /mnt/user; then
    error "Array is not started — /mnt/user is not mounted"
    warn "Start the array in the unRAID UI before running this script"
    notify "Recreate shares failed on $(hostname) — array is not started" \
        "Recreate Shares" "warning"
    exit 1
fi
log "Array is started — /mnt/user is mounted ✅"

# Check share cfg directory exists and has files
if [[ ! -d "$SHARE_CFG_DIR" ]]; then
    error "Share config directory not found: $SHARE_CFG_DIR"
    error "Is /boot mounted? Is this the correct server?"
    exit 1
fi

CFG_FILES=("$SHARE_CFG_DIR"/*.cfg)
if [[ ! -f "${CFG_FILES[0]}" ]]; then
    warn "No share .cfg files found in $SHARE_CFG_DIR"
    warn "Nothing to recreate — are share configs present on /boot?"
    exit 0
fi

log "Found ${#CFG_FILES[@]} share .cfg file(s) in $SHARE_CFG_DIR"

# ==============================================================================================
# ━━━ Recreate Shares ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_DISK Recreate Shares — $MY_ID ━━━"
echo ""

for cfg in "${CFG_FILES[@]}"; do
    [[ ! -f "$cfg" ]] && continue
    SHARE_NAME=$(basename "$cfg" .cfg)
    INCLUDE=$(grep '^shareInclude=' "$cfg" 2>/dev/null | cut -d'"' -f2)

    echo "━━━ $ICON_DISK $SHARE_NAME ━━━"

    if [[ -z "$INCLUDE" ]]; then
        warn "$SHARE_NAME — no shareInclude in .cfg — skipping"
        SKIPPED+=("$SHARE_NAME")
        echo ""
        continue
    fi

    log "$SHARE_NAME — disks: $INCLUDE"

    SHARE_OK=true
    DIRS_CREATED=0
    DIRS_EXISTED=0

    # Create directory on each listed disk
    IFS=',' read -ra DISKS <<< "$INCLUDE"
    for disk in "${DISKS[@]}"; do
        disk="${disk// /}"    # trim whitespace
        [[ -z "$disk" ]] && continue

        DISK_MOUNT="/mnt/${disk}"
        DISK_PATH="${DISK_MOUNT}/${SHARE_NAME}"

        # Verify disk is mounted
        if ! mountpoint -q "$DISK_MOUNT" 2>/dev/null; then
            warn "$disk not mounted — skipping $DISK_PATH"
            continue
        fi

        if [[ -d "$DISK_PATH" ]]; then
            log "$disk/$SHARE_NAME already exists — skipping"
            (( DIRS_EXISTED++ ))
        else
            if [[ "$DRY_RUN" == true ]]; then
                warn "DRY RUN — would create: $DISK_PATH"
                (( DIRS_CREATED++ ))
            elif mkdir -p "$DISK_PATH"; then
                log "Created: $DISK_PATH ✅"
                (( DIRS_CREATED++ ))
            else
                error "Failed to create: $DISK_PATH"
                SHARE_OK=false
            fi
        fi
    done

    # Place .recovery marker via /mnt/user (union filesystem)
    MARKER_PATH="/mnt/user/${SHARE_NAME}/${MARKER_FILE}"

    if [[ "$SHARE_OK" == true ]]; then
        if [[ -f "$MARKER_PATH" ]]; then
            log ".recovery marker already exists in $SHARE_NAME"
            CREATED+=("$SHARE_NAME")
        elif [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would place marker: $MARKER_PATH"
            CREATED+=("$SHARE_NAME")
        elif touch "$MARKER_PATH" 2>/dev/null; then
            log "Marker placed: $MARKER_PATH ✅"
            CREATED+=("$SHARE_NAME")
        else
            warn "$SHARE_NAME — could not place .recovery marker"
            warn "Share directory may not be visible via /mnt/user yet"
            warn "Try: touch /mnt/user/${SHARE_NAME}/.recovery manually after verifying share"
            SKIPPED+=("$SHARE_NAME")
        fi
    else
        FAILED+=("$SHARE_NAME")


Now let me read the Watchdog and Monitor files:


268	if [[ -n "$NPM_URL" ]]; then
269	    NPM_STRIKES=$(get_strikes "npm" "${NETWORK_WATCHDOG_NPM_STATE_FILE}")
270	    NPM_STRIKE_LIMIT="${NETWORK_WATCHDOG_NPM_STRIKE_LIMIT:-2}"
271
272	    if curl -sf --max-time "${NETWORK_WATCHDOG_NPM_TIMEOUT:-10}" "$NPM_URL" >/dev/null 2>&1; then
273	        log "$ICON_SUCCESS NPM proxy reachable — $NPM_URL"
274	        if [[ "$NPM_STRIKES" -gt 0 ]]; then
275	            log "NPM strikes cleared (was $NPM_STRIKES)"
276	            set_strikes "npm" 0 "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
277	        fi
278	    else
279	        NPM_STRIKES=$(( NPM_STRIKES + 1 ))
280	        set_strikes "npm" "$NPM_STRIKES" "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
281	        (( ISSUES++ ))
282

if [[ -n "$NPM_URL" ]]; then
    NPM_STRIKES=$(get_strikes "npm" "${NETWORK_WATCHDOG_NPM_STATE_FILE}")
    NPM_STRIKE_LIMIT="${NETWORK_WATCHDOG_NPM_STRIKE_LIMIT:-2}"

    if curl -sf --max-time "${NETWORK_WATCHDOG_NPM_TIMEOUT:-10}" "$NPM_URL" >/dev/null 2>&1; then
        log "$ICON_SUCCESS NPM proxy reachable — $NPM_URL"
        if [[ "$NPM_STRIKES" -gt 0 ]]; then
            log "NPM strikes cleared (was $NPM_STRIKES)"
            set_strikes "npm" 0 "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
        fi
    else
        NPM_STRIKES=$(( NPM_STRIKES + 1 ))
        set_strikes "npm" "$NPM_STRIKES" "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
        (( ISSUES++ ))


557	            rm_state_set "rm_recover_cycles" 0
558	            rm_state_set "rm_paused_containers" "$PAUSED_LIST"
559	            rm_state_set "rm_stopped_containers" "$STOPPED_LIST"
560
561	            if [[ "$NEW_LEVEL" -gt 0 ]]; then
562	                warn "De-escalated to level $NEW_LEVEL (${LEVEL_NAMES[$NEW_LEVEL]}) — ${RW_RECOVER_CYCLES:-3} more cycles to fully clear"
563	            else
564	                log "All pressure cleared — system at normal operation ✅"
565	                notify "Resource Manager: pressure resolved on $(hostname) ($MY_ID) — system back to normal" \
566	                    "Resource Manager" "normal"
567	            fi
568	        fi
569	    fi
570
571	else

            rm_state_set "rm_recover_cycles" 0
            rm_state_set "rm_paused_containers" "$PAUSED_LIST"
            rm_state_set "rm_stopped_containers" "$STOPPED_LIST"

            if [[ "$NEW_LEVEL" -gt 0 ]]; then
                warn "De-escalated to level $NEW_LEVEL (${LEVEL_NAMES[$NEW_LEVEL]}) — ${RW_RECOVER_CYCLES:-3} more cycles to fully clear"
            else
                log "All pressure cleared — system at normal operation ✅"
                notify "Resource Manager: pressure resolved on $(hostname) ($MY_ID) — system back to normal" \
                    "Resource Manager" "normal"
            fi
        fi
    fi

else


113	        warn "DRY RUN — would run: $script_name"
114	        PASSED+=("$script_name")
115	        continue
116	    fi
117
118	    _ss=$(date +%s)
119	    if bash "$script_path"; then
120	        log "$script_name — done in $(format_duration $(( $(date +%s) - _ss ))) ✅"
121	        PASSED+=("$script_name")
122	    else
123	        warn "$script_name — exit non-zero in $(format_duration $(( $(date +%s) - _ss ))) (issues found or fixed) — continuing"
124	        FAILED+=("$script_name")
125	    fi
126	done
127

        warn "DRY RUN — would run: $script_name"
        PASSED+=("$script_name")
        continue
    fi

    _ss=$(date +%s)
    if bash "$script_path"; then
        log "$script_name — done in $(format_duration $(( $(date +%s) - _ss ))) ✅"
        PASSED+=("$script_name")
    else
        warn "$script_name — exit non-zero in $(format_duration $(( $(date +%s) - _ss ))) (issues found or fixed) — continuing"
        FAILED+=("$script_name")
    fi
done


173	echo ""
174	echo "━━━ $ICON_SHIELD Pre-flight ━━━"
175
176	resolve_remote_ip
177
178	# Connectivity — no point making 100+ SSH calls if remote is unreachable
179	check_connectivity
180	log "Connectivity to $REMOTE_SERVER_NAME ✅"
181
182	# Version parity — mismatched unRAID could cause md5sum path differences
183	check_os_version_parity || {
184	    warn "Version parity check failed — proceeding with caution"
185	    warn "Checksum results may be unreliable if md5sum path changed between versions"
186	}
187	log "Version parity with $REMOTE_SERVER_NAME ✅"
188
189	# Remote array — if array is down all files appear "missing" = false alarm
190	if ! check_remote_array; then
191	    error "Remote array not mounted on $REMOTE_SERVER_NAME"
192	    error "All files would appear as MISSING — aborting to prevent false alarm"
193	    notify "Backup verify aborted on $(hostname) — remote array not mounted on $REMOTE_SERVER_NAME" \
194	        "Backup Verify" "warning"
195	    exit 1
196	fi
197	log "Remote array mounted on $REMOTE_SERVER_NAME ✅"
198
199	echo "Pre-flight passed ✅"
200
201	# ==============================================================================================
202	# ━━━ Backup Verification ━━━
203	# ==============================================================================================
204	echo ""
205	echo "━━━ $ICON_VERIFY Backup Verification — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
206	echo "$ICON_HOST    $MY_ID ($LOCAL_SERVER_NAME) → $REMOTE_ID ($REMOTE_SERVER_NAME)"
207	echo "$ICON_VERIFY  Sample: $BACKUP_VERIFY_SAMPLE files per share (min: $BACKUP_VERIFY_MIN_SIZE)"
208	echo ""
209
210	START=$(date +%s)
211	TOTAL_CHECKED=0
212	TOTAL_MATCH=0
213	TOTAL_MISMATCH=0
214	TOTAL_MISSING=0
215	SHARES_WITH_ISSUES=()
216
217	for share in "${VERIFY_SHARES[@]}"; do
218	    SHARE_NAME=$(basename "$share")
219	    echo "━━━ $ICON_VERIFY $SHARE_NAME ━━━"
220
221	    if [[ ! -d "$share" ]]; then
222	        warn "$SHARE_NAME not found locally — skipping"
223	        echo ""
224	        continue
225	    fi
226
227	    # Sample random files above minimum size
228	    mapfile -t SAMPLE_FILES < <(
229	        find "$share" -type f -size +"$BACKUP_VERIFY_MIN_SIZE" 2>/dev/null | \
230	        shuf | head -n "$BACKUP_VERIFY_SAMPLE"
231	    )
232
233	    if [[ ${#SAMPLE_FILES[@]} -eq 0 ]]; then
234	        log "$SHARE_NAME — no files found above $BACKUP_VERIFY_MIN_SIZE"
235	        echo ""
236	        continue
237	    fi
238
239	    log "$SHARE_NAME — sampled ${#SAMPLE_FILES[@]} files"
240
241	    if [[ "$DRY_RUN" == true ]]; then
242	        for f in "${SAMPLE_FILES[@]}"; do
243	            warn "DRY RUN — would check: $(basename "$f")"
244	        done
245	        echo ""
246	        continue
247	    fi
248
249	    SHARE_MATCH=0
250	    SHARE_MISMATCH=0
251	    SHARE_MISSING=0
252
253	    for local_file in "${SAMPLE_FILES[@]}"; do
254	        [[ -z "$local_file" ]] && continue
255
256	        # Local checksum
257	        local_md5=$(md5sum "$local_file" 2>/dev/null | awk '{print $1}')
258	        if [[ -z "$local_md5" ]]; then
259	            warn "Could not checksum locally: $(basename "$local_file") — skipping"
260	            continue
261	        fi
262
263	        # Remote checksum via SSH — timeout protected
264	        remote_md5=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
265	            -o ConnectTimeout="$SSH_TIMEOUT" \
266	            -o StrictHostKeyChecking=no \
267	            root@"$REMOTE_SERVER" \
268	            "md5sum '$local_file' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)
269
270	        (( TOTAL_CHECKED++ ))
271
272	        if [[ -z "$remote_md5" ]]; then
273	            warn "$ICON_ERROR MISSING: $(basename "$local_file")"
274	            (( SHARE_MISSING++ ))
275	            (( TOTAL_MISSING++ ))
276	        elif [[ "$local_md5" == "$remote_md5" ]]; then
277	            log "MATCH: $(basename "$local_file")"
278	            (( SHARE_MATCH++ ))
279	            (( TOTAL_MATCH++ ))
280	        else
281	            error "MISMATCH: $(basename "$local_file")"
282	            error "  local:  $local_md5"
283	            error "  remote: $remote_md5"
284	            (( SHARE_MISMATCH++ ))
285	            (( TOTAL_MISMATCH++ ))
286	        fi
287	    done
288
289	    # Per-share result — only visible if issues found
290	    if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 ]]; then
291	        warn "$SHARE_NAME — match: $SHARE_MATCH  missing: $SHARE_MISSING  mismatch: $SHARE_MISMATCH"
292	        SHARES_WITH_ISSUES+=("$SHARE_NAME")
293	    else
294	        log "$SHARE_NAME — all $SHARE_MATCH files match ✅"
295	    fi
296
297	    echo ""
298	done
299
300	END=$(date +%s)
301
302	# ==============================================================================================

echo ""
echo "━━━ $ICON_SHIELD Pre-flight ━━━"

resolve_remote_ip

# Connectivity — no point making 100+ SSH calls if remote is unreachable
check_connectivity
log "Connectivity to $REMOTE_SERVER_NAME ✅"

# Version parity — mismatched unRAID could cause md5sum path differences
check_os_version_parity || {
    warn "Version parity check failed — proceeding with caution"
    warn "Checksum results may be unreliable if md5sum path changed between versions"
}
log "Version parity with $REMOTE_SERVER_NAME ✅"

# Remote array — if array is down all files appear "missing" = false alarm
if ! check_remote_array; then
    error "Remote array not mounted on $REMOTE_SERVER_NAME"
    error "All files would appear as MISSING — aborting to prevent false alarm"
    notify "Backup verify aborted on $(hostname) — remote array not mounted on $REMOTE_SERVER_NAME" \
        "Backup Verify" "warning"
    exit 1
fi
log "Remote array mounted on $REMOTE_SERVER_NAME ✅"

echo "Pre-flight passed ✅"

# ==============================================================================================
# ━━━ Backup Verification ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_VERIFY Backup Verification — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo "$ICON_HOST    $MY_ID ($LOCAL_SERVER_NAME) → $REMOTE_ID ($REMOTE_SERVER_NAME)"
echo "$ICON_VERIFY  Sample: $BACKUP_VERIFY_SAMPLE files per share (min: $BACKUP_VERIFY_MIN_SIZE)"
echo ""

START=$(date +%s)
TOTAL_CHECKED=0
TOTAL_MATCH=0
TOTAL_MISMATCH=0
TOTAL_MISSING=0
SHARES_WITH_ISSUES=()

for share in "${VERIFY_SHARES[@]}"; do
    SHARE_NAME=$(basename "$share")
    echo "━━━ $ICON_VERIFY $SHARE_NAME ━━━"

    if [[ ! -d "$share" ]]; then
        warn "$SHARE_NAME not found locally — skipping"
        echo ""
        continue
    fi

    # Sample random files above minimum size
    mapfile -t SAMPLE_FILES < <(
        find "$share" -type f -size +"$BACKUP_VERIFY_MIN_SIZE" 2>/dev/null | \
        shuf | head -n "$BACKUP_VERIFY_SAMPLE"
    )

    if [[ ${#SAMPLE_FILES[@]} -eq 0 ]]; then
        log "$SHARE_NAME — no files found above $BACKUP_VERIFY_MIN_SIZE"
        echo ""
        continue
    fi

    log "$SHARE_NAME — sampled ${#SAMPLE_FILES[@]} files"

    if [[ "$DRY_RUN" == true ]]; then
        for f in "${SAMPLE_FILES[@]}"; do
            warn "DRY RUN — would check: $(basename "$f")"
        done
        echo ""
        continue
    fi

    SHARE_MATCH=0
    SHARE_MISMATCH=0
    SHARE_MISSING=0

    for local_file in "${SAMPLE_FILES[@]}"; do
        [[ -z "$local_file" ]] && continue

        # Local checksum
        local_md5=$(md5sum "$local_file" 2>/dev/null | awk '{print $1}')
        if [[ -z "$local_md5" ]]; then
            warn "Could not checksum locally: $(basename "$local_file") — skipping"
            continue
        fi

        # Remote checksum via SSH — timeout protected
        remote_md5=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" \
            -o StrictHostKeyChecking=no \
            root@"$REMOTE_SERVER" \
            "md5sum '$local_file' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)

        (( TOTAL_CHECKED++ ))

        if [[ -z "$remote_md5" ]]; then
            warn "$ICON_ERROR MISSING: $(basename "$local_file")"
            (( SHARE_MISSING++ ))
            (( TOTAL_MISSING++ ))
        elif [[ "$local_md5" == "$remote_md5" ]]; then
            log "MATCH: $(basename "$local_file")"
            (( SHARE_MATCH++ ))
            (( TOTAL_MATCH++ ))
        else
            error "MISMATCH: $(basename "$local_file")"
            error "  local:  $local_md5"
            error "  remote: $remote_md5"
            (( SHARE_MISMATCH++ ))
            (( TOTAL_MISMATCH++ ))
        fi
    done

    # Per-share result — only visible if issues found
    if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 ]]; then
        warn "$SHARE_NAME — match: $SHARE_MATCH  missing: $SHARE_MISSING  mismatch: $SHARE_MISMATCH"
        SHARES_WITH_ISSUES+=("$SHARE_NAME")
    else
        log "$SHARE_NAME — all $SHARE_MATCH files match ✅"
    fi

    echo ""
done

END=$(date +%s)

# ==============================================================================================


183	    error "Cannot connect to Emby at $EMBY_URL"
184	    exit 1
185	}
186
187	SERVER_NAME=$(echo "$SYSTEM_INFO" | jq -r '.ServerName // "Unknown"' 2>/dev/null)
188	SERVER_VERSION=$(echo "$SYSTEM_INFO" | jq -r '.Version // "Unknown"' 2>/dev/null)
189	log "$ICON_EMBY Connected to: $SERVER_NAME (v$SERVER_VERSION) ✅"
190
191	# ── Active Sessions ───────────────────────────────────────────────────────────────────────────
192	echo "━━━ $ICON_EMBY Active Sessions ━━━"
193	SESSIONS=$(emby_api "Sessions" 2>/dev/null) || { warn "Could not fetch sessions"; SESSIONS="[]"; }
194
195	ACTIVE_COUNT=$(echo "$SESSIONS" | \
196	    jq '[.[] | select(.NowPlayingItem != null)] | length' 2>/dev/null || echo 0)
197	TRANSCODE_NOW=$(echo "$SESSIONS" | \

    error "Cannot connect to Emby at $EMBY_URL"
    exit 1
}

SERVER_NAME=$(echo "$SYSTEM_INFO" | jq -r '.ServerName // "Unknown"' 2>/dev/null)
SERVER_VERSION=$(echo "$SYSTEM_INFO" | jq -r '.Version // "Unknown"' 2>/dev/null)
log "$ICON_EMBY Connected to: $SERVER_NAME (v$SERVER_VERSION) ✅"

# ── Active Sessions ───────────────────────────────────────────────────────────────────────────
echo "━━━ $ICON_EMBY Active Sessions ━━━"
SESSIONS=$(emby_api "Sessions" 2>/dev/null) || { warn "Could not fetch sessions"; SESSIONS="[]"; }

ACTIVE_COUNT=$(echo "$SESSIONS" | \
    jq '[.[] | select(.NowPlayingItem != null)] | length' 2>/dev/null || echo 0)
TRANSCODE_NOW=$(echo "$SESSIONS" | \


Now I have everything I need. Making all changes in parallel across the files:


Edit

            if timeout "$DOCKER_TIMEOUT" docker stop "$CONTAINER_NAME" >/dev/null 2>&1; then
                log "$CONTAINER_NAME stopped ✅"

            if timeout "$DOCKER_TIMEOUT" docker stop "$CONTAINER_NAME" >/dev/null 2>&1; then
                echo "$CONTAINER_NAME stopped ✅"


log "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"

echo "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"


log "$CONTAINER_NAME restarted and running ✅"

echo "$CONTAINER_NAME restarted and running ✅"


log "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"

echo "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"


The file /boot/config/plugins/varaverk/Tools/container_data_export.sh has been updated successfully. (file state is current in your context — no need to Read it back)

            if timeout "$DOCKER_TIMEOUT" docker stop "$CONTAINER_NAME" >/dev/null 2>&1; then
                log "$CONTAINER_NAME stopped ✅"

            if timeout "$DOCKER_TIMEOUT" docker stop "$CONTAINER_NAME" >/dev/null 2>&1; then
                echo "$CONTAINER_NAME stopped ✅"

log "Stopping $CONTAINER_NAME for clean export..."

-                log "$CONTAINER_NAME stopped ✅"

+                echo "$CONTAINER_NAME stopped ✅"

error "Failed to stop $CONTAINER_NAME — aborting export"


-        log "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"

+        echo "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"

error "$CONTAINER_NAME not found — check container name"


if tar --test-label -f "$ARCHIVE_PATH" 2>/dev/null || \

tar -tzf "$ARCHIVE_PATH" >/dev/null 2>&1; then

error "Archive verification FAILED — archive may be corrupt"


POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \

-                log "$CONTAINER_NAME restarted and running ✅"

+                echo "$CONTAINER_NAME restarted and running ✅"

error "$CONTAINER_NAME started but crashed immediately — check container logs"


-    log "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"

+    echo "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"


The file /boot/config/plugins/varaverk/Tools/zfs_pool_scrub.sh has been updated successfully. (file state is current in your context — no need to Read it back)

elif zpool scrub "$pool" 2>/dev/null; then


log "Safe to interrupt — scrubs continue in background if script is stopped"

echo "Safe to interrupt — scrubs continue in background if script is stopped"


echo "━━━ $ICON_TIME Waiting for Scrubs to Complete ━━━"

log "Polling every 60 seconds — scrubs may take hours on large pools"

-log "Safe to interrupt — scrubs continue in background if script is stopped"

+echo "Safe to interrupt — scrubs continue in background if script is stopped"

while [[ "$SCRUB_RUNNING" == true ]]; do


The file /boot/config/plugins/varaverk/Tools/ramdisk_stop.sh has been updated successfully. (file state is current in your context — no need to Read it back)

# Warn if transcode_manager is running — it may flip symlink back on next cycle

if pgrep -f "transcode_manager.sh" >/dev/null 2>&1; then


error "Cannot safely redirect symlink — aborting"

log "SSD fallback exists: $TRANSCODE_SSD ✅"


log "SSD fallback exists: $TRANSCODE_SSD ✅"

echo "SSD fallback exists: $TRANSCODE_SSD ✅"


-    log "SSD fallback exists: $TRANSCODE_SSD ✅"

+    echo "SSD fallback exists: $TRANSCODE_SSD ✅"


+    echo "No active files on ramdisk ✅"


log "State file updated: current_target=$TRANSCODE_SSD"

echo "State file updated: current_target=$TRANSCODE_SSD"


-    log "State file updated: current_target=$TRANSCODE_SSD"

+    echo "State file updated: current_target=$TRANSCODE_SSD"

warn "Skipping state file update — stop had errors"


            if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
                log "$EMBY_CONTAINER stopped ✅"

            if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
                echo "$EMBY_CONTAINER stopped ✅"


The file /boot/config/plugins/varaverk/Tools/emby_database_repair.sh has been updated successfully. (file state is current in your context — no need to Read it back)

            if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
                log "$EMBY_CONTAINER stopped ✅"

            if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
                echo "$EMBY_CONTAINER stopped ✅"

warn "Stopping $EMBY_CONTAINER — active sessions will be interrupted"

-                log "$EMBY_CONTAINER stopped ✅"

+                echo "$EMBY_CONTAINER stopped ✅"

error "Failed to stop $EMBY_CONTAINER — aborting"


log "$db_name exists but is empty — no pending transactions ✅"

echo "$db_name exists but is empty — no pending transactions ✅"


warn "Uncommitted WAL data — will be merged when Emby next starts cleanly"

PASS_DBS+=("$db_name (WAL — see warning)")

-            log "$db_name exists but is empty — no pending transactions ✅"

+            echo "$db_name exists but is empty — no pending transactions ✅"


log "$db_name — integrity check passed ✅"

echo "$db_name — integrity check passed ✅"


error "$db_name — sqlite3 could not open database (locked or corrupt)"

-        log "$db_name — integrity check passed ✅"

+        echo "$db_name — integrity check passed ✅"


log "$EMBY_CONTAINER restarted and running ✅"

echo "$EMBY_CONTAINER restarted and running ✅"


POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \

-                log "$EMBY_CONTAINER restarted and running ✅"

+                echo "$EMBY_CONTAINER restarted and running ✅"

error "$EMBY_CONTAINER started but crashed — database may be corrupt"


log "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"

echo "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"


-    log "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"

+    echo "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"


[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do log "  $ICON_SUCCESS $db"; done

[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do echo "  $ICON_SUCCESS $db"; done


[[ ${#MISSING_DBS[@]} -gt 0 ]] && echo "  Skipped: ${#MISSING_DBS[@]} (not found)"

-[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do log "  $ICON_SUCCESS $db"; done

+[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do echo "  $ICON_SUCCESS $db"; done

[[ ${#FAIL_DBS[@]} -gt 0 ]] && for db in "${FAIL_DBS[@]}"; do echo "  $ICON_ERROR  $db"; done


log "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"

echo "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"


The file /boot/config/plugins/varaverk/Tools/bulk_permissions_repair.sh has been updated successfully. (file state is current in your context — no need to Read it back)

#!/bin/bash
# ==============================================================================================
# ============================= Bulk Permissions Repair ========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Applies correct ownership and permissions to one or more specific paths.
# Targeted repair — faster than media_shares_permissions.sh, which processes
# every configured share. Use after failed transfers that left root:root ownership,
# containers writing as root before PUID/PGID was fixed, manual file copies, or
# new shares that need permissions applied before the next nightly run.
#
# Counts files with wrong ownership before fixing. A high count on a recently
# written share means a container has wrong PUID/PGID — add PUID=99 PGID=100
# to its Docker template. Common culprits: SABnzbd, qBittorrent, slskd.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Permissions Model
#   Directories (PERMISSIONS_DIR_MODE, default 755):
#     Owner (nobody) — rwx  enter, list, create files
#     Group (users)  — r-x  enter and list
#     Others         — r-x  Samba guests can browse
#   Files (PERMISSIONS_FILE_MODE, default 664):
#     Owner (nobody) — rw   read + write
#     Group (users)  — rw   arrs can import and rename
#     Others         — r    Samba guests can read
#     No execute bit — media files are never executable
#
# Separate Passes
#   Directories and files are chmod'd in separate find passes. A combined pass
#   with mode 664 would wrongly strip the execute bit from directories, making
#   them untraversable.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Required
#   chown requires root — exits immediately if not running as root.
#
# Path Existence Check
#   Each path is verified before processing — missing paths log an error and
#   are skipped rather than silently passing.
#
# Notification Validated
#   platform_require_cmd confirms the notify script is present before use.
#
# Silent on Success
#   Only failures and the wrong-owner diagnostic produce visible output.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   PERMISSIONS_OWNER
#       Owner applied to all paths. (default: nobody:users)
#
#   PERMISSIONS_DIR_MODE
#       chmod mode for directories. (default: 755)
#
#   PERMISSIONS_FILE_MODE
#       chmod mode for files. (default: 664)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# bulk_permissions_repair.sh /path/to/share [/another/path ...]
#     Apply ownership and permissions to each specified path.
#
# bulk_permissions_repair.sh /path/to/share --dry-run
#     Show wrong-owner count per path. No chown or chmod applied.
#
# bulk_permissions_repair.sh /path/to/share --log
#     Verbose output including per-path file counts and modes applied.
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "$SCRIPT_DIR/../load_config.sh"

parse_args "$@"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root — chown requires root"
    exit 1
fi

acquire_lock

# detect_hosts() sets MY_ID — used in summary
detect_hosts

if [[ ${#PARSED_ARGS[@]} -eq 0 ]]; then
    error "No paths specified"
    error "Usage: bulk_permissions_repair.sh /path/to/share [/another/path] [--dry-run]"
    exit 1
fi

log "Dir mode:  ${PERMISSIONS_DIR_MODE:-755}"
log "File mode: ${PERMISSIONS_FILE_MODE:-664}"
log "Owner:     $PERMISSIONS_OWNER"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permissions will be changed"

# ==============================================================================================
# ━━━ Apply Permissions ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_PERMS Permissions Repair — $MY_ID ━━━"

START=$(date +%s)
PASS=()
FAIL=()
TOTAL_WRONG_OWNER=0

for share_path in "${PARSED_ARGS[@]}"; do
    [[ -z "$share_path" ]] && continue

    echo ""
    echo "━━━ $ICON_PERMS $(basename "$share_path") ━━━"

    if [[ ! -d "$share_path" ]]; then
        error "$share_path — not found"
        FAIL+=("$share_path")
        continue
    fi

    # Count files for context — warn level so user knows what they're in for on large shares
    FILE_COUNT=$(find "$share_path" -type f 2>/dev/null | wc -l)
    DIR_COUNT=$(find  "$share_path" -type d 2>/dev/null | wc -l)
    SIZE=$(du -sh "$share_path" 2>/dev/null | cut -f1)
    warn "$share_path — $FILE_COUNT files, $DIR_COUNT dirs ($SIZE)"

    # Count files with wrong ownership before fixing — diagnostic
    WRONG_OWNER=$(find "$share_path" \( ! -user nobody -o ! -group users \) \
        2>/dev/null | wc -l)
    if [[ "$WRONG_OWNER" -gt 0 ]]; then
        warn "$WRONG_OWNER file(s) with wrong ownership — fixing..."
        if [[ "$WRONG_OWNER" -gt 500 ]]; then
            warn "High wrong-owner count — check container PUID/PGID settings (should be PUID=99 PGID=100)"
            warn "Common culprits: SABnzbd, qBittorrent, slskd"
        fi
        TOTAL_WRONG_OWNER=$(( TOTAL_WRONG_OWNER + WRONG_OWNER ))
    else
        log "Ownership already correct — applying mode only"
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would apply:"
        warn "  chown -R $PERMISSIONS_OWNER $share_path"
        warn "  find -type d → chmod ${PERMISSIONS_DIR_MODE:-755}"
        warn "  find -type f → chmod ${PERMISSIONS_FILE_MODE:-664}"
        PASS+=("$(basename "$share_path")")
        continue
    fi

    CHOWN_OK=true
    CHMOD_DIR_OK=true
    CHMOD_FILE_OK=true

    # Apply ownership first
    log "Applying ownership: $PERMISSIONS_OWNER..."
    chown -R "$PERMISSIONS_OWNER" "$share_path" 2>/dev/null || CHOWN_OK=false

    # Apply directory permissions — separate pass (dirs need execute bit)
    log "Applying directory permissions: ${PERMISSIONS_DIR_MODE:-755}..."
    find "$share_path" -type d \
        -exec chmod "${PERMISSIONS_DIR_MODE:-755}" {} + 2>/dev/null || CHMOD_DIR_OK=false

    # Apply file permissions — no execute bit on media files
    log "Applying file permissions: ${PERMISSIONS_FILE_MODE:-664}..."
    find "$share_path" -type f \
        -exec chmod "${PERMISSIONS_FILE_MODE:-664}" {} + 2>/dev/null || CHMOD_FILE_OK=false

    if [[ "$CHOWN_OK" == true && "$CHMOD_DIR_OK" == true && "$CHMOD_FILE_OK" == true ]]; then
        log "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"
        PASS+=("$(basename "$share_path")")
    else
        error "$(basename "$share_path") — repair failed"
        error "  chown: $CHOWN_OK  chmod dirs: $CHMOD_DIR_OK  chmod files: $CHMOD_FILE_OK"
        FAIL+=("$(basename "$share_path")")
    fi
done

END=$(date +%s)

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY PERMISSIONS REPAIR SUMMARY ━━━━━"
echo "$ICON_HOST    Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_PERMS   Dir mode:  ${PERMISSIONS_DIR_MODE:-755}"
echo "$ICON_PERMS   File mode: ${PERMISSIONS_FILE_MODE:-664}"
echo "$ICON_PERMS   Owner:     $PERMISSIONS_OWNER"
echo "$ICON_TIME    Duration:  $(format_duration $(( END - START )))"
echo ""
[[ ${#PASS[@]} -gt 0 ]] && log "Pass: ${PASS[*]}"
[[ ${#FAIL[@]} -gt 0 ]] && echo "$ICON_ERROR Fail: ${FAIL[*]}"

if [[ "$TOTAL_WRONG_OWNER" -gt 0 ]]; then
    warn "Total wrong-owner files fixed: $TOTAL_WRONG_OWNER"
fi
echo ""

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — no changes made"
elif [[ ${#FAIL[@]} -gt 0 ]]; then
    echo "$ICON_ERROR Status: SOME REPAIRS FAILED"
    notify "Permissions repair failed on $(hostname) — ${FAIL[*]}" \
        "Permissions Repair" "warning"
else
    echo "$ICON_DONE Status: done — ${#PASS[@]} path(s) repaired"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

[[ ${#FAIL[@]} -gt 0 ]] && exit 1
exit 0

-        log "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"

+        echo "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"


The file /boot/config/plugins/varaverk/Tools/docker_prune_images.sh has been updated successfully. (file state is current in your context — no need to Read it back)

#!/bin/bash
# ==============================================================================================
# ============================= Docker Prune Images ============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Removes orphaned Docker images. Two modes:
#
# Default — dangling only (safe, fast):
#   Removes untagged images (no name, no container reference). These accumulate
#   after container updates pull a new image, leaving the old one untagged.
#   Running containers are never affected.
#
# --all — full orphan cleanup:
#   Step 1: removes stopped containers (exited/created state).
#   Step 2: removes all images not used by any running container.
#   Use this to clear tagged images left behind by removed or stopped apps.
#   CAUTION: also removes intentionally stopped containers — only run when you
#   know all stopped containers are safe to delete.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_prune_images.sh
#     Remove dangling (untagged) images only.
#
# docker_prune_images.sh --all
#     Remove stopped containers, then remove all unused images.
#
# docker_prune_images.sh --dry-run
#     Show what would be removed without making changes.
#
# docker_prune_images.sh --status
#     Show dangling images and stopped containers. No changes.
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "$SCRIPT_DIR/../load_config.sh"

# ── Handle --all before parse_args ────────────────────────────────────────────
ALL_MODE=false
FILTERED_ARGS=()
for _arg in "$@"; do
    [[ "$_arg" == "--all" ]] && ALL_MODE=true || FILTERED_ARGS+=("$_arg")
done
parse_args "${FILTERED_ARGS[@]}"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

acquire_lock

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY DOCKER PRUNE IMAGES STATUS ━━━━━"

    DANGLING=$(docker images -f "dangling=true" --format "{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null)
    STOPPED=$(docker ps -a --filter "status=exited" --filter "status=created" \
        --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null)

    echo ""
    echo "$ICON_CONTAINERS  Dangling images (no tag, no container):"
    if [[ -z "$DANGLING" ]]; then
        echo "    none"
    else
        echo "$DANGLING" | while IFS=$'\t' read -r id repo size age; do
            echo "    $id   $repo   $size   $age"
        done
    fi

    echo ""
    echo "$ICON_CONTAINERS  Stopped containers (--all would remove these first):"
    if [[ -z "$STOPPED" ]]; then
        echo "    none"
    else
        echo "$STOPPED" | while IFS=$'\t' read -r id name image status; do
            echo "    $name   ($image)   $status"
        done
    fi

    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ━━━ Run ━━━
# ==============================================================================================
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
log "$ICON_GEAR Config: mode=${MODE_LABEL} dry-run=${DRY_RUN}"
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

TOTAL_RECLAIMED=0

# ── Step 1 (--all only): remove stopped containers ────────────────────────────
if [[ "$ALL_MODE" == true ]]; then
    STOPPED_IDS=$(docker ps -a --filter "status=exited" --filter "status=created" -q 2>/dev/null)
    STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)

    if [[ "$STOPPED_COUNT" -eq 0 ]]; then
        log "No stopped containers"
    elif [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
        docker ps -a --filter "status=exited" --filter "status=created" \
            --format "  {{.Names}}   {{.Image}}   {{.Status}}" 2>/dev/null
    else
        log "Removing $STOPPED_COUNT stopped container(s)..."
        docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
        success "Removed $STOPPED_COUNT stopped container(s)"
    fi
    echo ""
fi

# ── Step 2: prune images ───────────────────────────────────────────────────────
PRUNE_FLAGS=$([[ "$ALL_MODE" == true ]] && echo "-a" || echo "")
PRUNE_FILTER=$([[ "$ALL_MODE" == true ]] && echo "" || echo '-f "dangling=true"')

# Preview count
if [[ "$ALL_MODE" == true ]]; then
    # Images not used by any running container
    RUNNING_IMAGES=$(docker ps --format "{{.Image}}" 2>/dev/null)
    UNUSED_COUNT=$(docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null \
        | grep -vxF "$RUNNING_IMAGES" | grep -c . || echo 0)
    TARGET_LABEL="$UNUSED_COUNT unused image(s)"
else
    DANGLING_IDS=$(docker images -f "dangling=true" -q 2>/dev/null)
    UNUSED_COUNT=$(echo "$DANGLING_IDS" | grep -c . || echo 0)
    TARGET_LABEL="$UNUSED_COUNT dangling image(s)"
fi

if [[ "$UNUSED_COUNT" -eq 0 ]]; then
    success "No images to remove"
    exit 0
fi

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would remove $TARGET_LABEL:"
    if [[ "$ALL_MODE" == true ]]; then
        docker images --format "  {{.Repository}}:{{.Tag}}   {{.Size}}   {{.CreatedSince}}" 2>/dev/null \
            | grep -vF "$(docker ps --format '{{.Image}}' 2>/dev/null)" || true
    else
        docker images -f "dangling=true" \
            --format "  {{.ID}}   {{.Size}}   created {{.CreatedSince}}" 2>/dev/null
    fi
    exit 0
fi

log "Pruning $TARGET_LABEL..."
OUTPUT=$(docker image prune $PRUNE_FLAGS -f 2>&1)
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$OUTPUT" | sed 's/^/  /'
RECLAIMED=$(echo "$OUTPUT" | grep -E "^Total reclaimed" || echo "Total reclaimed space: unknown")
success "Done — $RECLAIMED"


log "Removing $STOPPED_COUNT stopped container(s)..."

echo "Removing $STOPPED_COUNT stopped container(s)..."


#!/bin/bash
# ==============================================================================================
# ============================= Docker Prune Images ============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Removes orphaned Docker images. Two modes:
#
# Default — dangling only (safe, fast):
#   Removes untagged images (no name, no container reference). These accumulate
#   after container updates pull a new image, leaving the old one untagged.
#   Running containers are never affected.
#
# --all — full orphan cleanup:
#   Step 1: removes stopped containers (exited/created state).
#   Step 2: removes all images not used by any running container.
#   Use this to clear tagged images left behind by removed or stopped apps.
#   CAUTION: also removes intentionally stopped containers — only run when you
#   know all stopped containers are safe to delete.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_prune_images.sh
#     Remove dangling (untagged) images only.
#
# docker_prune_images.sh --all
#     Remove stopped containers, then remove all unused images.
#
# docker_prune_images.sh --dry-run
#     Show what would be removed without making changes.
#
# docker_prune_images.sh --status
#     Show dangling images and stopped containers. No changes.
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "$SCRIPT_DIR/../load_config.sh"

# ── Handle --all before parse_args ────────────────────────────────────────────
ALL_MODE=false
FILTERED_ARGS=()
for _arg in "$@"; do
    [[ "$_arg" == "--all" ]] && ALL_MODE=true || FILTERED_ARGS+=("$_arg")
done
parse_args "${FILTERED_ARGS[@]}"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

acquire_lock

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY DOCKER PRUNE IMAGES STATUS ━━━━━"

    DANGLING=$(docker images -f "dangling=true" --format "{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null)
    STOPPED=$(docker ps -a --filter "status=exited" --filter "status=created" \
        --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null)

    echo ""
    echo "$ICON_CONTAINERS  Dangling images (no tag, no container):"
    if [[ -z "$DANGLING" ]]; then
        echo "    none"
    else
        echo "$DANGLING" | while IFS=$'\t' read -r id repo size age; do
            echo "    $id   $repo   $size   $age"
        done
    fi

    echo ""
    echo "$ICON_CONTAINERS  Stopped containers (--all would remove these first):"
    if [[ -z "$STOPPED" ]]; then
        echo "    none"
    else
        echo "$STOPPED" | while IFS=$'\t' read -r id name image status; do
            echo "    $name   ($image)   $status"
        done
    fi

    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ━━━ Run ━━━
# ==============================================================================================
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
log "$ICON_GEAR Config: mode=${MODE_LABEL} dry-run=${DRY_RUN}"
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

TOTAL_RECLAIMED=0

# ── Step 1 (--all only): remove stopped containers ────────────────────────────
if [[ "$ALL_MODE" == true ]]; then
    STOPPED_IDS=$(docker ps -a --filter "status=exited" --filter "status=created" -q 2>/dev/null)
    STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)

    if [[ "$STOPPED_COUNT" -eq 0 ]]; then
        echo "No stopped containers"
    elif [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
        docker ps -a --filter "status=exited" --filter "status=created" \
            --format "  {{.Names}}   {{.Image}}   {{.Status}}" 2>/dev/null
    else
        log "Removing $STOPPED_COUNT stopped container(s)..."
        docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
        success "Removed $STOPPED_COUNT stopped container(s)"
    fi
    echo ""
fi

# ── Step 2: prune images ───────────────────────────────────────────────────────
PRUNE_FLAGS=$([[ "$ALL_MODE" == true ]] && echo "-a" || echo "")
PRUNE_FILTER=$([[ "$ALL_MODE" == true ]] && echo "" || echo '-f "dangling=true"')

# Preview count
if [[ "$ALL_MODE" == true ]]; then
    # Images not used by any running container
    RUNNING_IMAGES=$(docker ps --format "{{.Image}}" 2>/dev/null)
    UNUSED_COUNT=$(docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null \
        | grep -vxF "$RUNNING_IMAGES" | grep -c . || echo 0)
    TARGET_LABEL="$UNUSED_COUNT unused image(s)"
else
    DANGLING_IDS=$(docker images -f "dangling=true" -q 2>/dev/null)
    UNUSED_COUNT=$(echo "$DANGLING_IDS" | grep -c . || echo 0)
    TARGET_LABEL="$UNUSED_COUNT dangling image(s)"
fi

if [[ "$UNUSED_COUNT" -eq 0 ]]; then
    success "No images to remove"
    exit 0
fi

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would remove $TARGET_LABEL:"
    if [[ "$ALL_MODE" == true ]]; then
        docker images --format "  {{.Repository}}:{{.Tag}}   {{.Size}}   {{.CreatedSince}}" 2>/dev/null \
            | grep -vF "$(docker ps --format '{{.Image}}' 2>/dev/null)" || true
    else
        docker images -f "dangling=true" \
            --format "  {{.ID}}   {{.Size}}   created {{.CreatedSince}}" 2>/dev/null
    fi
    exit 0
fi

log "Pruning $TARGET_LABEL..."
OUTPUT=$(docker image prune $PRUNE_FLAGS -f 2>&1)
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$OUTPUT" | sed 's/^/  /'
RECLAIMED=$(echo "$OUTPUT" | grep -E "^Total reclaimed" || echo "Total reclaimed space: unknown")
success "Done — $RECLAIMED"

-        log "Removing $STOPPED_COUNT stopped container(s)..."

+        echo "Removing $STOPPED_COUNT stopped container(s)..."


215	if [[ "${#MISSING[@]}" -eq 0 ]]; then
216	    echo "Lidarr already tracks everything played on Emby"
217	    exit 0
218	fi
219
220	echo ""
221	echo "━━━ $ICON_SUMMARY Artists to add (${#MISSING[@]}) ━━━"
222	for a in "${MISSING[@]}"; do log "  $a"; done
223
224	if [[ "$DRY_RUN" == true ]]; then
225	    warn "DRY RUN complete — run without --dry-run to add these artists"
226	    exit 0
227	fi
228
229	# ==============================================================================================

if [[ "${#MISSING[@]}" -eq 0 ]]; then
    echo "Lidarr already tracks everything played on Emby"
    exit 0
fi

echo ""
echo "━━━ $ICON_SUMMARY Artists to add (${#MISSING[@]}) ━━━"
for a in "${MISSING[@]}"; do log "  $a"; done

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN complete — run without --dry-run to add these artists"
    exit 0
fi

# ==============================================================================================


for a in "${MISSING[@]}"; do log "  $a"; done

for a in "${MISSING[@]}"; do echo "  $a"; done


The file /boot/config/plugins/varaverk/Tools/emby_to_lidarr_sync.sh has been updated successfully. (file state is current in your context — no need to Read it back)

echo "━━━ $ICON_SUMMARY Artists to add (${#MISSING[@]}) ━━━"

-for a in "${MISSING[@]}"; do log "  $a"; done

+for a in "${MISSING[@]}"; do echo "  $a"; done

warn "DRY RUN complete — run without --dry-run to add these artists"


Now System_Essentials:


log "Own conf cached ✅" || warn "Failed to cache own conf"

echo "Own conf cached ✅" || warn "Failed to cache own conf"


log "Pulled ${partner_slot}.conf from $partner_host ✅"

echo "Pulled ${partner_slot}.conf from $partner_host ✅"


log "Pushed ${MY_ID,,}.conf to $partner_host ✅"

echo "Pushed ${MY_ID,,}.conf to $partner_host ✅"


The file /boot/config/plugins/varaverk/System_Essentials/conf_sync.sh has been updated successfully. (file state is current in your context — no need to Read it back)

#!/bin/bash
# ==============================================================================================
# ============================= Conf Cache Sync ================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Maintains a RAM-resident conf cache at /tmp/.vv/config/cached/.confs/.
# Credentials and partner keys live in RAM only — never on disk across hosts.
#
# On array start (default / --array-start):
#   1. Copy own conf to local cache
#   2. Pull each available partner's conf from their disk → local cache
#   3. Push own conf to each available partner's /tmp/.vv/ cache
#
# On conf save (--push-only):
#   Fast path — push updated own conf to all partners' /tmp/.vv/ cache only.
#   No pulls, no local cache rebuild.
#
# Cache is /tmp (tmpfs) — cleared every reboot, repopulated by this script
# on next array start. Scripts source from cache for partner vars; own vars
# always come from disk (load_config.sh skips cached copy of own conf).
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
#   conf_sync.sh                   Full sync: pull from all partners + push to all partners
#   conf_sync.sh --push-only       Push own conf to all partners (fast, for conf-save hook)
#   conf_sync.sh --pull-only       Pull partner confs into local cache only (for intermediate orch)
#   conf_sync.sh --dry-run         Show what would happen, no changes
#   conf_sync.sh --log             Verbose output
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"

PUSH_ONLY=false
PULL_ONLY=false
FILTERED_ARGS=()
for arg in "$@"; do
    case "$arg" in
        --push-only) PUSH_ONLY=true ;;
        --pull-only) PULL_ONLY=true ;;
        *)           FILTERED_ARGS+=("$arg") ;;
    esac
done
parse_args "${FILTERED_ARGS[@]}"
detect_hosts

if [[ "${CONF_SYNC_ENABLED:-true}" == false ]]; then
    log "CONF_SYNC_ENABLED=false — skipping"
    exit 0
fi

CACHE_DIR="/tmp/.vv/config/cached/.confs"
MY_CONF="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
SSH_TIMEOUT=10

[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"

# ── Ensure cache dir exists ───────────────────────────────────────────────────
if [[ "$DRY_RUN" == false ]]; then
    mkdir -p "$CACHE_DIR"
fi

# ── Copy own conf into local cache ───────────────────────────────────────────
if [[ "$PUSH_ONLY" == false ]] && [[ "$PULL_ONLY" == false ]]; then
    if [[ -f "$MY_CONF" ]]; then
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/"
        else
            cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \
                log "Own conf cached ✅" || warn "Failed to cache own conf"
        fi
    else
        warn "Own conf not found: $MY_CONF"
    fi
fi

# ── Per-partner sync ──────────────────────────────────────────────────────────
PUSHED=0
PULLED=0
FAILED=0

for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
    partner_host="${!host_var}"
    [[ -z "$partner_host" ]] && continue
    [[ "${host_var,,}" == "${MY_ID,,}" ]] && continue

    partner_slot="${host_var,,}"   # e.g. host2
    partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)

    if [[ -z "$partner_ip" ]]; then
        warn "$partner_host — cannot resolve Tailscale IP, skipping"
        (( FAILED++ ))
        continue
    fi

    # ── Pull: grab partner's conf from their disk → our local cache ──────────
    if [[ "$PUSH_ONLY" == false ]]; then
        remote_conf="${SCRIPTS_DIR}/Configurations/${partner_slot}.conf"
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would pull $partner_host:$remote_conf → $CACHE_DIR/${partner_slot}.conf"
        elif timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
            "root@${partner_ip}:${remote_conf}" \
            "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then
            log "Pulled ${partner_slot}.conf from $partner_host ✅"
            (( PULLED++ ))
        else
            warn "Could not pull ${partner_slot}.conf from $partner_host"
            (( FAILED++ ))
        fi
    fi

    # ── Push: send own conf to partner's /tmp/.vv/ cache ────────────────────
    if [[ "$PULL_ONLY" == true ]]; then
        continue
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would push ${MY_ID,,}.conf → $partner_host:/tmp/.vv/config/cached/.confs/"
        continue
    fi

    # Ensure partner's cache dir exists, then SCP own conf into it
    timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "root@${partner_ip}" "mkdir -p '$CACHE_DIR'" 2>/dev/null

    if timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "$MY_CONF" \
        "root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then
        log "Pushed ${MY_ID,,}.conf to $partner_host ✅"
        (( PUSHED++ ))
    else
        warn "Could not push to $partner_host"
        (( FAILED++ ))
    fi
done

# ── Summary ───────────────────────────────────────────────────────────────────
if [[ "$PUSH_ONLY" == true ]]; then
    info "Conf push complete — pushed to $PUSHED host(s)${FAILED:+, $FAILED failed}"
elif [[ "$PULL_ONLY" == true ]]; then
    info "Conf pull complete — pulled $PULLED partner conf(s)${FAILED:+, $FAILED failed}"
else
    info "Conf sync complete — pulled $PULLED, pushed $PUSHED${FAILED:+, $FAILED failed}"
fi

if [[ "$FAILED" -gt 0 ]]; then
    notify "Conf sync on $LOCAL_SERVER_NAME ($MY_ID) — $FAILED partner(s) failed. Partner config cache may be stale." \
        "Conf Sync" "warning"
    exit 1
fi

-                log "Own conf cached ✅" || warn "Failed to cache own conf"

+                echo "Own conf cached ✅" || warn "Failed to cache own conf"