570	    else
571	        log "/root/.ssh/authorized_keys not found — nothing to remove locally"
572	        SSH_REVOKE_LOCAL_OK=true
573	    fi
574	}
575
576	# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
577	FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
578	FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"
579
580	# Derive short partner name: strip unraid- prefix (case-insensitive) if present
581	derive_partner_folder_name() {
582	    local hostname="$1"
583	    local short="${hostname,,}"
584	    [[ "$short" == unraid-* ]] && short="${short:7}"
585	    # Capitalise first char for readability: jayred365 → Jayred365-fallback
586	    echo "${short^}-Fallback"
587	}
588
589	folderview3_ensure_plugin() {
590	    if [[ -d "$FOLDERVIEW3_DIR" ]]; then
591	        log "FolderView3 plugin present ✅"
592	        return 0
593	    fi
594	    if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
595	        warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"
596	        warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"
597	        return 1
598	    fi
599	    warn "FolderView3 not found — installing from CA..."
600	    if [[ "$DRY_RUN" == true ]]; then
601	        warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"
602	        return 0
603	    fi
604	    plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \
605	        log "FolderView3 installed ✅" || {
606	        warn "FolderView3 install failed — folder will not be created"
607	        return 1
608	    }
609	}
610
611	folderview3_create_partner_folder() {
612	    local partner_name="$1"
613	    shift
614	    local containers=("$@")
615
616	    log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."
617
618	    folderview3_ensure_plugin || return 1
619
620	    if [[ "$DRY_RUN" == true ]]; then
621	        warn "DRY RUN — would create FolderView3 folder: $partner_name"
622	        for c in "${containers[@]}"; do
623	            [[ -n "$c" ]] && warn "  DRY RUN — container: $c"
624	        done
625	        return 0
626	    fi
627
628	    # Initialise JSON if missing
629	    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
630	        mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"
631	        echo '{}' > "$FOLDERVIEW3_JSON"
632	    fi
633
634	    # Check folder doesn't already exist
635	    local existing
636	    existing=$(jq -r --arg name "$partner_name" \
637	        'to_entries[] | select(.value.name == $name) | .key' \
638	        "$FOLDERVIEW3_JSON" 2>/dev/null)
639	    if [[ -n "$existing" ]]; then
640	        log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"
641	        return 0
642	    fi
643
644	    # Build containers JSON array
645	    local containers_json
646	    containers_json=$(printf '%s\n' "${containers[@]}" | \
647	        grep -v '^$' | jq -R . | jq -s .)
648
649	    # Generate a stable random ID from timestamp+name
650	    local folder_id
651	    folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)
652
653	    jq --arg id "$folder_id" --arg name "$partner_name" \
654	       --argjson containers "$containers_json" \
655	       '.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \
656	       "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
657	       mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
658	       log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {
659	       warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"
660	       return 1
661	    }
662	}
663
664	folderview3_remove_partner_folder() {
665	    local partner_name="$1"
666
667	    log "FolderView3: removing folder '$partner_name' and stopping its containers..."
668
669	    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
670	        log "FolderView3: JSON not found — nothing to remove"
671	        return 0
672	    fi
673
674	    if ! command -v jq >/dev/null 2>&1; then
675	        warn "jq not found — cannot manage FolderView3 JSON"
676	        return 1
677	    fi
678
679	    # Get containers from this folder
680	    local containers_json
681	    containers_json=$(jq -r --arg name "$partner_name" \
682	        'to_entries[] | select(.value.name == $name) | .value.containers[]' \
683	        "$FOLDERVIEW3_JSON" 2>/dev/null)
684
685	    if [[ -z "$containers_json" ]]; then
686	        log "FolderView3: folder '$partner_name' not found — nothing to remove"
687	        return 0
688	    fi
689
690	    # Stop and remove each container in the folder
691	    local stopped=0 removed=0 failed=0
692	    while IFS= read -r container; do
693	        [[ -z "$container" ]] && continue
694	        if [[ "$DRY_RUN" == true ]]; then
695	            warn "DRY RUN — would stop + rm: $container"
696	            continue
697	        fi
698	        # Stop if running
699	        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
700	            if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then
701	                log "$container stopped ✅"
702	                _PM_TRAP_STOPPED+=("$container")
703	                (( stopped++ ))
704	            else
705	                warn "$container stop failed"
706	                (( failed++ ))
707	            fi
708	            if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then
709	                log "$container removed ✅"
710	                (( removed++ ))
711	            else
712	                warn "$container rm failed (may not exist)"
713	            fi
714	        else
715	            log "$container not found locally — skipping"
716	        fi
717	    done <<< "$containers_json"
718
719	    # Remove folder entry from JSON
720	    if [[ "$DRY_RUN" == false ]]; then
721	        jq --arg name "$partner_name" \
722	            'with_entries(select(.value.name != $name))' \
723	            "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
724	            mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
725	            log "FolderView3: folder '$partner_name' removed ✅" || \
726	            warn "FolderView3: failed to remove folder from JSON"
727	    else
728	        warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"
729	    fi
730
731	    [[ "$DRY_RUN" == false ]] && \
732	        log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"
733	    return 0
734	}
735
736	# Gather all partner fallback containers for this server (all tiers)
737	gather_partner_fallback_containers() {
738	    local out_var="$1"
739	    eval "${out_var}=()"
740	    local tier var
741	    for tier in TIER1 TIER2 TIER3 TIER4; do
742	        var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_${tier}"
743	        # Check if var is set and is an array
744	        if declare -p "$var" >/dev/null 2>&1; then

    else
        log "/root/.ssh/authorized_keys not found — nothing to remove locally"
        SSH_REVOKE_LOCAL_OK=true
    fi
}

# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"

# Derive short partner name: strip unraid- prefix (case-insensitive) if present
derive_partner_folder_name() {
    local hostname="$1"
    local short="${hostname,,}"
    [[ "$short" == unraid-* ]] && short="${short:7}"
    # Capitalise first char for readability: jayred365 → Jayred365-fallback
    echo "${short^}-Fallback"
}

folderview3_ensure_plugin() {
    if [[ -d "$FOLDERVIEW3_DIR" ]]; then
        log "FolderView3 plugin present ✅"
        return 0
    fi
    if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
        warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"
        warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"
        return 1
    fi
    warn "FolderView3 not found — installing from CA..."
    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"
        return 0
    fi
    plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \
        log "FolderView3 installed ✅" || {
        warn "FolderView3 install failed — folder will not be created"
        return 1
    }
}

folderview3_create_partner_folder() {
    local partner_name="$1"
    shift
    local containers=("$@")

    log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."

    folderview3_ensure_plugin || return 1

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would create FolderView3 folder: $partner_name"
        for c in "${containers[@]}"; do
            [[ -n "$c" ]] && warn "  DRY RUN — container: $c"
        done
        return 0
    fi

    # Initialise JSON if missing
    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
        mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"
        echo '{}' > "$FOLDERVIEW3_JSON"
    fi

    # Check folder doesn't already exist
    local existing
    existing=$(jq -r --arg name "$partner_name" \
        'to_entries[] | select(.value.name == $name) | .key' \
        "$FOLDERVIEW3_JSON" 2>/dev/null)
    if [[ -n "$existing" ]]; then
        log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"
        return 0
    fi

    # Build containers JSON array
    local containers_json
    containers_json=$(printf '%s\n' "${containers[@]}" | \
        grep -v '^$' | jq -R . | jq -s .)

    # Generate a stable random ID from timestamp+name
    local folder_id
    folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)

    jq --arg id "$folder_id" --arg name "$partner_name" \
       --argjson containers "$containers_json" \
       '.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \
       "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
       mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
       log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {
       warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"
       return 1
    }
}

folderview3_remove_partner_folder() {
    local partner_name="$1"

    log "FolderView3: removing folder '$partner_name' and stopping its containers..."

    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
        log "FolderView3: JSON not found — nothing to remove"
        return 0
    fi

    if ! command -v jq >/dev/null 2>&1; then
        warn "jq not found — cannot manage FolderView3 JSON"
        return 1
    fi

    # Get containers from this folder
    local containers_json
    containers_json=$(jq -r --arg name "$partner_name" \
        'to_entries[] | select(.value.name == $name) | .value.containers[]' \
        "$FOLDERVIEW3_JSON" 2>/dev/null)

    if [[ -z "$containers_json" ]]; then
        log "FolderView3: folder '$partner_name' not found — nothing to remove"
        return 0
    fi

    # Stop and remove each container in the folder
    local stopped=0 removed=0 failed=0
    while IFS= read -r container; do
        [[ -z "$container" ]] && continue
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would stop + rm: $container"
            continue
        fi
        # Stop if running
        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
            if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then
                log "$container stopped ✅"
                _PM_TRAP_STOPPED+=("$container")
                (( stopped++ ))
            else
                warn "$container stop failed"
                (( failed++ ))
            fi
            if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then
                log "$container removed ✅"
                (( removed++ ))
            else
                warn "$container rm failed (may not exist)"
            fi
        else
            log "$container not found locally — skipping"
        fi
    done <<< "$containers_json"

    # Remove folder entry from JSON
    if [[ "$DRY_RUN" == false ]]; then
        jq --arg name "$partner_name" \
            'with_entries(select(.value.name != $name))' \
            "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
            mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
            log "FolderView3: folder '$partner_name' removed ✅" || \
            warn "FolderView3: failed to remove folder from JSON"
    else
        warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"
    fi

    [[ "$DRY_RUN" == false ]] && \
        log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"
    return 0
}

# Gather all partner fallback containers for this server (all tiers)
gather_partner_fallback_containers() {
    local out_var="$1"
    eval "${out_var}=()"
    local tier var
    for tier in TIER1 TIER2 TIER3 TIER4; do
        var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_${tier}"
        # Check if var is set and is an array
        if declare -p "$var" >/dev/null 2>&1; then


795	            continue
796	        fi
797	        if timeout "${DOCKER_TIMEOUT:-30}" docker start "$container" >/dev/null 2>&1; then
798	            log "$container started ✅"
799	        else
800	            warn "$container failed to start — check manually"
801	        fi
802	    done
803	}
804
805	# Remove partnership containers on this server + their appdata bind-mount paths.
806	# Uses FolderView3 folder if enabled (precise list), else falls back to FALLBACK_*_COVERS_* config.
807	# Appdata paths collected via docker inspect BEFORE removal — inspect fails on removed containers.
808	# Safety gate: only paths matching /mnt/*/appdata* are deleted.
809	cleanup_partner_containers() {
810	    local folder_name="$1"
811	    declare -a containers=()
812	    gather_partner_fallback_containers containers
813
814	    if [[ ${#containers[@]} -eq 0 ]]; then
815	        log "No partner containers found to remove"
816	        if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
817	            folderview3_remove_partner_folder "$folder_name"
818	        fi
819	        return 0
820	    fi
821
822	    # Collect appdata paths BEFORE any removal — inspect fails once container is gone
823	    local all_appdata_paths=""
824	    for container in "${containers[@]}"; do
825	        [[ -z "$container" ]] && continue
826	        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
827	            local cpaths
828	            cpaths=$(docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
829	                "$container" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
830	            [[ -n "$cpaths" ]] && all_appdata_paths+=$'\n'"$cpaths"
831	        fi
832	    done
833
834	    # Remove containers
835	    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
836	        folderview3_remove_partner_folder "$folder_name"
837	    else
838	        for container in "${containers[@]}"; do
839	            [[ -z "$container" ]] && continue
840	            if [[ "$DRY_RUN" == true ]]; then
841	                warn "DRY RUN — would stop + rm: $container"
842	                continue
843	            fi
844	            if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
845	                timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1 || true
846	                _PM_TRAP_STOPPED+=("$container")
847	                timeout "${DOCKER_TIMEOUT:-30}" docker rm   "$container" >/dev/null 2>&1 && \
848	                    log "$container removed ✅" || warn "$container rm failed"
849	            else
850	                log "$container not found — skipping"
851	            fi
852	        done
853	    fi
854

            continue
        fi
        if timeout "${DOCKER_TIMEOUT:-30}" docker start "$container" >/dev/null 2>&1; then
            log "$container started ✅"
        else
            warn "$container failed to start — check manually"
        fi
    done
}

# Remove partnership containers on this server + their appdata bind-mount paths.
# Uses FolderView3 folder if enabled (precise list), else falls back to FALLBACK_*_COVERS_* config.
# Appdata paths collected via docker inspect BEFORE removal — inspect fails on removed containers.
# Safety gate: only paths matching /mnt/*/appdata* are deleted.
cleanup_partner_containers() {
    local folder_name="$1"
    declare -a containers=()
    gather_partner_fallback_containers containers

    if [[ ${#containers[@]} -eq 0 ]]; then
        log "No partner containers found to remove"
        if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
            folderview3_remove_partner_folder "$folder_name"
        fi
        return 0
    fi

    # Collect appdata paths BEFORE any removal — inspect fails once container is gone
    local all_appdata_paths=""
    for container in "${containers[@]}"; do
        [[ -z "$container" ]] && continue
        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
            local cpaths
            cpaths=$(docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
                "$container" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
            [[ -n "$cpaths" ]] && all_appdata_paths+=$'\n'"$cpaths"
        fi
    done

    # Remove containers
    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
        folderview3_remove_partner_folder "$folder_name"
    else
        for container in "${containers[@]}"; do
            [[ -z "$container" ]] && continue
            if [[ "$DRY_RUN" == true ]]; then
                warn "DRY RUN — would stop + rm: $container"
                continue
            fi
            if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
                timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1 || true
                _PM_TRAP_STOPPED+=("$container")
                timeout "${DOCKER_TIMEOUT:-30}" docker rm   "$container" >/dev/null 2>&1 && \
                    log "$container removed ✅" || warn "$container rm failed"
            else
                log "$container not found — skipping"
            fi
        done
    fi


1265	    fi
1266
1267	    echo ""
1268	    echo "  Auth WebUIs:"
1269	    for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]}"; do
1270	        [[ -z "$entry" ]] && continue
1271	        echo "    ${entry%%|*} → port ${entry##*|}"
1272	    done
1273
1274	    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
1275	        echo ""
1276	        FOLDER_STATUS_NAME=$(derive_partner_folder_name "$REMOTE_SERVER_NAME")
1277	        if [[ -f "$FOLDERVIEW3_JSON" ]]; then
1278	            FOLDER_EXISTS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
1279	                'to_entries[] | select(.value.name == $name) | .key' \
1280	                "$FOLDERVIEW3_JSON" 2>/dev/null)
1281	            if [[ -n "$FOLDER_EXISTS" ]]; then
1282	                FOLDER_CONTAINERS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
1283	                    '[to_entries[] | select(.value.name == $name) | .value.containers[]] | join(", ")' \
1284	                    "$FOLDERVIEW3_JSON" 2>/dev/null)
1285	                echo "  FolderView3:  $FOLDER_STATUS_NAME ✅"
1286	                echo "    Containers: $FOLDER_CONTAINERS"
1287	            else
1288	                echo "  FolderView3:  $FOLDER_STATUS_NAME — not found in JSON"
1289	            fi
1290	        else
1291	            echo "  FolderView3:  config not found ($FOLDERVIEW3_JSON)"
1292	        fi
1293	    fi
1294
1295	    if [[ -f "$OFFLINE_COUNTER" ]]; then
1296	        OFFLINE_DAYS=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
1297	        [[ "$OFFLINE_DAYS" -gt 0 ]] && \
1298	            echo "" && \
1299	            echo "  ⚠  Remote offline counter: ${OFFLINE_DAYS}/${PARTNERSHIP_OFFLINE_THRESHOLD}"
1300	    fi
1301
1302	    echo ""
1303	    if [[ -f "$BLOCKLIST_FILE" ]] && [[ -s "$BLOCKLIST_FILE" ]]; then
1304	        echo "  Blocklist:"
1305	        while IFS='|' read -r host ts reason; do
1306	            echo "    ⛔ $host  (blocked $ts — $reason)"
1307	        done < "$BLOCKLIST_FILE"
1308	    else
1309	        echo "  Blocklist:    empty"
1310	    fi
1311
1312	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
1313	    exit 0
1314	fi

    fi

    echo ""
    echo "  Auth WebUIs:"
    for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]}"; do
        [[ -z "$entry" ]] && continue
        echo "    ${entry%%|*} → port ${entry##*|}"
    done

    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
        echo ""
        FOLDER_STATUS_NAME=$(derive_partner_folder_name "$REMOTE_SERVER_NAME")
        if [[ -f "$FOLDERVIEW3_JSON" ]]; then
            FOLDER_EXISTS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
                'to_entries[] | select(.value.name == $name) | .key' \
                "$FOLDERVIEW3_JSON" 2>/dev/null)
            if [[ -n "$FOLDER_EXISTS" ]]; then
                FOLDER_CONTAINERS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
                    '[to_entries[] | select(.value.name == $name) | .value.containers[]] | join(", ")' \
                    "$FOLDERVIEW3_JSON" 2>/dev/null)
                echo "  FolderView3:  $FOLDER_STATUS_NAME ✅"
                echo "    Containers: $FOLDER_CONTAINERS"
            else
                echo "  FolderView3:  $FOLDER_STATUS_NAME — not found in JSON"
            fi
        else
            echo "  FolderView3:  config not found ($FOLDERVIEW3_JSON)"
        fi
    fi

    if [[ -f "$OFFLINE_COUNTER" ]]; then
        OFFLINE_DAYS=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
        [[ "$OFFLINE_DAYS" -gt 0 ]] && \
            echo "" && \
            echo "  ⚠  Remote offline counter: ${OFFLINE_DAYS}/${PARTNERSHIP_OFFLINE_THRESHOLD}"
    fi

    echo ""
    if [[ -f "$BLOCKLIST_FILE" ]] && [[ -s "$BLOCKLIST_FILE" ]]; then
        echo "  Blocklist:"
        while IFS='|' read -r host ts reason; do
            echo "    ⛔ $host  (blocked $ts — $reason)"
        done < "$BLOCKLIST_FILE"
    else
        echo "  Blocklist:    empty"
    fi

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


lets remove it. our own page has support for peoplw who already have it, but is a standalone feature also


1410	if [[ "$MODE" == "onboard" ]]; then
1411	    echo ""
1412	    echo "━━━ $ICON_FALLBACK Onboard — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
1413
1414	    # ── LOCAL-ONLY PATH ──────────────────────────────────────────────────────────
1415	    # HOST1-local setup steps that don't need HOST2 present. Called from
1416	    # partnership_onboard.sh --phase1-only so HOST1 can complete its own side
1417	    # (FolderView3, setup.db flag) while waiting for HOST2 to install and onboard.
1418	    if [[ "$LOCAL_ONLY" == true ]]; then
1419	        log "Mode: local-only — skipping remote pre-flight and WebUI steps"
1420	        log "Owner: $OWNER_ID ($OWNER)  ·  Mirror: $MIRROR_ID ($MIRROR)"
1421	        echo ""
1422
1423	        if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
1424	            echo "FolderView3 Integration"
1425	            PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
1426	            declare -a PARTNER_CONTAINERS=()
1427	            gather_partner_fallback_containers PARTNER_CONTAINERS
1428	            if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
1429	                folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
1430	            else
1431	                log "No FALLBACK_${MY_ID}_COVERS_${MIRROR_ID}_TIER* containers — skipping folder"
1432	            fi
1433	        else
1434	            log "FolderView3 not configured — skipping"
1435	        fi
1436
1437	        # Enable partnership in master.conf + push to all hosts
1438	        echo ""
1439	        echo "Enabling partnership in master.conf..."
1440	        if [[ "$DRY_RUN" == false ]]; then
1441	            if command -v php &>/dev/null; then
1442	                php -r "
1443	require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
1444	\$master = vv_read_conf_raw('master.conf');
1445	\$master = preg_replace('/^\s*PARTNERSHIP_ENABLED\s*=.*/m', 'PARTNERSHIP_ENABLED=true', \$master);
1446	if (vv_write_conf_raw('master.conf', \$master)) {
1447	    \$results = vv_push_master_conf();
1448	    foreach (\$results as \$r) {
1449	        echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
1450	    }
1451	    echo 'PARTNERSHIP_ENABLED=true' . PHP_EOL;
1452	} else {
1453	    echo 'write_failed' . PHP_EOL;
1454	}
1455	" 2>/dev/null | grep -v '^$' | while IFS= read -r line; do
1456	                    [[ "$line" == "PARTNERSHIP_ENABLED=true" ]] && log "PARTNERSHIP_ENABLED=true in master.conf ✅" || log "$line"
1457	                done
1458	            else
1459	                warn "php not available — set PARTNERSHIP_ENABLED=true in master.conf manually"
1460	            fi
1461	        else
1462	            warn "DRY RUN — would set PARTNERSHIP_ENABLED=true in master.conf and push"
1463	        fi
1464
1465	        # Write HOST1_LOCAL_DONE flag to setup.db
1466	        local_state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
1467	        if [[ "$DRY_RUN" == false ]]; then
1468	            flag_key="${MY_ID}_LOCAL_DONE"
1469	            if grep -q "^${flag_key}=" "$local_state_file" 2>/dev/null; then

if [[ "$MODE" == "onboard" ]]; then
    echo ""
    echo "━━━ $ICON_FALLBACK Onboard — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

    # ── LOCAL-ONLY PATH ──────────────────────────────────────────────────────────
    # HOST1-local setup steps that don't need HOST2 present. Called from
    # partnership_onboard.sh --phase1-only so HOST1 can complete its own side
    # (FolderView3, setup.db flag) while waiting for HOST2 to install and onboard.
    if [[ "$LOCAL_ONLY" == true ]]; then
        log "Mode: local-only — skipping remote pre-flight and WebUI steps"
        log "Owner: $OWNER_ID ($OWNER)  ·  Mirror: $MIRROR_ID ($MIRROR)"
        echo ""

        if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
            echo "FolderView3 Integration"
            PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
            declare -a PARTNER_CONTAINERS=()
            gather_partner_fallback_containers PARTNER_CONTAINERS
            if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
                folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
            else
                log "No FALLBACK_${MY_ID}_COVERS_${MIRROR_ID}_TIER* containers — skipping folder"
            fi
        else
            log "FolderView3 not configured — skipping"
        fi

        # Enable partnership in master.conf + push to all hosts
        echo ""
        echo "Enabling partnership in master.conf..."
        if [[ "$DRY_RUN" == false ]]; then
            if command -v php &>/dev/null; then
                php -r "
require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
\$master = vv_read_conf_raw('master.conf');
\$master = preg_replace('/^\s*PARTNERSHIP_ENABLED\s*=.*/m', 'PARTNERSHIP_ENABLED=true', \$master);
if (vv_write_conf_raw('master.conf', \$master)) {
    \$results = vv_push_master_conf();
    foreach (\$results as \$r) {
        echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
    }
    echo 'PARTNERSHIP_ENABLED=true' . PHP_EOL;
} else {
    echo 'write_failed' . PHP_EOL;
}
" 2>/dev/null | grep -v '^$' | while IFS= read -r line; do
                    [[ "$line" == "PARTNERSHIP_ENABLED=true" ]] && log "PARTNERSHIP_ENABLED=true in master.conf ✅" || log "$line"
                done
            else
                warn "php not available — set PARTNERSHIP_ENABLED=true in master.conf manually"
            fi
        else
            warn "DRY RUN — would set PARTNERSHIP_ENABLED=true in master.conf and push"
        fi

        # Write HOST1_LOCAL_DONE flag to setup.db
        local_state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
        if [[ "$DRY_RUN" == false ]]; then
            flag_key="${MY_ID}_LOCAL_DONE"
            if grep -q "^${flag_key}=" "$local_state_file" 2>/dev/null; then


1560	    echo "━━━ $ICON_GEAR Write State ━━━"
1561	    NOW=$(date '+%Y-%m-%d %H:%M:%S')
1562
1563	    if [[ "$DRY_RUN" == false ]]; then
1564	        write_state_file "$LOCAL_STATE_FILE" "ACTIVE" "$NOW" "" "$LOCAL_SERVER_NAME" "onboard"
1565	        echo "Local state: ACTIVE ✅"
1566	        remove_from_blocklist "$MIRROR"
1567	        push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
1568	        echo "0" > "$OFFLINE_COUNTER"
1569	    else
1570	        warn "DRY RUN — would write ACTIVE state and push to remote"
1571	    fi
1572
1573	    # FolderView3 — create partner folder with this server's fallback containers for remote
1574	    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
1575	        echo ""
1576	        echo "━━━ $ICON_CONTAINERS FolderView3 Integration ━━━"
1577	        PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
1578	        declare -a PARTNER_CONTAINERS=()
1579	        gather_partner_fallback_containers PARTNER_CONTAINERS
1580	        if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
1581	            folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
1582	        else
1583	            log "No FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER* containers configured — skipping folder creation"
1584	        fi
1585	    fi
1586
1587	    # Emby admin provisioning — runs after container deployment (deploy step not yet built)
1588	    provision_emby_admin "$MIRROR_IP"
1589
1590	    # Summary
1591	    echo ""
1592	    echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
1593	    echo "  Owner:          $OWNER_ID ($OWNER_IP)"
1594	    echo "  Mirror:         $MIRROR_ID ($MIRROR_IP)"
1595	    echo "  WebUI failures: $WEBUI_FAILURES"
1596	    echo "  Sync interval:  ${PARTNERSHIP_SYNC_INTERVAL}min"
1597	    echo ""
1598	    if [[ "$DRY_RUN" == true ]]; then
1599	        warn "DRY RUN — no changes made"

    echo "━━━ $ICON_GEAR Write State ━━━"
    NOW=$(date '+%Y-%m-%d %H:%M:%S')

    if [[ "$DRY_RUN" == false ]]; then
        write_state_file "$LOCAL_STATE_FILE" "ACTIVE" "$NOW" "" "$LOCAL_SERVER_NAME" "onboard"
        echo "Local state: ACTIVE ✅"
        remove_from_blocklist "$MIRROR"
        push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
        echo "0" > "$OFFLINE_COUNTER"
    else
        warn "DRY RUN — would write ACTIVE state and push to remote"
    fi

    # FolderView3 — create partner folder with this server's fallback containers for remote
    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
        echo ""
        echo "━━━ $ICON_CONTAINERS FolderView3 Integration ━━━"
        PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
        declare -a PARTNER_CONTAINERS=()
        gather_partner_fallback_containers PARTNER_CONTAINERS
        if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
            folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
        else
            log "No FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER* containers configured — skipping folder creation"
        fi
    fi

    # Emby admin provisioning — runs after container deployment (deploy step not yet built)
    provision_emby_admin "$MIRROR_IP"

    # Summary
    echo ""
    echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
    echo "  Owner:          $OWNER_ID ($OWNER_IP)"
    echo "  Mirror:         $MIRROR_ID ($MIRROR_IP)"
    echo "  WebUI failures: $WEBUI_FAILURES"
    echo "  Sync interval:  ${PARTNERSHIP_SYNC_INTERVAL}min"
    echo ""
    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — no changes made"


Let me also check `cleanup_owner_containers_on_mirror` and `derive_partner_folder_name` usage in offboard to know what stays:


854
855	    # Delete appdata after containers are gone
856	    while IFS= read -r path; do
857	        [[ -z "$path" ]] && continue
858	        if [[ "$DRY_RUN" == true ]]; then
859	            warn "  DRY RUN — would rm -rf $path"
860	            continue
861	        fi
862	        rm -rf "$path" && log "  Appdata removed: $path ✅" || warn "  Failed to remove: $path"
863	    done <<< "$all_appdata_paths"
864	}
865
866	# SSH to mirror — remove all containers named *-${OWNER_SHORT} (owner's deployed containers)
867	# and their appdata bind-mount paths.
868	# Appdata paths collected via SSH docker inspect before removal, then deleted via SSH.
869	# Safety gate: only paths matching /mnt/*/appdata* are deleted on the remote.
870	cleanup_owner_containers_on_mirror() {
871	    local mirror_ip="$1"
872	    local owner_short
873	    owner_short=$(derive_short_name "$OWNER")
874
875	    log "Removing owner-deployed containers from $MIRROR..."
876	    if [[ "$DRY_RUN" == true ]]; then
877	        warn "DRY RUN — would remove *-${owner_short} containers + appdata from $MIRROR"
878	        return 0
879	    fi
880
881	    local container_list
882	    container_list=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
883	        -o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
884	        "docker ps -a --format '{{.Names}}' 2>/dev/null | grep -i -- '-${owner_short}$'" 2>/dev/null)
885
886	    if [[ -z "$container_list" ]]; then
887	        log "No *-${owner_short} containers found on $MIRROR"
888	        return 0
889	    fi
890
891	    while IFS= read -r container; do
892	        [[ -z "$container" ]] && continue
893
894	        # Collect appdata paths before removal
895	        local appdata_paths
896	        appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
897	            -o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
898	            "docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$container' 2>/dev/null \
899	             | awk -F: '{print \$1}' | grep '^/mnt/.*/appdata'" 2>/dev/null)
900
901	        timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
902	            -o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
903	            "docker stop '$container' >/dev/null 2>&1

    # Delete appdata after containers are gone
    while IFS= read -r path; do
        [[ -z "$path" ]] && continue
        if [[ "$DRY_RUN" == true ]]; then
            warn "  DRY RUN — would rm -rf $path"
            continue
        fi
        rm -rf "$path" && log "  Appdata removed: $path ✅" || warn "  Failed to remove: $path"
    done <<< "$all_appdata_paths"
}

# SSH to mirror — remove all containers named *-${OWNER_SHORT} (owner's deployed containers)
# and their appdata bind-mount paths.
# Appdata paths collected via SSH docker inspect before removal, then deleted via SSH.
# Safety gate: only paths matching /mnt/*/appdata* are deleted on the remote.
cleanup_owner_containers_on_mirror() {
    local mirror_ip="$1"
    local owner_short
    owner_short=$(derive_short_name "$OWNER")

    log "Removing owner-deployed containers from $MIRROR..."
    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would remove *-${owner_short} containers + appdata from $MIRROR"
        return 0
    fi

    local container_list
    container_list=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
        "docker ps -a --format '{{.Names}}' 2>/dev/null | grep -i -- '-${owner_short}$'" 2>/dev/null)

    if [[ -z "$container_list" ]]; then
        log "No *-${owner_short} containers found on $MIRROR"
        return 0
    fi

    while IFS= read -r container; do
        [[ -z "$container" ]] && continue

        # Collect appdata paths before removal
        local appdata_paths
        appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
            "docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$container' 2>/dev/null \
             | awk -F: '{print \$1}' | grep '^/mnt/.*/appdata'" 2>/dev/null)

        timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
            "docker stop '$container' >/dev/null 2>&1


Now I have the full picture. Here's everything that needs to go:

**containers.sh** — `platform_install_plugins()` and `platform_remove_folderview3_on_remote()`
**onboard.sh** — Step 2 + renumber 3-9 → 2-8, remove `STEP_PLUGINS_OK`
**offboard.sh** — FV3 call in Step 7, FV3 summary line
**manager.sh** — `FOLDERVIEW3_DIR/JSON`, `folderview3_ensure_plugin/create/remove()`, FV3 blocks in `cleanup_partner_containers`, status, and onboard; `derive_partner_folder_name()` (no remaining callers after removal); simplify `cleanup_partner_containers` to no args
**master.conf** — remove `PARTNERSHIP_FOLDERVIEW3` and `PARTNERSHIP_FOLDERVIEW3_URL`

Let me read the offboard section and manager's `cleanup_partner_containers` in full first:


395	MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
396	MIRROR_REACHABLE=false
397	[[ -n "$MIRROR_IP" ]] && MIRROR_REACHABLE=true
398
399	if [[ "$MIRROR_REACHABLE" == true ]]; then
400	    for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]}"; do
401	        [[ -z "$entry" ]] && continue
402	        container="${entry%%|*}"
403	        port="${entry##*|}"
404	        reconfigure_webui "$container" "$port" "localhost" \
405	            "$MIRROR_SSH_KEY" "$MIRROR_IP" "$MIRROR" || (( WEBUI_FAILURES++ ))
406	    done
407	else
408	    warn "$MIRROR unreachable — WebUI reconfiguration skipped"
409	    warn "$MIRROR will reconfigure its own WebUIs when it sees INACTIVE state on --check"
410	    (( WEBUI_FAILURES++ ))
411	fi
412
413	# ── Step 4: Disable critical sync ─────────────────────────────────────────────────────────────
414	echo ""

MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
MIRROR_REACHABLE=false
[[ -n "$MIRROR_IP" ]] && MIRROR_REACHABLE=true

if [[ "$MIRROR_REACHABLE" == true ]]; then
    for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]}"; do
        [[ -z "$entry" ]] && continue
        container="${entry%%|*}"
        port="${entry##*|}"
        reconfigure_webui "$container" "$port" "localhost" \
            "$MIRROR_SSH_KEY" "$MIRROR_IP" "$MIRROR" || (( WEBUI_FAILURES++ ))
    done
else
    warn "$MIRROR unreachable — WebUI reconfiguration skipped"
    warn "$MIRROR will reconfigure its own WebUIs when it sees INACTIVE state on --check"
    (( WEBUI_FAILURES++ ))
fi

# ── Step 4: Disable critical sync ─────────────────────────────────────────────────────────────
echo ""


415	echo "━━━ $ICON_GEAR Step 4/10 — Disable Critical Sync ━━━"
416
417	if [[ "$DRY_RUN" == false ]]; then
418	    update_master_conf "CRITICAL_RSYNC_ENABLED" "false"
419	    warn "CRITICAL_RSYNC_ENABLED=false ✅"
420	else
421	    warn "DRY RUN — would set CRITICAL_RSYNC_ENABLED=false"
422	fi
423
424	# ── Step 5: Local container cleanup ───────────────────────────────────────────────────────────
425	echo ""
426	echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"
427
428	PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
429	cleanup_partner_containers "$PARTNER_FOLDER_NAME"
430
431	# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────
432	start_own_stack
433
434	# ── Step 7: Remote container cleanup ──────────────────────────────────────────────────────────
435	echo ""
436	echo "━━━ $ICON_CONTAINERS Step 7/10 — Remote Container Cleanup ━━━"
437
438	if [[ "$MIRROR_REACHABLE" == true ]]; then
439	    # Remove auth/arr stack containers deployed during onboard (by config array)
440	    cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
441	    # Remove fallback coverage containers (by *-owner_short naming pattern)
442	    cleanup_owner_containers_on_mirror "$MIRROR_IP"
443	    # Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)
444	    OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")
445	    platform_remove_folderview3_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY" "$OWNER_FOLDER_ON_MIRROR" "$MIRROR"
446	else
447	    warn "$MIRROR unreachable — remote container cleanup skipped"
448	    warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"
449	fi
450
451	# ── Step 8: Restart mirror's own stack ────────────────────────────────────────────────────────
452	echo ""
453	echo "━━━ $ICON_START Step 8/10 — Restart Mirror Stack ━━━"
454
455	[[ "$MIRROR_REACHABLE" == true ]] && start_mirror_own_stack "$MIRROR_IP"
456
457	# ── Step 9: Revocation (Emby + SSH) ──────────────────────────────────────────────────────────
458	echo ""
459	echo "━━━ $ICON_SHIELD Step 9/10 — Revocation ━━━"
460
461	# Emby admin — before SSH key revocation while Emby still reachable
462	[[ "$MIRROR_REACHABLE" == true ]] && revoke_emby_admin "$MIRROR_IP"
463
464	# SSH key revocation — mutual, both directions; must run while Tailscale still active

echo "━━━ $ICON_GEAR Step 4/10 — Disable Critical Sync ━━━"

if [[ "$DRY_RUN" == false ]]; then
    update_master_conf "CRITICAL_RSYNC_ENABLED" "false"
    warn "CRITICAL_RSYNC_ENABLED=false ✅"
else
    warn "DRY RUN — would set CRITICAL_RSYNC_ENABLED=false"
fi

# ── Step 5: Local container cleanup ───────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"

PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
cleanup_partner_containers "$PARTNER_FOLDER_NAME"

# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────
start_own_stack

# ── Step 7: Remote container cleanup ──────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_CONTAINERS Step 7/10 — Remote Container Cleanup ━━━"

if [[ "$MIRROR_REACHABLE" == true ]]; then
    # Remove auth/arr stack containers deployed during onboard (by config array)
    cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
    # Remove fallback coverage containers (by *-owner_short naming pattern)
    cleanup_owner_containers_on_mirror "$MIRROR_IP"
    # Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)
    OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")
    platform_remove_folderview3_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY" "$OWNER_FOLDER_ON_MIRROR" "$MIRROR"
else
    warn "$MIRROR unreachable — remote container cleanup skipped"
    warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"
fi

# ── Step 8: Restart mirror's own stack ────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_START Step 8/10 — Restart Mirror Stack ━━━"

[[ "$MIRROR_REACHABLE" == true ]] && start_mirror_own_stack "$MIRROR_IP"

# ── Step 9: Revocation (Emby + SSH) ──────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_SHIELD Step 9/10 — Revocation ━━━"

# Emby admin — before SSH key revocation while Emby still reachable
[[ "$MIRROR_REACHABLE" == true ]] && revoke_emby_admin "$MIRROR_IP"

# SSH key revocation — mutual, both directions; must run while Tailscale still active


540	echo "  Step 5  — Local cleanup:  ✅"
541	echo "  Step 6  — Own stack:      started"
542	echo "  Step 7  — Remote cleanup: $( [[ "$MIRROR_REACHABLE" == true ]] && echo "✅" || echo "skipped (unreachable)" )"
543	echo "  Step 8  — Mirror stack:   $( [[ "$MIRROR_REACHABLE" == true ]] && echo "started" || echo "skipped (unreachable)" )"
544	echo "  Step 9  — Keys revoked:   $(_revoke_status)"
545	echo "  Step 10 — State:          INACTIVE ✅"
546	echo ""
547	echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
548	[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
549	    echo "  FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"
550	[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
551	    echo "  Tailscale:   $MIRROR removed ✅"
552	echo ""
553	echo "  $MIRROR leaves with:"
554	echo "    ✓ Current auth config (final sync)"
555	echo "    ✓ Auth WebUIs → localhost"
556	echo "    ✓ ${PARTNERSHIP_GRACE_HOURS:-6}hr to collect backups"
557	echo ""
558	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
559	    warn "$ICON_DONE DONE — clean separation complete ✅"
560	echo "━━━━━━━━━━━━━━━━━━━━━━━"
561	exit 0
562

echo "  Step 5  — Local cleanup:  ✅"
echo "  Step 6  — Own stack:      started"
echo "  Step 7  — Remote cleanup: $( [[ "$MIRROR_REACHABLE" == true ]] && echo "✅" || echo "skipped (unreachable)" )"
echo "  Step 8  — Mirror stack:   $( [[ "$MIRROR_REACHABLE" == true ]] && echo "started" || echo "skipped (unreachable)" )"
echo "  Step 9  — Keys revoked:   $(_revoke_status)"
echo "  Step 10 — State:          INACTIVE ✅"
echo ""
echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
    echo "  FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
    echo "  Tailscale:   $MIRROR removed ✅"
echo ""
echo "  $MIRROR leaves with:"
echo "    ✓ Current auth config (final sync)"
echo "    ✓ Auth WebUIs → localhost"
echo "    ✓ ${PARTNERSHIP_GRACE_HOURS:-6}hr to collect backups"
echo ""
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
    warn "$ICON_DONE DONE — clean separation complete ✅"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0


Now I have everything I need. Making all changes in sequence:


248	    # ── Step 2: Reconfigure local WebUIs → localhost ──────────────────────────────────────────
249	    echo ""
250	    echo "━━━ $ICON_CONTAINERS Step 2/8 — Reconfigure Local WebUIs → localhost ━━━"
251
252	    reconfigure_local_webuis "localhost" || STEP_WEBUI_OK=false
253
254	    # ── Step 3: Remove owner-deployed containers (auth/arr stack) locally ─────────────────────
255	    echo ""
256	    echo "━━━ $ICON_CONTAINERS Step 3/8 — Remove Owner-Deployed Containers ━━━"
257
258	    if [[ "$OWNER_REACHABLE" == true ]]; then
259	        cleanup_deployed_stack_locally "$OWNER_IP" "$OWNER_SSH_KEY" || STEP_STACK_CLEANUP_OK=false
260	    else
261	        warn "Owner unreachable — cannot read deployed stack list"
262	        warn "Auth/arr containers will remain — remove manually or re-run when owner is reachable"
263	        STEP_STACK_CLEANUP_OK=false
264	    fi
265
266	    # ── Step 4: Remove fallback coverage containers ───────────────────────────────────────────
267	    echo ""

    # ── Step 2: Reconfigure local WebUIs → localhost ──────────────────────────────────────────
    echo ""
    echo "━━━ $ICON_CONTAINERS Step 2/8 — Reconfigure Local WebUIs → localhost ━━━"

    reconfigure_local_webuis "localhost" || STEP_WEBUI_OK=false

    # ── Step 3: Remove owner-deployed containers (auth/arr stack) locally ─────────────────────
    echo ""
    echo "━━━ $ICON_CONTAINERS Step 3/8 — Remove Owner-Deployed Containers ━━━"

    if [[ "$OWNER_REACHABLE" == true ]]; then
        cleanup_deployed_stack_locally "$OWNER_IP" "$OWNER_SSH_KEY" || STEP_STACK_CLEANUP_OK=false
    else
        warn "Owner unreachable — cannot read deployed stack list"
        warn "Auth/arr containers will remain — remove manually or re-run when owner is reachable"
        STEP_STACK_CLEANUP_OK=false
    fi

    # ── Step 4: Remove fallback coverage containers ───────────────────────────────────────────
    echo ""


266	    # ── Step 4: Remove fallback coverage containers ───────────────────────────────────────────
267	    echo ""
268	    echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"
269
270	    PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
271	    cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false
272
273	    # ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────
274	    echo ""
275	    echo "━━━ $ICON_GEAR Step 5/8 — Disable Critical Sync ━━━"

    # ── Step 4: Remove fallback coverage containers ───────────────────────────────────────────
    echo ""
    echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"

    PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
    cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false

    # ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────
    echo ""
    echo "━━━ $ICON_GEAR Step 5/8 — Disable Critical Sync ━━━"


Good. Now I have all the touch points. Let's do all changes:


1	#!/bin/bash
2	# ==============================================================================================
3	# ====================== Partnership — Unraid Container Adapter ================================
4	# ==============================================================================================
5	#
6	# Sourced by partnership_onboard.sh and partnership_offboard.sh via:
7	#   source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
8	#
9	# Provides container deploy/cleanup functions specific to the Unraid platform:
10	#   - Docker container deployment from Unraid CA XML templates
11	#   - Community Applications plugin install via `plugin install`
12	#   - FolderView3 docker.json manipulation
13	#
14	# Functions use variables from the calling script's scope (sourced, not exec'd):
15	#   MIRROR, MIRROR_IP, MIRROR_SSH_KEY, SSH_TIMEOUT, DRY_RUN, SCRIPTS_ROOT
16	#
17	# ==============================================================================================
18
19	TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
20

#!/bin/bash
# ==============================================================================================
# ====================== Partnership — Unraid Container Adapter ================================
# ==============================================================================================
#
# Sourced by partnership_onboard.sh and partnership_offboard.sh via:
#   source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
#
# Provides container deploy/cleanup functions specific to the Unraid platform:
#   - Docker container deployment from Unraid CA XML templates
#   - Community Applications plugin install via `plugin install`
#   - FolderView3 docker.json manipulation
#
# Functions use variables from the calling script's scope (sourced, not exec'd):
#   MIRROR, MIRROR_IP, MIRROR_SSH_KEY, SSH_TIMEOUT, DRY_RUN, SCRIPTS_ROOT
#
# ==============================================================================================

TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"


Edit

# Provides container deploy/cleanup functions specific to the Unraid platform:
#   - Docker container deployment from Unraid CA XML templates
#   - Community Applications plugin install via `plugin install`
#   - FolderView3 docker.json manipulation

# Provides container deploy/cleanup functions specific to the Unraid platform:
#   - Docker container deployment from Unraid CA XML templates


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

# Provides container deploy/cleanup functions specific to the Unraid platform:
#   - Docker container deployment from Unraid CA XML templates
#   - Community Applications plugin install via `plugin install`
#   - FolderView3 docker.json manipulation

-#   - Community Applications plugin install via `plugin install`

-#   - FolderView3 docker.json manipulation

# Functions use variables from the calling script's scope (sourced, not exec'd):

#   MIRROR, MIRROR_IP, MIRROR_SSH_KEY, SSH_TIMEOUT, DRY_RUN, SCRIPTS_ROOT


Now remove `platform_install_plugins()` and `platform_remove_folderview3_on_remote()` from containers.sh:


# ==============================================================================================
# ── Install Unraid plugins on the mirror ──────────────────────────────────────────────────────
#
# Installs FolderView3 via `plugin install` — Unraid Community Applications command.
# Checks if already installed before attempting. Returns 1 if install fails.
# ==============================================================================================
platform_install_plugins() {
    local remote_ip="$1" ssh_key="$2"

    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" != true ]] || [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
        log "FolderView3 not configured — skipping"
        return 0
    fi

    local fv3_present
    fv3_present=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
        "test -d /usr/local/emhttp/plugins/folder.view3 && echo yes" 2>/dev/null)

    if [[ "$fv3_present" == "yes" ]]; then
        log "FolderView3 already installed on $MIRROR ✅"
        return 0
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would install FolderView3 on $MIRROR"
        return 0
    fi

    log "Installing FolderView3 on $MIRROR..."
    if timeout 60 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
        "plugin install '$PARTNERSHIP_FOLDERVIEW3_URL' 2>/dev/null && echo installed" \
        2>/dev/null | grep -q installed; then
        log "FolderView3 installed ✅"
        return 0
    else
        warn "FolderView3 install failed — install manually from Community Applications"
        return 1
    fi
}

# ==============================================================================================
# ── Wait for a container on the remote to be healthy/running ─────────────────────────────────


# ==============================================================================================
# ── Install Unraid plugins on the mirror ──────────────────────────────────────────────────────
#
# Installs FolderView3 via `plugin install` — Unraid Community Applications command.
# Checks if already installed before attempting. Returns 1 if install fails.
# ==============================================================================================
platform_install_plugins() {
    local remote_ip="$1" ssh_key="$2"

    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" != true ]] || [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
        log "FolderView3 not configured — skipping"
        return 0
    fi

    local fv3_present
    fv3_present=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
        "test -d /usr/local/emhttp/plugins/folder.view3 && echo yes" 2>/dev/null)

    if [[ "$fv3_present" == "yes" ]]; then
        log "FolderView3 already installed on $MIRROR ✅"
        return 0
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would install FolderView3 on $MIRROR"
        return 0
    fi

    log "Installing FolderView3 on $MIRROR..."
    if timeout 60 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
        "plugin install '$PARTNERSHIP_FOLDERVIEW3_URL' 2>/dev/null && echo installed" \
        2>/dev/null | grep -q installed; then
        log "FolderView3 installed ✅"
        return 0
    else
        warn "FolderView3 install failed — install manually from Community Applications"
        return 1
    fi
}

# ==============================================================================================
# ── Wait for a container on the remote to be healthy/running ─────────────────────────────────

-# ── Install Unraid plugins on the mirror ──────────────────────────────────────────────────────

-# Installs FolderView3 via `plugin install` — Unraid Community Applications command.

-# Checks if already installed before attempting. Returns 1 if install fails.

-    if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" != true ]] || [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then

-        log "FolderView3 not configured — skipping"

-    fv3_present=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \

-        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \

-        "test -d /usr/local/emhttp/plugins/folder.view3 && echo yes" 2>/dev/null)

-    if [[ "$fv3_present" == "yes" ]]; then

-        log "FolderView3 already installed on $MIRROR ✅"

-        warn "DRY RUN — would install FolderView3 on $MIRROR"

-    log "Installing FolderView3 on $MIRROR..."

-    if timeout 60 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \

-        "plugin install '$PARTNERSHIP_FOLDERVIEW3_URL' 2>/dev/null && echo installed" \

-        2>/dev/null | grep -q installed; then

-        warn "FolderView3 install failed — install manually from Community Applications"

# Polls docker inspect on the remote. Prefers the health status if a healthcheck is defined;


# ==============================================================================================
# ── Remove a FolderView3 folder from a remote host ───────────────────────────────────────────
#
# Uses jq on the remote to delete the named folder from docker.json.
# No-op if PARTNERSHIP_FOLDERVIEW3 is not enabled or jq is unavailable on remote.
# ==============================================================================================
platform_remove_folderview3_on_remote() {
    local remote_ip="$1" ssh_key="$2" folder_name="$3" server_label="${4:-remote}"

    [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" != true ]] && return 0

    log "Removing FolderView3 folder '$folder_name' from $server_label..."
    if [[ "$DRY_RUN" == false ]]; then
        timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
            -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
            "fv3='/boot/config/plugins/folder.view3/docker.json'
             [[ -f \"\$fv3\" ]] && command -v jq >/dev/null 2>&1 && \
             jq --arg n '$folder_name' \
                'with_entries(select(.value.name != \$n))' \
                \"\$fv3\" > \"\${fv3}.tmp\" && \
             mv \"\${fv3}.tmp\" \"\$fv3\" && echo removed" 2>/dev/null | \
            grep -q removed && \
            log "FolderView3 '$folder_name' removed from $server_label ✅" || \
            warn "FolderView3 folder not found on $server_label or jq unavailable — skipping"
    else
        warn "DRY RUN — would remove FolderView3 folder '$folder_name' from $server_label"
    fi
}


# ==============================================================================================
# ── Remove a FolderView3 folder from a remote host ───────────────────────────────────────────
#
# Uses jq on the remote to delete the named folder from docker.json.
# No-op if PARTNERSHIP_FOLDERVIEW3 is not enabled or jq is unavailable on remote.
# ==============================================================================================
platform_remove_folderview3_on_remote() {
    local remote_ip="$1" ssh_key="$2" folder_name="$3" server_label="${4:-remote}"

    [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" != true ]] && return 0

    log "Removing FolderView3 folder '$folder_name' from $server_label..."
    if [[ "$DRY_RUN" == false ]]; then
        timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
            -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
            "fv3='/boot/config/plugins/folder.view3/docker.json'
             [[ -f \"\$fv3\" ]] && command -v jq >/dev/null 2>&1 && \
             jq --arg n '$folder_name' \
                'with_entries(select(.value.name != \$n))' \
                \"\$fv3\" > \"\${fv3}.tmp\" && \
             mv \"\${fv3}.tmp\" \"\$fv3\" && echo removed" 2>/dev/null | \
            grep -q removed && \
            log "FolderView3 '$folder_name' removed from $server_label ✅" || \
            warn "FolderView3 folder not found on $server_label or jq unavailable — skipping"
    else
        warn "DRY RUN — would remove FolderView3 folder '$folder_name' from $server_label"
    fi
}

-# ── Remove a FolderView3 folder from a remote host ───────────────────────────────────────────

-# Uses jq on the remote to delete the named folder from docker.json.

-# No-op if PARTNERSHIP_FOLDERVIEW3 is not enabled or jq is unavailable on remote.

-    local remote_ip="$1" ssh_key="$2" folder_name="$3" server_label="${4:-remote}"

-    [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" != true ]] && return 0

-    log "Removing FolderView3 folder '$folder_name' from $server_label..."

-        timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \

-            -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \

-            "fv3='/boot/config/plugins/folder.view3/docker.json'

-             jq --arg n '$folder_name' \

-                'with_entries(select(.value.name != \$n))' \

-             mv \"\${fv3}.tmp\" \"\$fv3\" && echo removed" 2>/dev/null | \

-            log "FolderView3 '$folder_name' removed from $server_label ✅" || \

-            warn "FolderView3 folder not found on $server_label or jq unavailable — skipping"

-        warn "DRY RUN — would remove FolderView3 folder '$folder_name' from $server_label"


Now update onboard.sh — remove Step 2 and renumber steps 3-9 → 2-8:


#   Step 2: Plugin install       — FolderView3 and required plugins on mirror
#   Step 3: Stop mirror auth     — stop mirror's existing auth containers before replacing
#   Step 4: Deploy auth stack    — push XMLs, pull images, create + start on mirror
#                                  Mariadb/Redis health-checked before Authelia deploys
#   Step 5: Stop mirror arr      — stop mirror's existing arr containers before replacing
#   Step 6: Deploy arr stack     — push arr XMLs, pull images, create + start on mirror
#   Step 7: Partnership onboard  — configure WebUIs → owner IP, write state, FolderView3, Emby
#   Step 8: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 9: Conf push            — push master.conf + setup state to all listed hosts

#   Step 2: Stop mirror auth     — stop mirror's existing auth containers before replacing
#   Step 3: Deploy auth stack    — push XMLs, pull images, create + start on mirror
#                                  Mariadb/Redis health-checked before Authelia deploys
#   Step 4: Stop mirror arr      — stop mirror's existing arr containers before replacing
#   Step 5: Deploy arr stack     — push arr XMLs, pull images, create + start on mirror
#   Step 6: Partnership onboard  — configure WebUIs → owner IP, write state, Emby
#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 8: Conf push            — push master.conf + setup state to all listed hosts


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

#   Step 2: Plugin install       — FolderView3 and required plugins on mirror
#   Step 3: Stop mirror auth     — stop mirror's existing auth containers before replacing
#   Step 4: Deploy auth stack    — push XMLs, pull images, create + start on mirror
#                                  Mariadb/Redis health-checked before Authelia deploys
#   Step 5: Stop mirror arr      — stop mirror's existing arr containers before replacing
#   Step 6: Deploy arr stack     — push arr XMLs, pull images, create + start on mirror
#   Step 7: Partnership onboard  — configure WebUIs → owner IP, write state, FolderView3, Emby
#   Step 8: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 9: Conf push            — push master.conf + setup state to all listed hosts

#   Step 2: Stop mirror auth     — stop mirror's existing auth containers before replacing
#   Step 3: Deploy auth stack    — push XMLs, pull images, create + start on mirror
#                                  Mariadb/Redis health-checked before Authelia deploys
#   Step 4: Stop mirror arr      — stop mirror's existing arr containers before replacing
#   Step 5: Deploy arr stack     — push arr XMLs, pull images, create + start on mirror
#   Step 6: Partnership onboard  — configure WebUIs → owner IP, write state, Emby
#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 8: Conf push            — push master.conf + setup state to all listed hosts

#   Step 1: SSH key setup        — generate keypair, install on mirror, update conf

-#   Step 2: Plugin install       — FolderView3 and required plugins on mirror

-#   Step 3: Stop mirror auth     — stop mirror's existing auth containers before replacing

-#   Step 4: Deploy auth stack    — push XMLs, pull images, create + start on mirror

+#   Step 2: Stop mirror auth     — stop mirror's existing auth containers before replacing

+#   Step 3: Deploy auth stack    — push XMLs, pull images, create + start on mirror

-#   Step 5: Stop mirror arr      — stop mirror's existing arr containers before replacing

-#   Step 6: Deploy arr stack     — push arr XMLs, pull images, create + start on mirror

-#   Step 7: Partnership onboard  — configure WebUIs → owner IP, write state, FolderView3, Emby

-#   Step 8: Arr bootstrap        — bidirectional library sync (arr_sync.sh)

-#   Step 9: Conf push            — push master.conf + setup state to all listed hosts

+#   Step 4: Stop mirror arr      — stop mirror's existing arr containers before replacing

+#   Step 5: Deploy arr stack     — push arr XMLs, pull images, create + start on mirror

+#   Step 6: Partnership onboard  — configure WebUIs → owner IP, write state, Emby

+#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)

+#   Step 8: Conf push            — push master.conf + setup state to all listed hosts


# ── Step 2: Plugins ───────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 2 — Plugin Install on Mirror ━━━"

platform_install_plugins "$MIRROR_IP" "$MIRROR_SSH_KEY" || STEP_PLUGINS_OK=false

# ── Step 3: Stop mirror's existing auth stack ─────────────────────────────────────────────────
echo ""
echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━"

# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
echo ""
echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"


# ── Step 2: Plugins ───────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 2 — Plugin Install on Mirror ━━━"

platform_install_plugins "$MIRROR_IP" "$MIRROR_SSH_KEY" || STEP_PLUGINS_OK=false

# ── Step 3: Stop mirror's existing auth stack ─────────────────────────────────────────────────
echo ""
echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━"

# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
echo ""
echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"

-# ── Step 2: Plugins ───────────────────────────────────────────────────────────────────────────

+# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────

-echo "━━━ Step 2 — Plugin Install on Mirror ━━━"

+echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"

-platform_install_plugins "$MIRROR_IP" "$MIRROR_SSH_KEY" || STEP_PLUGINS_OK=false

-# ── Step 3: Stop mirror's existing auth stack ─────────────────────────────────────────────────

-echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━"

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


echo "━━━ Step 4 — Deploy Auth Stack on Mirror ━━━"

echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"


# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────

-echo "━━━ Step 4 — Deploy Auth Stack on Mirror ━━━"

+echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"


echo "━━━ Step 5 — Stop Mirror Arr Stack ━━━"

echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"


# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────

-echo "━━━ Step 5 — Stop Mirror Arr Stack ━━━"

+echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"

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


echo "━━━ Step 6 — Deploy Arr Stack on Mirror ━━━"

echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"


# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────

-echo "━━━ Step 6 — Deploy Arr Stack on Mirror ━━━"

+echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"


echo "━━━ Step 7 — Partnership Onboard ━━━"

echo "━━━ Step 6 — Partnership Onboard ━━━"


# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────

-echo "━━━ Step 7 — Partnership Onboard ━━━"

+echo "━━━ Step 6 — Partnership Onboard ━━━"

if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then


echo "━━━ Step 8 — Arr Library Bootstrap ━━━"

echo "━━━ Step 7 — Arr Library Bootstrap ━━━"


# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────

-echo "━━━ Step 8 — Arr Library Bootstrap ━━━"

+echo "━━━ Step 7 — Arr Library Bootstrap ━━━"

warn "Skipping — onboard did not complete"


echo "━━━ $ICON_GEAR Step 9 — master.conf Push ━━━"

echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"


# SSH is now established and all partners have the plugin installed.

# Push the authoritative master.conf so every listed host is in sync immediately.

-echo "━━━ $ICON_GEAR Step 9 — master.conf Push ━━━"

+echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"


Now fix the summary block in onboard:


echo "  Step 1 — SSH keys:      $(_skip "$SKIP_SSH"        "$STEP_SSH_OK")"
echo "  Step 2 — Plugins:       $(_ok "$STEP_PLUGINS_OK")"
echo "  Step 3 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"
echo "  Step 4 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
echo "  Step 5 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"
echo "  Step 6 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
echo "  Step 7 — Onboard:       $(_ok "$ONBOARD_OK")"
echo "  Step 8 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
echo "  Step 9 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"

echo "  Step 1 — SSH keys:      $(_skip "$SKIP_SSH"        "$STEP_SSH_OK")"
echo "  Step 2 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"
echo "  Step 3 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
echo "  Step 4 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"
echo "  Step 5 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
echo "  Step 6 — Onboard:       $(_ok "$ONBOARD_OK")"
echo "  Step 7 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
echo "  Step 8 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"


echo "  Step 1 — SSH keys:      $(_skip "$SKIP_SSH"        "$STEP_SSH_OK")"
echo "  Step 2 — Plugins:       $(_ok "$STEP_PLUGINS_OK")"
echo "  Step 3 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"
echo "  Step 4 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
echo "  Step 5 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"
echo "  Step 6 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
echo "  Step 7 — Onboard:       $(_ok "$ONBOARD_OK")"
echo "  Step 8 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
echo "  Step 9 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"

echo "  Step 1 — SSH keys:      $(_skip "$SKIP_SSH"        "$STEP_SSH_OK")"
echo "  Step 2 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"
echo "  Step 3 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
echo "  Step 4 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"
echo "  Step 5 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
echo "  Step 6 — Onboard:       $(_ok "$ONBOARD_OK")"
echo "  Step 7 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
echo "  Step 8 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"

-echo "  Step 2 — Plugins:       $(_ok "$STEP_PLUGINS_OK")"

-echo "  Step 3 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"

-echo "  Step 4 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"

-echo "  Step 5 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"

-echo "  Step 6 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"

-echo "  Step 7 — Onboard:       $(_ok "$ONBOARD_OK")"

-echo "  Step 8 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"

-echo "  Step 9 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"

+echo "  Step 2 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"

+echo "  Step 3 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"

+echo "  Step 4 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"

+echo "  Step 5 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"

+echo "  Step 6 — Onboard:       $(_ok "$ONBOARD_OK")"

+echo "  Step 7 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"

+echo "  Step 8 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"


Now also fix the "FolderView3 may need manual setup" messages in onboard:


warn "Local setup had issues — FolderView3 may need manual setup"

warn "Local setup had issues — check partnership_manager.sh output above"


The file /boot/config/plugins/varaverk/Partnership/partnership_onboard.sh has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"

bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \

-            warn "Local setup had issues — FolderView3 may need manual setup"

+            warn "Local setup had issues — check partnership_manager.sh output above"

echo "━━━ Phase 1 — HOST1 Local Setup ━━━"

[[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1


Now update offboard.sh — remove FV3 call in Step 7 and the summary line, and remove the `derive_partner_folder_name` calls:


    PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
    cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false


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

    PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
    cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false

echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"

-    PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")

-    cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false

+    cleanup_partner_containers || STEP_FALLBACK_CLEANUP_OK=false

# ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────


    # Remove auth/arr stack containers deployed during onboard (by config array)
    cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
    # Remove fallback coverage containers (by *-owner_short naming pattern)
    cleanup_owner_containers_on_mirror "$MIRROR_IP"
    # Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)
    OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")
    platform_remove_folderview3_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY" "$OWNER_FOLDER_ON_MIRROR" "$MIRROR"

    # Remove auth/arr stack containers deployed during onboard (by config array)
    cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
    # Remove fallback coverage containers (by *-owner_short naming pattern)
    cleanup_owner_containers_on_mirror "$MIRROR_IP"


    # Remove auth/arr stack containers deployed during onboard (by config array)
    cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
    # Remove fallback coverage containers (by *-owner_short naming pattern)
    cleanup_owner_containers_on_mirror "$MIRROR_IP"
    # Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)
    OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")
    platform_remove_folderview3_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY" "$OWNER_FOLDER_ON_MIRROR" "$MIRROR"

-    # Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)

-    OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")

-    platform_remove_folderview3_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY" "$OWNER_FOLDER_ON_MIRROR" "$MIRROR"

warn "$MIRROR unreachable — remote container cleanup skipped"

warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"


PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
cleanup_partner_containers "$PARTNER_FOLDER_NAME"


PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
cleanup_partner_containers "$PARTNER_FOLDER_NAME"

echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"

# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────


echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
    echo "  FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \

echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \


echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
    echo "  FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \

echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \

echo "  Step 10 — State:          INACTIVE ✅"

-[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \

-    echo "  FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"


Now the big one — manager.sh. Remove the FolderView3 functions, simplify `cleanup_partner_containers`, remove FV3 blocks from status and onboard:


# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"

# Derive short partner name: strip unraid- prefix (case-insensitive) if present
derive_partner_folder_name() {
    local hostname="$1"
    local short="${hostname,,}"
    [[ "$short" == unraid-* ]] && short="${short:7}"
    # Capitalise first char for readability: jayred365 → Jayred365-fallback
    echo "${short^}-Fallback"
}

folderview3_ensure_plugin() {
    if [[ -d "$FOLDERVIEW3_DIR" ]]; then
        log "FolderView3 plugin present ✅"
        return 0
    fi
    if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
        warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"
        warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"
        return 1
    fi
    warn "FolderView3 not found — installing from CA..."
    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"
        return 0
    fi
    plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \
        log "FolderView3 installed ✅" || {
        warn "FolderView3 install failed — folder will not be created"
        return 1
    }
}

folderview3_create_partner_folder() {
    local partner_name="$1"
    shift
    local containers=("$@")

    log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."

    folderview3_ensure_plugin || return 1

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would create FolderView3 folder: $partner_name"
        for c in "${containers[@]}"; do
            [[ -n "$c" ]] && warn "  DRY RUN — container: $c"
        done
        return 0
    fi

    # Initialise JSON if missing
    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
        mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"
        echo '{}' > "$FOLDERVIEW3_JSON"
    fi

    # Check folder doesn't already exist
    local existing
    existing=$(jq -r --arg name "$partner_name" \
        'to_entries[] | select(.value.name == $name) | .key' \
        "$FOLDERVIEW3_JSON" 2>/dev/null)
    if [[ -n "$existing" ]]; then
        log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"
        return 0
    fi

    # Build containers JSON array
    local containers_json
    containers_json=$(printf '%s\n' "${containers[@]}" | \
        grep -v '^$' | jq -R . | jq -s .)

    # Generate a stable random ID from timestamp+name
    local folder_id
    folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)

    jq --arg id "$folder_id" --arg name "$partner_name" \
       --argjson containers "$containers_json" \
       '.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \
       "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
       mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
       log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {
       warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"
       return 1
    }
}

folderview3_remove_partner_folder() {
    local partner_name="$1"

    log "FolderView3: removing folder '$partner_name' and stopping its containers..."

    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
        log "FolderView3: JSON not found — nothing to remove"
        return 0
    fi

    if ! command -v jq >/dev/null 2>&1; then
        warn "jq not found — cannot manage FolderView3 JSON"
        return 1
    fi

    # Get containers from this folder
    local containers_json
    containers_json=$(jq -r --arg name "$partner_name" \
        'to_entries[] | select(.value.name == $name) | .value.containers[]' \
        "$FOLDERVIEW3_JSON" 2>/dev/null)

    if [[ -z "$containers_json" ]]; then
        log "FolderView3: folder '$partner_name' not found — nothing to remove"
        return 0
    fi

    # Stop and remove each container in the folder
    local stopped=0 removed=0 failed=0
    while IFS= read -r container; do
        [[ -z "$container" ]] && continue
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would stop + rm: $container"
            continue
        fi
        # Stop if running
        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
            if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then
                log "$container stopped ✅"
                _PM_TRAP_STOPPED+=("$container")
                (( stopped++ ))
            else
                warn "$container stop failed"
                (( failed++ ))
            fi
            if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then
                log "$container removed ✅"
                (( removed++ ))
            else
                warn "$container rm failed (may not exist)"
            fi
        else
            log "$container not found locally — skipping"
        fi
    done <<< "$containers_json"

    # Remove folder entry from JSON
    if [[ "$DRY_RUN" == false ]]; then
        jq --arg name "$partner_name" \
            'with_entries(select(.value.name != $name))' \
            "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
            mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
            log "FolderView3: folder '$partner_name' removed ✅" || \
            warn "FolderView3: failed to remove folder from JSON"
    else
        warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"
    fi

    [[ "$DRY_RUN" == false ]] && \
        log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"
    return 0
}

# Gather all partner fallback containers for this server (all tiers)

# Gather all partner fallback containers for this server (all tiers)


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

# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"

# Derive short partner name: strip unraid- prefix (case-insensitive) if present
derive_partner_folder_name() {
    local hostname="$1"
    local short="${hostname,,}"
    [[ "$short" == unraid-* ]] && short="${short:7}"
    # Capitalise first char for readability: jayred365 → Jayred365-fallback
    echo "${short^}-Fallback"
}

folderview3_ensure_plugin() {
    if [[ -d "$FOLDERVIEW3_DIR" ]]; then
        log "FolderView3 plugin present ✅"
        return 0
    fi
    if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
        warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"
        warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"
        return 1
    fi
    warn "FolderView3 not found — installing from CA..."
    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"
        return 0
    fi
    plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \
        log "FolderView3 installed ✅" || {
        warn "FolderView3 install failed — folder will not be created"
        return 1
    }
}

folderview3_create_partner_folder() {
    local partner_name="$1"
    shift
    local containers=("$@")

    log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."

    folderview3_ensure_plugin || return 1

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would create FolderView3 folder: $partner_name"
        for c in "${containers[@]}"; do
            [[ -n "$c" ]] && warn "  DRY RUN — container: $c"
        done
        return 0
    fi

    # Initialise JSON if missing
    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
        mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"
        echo '{}' > "$FOLDERVIEW3_JSON"
    fi

    # Check folder doesn't already exist
    local existing
    existing=$(jq -r --arg name "$partner_name" \
        'to_entries[] | select(.value.name == $name) | .key' \
        "$FOLDERVIEW3_JSON" 2>/dev/null)
    if [[ -n "$existing" ]]; then
        log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"
        return 0
    fi

    # Build containers JSON array
    local containers_json
    containers_json=$(printf '%s\n' "${containers[@]}" | \
        grep -v '^$' | jq -R . | jq -s .)

    # Generate a stable random ID from timestamp+name
    local folder_id
    folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)

    jq --arg id "$folder_id" --arg name "$partner_name" \
       --argjson containers "$containers_json" \
       '.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \
       "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
       mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
       log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {
       warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"
       return 1
    }
}

folderview3_remove_partner_folder() {
    local partner_name="$1"

    log "FolderView3: removing folder '$partner_name' and stopping its containers..."

    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
        log "FolderView3: JSON not found — nothing to remove"
        return 0
    fi

    if ! command -v jq >/dev/null 2>&1; then
        warn "jq not found — cannot manage FolderView3 JSON"
        return 1
    fi

    # Get containers from this folder
    local containers_json
    containers_json=$(jq -r --arg name "$partner_name" \
        'to_entries[] | select(.value.name == $name) | .value.containers[]' \
        "$FOLDERVIEW3_JSON" 2>/dev/null)

    if [[ -z "$containers_json" ]]; then
        log "FolderView3: folder '$partner_name' not found — nothing to remove"
        return 0
    fi

    # Stop and remove each container in the folder
    local stopped=0 removed=0 failed=0
    while IFS= read -r container; do
        [[ -z "$container" ]] && continue
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would stop + rm: $container"
            continue
        fi
        # Stop if running
        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
            if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then
                log "$container stopped ✅"
                _PM_TRAP_STOPPED+=("$container")
                (( stopped++ ))
            else
                warn "$container stop failed"
                (( failed++ ))
            fi
            if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then
                log "$container removed ✅"
                (( removed++ ))
            else
                warn "$container rm failed (may not exist)"
            fi
        else
            log "$container not found locally — skipping"
        fi
    done <<< "$containers_json"

    # Remove folder entry from JSON
    if [[ "$DRY_RUN" == false ]]; then
        jq --arg name "$partner_name" \
            'with_entries(select(.value.name != $name))' \
            "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
            mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
            log "FolderView3: folder '$partner_name' removed ✅" || \
            warn "FolderView3: failed to remove folder from JSON"
    else
        warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"
    fi

    [[ "$DRY_RUN" == false ]] && \
        log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"
    return 0
}

# Gather all partner fallback containers for this server (all tiers)

-# ── FolderView3 integration ───────────────────────────────────────────────────────────────────

-# Derive short partner name: strip unraid- prefix (case-insensitive) if present

-    # Capitalise first char for readability: jayred365 → Jayred365-fallback

-    if [[ -d "$FOLDERVIEW3_DIR" ]]; then

-        log "FolderView3 plugin present ✅"

-    if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then

-        warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"

-        warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"

-    warn "FolderView3 not found — installing from CA..."

-        warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"

-    plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \

-        log "FolderView3 installed ✅" || {

-        warn "FolderView3 install failed — folder will not be created"

-    log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."

-    folderview3_ensure_plugin || return 1

-        warn "DRY RUN — would create FolderView3 folder: $partner_name"

-        for c in "${containers[@]}"; do

-            [[ -n "$c" ]] && warn "  DRY RUN — container: $c"

-    if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then

-        mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"

-        echo '{}' > "$FOLDERVIEW3_JSON"

-    # Check folder doesn't already exist

-    existing=$(jq -r --arg name "$partner_name" \

-        'to_entries[] | select(.value.name == $name) | .key' \

-        "$FOLDERVIEW3_JSON" 2>/dev/null)

-        log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"

-    containers_json=$(printf '%s\n' "${containers[@]}" | \

-    # Generate a stable random ID from timestamp+name

-    folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)

-    jq --arg id "$folder_id" --arg name "$partner_name" \

-       --argjson containers "$containers_json" \

-       '.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \

-       "$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \

-       mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \

-       log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {

-       warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"

-    log "FolderView3: removing folder '$partner_name' and stopping its containers..."

-        log "FolderView3: JSON not found — nothing to remove"

-    if ! command -v jq >/dev/null 2>&1; then

-        warn "jq not found — cannot manage FolderView3 JSON"

-    containers_json=$(jq -r --arg name "$partner_name" \

-        'to_entries[] | select(.value.name == $name) | .value.containers[]' \

-        "$FOLDERVIEW3_JSON" 2>/dev/null)

-    if [[ -z "$containers_json" ]]; then

-        log "FolderView3: folder '$partner_name' not found — nothing to remove"

-    # Stop and remove each container in the folder

-        [[ -z "$container" ]] && continue

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

-            warn "DRY RUN — would stop + rm: $container"

-        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then

-            if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then

-                log "$container stopped ✅"

-                _PM_TRAP_STOPPED+=("$container")

-                warn "$container stop failed"

-            if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then

-                log "$container removed ✅"

-                warn "$container rm failed (may not exist)"

-            log "$container not found locally — skipping"

-        jq --arg name "$partner_name" \

-            'with_entries(select(.value.name != $name))' \

-            log "FolderView3: folder '$partner_name' removed ✅" || \

-            warn "FolderView3: failed to remove folder from JSON"

-        warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"

-        log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"


Now simplify `cleanup_partner_containers` — remove the FV3 branches, drop the `folder_name` param: