so read the patnership folder, we will update and add the shared services first. then when all is good we will discuss on how to move foreward


Monitor media server sync and backup health


Let me also read the containers helper and offboard script:


Bash

find /boot/config/plugins/varaverk/Plugin -name "containers.sh" 2>/dev/null; ls /boot/config/plugins/varaverk/Plugin/unraid/Partnership/ 2>/dev/null

Find containers.sh and Partnership plugin files


/boot/config/plugins/varaverk/Plugin/unraid/Partnership/containers.sh
containers.sh*


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	#
12	# Functions use variables from the calling script's scope (sourced, not exec'd):
13	#   MIRROR, MIRROR_IP, MIRROR_SSH_KEY, SSH_TIMEOUT, DRY_RUN, SCRIPTS_ROOT
14	#
15	# ==============================================================================================
16
17	TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
18
19	_STACK_DEPLOYED=0
20	_STACK_FAILED=0
21
22	# ==============================================================================================
23	# ── Wait for a container on the remote to be healthy/running ─────────────────────────────────
24	#
25	# Polls docker inspect on the remote. Prefers the health status if a healthcheck is defined;
26	# falls back to the running state. Non-fatal after timeout — some containers take time to
27	# fully initialize but the deploy itself succeeded.
28	# ==============================================================================================
29	wait_for_container_healthy() {
30	    local name="$1" remote_ip="$2" ssh_key="$3"
31	    local max_wait=60 interval=5 elapsed=0
32
33	    [[ "$DRY_RUN" == true ]] && return 0
34
35	    log "  Waiting for $name to be ready..."
36	    while (( elapsed < max_wait )); do
37	        local status
38	        status=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
39	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
40	            "h=\$(docker inspect --format '{{.State.Health.Status}}' '$name' 2>/dev/null)
41	             r=\$(docker inspect --format '{{.State.Running}}' '$name' 2>/dev/null)
42	             echo \${h:-\$r}" 2>/dev/null)
43
44	        case "$status" in
45	            healthy|true)
46	                log "  $name ready ✅"
47	                return 0
48	                ;;
49	            *)
50	                sleep "$interval"
51	                (( elapsed += interval ))
52	                ;;
53	        esac
54	    done
55
56	    warn "  $name not confirmed healthy after ${max_wait}s — continuing (may affect dependents)"
57	    return 0
58	}
59
60	# ==============================================================================================
61	# ── Deploy a container from a local Unraid CA XML template to a remote host ──────────────────
62	#
63	# Parses Port / Path / Variable Config entries from the Unraid XML, SCPs the template and a
64	# self-contained deploy script to the remote, executes it, then cleans up both sides.
65	# Credentials are never passed as SSH command-line args — they stay in the SCPed script.
66	# ==============================================================================================
67	deploy_container_from_xml() {
68	    local xml_file="$1" remote_ip="$2" ssh_key="$3"
69	    local xml_name
70	    xml_name=$(basename "$xml_file")
71
72	    local name repo network extra privileged
73	    name=$(       awk 'match($0,/<Name>([^<]+)<\/Name>/,        a){print a[1];exit}' "$xml_file")
74	    repo=$(       awk 'match($0,/<Repository>([^<]+)<\/Repository>/,a){print a[1];exit}' "$xml_file")
75	    network=$(    awk 'match($0,/<Network>([^<]+)<\/Network>/,   a){print a[1];exit}' "$xml_file")
76	    extra=$(      awk 'match($0,/<ExtraParams>([^<]*)<\/ExtraParams>/,a){print a[1];exit}' "$xml_file")
77	    privileged=$( awk 'match($0,/<Privileged>([^<]+)<\/Privileged>/,a){print a[1];exit}' "$xml_file")
78
79	    if [[ -z "$name" || -z "$repo" ]]; then
80	        warn "  Cannot parse Name/Repository from $xml_name — skipping"
81	        return 1
82	    fi
83
84	    log "Deploying $name..."
85
86	    if [[ "$DRY_RUN" == false ]]; then
87	        timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
88	            "$xml_file" "root@${remote_ip}:${TEMPLATES_DIR}/${xml_name}" 2>/dev/null || {
89	            warn "  SCP failed for $xml_name — skipping $name"
90	            return 1
91	        }
92	    else
93	        warn "  DRY RUN — would SCP $xml_name → $MIRROR:${TEMPLATES_DIR}/"
94	    fi
95
96	    local tmp_script
97	    tmp_script=$(mktemp /tmp/deploy_XXXXXX.sh)
98	    chmod 600 "$tmp_script"
99
100	    {
101	        echo "#!/bin/bash"
102	        echo "set -e"
103	        echo ""
104	        printf "docker pull %q 2>/dev/null || true\n" "$repo"
105	        printf "docker stop %q 2>/dev/null || true\n" "$name"
106	        printf "docker rm   %q 2>/dev/null || true\n" "$name"
107	        echo ""
108	        printf "docker create --name %q --restart=unless-stopped" "$name"
109	        [[ -n "$network"           ]] && printf " --network=%q"  "$network"
110	        [[ "$privileged" == "true" ]] && printf " --privileged"
111	        [[ -n "$extra"             ]] && printf " %s" "$extra"
112
113	        # Port mappings → -p host:container/proto
114	        awk '/Type="Port"/ {
115	            match($0, /Target="([^"]+)"/, t)
116	            match($0, /Mode="([^"]+)"/,   m)
117	            match($0, />([^<]+)<\/Config>/, v)
118	            if (t[1] != "" && v[1] != "") {
119	                proto = (m[1] == "udp") ? "udp" : "tcp"
120	                printf " -p %s:%s/%s", v[1], t[1], proto
121	            }
122	        }' "$xml_file"
123
124	        # Volume mappings → -v 'host:container:mode'
125	        awk 'BEGIN{q=sprintf("%c",39)} /Type="Path"/ {
126	            match($0, /Target="([^"]+)"/, t)
127	            match($0, /Mode="([^"]+)"/,   m)
128	            match($0, />([^<]+)<\/Config>/, v)
129	            if (t[1] != "" && v[1] != "") {
130	                mode = (m[1] == "ro") ? "ro" : "rw"
131	                printf " -v %s%s:%s:%s%s", q, v[1], t[1], mode, q
132	            }
133	        }' "$xml_file"
134
135	        # Environment variables → -e 'KEY=VALUE'  (single-quoted to protect $ and special chars)
136	        awk 'BEGIN{q=sprintf("%c",39)} /Type="Variable"/ {
137	            match($0, /Target="([^"]+)"/, t)
138	            match($0, />([^<]+)<\/Config>/, v)
139	            if (t[1] != "" && v[1] != "") {
140	                printf " -e %s%s=%s%s", q, t[1], v[1], q
141	            }
142	        }' "$xml_file"
143
144	        printf " %q\n" "$repo"
145	        echo ""
146	        printf "docker start %q && echo 'deployed:%s'\n" "$name" "$name"
147	    } > "$tmp_script"
148
149	    if [[ "$DRY_RUN" == true ]]; then
150	        warn "  DRY RUN — would deploy $name on $MIRROR"
151	        rm -f "$tmp_script"
152	        return 0
153	    fi
154
155	    local remote_script="/tmp/deploy_${name//[^a-zA-Z0-9_]/_}.sh"
156
157	    if timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
158	        "$tmp_script" "root@${remote_ip}:${remote_script}" 2>/dev/null && \
159	       timeout 120 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
160	        "bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
161	        grep -q "deployed:${name}"; then
162	        log "  $name deployed ✅"
163	        rm -f "$tmp_script"
164	        return 0
165	    else
166	        warn "  $name deployment failed — check $MIRROR manually"
167	        rm -f "$tmp_script"
168	        return 1
169	    fi
170	}
171
172	# ==============================================================================================
173	# ── Deploy a stack of Unraid CA XMLs to the mirror ───────────────────────────────────────────
174	#
175	# Sets globals _STACK_DEPLOYED and _STACK_FAILED rather than printing to stdout.
176	# Health-checks database deps (Mariadb/Redis/Postgres) between batches so dependents
177	# (e.g. Authelia) start cleanly.
178	# ==============================================================================================
179	deploy_xml_stack() {
180	    local -n xml_array_ref="$1"
181	    _STACK_DEPLOYED=0
182	    _STACK_FAILED=0
183
184	    for xml_name in "${xml_array_ref[@]}"; do
185	        local xml_file="${TEMPLATES_DIR}/${xml_name}"
186	        if [[ ! -f "$xml_file" ]]; then
187	            warn "$xml_name not found in $TEMPLATES_DIR — skipping"
188	            (( _STACK_FAILED++ ))
189	            continue
190	        fi
191
192	        local cname
193	        cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
194
195	        if deploy_container_from_xml "$xml_file" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then
196	            (( _STACK_DEPLOYED++ ))
197	            if [[ -n "$cname" ]] && echo "$cname" | grep -qiE 'mariadb|redis|postgres|mysql'; then
198	                wait_for_container_healthy "$cname" "$MIRROR_IP" "$MIRROR_SSH_KEY"
199	            fi
200	        else
201	            (( _STACK_FAILED++ ))
202	        fi
203	    done
204	}
205
206	# ==============================================================================================
207	# ── Remove owner-deployed containers from a remote host ──────────────────────────────────────
208	#
209	# Uses PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK (owner's conf) to derive container
210	# names from local XML templates. SSHes to remote to stop, remove, and delete appdata.
211	# Appdata paths collected via docker inspect before removal. Safety gate: only
212	# /mnt/*/appdata* paths are deleted.
213	# ==============================================================================================
214	cleanup_deployed_stack_on_remote() {
215	    local remote_ip="$1" ssh_key="$2"
216	    local -a xml_names=()
217	    [[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
218	    [[ ${#PARTNERSHIP_ARR_STACK[@]}  -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
219
220	    if [[ ${#xml_names[@]} -eq 0 ]]; then
221	        log "No auth/arr stack arrays configured — skipping deployed stack cleanup"
222	        return 0
223	    fi
224
225	    log "Removing owner-deployed containers (auth/arr stacks) from $MIRROR..."
226	    for xml_name in "${xml_names[@]}"; do
227	        [[ -z "$xml_name" ]] && continue
228	        local xml_file="${TEMPLATES_DIR}/${xml_name}"
229	        if [[ ! -f "$xml_file" ]]; then
230	            warn "  $xml_name not found in local $TEMPLATES_DIR — skipping"
231	            continue
232	        fi
233
234	        local cname
235	        cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
236	        [[ -z "$cname" ]] && continue
237
238	        if [[ "$DRY_RUN" == true ]]; then
239	            warn "  DRY RUN — would stop + rm $cname on $MIRROR"
240	            warn "  DRY RUN — would delete appdata for $cname on $MIRROR"
241	            continue
242	        fi
243
244	        local appdata_paths
245	        appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
246	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
247	            "docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$cname' 2>/dev/null \
248	             | awk -F: '{print \$1}' | grep '^/mnt/.*/appdata'" 2>/dev/null)
249
250	        timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
251	            -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
252	            "docker stop '$cname' >/dev/null 2>&1
253	             docker rm   '$cname' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
254	            grep -q removed && \
255	            log "  $cname removed from $MIRROR ✅" || \
256	            log "  $cname not found on $MIRROR — skipping"
257
258	        while IFS= read -r path; do
259	            [[ -z "$path" ]] && continue
260	            timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
261	                -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
262	                "rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
263	                log "  Appdata removed on $MIRROR: $path ✅" || \
264	                warn "  Failed to remove appdata on $MIRROR: $path"
265	        done <<< "$appdata_paths"
266	    done
267	}
268
269	# ==============================================================================================
270	# ── Remove owner-deployed containers locally (mirror-initiated offboard) ─────────────────────
271	#
272	# SSHes to owner to read PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK, then uses the
273	# local templates-user/ copies (SCPed there during onboard) to get container names and
274	# appdata paths. Appdata collected before removal. Skips gracefully if owner unreachable.
275	# ==============================================================================================
276	cleanup_deployed_stack_locally() {
277	    local owner_ip="$1" ssh_key="$2"
278	    local -a xml_names=()
279
280	    if [[ -n "$owner_ip" ]]; then
281	        local -a auth_arr arr_arr
282	        mapfile -t auth_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
283	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
284	            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
285	             detect_hosts 2>/dev/null
286	             printf '%s\n' \"\${PARTNERSHIP_AUTH_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
287	        mapfile -t arr_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
288	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
289	            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
290	             detect_hosts 2>/dev/null
291	             printf '%s\n' \"\${PARTNERSHIP_ARR_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
292	        xml_names=("${auth_arr[@]}" "${arr_arr[@]}")
293	    fi
294
295	    if [[ ${#xml_names[@]} -eq 0 ]]; then
296	        log "Could not read deployed stack from owner — skipping auth/arr cleanup"
297	        return 0
298	    fi
299
300	    log "Removing owner-deployed containers (auth/arr stacks) locally..."
301	    for xml_name in "${xml_names[@]}"; do
302	        [[ -z "$xml_name" ]] && continue
303	        local xml_file="${TEMPLATES_DIR}/${xml_name}"
304	        if [[ ! -f "$xml_file" ]]; then
305	            warn "  $xml_name not found locally — skipping"
306	            continue
307	        fi
308
309	        local cname
310	        cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
311	        [[ -z "$cname" ]] && continue
312
313	        if [[ "$DRY_RUN" == true ]]; then
314	            warn "  DRY RUN — would stop + rm $cname"
315	            warn "  DRY RUN — would delete appdata for $cname"
316	            continue
317	        fi
318
319	        local appdata_paths=""
320	        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$cname" >/dev/null 2>&1; then
321	            appdata_paths=$(docker inspect \
322	                --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
323	                "$cname" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
324	            timeout "${DOCKER_TIMEOUT:-30}" docker stop "$cname" >/dev/null 2>&1 || true
325	            _PM_TRAP_STOPPED+=("$cname")
326	            timeout "${DOCKER_TIMEOUT:-30}" docker rm   "$cname" >/dev/null 2>&1 && \
327	                log "  $cname removed ✅" || warn "  $cname rm failed"
328	        else
329	            log "  $cname not found locally — skipping"
330	        fi
331
332	        while IFS= read -r path; do
333	            [[ -z "$path" ]] && continue
334	            rm -rf "$path" && log "  Appdata removed: $path ✅" || warn "  Failed to remove: $path"
335	        done <<< "$appdata_paths"
336	    done
337	}
338
339

#!/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
#
# 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"

_STACK_DEPLOYED=0
_STACK_FAILED=0

# ==============================================================================================
# ── Wait for a container on the remote to be healthy/running ─────────────────────────────────
#
# Polls docker inspect on the remote. Prefers the health status if a healthcheck is defined;
# falls back to the running state. Non-fatal after timeout — some containers take time to
# fully initialize but the deploy itself succeeded.
# ==============================================================================================
wait_for_container_healthy() {
    local name="$1" remote_ip="$2" ssh_key="$3"
    local max_wait=60 interval=5 elapsed=0

    [[ "$DRY_RUN" == true ]] && return 0

    log "  Waiting for $name to be ready..."
    while (( elapsed < max_wait )); do
        local status
        status=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
            "h=\$(docker inspect --format '{{.State.Health.Status}}' '$name' 2>/dev/null)
             r=\$(docker inspect --format '{{.State.Running}}' '$name' 2>/dev/null)
             echo \${h:-\$r}" 2>/dev/null)

        case "$status" in
            healthy|true)
                log "  $name ready ✅"
                return 0
                ;;
            *)
                sleep "$interval"
                (( elapsed += interval ))
                ;;
        esac
    done

    warn "  $name not confirmed healthy after ${max_wait}s — continuing (may affect dependents)"
    return 0
}

# ==============================================================================================
# ── Deploy a container from a local Unraid CA XML template to a remote host ──────────────────
#
# Parses Port / Path / Variable Config entries from the Unraid XML, SCPs the template and a
# self-contained deploy script to the remote, executes it, then cleans up both sides.
# Credentials are never passed as SSH command-line args — they stay in the SCPed script.
# ==============================================================================================
deploy_container_from_xml() {
    local xml_file="$1" remote_ip="$2" ssh_key="$3"
    local xml_name
    xml_name=$(basename "$xml_file")

    local name repo network extra privileged
    name=$(       awk 'match($0,/<Name>([^<]+)<\/Name>/,        a){print a[1];exit}' "$xml_file")
    repo=$(       awk 'match($0,/<Repository>([^<]+)<\/Repository>/,a){print a[1];exit}' "$xml_file")
    network=$(    awk 'match($0,/<Network>([^<]+)<\/Network>/,   a){print a[1];exit}' "$xml_file")
    extra=$(      awk 'match($0,/<ExtraParams>([^<]*)<\/ExtraParams>/,a){print a[1];exit}' "$xml_file")
    privileged=$( awk 'match($0,/<Privileged>([^<]+)<\/Privileged>/,a){print a[1];exit}' "$xml_file")

    if [[ -z "$name" || -z "$repo" ]]; then
        warn "  Cannot parse Name/Repository from $xml_name — skipping"
        return 1
    fi

    log "Deploying $name..."

    if [[ "$DRY_RUN" == false ]]; then
        timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
            "$xml_file" "root@${remote_ip}:${TEMPLATES_DIR}/${xml_name}" 2>/dev/null || {
            warn "  SCP failed for $xml_name — skipping $name"
            return 1
        }
    else
        warn "  DRY RUN — would SCP $xml_name → $MIRROR:${TEMPLATES_DIR}/"
    fi

    local tmp_script
    tmp_script=$(mktemp /tmp/deploy_XXXXXX.sh)
    chmod 600 "$tmp_script"

    {
        echo "#!/bin/bash"
        echo "set -e"
        echo ""
        printf "docker pull %q 2>/dev/null || true\n" "$repo"
        printf "docker stop %q 2>/dev/null || true\n" "$name"
        printf "docker rm   %q 2>/dev/null || true\n" "$name"
        echo ""
        printf "docker create --name %q --restart=unless-stopped" "$name"
        [[ -n "$network"           ]] && printf " --network=%q"  "$network"
        [[ "$privileged" == "true" ]] && printf " --privileged"
        [[ -n "$extra"             ]] && printf " %s" "$extra"

        # Port mappings → -p host:container/proto
        awk '/Type="Port"/ {
            match($0, /Target="([^"]+)"/, t)
            match($0, /Mode="([^"]+)"/,   m)
            match($0, />([^<]+)<\/Config>/, v)
            if (t[1] != "" && v[1] != "") {
                proto = (m[1] == "udp") ? "udp" : "tcp"
                printf " -p %s:%s/%s", v[1], t[1], proto
            }
        }' "$xml_file"

        # Volume mappings → -v 'host:container:mode'
        awk 'BEGIN{q=sprintf("%c",39)} /Type="Path"/ {
            match($0, /Target="([^"]+)"/, t)
            match($0, /Mode="([^"]+)"/,   m)
            match($0, />([^<]+)<\/Config>/, v)
            if (t[1] != "" && v[1] != "") {
                mode = (m[1] == "ro") ? "ro" : "rw"
                printf " -v %s%s:%s:%s%s", q, v[1], t[1], mode, q
            }
        }' "$xml_file"

        # Environment variables → -e 'KEY=VALUE'  (single-quoted to protect $ and special chars)
        awk 'BEGIN{q=sprintf("%c",39)} /Type="Variable"/ {
            match($0, /Target="([^"]+)"/, t)
            match($0, />([^<]+)<\/Config>/, v)
            if (t[1] != "" && v[1] != "") {
                printf " -e %s%s=%s%s", q, t[1], v[1], q
            }
        }' "$xml_file"

        printf " %q\n" "$repo"
        echo ""
        printf "docker start %q && echo 'deployed:%s'\n" "$name" "$name"
    } > "$tmp_script"

    if [[ "$DRY_RUN" == true ]]; then
        warn "  DRY RUN — would deploy $name on $MIRROR"
        rm -f "$tmp_script"
        return 0
    fi

    local remote_script="/tmp/deploy_${name//[^a-zA-Z0-9_]/_}.sh"

    if timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
        "$tmp_script" "root@${remote_ip}:${remote_script}" 2>/dev/null && \
       timeout 120 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
        "bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
        grep -q "deployed:${name}"; then
        log "  $name deployed ✅"
        rm -f "$tmp_script"
        return 0
    else
        warn "  $name deployment failed — check $MIRROR manually"
        rm -f "$tmp_script"
        return 1
    fi
}

# ==============================================================================================
# ── Deploy a stack of Unraid CA XMLs to the mirror ───────────────────────────────────────────
#
# Sets globals _STACK_DEPLOYED and _STACK_FAILED rather than printing to stdout.
# Health-checks database deps (Mariadb/Redis/Postgres) between batches so dependents
# (e.g. Authelia) start cleanly.
# ==============================================================================================
deploy_xml_stack() {
    local -n xml_array_ref="$1"
    _STACK_DEPLOYED=0
    _STACK_FAILED=0

    for xml_name in "${xml_array_ref[@]}"; do
        local xml_file="${TEMPLATES_DIR}/${xml_name}"
        if [[ ! -f "$xml_file" ]]; then
            warn "$xml_name not found in $TEMPLATES_DIR — skipping"
            (( _STACK_FAILED++ ))
            continue
        fi

        local cname
        cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")

        if deploy_container_from_xml "$xml_file" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then
            (( _STACK_DEPLOYED++ ))
            if [[ -n "$cname" ]] && echo "$cname" | grep -qiE 'mariadb|redis|postgres|mysql'; then
                wait_for_container_healthy "$cname" "$MIRROR_IP" "$MIRROR_SSH_KEY"
            fi
        else
            (( _STACK_FAILED++ ))
        fi
    done
}

# ==============================================================================================
# ── Remove owner-deployed containers from a remote host ──────────────────────────────────────
#
# Uses PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK (owner's conf) to derive container
# names from local XML templates. SSHes to remote to stop, remove, and delete appdata.
# Appdata paths collected via docker inspect before removal. Safety gate: only
# /mnt/*/appdata* paths are deleted.
# ==============================================================================================
cleanup_deployed_stack_on_remote() {
    local remote_ip="$1" ssh_key="$2"
    local -a xml_names=()
    [[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
    [[ ${#PARTNERSHIP_ARR_STACK[@]}  -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")

    if [[ ${#xml_names[@]} -eq 0 ]]; then
        log "No auth/arr stack arrays configured — skipping deployed stack cleanup"
        return 0
    fi

    log "Removing owner-deployed containers (auth/arr stacks) from $MIRROR..."
    for xml_name in "${xml_names[@]}"; do
        [[ -z "$xml_name" ]] && continue
        local xml_file="${TEMPLATES_DIR}/${xml_name}"
        if [[ ! -f "$xml_file" ]]; then
            warn "  $xml_name not found in local $TEMPLATES_DIR — skipping"
            continue
        fi

        local cname
        cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
        [[ -z "$cname" ]] && continue

        if [[ "$DRY_RUN" == true ]]; then
            warn "  DRY RUN — would stop + rm $cname on $MIRROR"
            warn "  DRY RUN — would delete appdata for $cname on $MIRROR"
            continue
        fi

        local appdata_paths
        appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
            "docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$cname' 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@"$remote_ip" \
            "docker stop '$cname' >/dev/null 2>&1
             docker rm   '$cname' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
            grep -q removed && \
            log "  $cname removed from $MIRROR ✅" || \
            log "  $cname not found on $MIRROR — skipping"

        while IFS= read -r path; do
            [[ -z "$path" ]] && continue
            timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
                -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
                "rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
                log "  Appdata removed on $MIRROR: $path ✅" || \
                warn "  Failed to remove appdata on $MIRROR: $path"
        done <<< "$appdata_paths"
    done
}

# ==============================================================================================
# ── Remove owner-deployed containers locally (mirror-initiated offboard) ─────────────────────
#
# SSHes to owner to read PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK, then uses the
# local templates-user/ copies (SCPed there during onboard) to get container names and
# appdata paths. Appdata collected before removal. Skips gracefully if owner unreachable.
# ==============================================================================================
cleanup_deployed_stack_locally() {
    local owner_ip="$1" ssh_key="$2"
    local -a xml_names=()

    if [[ -n "$owner_ip" ]]; then
        local -a auth_arr arr_arr
        mapfile -t auth_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
             detect_hosts 2>/dev/null
             printf '%s\n' \"\${PARTNERSHIP_AUTH_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
        mapfile -t arr_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
             detect_hosts 2>/dev/null
             printf '%s\n' \"\${PARTNERSHIP_ARR_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
        xml_names=("${auth_arr[@]}" "${arr_arr[@]}")
    fi

    if [[ ${#xml_names[@]} -eq 0 ]]; then
        log "Could not read deployed stack from owner — skipping auth/arr cleanup"
        return 0
    fi

    log "Removing owner-deployed containers (auth/arr stacks) locally..."
    for xml_name in "${xml_names[@]}"; do
        [[ -z "$xml_name" ]] && continue
        local xml_file="${TEMPLATES_DIR}/${xml_name}"
        if [[ ! -f "$xml_file" ]]; then
            warn "  $xml_name not found locally — skipping"
            continue
        fi

        local cname
        cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
        [[ -z "$cname" ]] && continue

        if [[ "$DRY_RUN" == true ]]; then
            warn "  DRY RUN — would stop + rm $cname"
            warn "  DRY RUN — would delete appdata for $cname"
            continue
        fi

        local appdata_paths=""
        if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$cname" >/dev/null 2>&1; then
            appdata_paths=$(docker inspect \
                --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
                "$cname" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
            timeout "${DOCKER_TIMEOUT:-30}" docker stop "$cname" >/dev/null 2>&1 || true
            _PM_TRAP_STOPPED+=("$cname")
            timeout "${DOCKER_TIMEOUT:-30}" docker rm   "$cname" >/dev/null 2>&1 && \
                log "  $cname removed ✅" || warn "  $cname rm failed"
        else
            log "  $cname not found locally — skipping"
        fi

        while IFS= read -r path; do
            [[ -z "$path" ]] && continue
            rm -rf "$path" && log "  Appdata removed: $path ✅" || warn "  Failed to remove: $path"
        done <<< "$appdata_paths"
    done
}


Now let me read the offboard script since it'll need updating too:


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Partnership Offboard ===========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Cleanly ends a partnership. Role is detected automatically — run on either server.
9	# Owner path runs the full sequence including remote cleanup and final sync.
10	# Mirror path handles the local side and signals the owner to complete its own cleanup.
11	#
12	# ==============================================================================================
13	# OPERATIONAL MODEL
14	# ==============================================================================================
15	#
16	# OWNER PATH (10 steps)
17	#   Step 1:  Stop rsync         — halt any running sync before state changes
18	#   Step 2:  Final sync         — mirror leaves with current Critical-Data state
19	#   Step 3:  Reconfigure WebUIs — mirror's auth WebUIs → localhost
20	#   Step 4:  Disable sync       — CRITICAL_RSYNC_ENABLED=false in master.conf
21	#   Step 5:  Local cleanup      — remove fallback coverage containers + appdata
22	#   Step 6:  Restart own stack  — bring up owner's own parked containers
23	#   Step 7:  Remote cleanup     — remove auth/arr stack + fallback containers from mirror
24	#   Step 8:  Restart mirror     — bring up mirror's own parked containers
25	#   Step 9:  Revocation         — Emby admin, SSH keys
26	#   Step 10: Write state        — INACTIVE locally + pushed to mirror, mirror blocklisted
27	#            Tailscale          — grace window then device removal (after state written)
28	#
29	# MIRROR PATH (8 steps)
30	#   Step 1: Stop rsync          — halt any running sync
31	#   Step 2: Reconfigure WebUIs  — local auth WebUIs → localhost
32	#   Step 3: Remote stack clean  — remove owner-deployed containers locally (auth/arr stack)
33	#   Step 4: Fallback cleanup    — remove fallback coverage containers
34	#   Step 5: Disable sync        — CRITICAL_RSYNC_ENABLED=false in master.conf
35	#   Step 6: Revoke Emby admin   — remove own admin account from local Emby instance
36	#   Step 7: Restart own stack   — bring up own parked containers
37	#   Step 8: SSH revocation      — revoke keys both directions, write state, signal owner
38	#
39	# ==============================================================================================
40	# CONFIGURATION
41	# ==============================================================================================
42	#
43	# host*.conf
44	#
45	#   HOST*_PARTNERSHIP_AUTH_STACK
46	#       Auth container XMLs to push during onboard — used on offboard to identify what
47	#       to remove. Owner's PARTNERSHIP_AUTH_STACK determines which containers get removed
48	#       from the mirror on both owner-initiated and mirror-initiated offboard.
49	#
50	#   HOST*_PARTNERSHIP_ARR_STACK
51	#       Arr container XMLs — same cleanup logic as auth stack.
52	#
53	# ==============================================================================================
54	# RUNTIME MODES
55	# ==============================================================================================
56	#
57	# Partnership/partnership_offboard.sh
58	#     Full offboard — role detected automatically
59	#
60	# Partnership/partnership_offboard.sh --dry-run
61	#     Preview all steps without executing
62	#
63	# Partnership/partnership_offboard.sh --log
64	#     Verbose per-step output
65	#
66	# Partnership/partnership_offboard.sh --reason=<string>
67	#     Tag the offboard reason in state file and blocklist (default: manual)
68	#     Called by partnership_manager.sh --offboard (reason passed through)
69	#
70	# ==============================================================================================
71
72	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
73	SCRIPTS_ROOT="$SCRIPT_DIR/.."
74	SSH_TIMEOUT=15
75
76	source "$SCRIPTS_ROOT/load_config.sh"
77	source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
78
79	# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
80	REASON="manual"
81	FILTERED_ARGS=()
82
83	for arg in "$@"; do
84	    case "$arg" in
85	        --reason=*) REASON="${arg#--reason=}" ;;
86	        *)          FILTERED_ARGS+=("$arg") ;;
87	    esac
88	done
89
90	parse_args "${FILTERED_ARGS[@]}"
91
92	# ── Source partnership_manager.sh for shared helpers ──────────────────────────────────────────
93	# PARTNERSHIP_LIB_MODE=1 skips mode dispatch — functions are defined, nothing is executed.
94	PARTNERSHIP_LIB_MODE=1 source "$SCRIPT_DIR/partnership_manager.sh"
95
96	# ==============================================================================================
97	# ━━━ Setup ━━━
98	# ==============================================================================================
99	[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
100
101	if ! command -v docker &>/dev/null; then
102	    error "Docker command not found"
103	    exit 1
104	fi
105
106	detect_hosts
107
108	OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
109	MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
110	OWNER="${!OWNER_ID}"
111	MIRROR="${!MIRROR_ID}"
112	MIRROR_SSH_KEY="$SSH_KEY"
113	OWNER_SSH_KEY="$SSH_KEY"
114
115	AM_OWNER=false
116	AM_MIRROR=false
117	[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
118	[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
119
120	LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db"
121	REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db"
122	OWNER_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${OWNER}.db"
123	MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db"
124	OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"
125
126	acquire_lock "strict"
127
128	trap _pm_trap_restart_stopped EXIT
129
130	# Check already offboarded
131	if [[ -f "$LOCAL_STATE_FILE" ]]; then
132	    CURRENT_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state")
133	    if [[ "$CURRENT_STATE" == "INACTIVE" ]]; then
134	        warn "Partnership already INACTIVE — use partnership_manager.sh --status to verify both servers agree"
135	        exit 0
136	    fi
137	fi
138
139	START=$(date +%s)
140
141	echo ""
142	echo "━━━ $ICON_FALLBACK Partnership Offboard — $MY_ID ($LOCAL_SERVER_NAME) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
143	echo ""
144	echo "  Role:    $( [[ "$AM_OWNER" == true ]] && echo "OWNER" || echo "MIRROR" )"
145	echo "  This:    $MY_ID ($LOCAL_SERVER_NAME)"
146	echo "  Partner: $( [[ "$AM_OWNER" == true ]] && echo "$MIRROR_ID ($MIRROR)" || echo "$OWNER_ID ($OWNER)" )"
147	echo "  Reason:  $REASON"
148	echo ""
149	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
150
151	# ==============================================================================================
152	# ── HELPER: revoke own admin account from local Emby instance ────────────────────────────────
153	#
154	# Mirror-initiated path only. Called before start_own_stack so Emby is still running.
155	# Uses local EMBY_API_KEY and the mirror's own short name as the username to delete.
156	# ==============================================================================================
157	revoke_local_emby_admin() {
158	    local emby_port="${PARTNERSHIP_EMBY_PORT:-8096}"
159	    local emby_url="http://127.0.0.1:${emby_port}"
160
161	    echo ""
162	    echo "━━━ $ICON_EMBY Emby Admin Revocation ━━━"
163
164	    if [[ "${PARTNERSHIP_PROVISION_EMBY_ADMIN:-false}" != true ]]; then
165	        log "PARTNERSHIP_PROVISION_EMBY_ADMIN=false — skipping"
166	        return 0
167	    fi
168
169	    if [[ -z "${EMBY_API_KEY:-}" ]]; then
170	        warn "EMBY_API_KEY not set — skipping local Emby admin revocation"
171	        return 1
172	    fi
173
174	    # The account to revoke is this server's own short name (the mirror user's account)
175	    local username="${PARTNERSHIP_EMBY_ADMIN_USER:-$(derive_short_name "$LOCAL_SERVER_NAME")}"
176
177	    if [[ "$DRY_RUN" == true ]]; then
178	        warn "DRY RUN — would delete Emby admin '$username' at $emby_url"
179	        return 0
180	    fi
181
182	    log "Looking up Emby user '$username' at $emby_url..."
183
184	    local users_json user_id
185	    users_json=$(curl -sf --max-time 15 \
186	        -H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
187	        "${emby_url}/Users" 2>/dev/null)
188
189	    user_id=$(echo "$users_json" | \
190	        grep -o "\"Id\":\"[^\"]*\"[^}]*\"Name\":\"${username}\"" | \
191	        grep -o '"Id":"[^"]*"' | cut -d'"' -f4 | head -1)
192
193	    if [[ -z "$user_id" ]]; then
194	        warn "Emby user '$username' not found at $emby_url — may already be removed"
195	        return 0
196	    fi
197
198	    local del_code
199	    del_code=$(curl -sf --max-time 15 -w "%{http_code}" -o /dev/null \
200	        -X DELETE \
201	        -H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
202	        "${emby_url}/Users/${user_id}" 2>/dev/null)
203
204	    if [[ "$del_code" == "200" ]] || [[ "$del_code" == "204" ]] || [[ "$del_code" == "404" ]]; then
205	        log "Emby admin '$username' removed ✅"
206	    else
207	        warn "Failed to delete Emby user '$username' (HTTP $del_code) — remove manually"
208	    fi
209	}
210
211	# ==============================================================================================
212	# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
213	# ==============================================================================================
214	if [[ "$AM_MIRROR" == true ]]; then
215	    warn "$MIRROR_ID ($MIRROR) is initiating offboard"
216	    warn "Owner ($OWNER) will see INACTIVE state on its next --check cycle and finalize"
217
218	    if [[ "$DRY_RUN" == false ]]; then
219	        echo ""
220	        echo "You have 10 seconds to cancel (Ctrl+C)..."
221	        sleep 10
222	    fi
223
224	    OWNER_IP=$(resolve_tailscale_ip "$OWNER")
225	    OWNER_REACHABLE=false
226	    [[ -n "$OWNER_IP" ]] && OWNER_REACHABLE=true
227
228	    STEP_STOP_RSYNC_OK=true
229	    STEP_WEBUI_OK=true
230	    STEP_STACK_CLEANUP_OK=true
231	    STEP_FALLBACK_CLEANUP_OK=true
232	    STEP_DISABLE_RSYNC_OK=true
233	    STEP_EMBY_OK=true
234	    SSH_REVOKE_REMOTE_OK=false
235	    SSH_REVOKE_LOCAL_OK=false
236
237	    # ── Step 1: Stop rsync ────────────────────────────────────────────────────────────────────
238	    echo ""
239	    echo "━━━ $ICON_STOP Step 1/8 — Stop Rsync ━━━"
240
241	    if [[ "$DRY_RUN" == false ]]; then
242	        bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || true
243	        log "Rsync stopped ✅"
244	    else
245	        warn "DRY RUN — would stop rsync"
246	    fi
247
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 ""
268	    echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"
269
270	    cleanup_partner_containers || STEP_FALLBACK_CLEANUP_OK=false
271
272	    # ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────
273	    echo ""
274	    echo "━━━ $ICON_GEAR Step 5/8 — Disable Critical Sync ━━━"
275
276	    if [[ "$DRY_RUN" == false ]]; then
277	        update_master_conf "CRITICAL_RSYNC_ENABLED" "false" && \
278	            warn "CRITICAL_RSYNC_ENABLED=false ✅" || \
279	            { warn "Failed to update CRITICAL_RSYNC_ENABLED"; STEP_DISABLE_RSYNC_OK=false; }
280	    else
281	        warn "DRY RUN — would set CRITICAL_RSYNC_ENABLED=false"
282	    fi
283
284	    # ── Step 6: Revoke Emby admin locally ─────────────────────────────────────────────────────
285	    revoke_local_emby_admin || STEP_EMBY_OK=false
286
287	    # ── Step 7: Restart own stack ─────────────────────────────────────────────────────────────
288	    start_own_stack
289
290	    # ── Step 8: SSH key revocation, write state, signal owner ────────────────────────────────
291	    echo ""
292	    echo "━━━ $ICON_SHIELD Step 8/8 — SSH Revocation + State ━━━"
293
294	    do_ssh_key_revocation "${OWNER_IP:-}"
295
296	    NOW=$(date '+%Y-%m-%d %H:%M:%S')
297	    if [[ "$DRY_RUN" == false ]]; then
298	        write_state_file "$LOCAL_STATE_FILE" \
299	            "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
300	        echo "Local state: INACTIVE ✅"
301	        add_to_blocklist "$OWNER" "$REASON"
302	    else
303	        warn "DRY RUN — would write INACTIVE state and blocklist $OWNER"
304	    fi
305
306	    if [[ "$OWNER_REACHABLE" == true ]]; then
307	        push_state_to_remote "$LOCAL_STATE_FILE" "$OWNER_IP" "$OWNER_SSH_KEY"
308	        notify "Partnership offboard requested by $MIRROR — $OWNER will finalise on next check" \
309	            "Partnership" "normal"
310	    else
311	        warn "$OWNER unreachable — state written locally, owner will see it when reachable"
312	    fi
313
314	    # ── Summary ───────────────────────────────────────────────────────────────────────────────
315	    END=$(date +%s)
316	    echo ""
317	    echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY (Mirror) ━━━━━"
318	    echo "  Mirror:  $MY_ID ($LOCAL_SERVER_NAME)"
319	    echo "  Owner:   $OWNER_ID ($OWNER)"
320	    echo "  Reason:  $REASON"
321	    echo "  Duration: $(format_duration $(( END - START )))"
322	    echo ""
323	    _ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
324	    _skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
325	    _revoke_status() {
326	        if   [[ "${SSH_REVOKE_REMOTE_OK:-false}" == true && "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
327	            echo "both directions ✅"
328	        elif [[ "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
329	            echo "local only ✅ — remote failed (revoke manually on $OWNER)"
330	        else
331	            echo "⚠  failed — check warnings above"
332	        fi
333	    }
334
335	    echo "  Step 1 — Stop rsync:       $(_ok "$STEP_STOP_RSYNC_OK")"
336	    echo "  Step 2 — WebUIs:           $(_ok "$STEP_WEBUI_OK")"
337	    echo "  Step 3 — Stack cleanup:    $(_ok "$STEP_STACK_CLEANUP_OK")"
338	    echo "  Step 4 — Fallback cleanup: $(_ok "$STEP_FALLBACK_CLEANUP_OK")"
339	    echo "  Step 5 — Disable sync:     $(_ok "$STEP_DISABLE_RSYNC_OK")"
340	    echo "  Step 6 — Emby revoke:      $(_ok "$STEP_EMBY_OK")"
341	    echo "  Step 7 — Own stack:        started"
342	    echo "  Step 8 — Keys revoked:     $(_revoke_status)"
343	    echo ""
344	    echo "  State:    INACTIVE ✅"
345	    echo "  Blocklist: $OWNER blocked ✅"
346	    echo "  Owner:    will finalise + final sync on next --check"
347	    echo ""
348	    [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
349	        warn "$ICON_DONE DONE — mirror separation complete ✅"
350	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
351	    exit 0
352	fi
353
354	# ==============================================================================================
355	# ── OWNER PATH ────────────────────────────────────────────────────────────────────────────────
356	# ==============================================================================================
357	warn "Offboarding $MIRROR_ID ($MIRROR) from partnership"
358	warn "Final sync will run — mirror leaves with current state"
359
360	if [[ "$DRY_RUN" == false ]]; then
361	    echo ""
362	    echo "You have 10 seconds to cancel (Ctrl+C)..."
363	    sleep 10
364	    echo "Proceeding..."
365	fi
366
367	WEBUI_FAILURES=0
368	STEP_STOP_OK=true
369	STEP_SYNC_OK=true
370	SSH_REVOKE_REMOTE_OK=false
371	SSH_REVOKE_LOCAL_OK=false
372
373	# ── Step 1: Stop rsync ────────────────────────────────────────────────────────────────────────
374	echo ""
375	echo "━━━ $ICON_STOP Step 1/10 — Stop Rsync ━━━"
376
377	if [[ "$DRY_RUN" == false ]]; then
378	    bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || STEP_STOP_OK=false
379	    log "Rsync stopped ✅"
380	else
381	    warn "DRY RUN — would stop rsync"
382	fi
383
384	# ── Step 2: Final sync ────────────────────────────────────────────────────────────────────────
385	echo ""
386	echo "━━━ $ICON_SYNC Step 2/10 — Final Sync ━━━"
387
388	do_final_sync || STEP_SYNC_OK=false
389
390	# ── Step 3: Reconfigure mirror WebUIs → localhost ─────────────────────────────────────────────
391	echo ""
392	echo "━━━ $ICON_CONTAINERS Step 3/10 — Reconfigure Mirror WebUIs → localhost ━━━"
393
394	MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
395	MIRROR_REACHABLE=false
396	[[ -n "$MIRROR_IP" ]] && MIRROR_REACHABLE=true
397
398	if [[ "$MIRROR_REACHABLE" == true ]]; then
399	    for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]}"; do
400	        [[ -z "$entry" ]] && continue
401	        container="${entry%%|*}"
402	        port="${entry##*|}"
403	        reconfigure_webui "$container" "$port" "localhost" \
404	            "$MIRROR_SSH_KEY" "$MIRROR_IP" "$MIRROR" || (( WEBUI_FAILURES++ ))
405	    done
406	else
407	    warn "$MIRROR unreachable — WebUI reconfiguration skipped"
408	    warn "$MIRROR will reconfigure its own WebUIs when it sees INACTIVE state on --check"
409	    (( WEBUI_FAILURES++ ))
410	fi
411
412	# ── Step 4: Disable critical sync ─────────────────────────────────────────────────────────────
413	echo ""
414	echo "━━━ $ICON_GEAR Step 4/10 — Disable Critical Sync ━━━"
415
416	if [[ "$DRY_RUN" == false ]]; then
417	    update_master_conf "CRITICAL_RSYNC_ENABLED" "false"
418	    warn "CRITICAL_RSYNC_ENABLED=false ✅"
419	else
420	    warn "DRY RUN — would set CRITICAL_RSYNC_ENABLED=false"
421	fi
422
423	# ── Step 5: Local container cleanup ───────────────────────────────────────────────────────────
424	echo ""
425	echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"
426
427	cleanup_partner_containers
428
429	# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────
430	start_own_stack
431
432	# ── Step 7: Remote container cleanup ──────────────────────────────────────────────────────────
433	echo ""
434	echo "━━━ $ICON_CONTAINERS Step 7/10 — Remote Container Cleanup ━━━"
435
436	if [[ "$MIRROR_REACHABLE" == true ]]; then
437	    # Remove auth/arr stack containers deployed during onboard (by config array)
438	    cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
439	    # Remove fallback coverage containers (by *-owner_short naming pattern)
440	    cleanup_owner_containers_on_mirror "$MIRROR_IP"
441	else
442	    warn "$MIRROR unreachable — remote container cleanup skipped"
443	    warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"
444	fi
445
446	# ── Step 8: Restart mirror's own stack ────────────────────────────────────────────────────────
447	echo ""
448	echo "━━━ $ICON_START Step 8/10 — Restart Mirror Stack ━━━"
449
450	[[ "$MIRROR_REACHABLE" == true ]] && start_mirror_own_stack "$MIRROR_IP"
451
452	# ── Step 9: Revocation (Emby + SSH) ──────────────────────────────────────────────────────────
453	echo ""
454	echo "━━━ $ICON_SHIELD Step 9/10 — Revocation ━━━"
455
456	# Emby admin — before SSH key revocation while Emby still reachable
457	[[ "$MIRROR_REACHABLE" == true ]] && revoke_emby_admin "$MIRROR_IP"
458
459	# SSH key revocation — mutual, both directions; must run while Tailscale still active
460	do_ssh_key_revocation "${MIRROR_IP:-}"
461
462	# ── Step 10: Write state, push to mirror, blocklist ───────────────────────────────────────────
463	# State is written after container cleanup and SSH revocation so that:
464	#   • Re-running after a crash between steps 5–9 restarts from scratch (no early-exit on INACTIVE)
465	#   • --check sees INACTIVE during the Tailscale grace sleep and does not re-trigger offboard
466	echo ""
467	echo "━━━ $ICON_GEAR Step 10/10 — Write State ━━━"
468
469	NOW=$(date '+%Y-%m-%d %H:%M:%S')
470
471	if [[ "$DRY_RUN" == false ]]; then
472	    write_state_file "$LOCAL_STATE_FILE" \
473	        "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
474	    log "Local state: INACTIVE ✅"
475	    add_to_blocklist "$MIRROR" "$REASON"
476	    [[ "$MIRROR_REACHABLE" == true ]] && \
477	        push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
478	else
479	    warn "DRY RUN — would write INACTIVE state, blocklist $MIRROR, push to remote"
480	fi
481
482	# Tailscale removal — after state written so --check does not re-trigger offboard during grace sleep
483	if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then
484	    echo ""
485	    echo "━━━ $ICON_NET Tailscale Separation ━━━"
486	    if [[ "$MIRROR_REACHABLE" == true ]]; then
487	        grace_seconds=$(( ${PARTNERSHIP_GRACE_HOURS:-6} * 3600 ))
488	        warn "Waiting ${PARTNERSHIP_GRACE_HOURS:-6}hr grace — mirror can collect backups..."
489	        if [[ "$DRY_RUN" == false ]]; then
490	            trap 'warn "Offboard interrupted during grace sleep"; exit 0' SIGTERM SIGINT
491	            sleep "$grace_seconds"
492	            trap - SIGTERM SIGINT
493	        fi
494	    fi
495	    remove_tailscale_device "$MIRROR"
496	fi
497
498	# Backup handover notification
499	if [[ ${#PARTNERSHIP_MIRROR_BACKUPS[@]} -gt 0 ]]; then
500	    echo ""
501	    echo "━━━ $ICON_DISK Backup Handover ━━━"
502	    echo "Backups available for $MIRROR:"
503	    for path in "${PARTNERSHIP_MIRROR_BACKUPS[@]}"; do
504	        [[ -z "$path" ]] && continue
505	        echo "  $path"
506	    done
507	    notify "$MIRROR offboard complete — backups available for ${PARTNERSHIP_GRACE_HOURS:-6}hr. Tailscale access expires then." \
508	        "Partnership" "warning"
509	fi
510
511	# ── Summary ───────────────────────────────────────────────────────────────────────────────────
512	END=$(date +%s)
513	echo ""
514	echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY (Owner) ━━━━━"
515	echo "  Owner:    $MY_ID ($LOCAL_SERVER_NAME)"
516	echo "  Mirror:   $MIRROR_ID ($MIRROR)"
517	echo "  Reason:   $REASON"
518	echo "  Duration: $(format_duration $(( END - START )))"
519	echo ""
520	_ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
521	_revoke_status() {
522	    if   [[ "${SSH_REVOKE_REMOTE_OK:-false}" == true && "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
523	        echo "both directions ✅"
524	    elif [[ "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
525	        echo "local only ✅ — remote failed (revoke manually on $MIRROR)"
526	    else
527	        echo "⚠  failed — check warnings above"
528	    fi
529	}
530
531	echo "  Step 1  — Stop rsync:     $(_ok "$STEP_STOP_OK")"
532	echo "  Step 2  — Final sync:     $(_ok "$STEP_SYNC_OK")"
533	echo "  Step 3  — WebUI failures: $WEBUI_FAILURES"
534	echo "  Step 4  — Disable sync:   ✅"
535	echo "  Step 5  — Local cleanup:  ✅"
536	echo "  Step 6  — Own stack:      started"
537	echo "  Step 7  — Remote cleanup: $( [[ "$MIRROR_REACHABLE" == true ]] && echo "✅" || echo "skipped (unreachable)" )"
538	echo "  Step 8  — Mirror stack:   $( [[ "$MIRROR_REACHABLE" == true ]] && echo "started" || echo "skipped (unreachable)" )"
539	echo "  Step 9  — Keys revoked:   $(_revoke_status)"
540	echo "  Step 10 — State:          INACTIVE ✅"
541	echo ""
542	echo "  Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
543	[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
544	    echo "  Tailscale:   $MIRROR removed ✅"
545	echo ""
546	echo "  $MIRROR leaves with:"
547	echo "    ✓ Current auth config (final sync)"
548	echo "    ✓ Auth WebUIs → localhost"
549	echo "    ✓ ${PARTNERSHIP_GRACE_HOURS:-6}hr to collect backups"
550	echo ""
551	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
552	    warn "$ICON_DONE DONE — clean separation complete ✅"
553	echo "━━━━━━━━━━━━━━━━━━━━━━━"
554	exit 0
555

#!/bin/bash
# ==============================================================================================
# ============================= Partnership Offboard ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Cleanly ends a partnership. Role is detected automatically — run on either server.
# Owner path runs the full sequence including remote cleanup and final sync.
# Mirror path handles the local side and signals the owner to complete its own cleanup.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# OWNER PATH (10 steps)
#   Step 1:  Stop rsync         — halt any running sync before state changes
#   Step 2:  Final sync         — mirror leaves with current Critical-Data state
#   Step 3:  Reconfigure WebUIs — mirror's auth WebUIs → localhost
#   Step 4:  Disable sync       — CRITICAL_RSYNC_ENABLED=false in master.conf
#   Step 5:  Local cleanup      — remove fallback coverage containers + appdata
#   Step 6:  Restart own stack  — bring up owner's own parked containers
#   Step 7:  Remote cleanup     — remove auth/arr stack + fallback containers from mirror
#   Step 8:  Restart mirror     — bring up mirror's own parked containers
#   Step 9:  Revocation         — Emby admin, SSH keys
#   Step 10: Write state        — INACTIVE locally + pushed to mirror, mirror blocklisted
#            Tailscale          — grace window then device removal (after state written)
#
# MIRROR PATH (8 steps)
#   Step 1: Stop rsync          — halt any running sync
#   Step 2: Reconfigure WebUIs  — local auth WebUIs → localhost
#   Step 3: Remote stack clean  — remove owner-deployed containers locally (auth/arr stack)
#   Step 4: Fallback cleanup    — remove fallback coverage containers
#   Step 5: Disable sync        — CRITICAL_RSYNC_ENABLED=false in master.conf
#   Step 6: Revoke Emby admin   — remove own admin account from local Emby instance
#   Step 7: Restart own stack   — bring up own parked containers
#   Step 8: SSH revocation      — revoke keys both directions, write state, signal owner
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_PARTNERSHIP_AUTH_STACK
#       Auth container XMLs to push during onboard — used on offboard to identify what
#       to remove. Owner's PARTNERSHIP_AUTH_STACK determines which containers get removed
#       from the mirror on both owner-initiated and mirror-initiated offboard.
#
#   HOST*_PARTNERSHIP_ARR_STACK
#       Arr container XMLs — same cleanup logic as auth stack.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# Partnership/partnership_offboard.sh
#     Full offboard — role detected automatically
#
# Partnership/partnership_offboard.sh --dry-run
#     Preview all steps without executing
#
# Partnership/partnership_offboard.sh --log
#     Verbose per-step output
#
# Partnership/partnership_offboard.sh --reason=<string>
#     Tag the offboard reason in state file and blocklist (default: manual)
#     Called by partnership_manager.sh --offboard (reason passed through)
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPTS_ROOT="$SCRIPT_DIR/.."
SSH_TIMEOUT=15

source "$SCRIPTS_ROOT/load_config.sh"
source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"

# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
REASON="manual"
FILTERED_ARGS=()

for arg in "$@"; do
    case "$arg" in
        --reason=*) REASON="${arg#--reason=}" ;;
        *)          FILTERED_ARGS+=("$arg") ;;
    esac
done

parse_args "${FILTERED_ARGS[@]}"

# ── Source partnership_manager.sh for shared helpers ──────────────────────────────────────────
# PARTNERSHIP_LIB_MODE=1 skips mode dispatch — functions are defined, nothing is executed.
PARTNERSHIP_LIB_MODE=1 source "$SCRIPT_DIR/partnership_manager.sh"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }

if ! command -v docker &>/dev/null; then
    error "Docker command not found"
    exit 1
fi

detect_hosts

OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
OWNER="${!OWNER_ID}"
MIRROR="${!MIRROR_ID}"
MIRROR_SSH_KEY="$SSH_KEY"
OWNER_SSH_KEY="$SSH_KEY"

AM_OWNER=false
AM_MIRROR=false
[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true

LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db"
REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db"
OWNER_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${OWNER}.db"
MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db"
OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"

acquire_lock "strict"

trap _pm_trap_restart_stopped EXIT

# Check already offboarded
if [[ -f "$LOCAL_STATE_FILE" ]]; then
    CURRENT_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state")
    if [[ "$CURRENT_STATE" == "INACTIVE" ]]; then
        warn "Partnership already INACTIVE — use partnership_manager.sh --status to verify both servers agree"
        exit 0
    fi
fi

START=$(date +%s)

echo ""
echo "━━━ $ICON_FALLBACK Partnership Offboard — $MY_ID ($LOCAL_SERVER_NAME) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo ""
echo "  Role:    $( [[ "$AM_OWNER" == true ]] && echo "OWNER" || echo "MIRROR" )"
echo "  This:    $MY_ID ($LOCAL_SERVER_NAME)"
echo "  Partner: $( [[ "$AM_OWNER" == true ]] && echo "$MIRROR_ID ($MIRROR)" || echo "$OWNER_ID ($OWNER)" )"
echo "  Reason:  $REASON"
echo ""
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"

# ==============================================================================================
# ── HELPER: revoke own admin account from local Emby instance ────────────────────────────────
#
# Mirror-initiated path only. Called before start_own_stack so Emby is still running.
# Uses local EMBY_API_KEY and the mirror's own short name as the username to delete.
# ==============================================================================================
revoke_local_emby_admin() {
    local emby_port="${PARTNERSHIP_EMBY_PORT:-8096}"
    local emby_url="http://127.0.0.1:${emby_port}"

    echo ""
    echo "━━━ $ICON_EMBY Emby Admin Revocation ━━━"

    if [[ "${PARTNERSHIP_PROVISION_EMBY_ADMIN:-false}" != true ]]; then
        log "PARTNERSHIP_PROVISION_EMBY_ADMIN=false — skipping"
        return 0
    fi

    if [[ -z "${EMBY_API_KEY:-}" ]]; then
        warn "EMBY_API_KEY not set — skipping local Emby admin revocation"
        return 1
    fi

    # The account to revoke is this server's own short name (the mirror user's account)
    local username="${PARTNERSHIP_EMBY_ADMIN_USER:-$(derive_short_name "$LOCAL_SERVER_NAME")}"

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would delete Emby admin '$username' at $emby_url"
        return 0
    fi

    log "Looking up Emby user '$username' at $emby_url..."

    local users_json user_id
    users_json=$(curl -sf --max-time 15 \
        -H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
        "${emby_url}/Users" 2>/dev/null)

    user_id=$(echo "$users_json" | \
        grep -o "\"Id\":\"[^\"]*\"[^}]*\"Name\":\"${username}\"" | \
        grep -o '"Id":"[^"]*"' | cut -d'"' -f4 | head -1)

    if [[ -z "$user_id" ]]; then
        warn "Emby user '$username' not found at $emby_url — may already be removed"
        return 0
    fi

    local del_code
    del_code=$(curl -sf --max-time 15 -w "%{http_code}" -o /dev/null \
        -X DELETE \
        -H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
        "${emby_url}/Users/${user_id}" 2>/dev/null)

    if [[ "$del_code" == "200" ]] || [[ "$del_code" == "204" ]] || [[ "$del_code" == "404" ]]; then
        log "Emby admin '$username' removed ✅"
    else
        warn "Failed to delete Emby user '$username' (HTTP $del_code) — remove manually"
    fi
}

# ==============================================================================================
# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
if [[ "$AM_MIRROR" == true ]]; then
    warn "$MIRROR_ID ($MIRROR) is initiating offboard"
    warn "Owner ($OWNER) will see INACTIVE state on its next --check cycle and finalize"

    if [[ "$DRY_RUN" == false ]]; then
        echo ""
        echo "You have 10 seconds to cancel (Ctrl+C)..."
        sleep 10
    fi

    OWNER_IP=$(resolve_tailscale_ip "$OWNER")
    OWNER_REACHABLE=false
    [[ -n "$OWNER_IP" ]] && OWNER_REACHABLE=true

    STEP_STOP_RSYNC_OK=true
    STEP_WEBUI_OK=true
    STEP_STACK_CLEANUP_OK=true
    STEP_FALLBACK_CLEANUP_OK=true
    STEP_DISABLE_RSYNC_OK=true
    STEP_EMBY_OK=true
    SSH_REVOKE_REMOTE_OK=false
    SSH_REVOKE_LOCAL_OK=false

    # ── Step 1: Stop rsync ────────────────────────────────────────────────────────────────────
    echo ""
    echo "━━━ $ICON_STOP Step 1/8 — Stop Rsync ━━━"

    if [[ "$DRY_RUN" == false ]]; then
        bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || true
        log "Rsync stopped ✅"
    else
        warn "DRY RUN — would stop rsync"
    fi

    # ── 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 ""
    echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"

    cleanup_partner_containers || STEP_FALLBACK_CLEANUP_OK=false

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

    if [[ "$DRY_RUN" == false ]]; then
        update_master_conf "CRITICAL_RSYNC_ENABLED" "false" && \
            warn "CRITICAL_RSYNC_ENABLED=false ✅" || \
            { warn "Failed to update CRITICAL_RSYNC_ENABLED"; STEP_DISABLE_RSYNC_OK=false; }
    else
        warn "DRY RUN — would set CRITICAL_RSYNC_ENABLED=false"
    fi

    # ── Step 6: Revoke Emby admin locally ─────────────────────────────────────────────────────
    revoke_local_emby_admin || STEP_EMBY_OK=false

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

    # ── Step 8: SSH key revocation, write state, signal owner ────────────────────────────────
    echo ""
    echo "━━━ $ICON_SHIELD Step 8/8 — SSH Revocation + State ━━━"

    do_ssh_key_revocation "${OWNER_IP:-}"

    NOW=$(date '+%Y-%m-%d %H:%M:%S')
    if [[ "$DRY_RUN" == false ]]; then
        write_state_file "$LOCAL_STATE_FILE" \
            "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
        echo "Local state: INACTIVE ✅"
        add_to_blocklist "$OWNER" "$REASON"
    else
        warn "DRY RUN — would write INACTIVE state and blocklist $OWNER"
    fi

    if [[ "$OWNER_REACHABLE" == true ]]; then
        push_state_to_remote "$LOCAL_STATE_FILE" "$OWNER_IP" "$OWNER_SSH_KEY"
        notify "Partnership offboard requested by $MIRROR — $OWNER will finalise on next check" \
            "Partnership" "normal"
    else
        warn "$OWNER unreachable — state written locally, owner will see it when reachable"
    fi

    # ── Summary ───────────────────────────────────────────────────────────────────────────────
    END=$(date +%s)
    echo ""
    echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY (Mirror) ━━━━━"
    echo "  Mirror:  $MY_ID ($LOCAL_SERVER_NAME)"
    echo "  Owner:   $OWNER_ID ($OWNER)"
    echo "  Reason:  $REASON"
    echo "  Duration: $(format_duration $(( END - START )))"
    echo ""
    _ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
    _skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
    _revoke_status() {
        if   [[ "${SSH_REVOKE_REMOTE_OK:-false}" == true && "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
            echo "both directions ✅"
        elif [[ "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
            echo "local only ✅ — remote failed (revoke manually on $OWNER)"
        else
            echo "⚠  failed — check warnings above"
        fi
    }

    echo "  Step 1 — Stop rsync:       $(_ok "$STEP_STOP_RSYNC_OK")"
    echo "  Step 2 — WebUIs:           $(_ok "$STEP_WEBUI_OK")"
    echo "  Step 3 — Stack cleanup:    $(_ok "$STEP_STACK_CLEANUP_OK")"
    echo "  Step 4 — Fallback cleanup: $(_ok "$STEP_FALLBACK_CLEANUP_OK")"
    echo "  Step 5 — Disable sync:     $(_ok "$STEP_DISABLE_RSYNC_OK")"
    echo "  Step 6 — Emby revoke:      $(_ok "$STEP_EMBY_OK")"
    echo "  Step 7 — Own stack:        started"
    echo "  Step 8 — Keys revoked:     $(_revoke_status)"
    echo ""
    echo "  State:    INACTIVE ✅"
    echo "  Blocklist: $OWNER blocked ✅"
    echo "  Owner:    will finalise + final sync on next --check"
    echo ""
    [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
        warn "$ICON_DONE DONE — mirror separation complete ✅"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ── OWNER PATH ────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
warn "Offboarding $MIRROR_ID ($MIRROR) from partnership"
warn "Final sync will run — mirror leaves with current state"

if [[ "$DRY_RUN" == false ]]; then
    echo ""
    echo "You have 10 seconds to cancel (Ctrl+C)..."
    sleep 10
    echo "Proceeding..."
fi

WEBUI_FAILURES=0
STEP_STOP_OK=true
STEP_SYNC_OK=true
SSH_REVOKE_REMOTE_OK=false
SSH_REVOKE_LOCAL_OK=false

# ── Step 1: Stop rsync ────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_STOP Step 1/10 — Stop Rsync ━━━"

if [[ "$DRY_RUN" == false ]]; then
    bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || STEP_STOP_OK=false
    log "Rsync stopped ✅"
else
    warn "DRY RUN — would stop rsync"
fi

# ── Step 2: Final sync ────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_SYNC Step 2/10 — Final Sync ━━━"

do_final_sync || STEP_SYNC_OK=false

# ── Step 3: Reconfigure mirror WebUIs → localhost ─────────────────────────────────────────────
echo ""
echo "━━━ $ICON_CONTAINERS Step 3/10 — Reconfigure Mirror WebUIs → localhost ━━━"

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 ""
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 ━━━"

cleanup_partner_containers

# ── 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"
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
do_ssh_key_revocation "${MIRROR_IP:-}"

# ── Step 10: Write state, push to mirror, blocklist ───────────────────────────────────────────
# State is written after container cleanup and SSH revocation so that:
#   • Re-running after a crash between steps 5–9 restarts from scratch (no early-exit on INACTIVE)
#   • --check sees INACTIVE during the Tailscale grace sleep and does not re-trigger offboard
echo ""
echo "━━━ $ICON_GEAR Step 10/10 — Write State ━━━"

NOW=$(date '+%Y-%m-%d %H:%M:%S')

if [[ "$DRY_RUN" == false ]]; then
    write_state_file "$LOCAL_STATE_FILE" \
        "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
    log "Local state: INACTIVE ✅"
    add_to_blocklist "$MIRROR" "$REASON"
    [[ "$MIRROR_REACHABLE" == true ]] && \
        push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
else
    warn "DRY RUN — would write INACTIVE state, blocklist $MIRROR, push to remote"
fi

# Tailscale removal — after state written so --check does not re-trigger offboard during grace sleep
if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then
    echo ""
    echo "━━━ $ICON_NET Tailscale Separation ━━━"
    if [[ "$MIRROR_REACHABLE" == true ]]; then
        grace_seconds=$(( ${PARTNERSHIP_GRACE_HOURS:-6} * 3600 ))
        warn "Waiting ${PARTNERSHIP_GRACE_HOURS:-6}hr grace — mirror can collect backups..."
        if [[ "$DRY_RUN" == false ]]; then
            trap 'warn "Offboard interrupted during grace sleep"; exit 0' SIGTERM SIGINT
            sleep "$grace_seconds"
            trap - SIGTERM SIGINT
        fi
    fi
    remove_tailscale_device "$MIRROR"
fi

# Backup handover notification
if [[ ${#PARTNERSHIP_MIRROR_BACKUPS[@]} -gt 0 ]]; then
    echo ""
    echo "━━━ $ICON_DISK Backup Handover ━━━"
    echo "Backups available for $MIRROR:"
    for path in "${PARTNERSHIP_MIRROR_BACKUPS[@]}"; do
        [[ -z "$path" ]] && continue
        echo "  $path"
    done
    notify "$MIRROR offboard complete — backups available for ${PARTNERSHIP_GRACE_HOURS:-6}hr. Tailscale access expires then." \
        "Partnership" "warning"
fi

# ── Summary ───────────────────────────────────────────────────────────────────────────────────
END=$(date +%s)
echo ""
echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY (Owner) ━━━━━"
echo "  Owner:    $MY_ID ($LOCAL_SERVER_NAME)"
echo "  Mirror:   $MIRROR_ID ($MIRROR)"
echo "  Reason:   $REASON"
echo "  Duration: $(format_duration $(( END - START )))"
echo ""
_ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
_revoke_status() {
    if   [[ "${SSH_REVOKE_REMOTE_OK:-false}" == true && "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
        echo "both directions ✅"
    elif [[ "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
        echo "local only ✅ — remote failed (revoke manually on $MIRROR)"
    else
        echo "⚠  failed — check warnings above"
    fi
}

echo "  Step 1  — Stop rsync:     $(_ok "$STEP_STOP_OK")"
echo "  Step 2  — Final sync:     $(_ok "$STEP_SYNC_OK")"
echo "  Step 3  — WebUI failures: $WEBUI_FAILURES"
echo "  Step 4  — Disable sync:   ✅"
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_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


Good full picture. I know exactly what needs to change. Here's the plan before touching anything:

**`partnership_onboard.sh`** — add 2 new steps (stop mirror services, deploy services stack) between arr deploy and the partnership_manager step. Renumber all subsequent steps. Add `--skip-services-stack` flag and tracking vars. Update header docs.

**`containers.sh`** — `cleanup_deployed_stack_on_remote()` and `cleanup_deployed_stack_locally()` both need to pull `PARTNERSHIP_SERVICES_STACK` alongside auth/arr.

**`partnership_offboard.sh`** — header docs only (the cleanup functions handle the rest automatically).

**`partnership_manager.sh`** — header docs only.

**`host1.conf`** — add `HOST1_PARTNERSHIP_SERVICES_STACK` alongside auth/arr stacks. Let me check what Emby/Jellyfin/Overseerr XMLs are named:


ls /boot/config/plugins/dockerMan/templates-user/ | grep -iE "emby|jellyfin|seerr|overseerr|jellyf"

Find Emby/Jellyfin/Seerr XML templates


my-Emby.xml
my-ErsatzTV-Emby.xml
my-Jellyfinh.xml
my-Jellyseerr.xml
my-Seerr.xml
my-SeerrFin.xml
my-emby-test.xml
my-jellyfin.xml
my-musicseerr.xml


awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1]}' \
    /boot/config/plugins/dockerMan/templates-user/my-Emby.xml \
    /boot/config/plugins/dockerMan/templates-user/my-Jellyfinh.xml \
    /boot/config/plugins/dockerMan/templates-user/my-jellyfin.xml \
    /boot/config/plugins/dockerMan/templates-user/my-Jellyseerr.xml \
    /boot/config/plugins/dockerMan/templates-user/my-Seerr.xml \
    /boot/config/plugins/dockerMan/templates-user/my-SeerrFin.xml 2>/dev/null

Get container names from service XMLs


Emby
Jellyfinh
Jellyfin
Jellyseerr
Seerr
SeerrFin


Got 6 candidates. Two ambiguities before I write anything:

1. **Jellyfin** — `my-jellyfin.xml` → `Jellyfin` vs `my-Jellyfinh.xml` → `Jellyfinh`. Which one (or both) goes in the services stack?

2. **Seerr** — you have `Jellyseerr`, `Seerr`, and `SeerrFin`. Which of these should the mirror get?

Once you tell me those, I'll make all the changes in one pass: `host1.conf` gets the new `HOST1_PARTNERSHIP_SERVICES_STACK`, `partnership_onboard.sh` gets the two new steps (stop + deploy services, renumbering everything after), `containers.sh` cleanup functions get services stack included, and the offboard/manager headers get updated docs.


jellyfin and seerr and seerrfin


Let me check how `detect_hosts()` aliases the stack vars before I write anything:


grep -n "PARTNERSHIP_ARR_STACK\|PARTNERSHIP_AUTH_STACK\|PARTNERSHIP_SERVICES" /boot/config/plugins/varaverk/common.sh | head -30

Find how detect_hosts aliases partnership stack vars


585:    _alias_array "PARTNERSHIP_AUTH_STACK"
587:    _alias_array "PARTNERSHIP_ARR_STACK"


575	    _alias_array "ANIME_CLEAN_FOLDERS"
576	    _alias_array "MEDIA_CLEAN_FOLDERS"
577	    _alias_array "CERT_MONITOR_DOMAINS"
578	    _alias_array "SMART_IGNORE_DRIVES"
579	    _alias_array "ZFS_REPORT_IGNORE_POOLS"
580	    _alias_array "TRANSCODE_SERVERS"
581	    _alias_array "DDNS_CONTAINERS"
582	    _alias_array "PARTNERSHIP_AUTH_WEBUIS"
583	    _alias_array "PARTNERSHIP_MIRROR_BACKUPS"
584	    _alias_array "PARTNERSHIP_OWN_CONTAINERS"
585	    _alias_array "PARTNERSHIP_AUTH_STACK"
586	    _alias_array "PARTNERSHIP_REPLACE_CONTAINERS"
587	    _alias_array "PARTNERSHIP_ARR_STACK"
588	    _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
589	    _alias_array "RW_PAUSE_CONTAINERS"
590	    _alias_array "RW_STOP_CONTAINERS"
591
592	    # ── Set array aliases — associative arrays ────────────────────────────────
593	    # Associative arrays cannot be copied with eval — must be rebuilt key by key
594

    _alias_array "ANIME_CLEAN_FOLDERS"
    _alias_array "MEDIA_CLEAN_FOLDERS"
    _alias_array "CERT_MONITOR_DOMAINS"
    _alias_array "SMART_IGNORE_DRIVES"
    _alias_array "ZFS_REPORT_IGNORE_POOLS"
    _alias_array "TRANSCODE_SERVERS"
    _alias_array "DDNS_CONTAINERS"
    _alias_array "PARTNERSHIP_AUTH_WEBUIS"
    _alias_array "PARTNERSHIP_MIRROR_BACKUPS"
    _alias_array "PARTNERSHIP_OWN_CONTAINERS"
    _alias_array "PARTNERSHIP_AUTH_STACK"
    _alias_array "PARTNERSHIP_REPLACE_CONTAINERS"
    _alias_array "PARTNERSHIP_ARR_STACK"
    _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
    _alias_array "RW_PAUSE_CONTAINERS"
    _alias_array "RW_STOP_CONTAINERS"

    # ── Set array aliases — associative arrays ────────────────────────────────
    # Associative arrays cannot be copied with eval — must be rebuilt key by key


Need to add the alias there too. Full changeset — `common.sh`, `host1.conf`, `partnership_onboard.sh`, `containers.sh`, and `partnership_offboard.sh` header. Let me do them all:


1	#!/bin/bash
2	# ==============================================================================================
3	# ========================== HOST1 CONFIGURATION — unRAID-Gmer4Lfe ============================
4	# ==============================================================================================
5	# HOST1-specific variables — credentials, container names, share paths, failover lists.
6	# Sourced after master.conf — values here extend shared profile arrays and add HOST1-specific
7	# identity, credentials, and container configuration.
8	#
9	# Sparse checkout (git) ensures HOST2 never receives this file.
10	# HOST2 never sees HOST1 credentials — clean separation at the file level.
11	#
12	# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
13	# DO NOT put HOST2 variables here — they belong in host2.conf.
14	#
15	# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
16	#
17	#   ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
18	#   IDENTITY                hostname, SSH key, Unraid API key
19	#   EMBY                    container name, URL, API key
20	#   JELLYFIN                container name, URL, API key

#!/bin/bash
# ==============================================================================================
# ========================== HOST1 CONFIGURATION — unRAID-Gmer4Lfe ============================
# ==============================================================================================
# HOST1-specific variables — credentials, container names, share paths, failover lists.
# Sourced after master.conf — values here extend shared profile arrays and add HOST1-specific
# identity, credentials, and container configuration.
#
# Sparse checkout (git) ensures HOST2 never receives this file.
# HOST2 never sees HOST1 credentials — clean separation at the file level.
#
# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
# DO NOT put HOST2 variables here — they belong in host2.conf.
#
# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
#
#   ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
#   IDENTITY                hostname, SSH key, Unraid API key
#   EMBY                    container name, URL, API key
#   JELLYFIN                container name, URL, API key


Find partnership arr stack section in host1.conf


162:    HOST1_PARTNERSHIP_ARR_STACK=(
176:    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(


162	    HOST1_PARTNERSHIP_ARR_STACK=(
163	         "my-Sonarr.xml"
164	         "my-Radarr.xml"
165	         "my-Lidarr.xml"
166	         "my-Prowlarr.xml"
167	         "my-Bazarr.xml"
168	    )
169
170	# Containers stopped on THIS server before deploying the mirror's stack on onboard.
171	# Only needed when this server parks its own stack to make room for the mirror's.
172	    HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
173	    )
174
175	# Arr containers stopped on this server when mirror's arr stack is deployed.
176	    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
177	    )
178
179	# Paths HOST2 should collect during the grace window after offboard.
180	# Notified on offboard — no auto-deletion, HOST2 must collect manually within PARTNERSHIP_GRACE_HOURS.
181	    HOST1_PARTNERSHIP_MIRROR_BACKUPS=(
182	        # "/mnt/user/appdata-Fallback/Jayred365-Emby"
183	    )
184
185	# Containers parked on this server when partnership is active.
186	# Stopped on onboard (owner deploys its stack instead), restarted on offboard.

    HOST1_PARTNERSHIP_ARR_STACK=(
         "my-Sonarr.xml"
         "my-Radarr.xml"
         "my-Lidarr.xml"
         "my-Prowlarr.xml"
         "my-Bazarr.xml"
    )

# Containers stopped on THIS server before deploying the mirror's stack on onboard.
# Only needed when this server parks its own stack to make room for the mirror's.
    HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
    )

# Arr containers stopped on this server when mirror's arr stack is deployed.
    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
    )

# Paths HOST2 should collect during the grace window after offboard.
# Notified on offboard — no auto-deletion, HOST2 must collect manually within PARTNERSHIP_GRACE_HOURS.
    HOST1_PARTNERSHIP_MIRROR_BACKUPS=(
        # "/mnt/user/appdata-Fallback/Jayred365-Emby"
    )

# Containers parked on this server when partnership is active.
# Stopped on onboard (owner deploys its stack instead), restarted on offboard.


Now I have everything. Making all changes:


Edit

    HOST1_PARTNERSHIP_ARR_STACK=(
         "my-Sonarr.xml"
         "my-Radarr.xml"
         "my-Lidarr.xml"
         "my-Prowlarr.xml"
         "my-Bazarr.xml"
    )

# Containers stopped on THIS server before deploying the mirror's stack on onboard.
# Only needed when this server parks its own stack to make room for the mirror's.
    HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
    )

# Arr containers stopped on this server when mirror's arr stack is deployed.
    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
    )

    HOST1_PARTNERSHIP_ARR_STACK=(
         "my-Sonarr.xml"
         "my-Radarr.xml"
         "my-Lidarr.xml"
         "my-Prowlarr.xml"
         "my-Bazarr.xml"
    )

# Shared media services deployed on the mirror during onboard.
# Emby, Jellyfin, and request managers — deployed after arrs so library paths exist.
    HOST1_PARTNERSHIP_SERVICES_STACK=(
        "my-Emby.xml"
        "my-jellyfin.xml"
        "my-Seerr.xml"
        "my-SeerrFin.xml"
    )

# Containers stopped on THIS server before deploying the mirror's stack on onboard.
# Only needed when this server parks its own stack to make room for the mirror's.
    HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
    )

# Arr containers stopped on this server when mirror's arr stack is deployed.
    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
    )

# Services containers stopped on this server when mirror's services stack is deployed.
    HOST1_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS=(
    )


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

    HOST1_PARTNERSHIP_ARR_STACK=(
         "my-Sonarr.xml"
         "my-Radarr.xml"
         "my-Lidarr.xml"
         "my-Prowlarr.xml"
         "my-Bazarr.xml"
    )

# Containers stopped on THIS server before deploying the mirror's stack on onboard.
# Only needed when this server parks its own stack to make room for the mirror's.
    HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
    )

# Arr containers stopped on this server when mirror's arr stack is deployed.
    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
    )

    HOST1_PARTNERSHIP_ARR_STACK=(
         "my-Sonarr.xml"
         "my-Radarr.xml"
         "my-Lidarr.xml"
         "my-Prowlarr.xml"
         "my-Bazarr.xml"
    )

# Shared media services deployed on the mirror during onboard.
# Emby, Jellyfin, and request managers — deployed after arrs so library paths exist.
    HOST1_PARTNERSHIP_SERVICES_STACK=(
        "my-Emby.xml"
        "my-jellyfin.xml"
        "my-Seerr.xml"
        "my-SeerrFin.xml"
    )

# Containers stopped on THIS server before deploying the mirror's stack on onboard.
# Only needed when this server parks its own stack to make room for the mirror's.
    HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
    )

# Arr containers stopped on this server when mirror's arr stack is deployed.
    HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
    )

# Services containers stopped on this server when mirror's services stack is deployed.
    HOST1_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS=(
    )

+# Shared media services deployed on the mirror during onboard.

+# Emby, Jellyfin, and request managers — deployed after arrs so library paths exist.

+# Services containers stopped on this server when mirror's services stack is deployed.

+    HOST1_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS=(

# Paths HOST2 should collect during the grace window after offboard.

# Notified on offboard — no auto-deletion, HOST2 must collect manually within PARTNERSHIP_GRACE_HOURS.


Conversation compacted