Now add Steps 6 and 7, and renumber the old steps:


Edit

# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
echo ""
echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"

if [[ "$SKIP_ARR_STACK" == true ]]; then
    warn "Skipping (--skip-arr-stack)"
else
    deploy_xml_stack PARTNERSHIP_ARR_STACK
    ARR_DEPLOYED=$_STACK_DEPLOYED
    ARR_FAILED=$_STACK_FAILED
    echo "Arr stack: $ARR_DEPLOYED deployed, $ARR_FAILED failed"
    [[ "$ARR_FAILED" -gt 0 ]] && STEP_ARR_OK=false
fi

# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 6 — Partnership Onboard ━━━"

# ── Step 5: Deploy arr stack on mirror ───────────────────────────────────────────────────────
echo ""
echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"

if [[ "$SKIP_ARR_STACK" == true ]]; then
    warn "Skipping (--skip-arr-stack)"
else
    deploy_xml_stack PARTNERSHIP_ARR_STACK
    ARR_DEPLOYED=$_STACK_DEPLOYED
    ARR_FAILED=$_STACK_FAILED
    echo "Arr stack: $ARR_DEPLOYED deployed, $ARR_FAILED failed"
    [[ "$ARR_FAILED" -gt 0 ]] && STEP_ARR_OK=false
fi

# ── Step 6: Stop mirror's existing services stack ─────────────────────────────────────────────
echo ""
echo "━━━ Step 6 — Stop Mirror Services Stack ━━━"

if [[ "$SKIP_SERVICES_STACK" == true ]]; then
    warn "Skipping (--skip-services-stack)"
elif [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -eq 0 ]]; then
    log "PARTNERSHIP_SERVICES_STACK not configured — skipping services stack deploy"
    SKIP_SERVICES_STACK=true
else
    stop_mirror_stack "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS" "services stack"
fi

# ── Step 7: Deploy services stack on mirror ───────────────────────────────────────────────────
echo ""
echo "━━━ Step 7 — Deploy Services Stack on Mirror ━━━"

if [[ "$SKIP_SERVICES_STACK" == true ]]; then
    warn "Skipping (--skip-services-stack)"
else
    deploy_xml_stack PARTNERSHIP_SERVICES_STACK
    SERVICES_DEPLOYED=$_STACK_DEPLOYED
    SERVICES_FAILED=$_STACK_FAILED
    echo "Services stack: $SERVICES_DEPLOYED deployed, $SERVICES_FAILED failed"
    [[ "$SERVICES_FAILED" -gt 0 ]] && STEP_SERVICES_OK=false
fi

# ── Step 8: Partnership onboard ───────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 8 — Partnership Onboard ━━━"


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 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
echo ""
echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"

if [[ "$SKIP_ARR_STACK" == true ]]; then
    warn "Skipping (--skip-arr-stack)"
else
    deploy_xml_stack PARTNERSHIP_ARR_STACK
    ARR_DEPLOYED=$_STACK_DEPLOYED
    ARR_FAILED=$_STACK_FAILED
    echo "Arr stack: $ARR_DEPLOYED deployed, $ARR_FAILED failed"
    [[ "$ARR_FAILED" -gt 0 ]] && STEP_ARR_OK=false
fi

# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 6 — Partnership Onboard ━━━"

# ── Step 5: Deploy arr stack on mirror ───────────────────────────────────────────────────────
echo ""
echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"

if [[ "$SKIP_ARR_STACK" == true ]]; then
    warn "Skipping (--skip-arr-stack)"
else
    deploy_xml_stack PARTNERSHIP_ARR_STACK
    ARR_DEPLOYED=$_STACK_DEPLOYED
    ARR_FAILED=$_STACK_FAILED
    echo "Arr stack: $ARR_DEPLOYED deployed, $ARR_FAILED failed"
    [[ "$ARR_FAILED" -gt 0 ]] && STEP_ARR_OK=false
fi

# ── Step 6: Stop mirror's existing services stack ─────────────────────────────────────────────
echo ""
echo "━━━ Step 6 — Stop Mirror Services Stack ━━━"

if [[ "$SKIP_SERVICES_STACK" == true ]]; then
    warn "Skipping (--skip-services-stack)"
elif [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -eq 0 ]]; then
    log "PARTNERSHIP_SERVICES_STACK not configured — skipping services stack deploy"
    SKIP_SERVICES_STACK=true
else
    stop_mirror_stack "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS" "services stack"
fi

# ── Step 7: Deploy services stack on mirror ───────────────────────────────────────────────────
echo ""
echo "━━━ Step 7 — Deploy Services Stack on Mirror ━━━"

if [[ "$SKIP_SERVICES_STACK" == true ]]; then
    warn "Skipping (--skip-services-stack)"
else
    deploy_xml_stack PARTNERSHIP_SERVICES_STACK
    SERVICES_DEPLOYED=$_STACK_DEPLOYED
    SERVICES_FAILED=$_STACK_FAILED
    echo "Services stack: $SERVICES_DEPLOYED deployed, $SERVICES_FAILED failed"
    [[ "$SERVICES_FAILED" -gt 0 ]] && STEP_SERVICES_OK=false
fi

# ── Step 8: Partnership onboard ───────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 8 — Partnership Onboard ━━━"

stop_mirror_stack "PARTNERSHIP_ARR_REPLACE_CONTAINERS" "arr stack"

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

+# ── Step 5: Deploy arr stack on mirror ───────────────────────────────────────────────────────

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

+# ── Step 6: Stop mirror's existing services stack ─────────────────────────────────────────────

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

+echo "━━━ Step 6 — Stop Mirror Services Stack ━━━"

+if [[ "$SKIP_SERVICES_STACK" == true ]]; then

+    warn "Skipping (--skip-services-stack)"

+elif [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -eq 0 ]]; then

+    log "PARTNERSHIP_SERVICES_STACK not configured — skipping services stack deploy"

+    stop_mirror_stack "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS" "services stack"

+# ── Step 7: Deploy services stack on mirror ───────────────────────────────────────────────────

+echo "━━━ Step 7 — Deploy Services Stack on Mirror ━━━"

+    deploy_xml_stack PARTNERSHIP_SERVICES_STACK

+    echo "Services stack: $SERVICES_DEPLOYED deployed, $SERVICES_FAILED failed"

+    [[ "$SERVICES_FAILED" -gt 0 ]] && STEP_SERVICES_OK=false

+# ── Step 8: Partnership onboard ───────────────────────────────────────────────────────────────

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

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


# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 7 — Arr Library Bootstrap ━━━"

# ── Step 9: Arr library bootstrap ─────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 9 — Arr Library Bootstrap ━━━"


# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 7 — Arr Library Bootstrap ━━━"

# ── Step 9: Arr library bootstrap ─────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 9 — Arr Library Bootstrap ━━━"

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

+# ── Step 9: Arr library bootstrap ─────────────────────────────────────────────────────────────

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

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

warn "Skipping — onboard did not complete"


# ── Step 9: Push master.conf to all listed hosts ──────────────────────────────────────────────
# 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 ""
echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"

# ── Step 10: Push master.conf to all listed hosts ─────────────────────────────────────────────
# 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 ""
echo "━━━ $ICON_GEAR Step 10 — master.conf Push ━━━"


# ── Step 9: Push master.conf to all listed hosts ──────────────────────────────────────────────
# 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 ""
echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"

# ── Step 10: Push master.conf to all listed hosts ─────────────────────────────────────────────
# 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 ""
echo "━━━ $ICON_GEAR Step 10 — master.conf Push ━━━"

warn "Re-run Media/arr_sync.sh once all arr containers are live"

-# ── Step 9: Push master.conf to all listed hosts ──────────────────────────────────────────────

+# ── Step 10: Push master.conf to all listed hosts ─────────────────────────────────────────────

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

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


Monitor media server sync and backup health


Now update the summary:


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  — 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  — Stop services:    $(_skip "$SKIP_SERVICES_STACK"   "$STEP_STOP_SERVICES_OK")"
echo "  Step 7  — Services stack:   $( [[ "$SKIP_SERVICES_STACK" == true ]] && echo "skipped" || echo "${SERVICES_DEPLOYED} deployed, ${SERVICES_FAILED} failed" )"
echo "  Step 8  — Onboard:          $(_ok "$ONBOARD_OK")"
echo "  Step 9  — Arr bootstrap:    $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
echo "  Step 10 — 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  — 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  — Stop services:    $(_skip "$SKIP_SERVICES_STACK"   "$STEP_STOP_SERVICES_OK")"
echo "  Step 7  — Services stack:   $( [[ "$SKIP_SERVICES_STACK" == true ]] && echo "skipped" || echo "${SERVICES_DEPLOYED} deployed, ${SERVICES_FAILED} failed" )"
echo "  Step 8  — Onboard:          $(_ok "$ONBOARD_OK")"
echo "  Step 9  — Arr bootstrap:    $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
echo "  Step 10 — 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  — 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  — Stop services:    $(_skip "$SKIP_SERVICES_STACK"   "$STEP_STOP_SERVICES_OK")"

+echo "  Step 7  — Services stack:   $( [[ "$SKIP_SERVICES_STACK" == true ]] && echo "skipped" || echo "${SERVICES_DEPLOYED} deployed, ${SERVICES_FAILED} failed" )"

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

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

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


Now `containers.sh` — the two cleanup functions:


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

# 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
}


    [[ ${#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..."

    [[ ${#PARTNERSHIP_AUTH_STACK[@]}     -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
    [[ ${#PARTNERSHIP_ARR_STACK[@]}      -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
    [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}")

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

    log "Removing owner-deployed containers (auth/arr/services stacks) from $MIRROR..."


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)

    [[ ${#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..."

    [[ ${#PARTNERSHIP_AUTH_STACK[@]}     -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
    [[ ${#PARTNERSHIP_ARR_STACK[@]}      -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
    [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}")

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

    log "Removing owner-deployed containers (auth/arr/services stacks) from $MIRROR..."

-    [[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")

-    [[ ${#PARTNERSHIP_ARR_STACK[@]}  -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")

+    [[ ${#PARTNERSHIP_AUTH_STACK[@]}     -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")

+    [[ ${#PARTNERSHIP_ARR_STACK[@]}      -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")

+    [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}")

-        log "No auth/arr stack arrays configured — skipping deployed stack cleanup"

+        log "No auth/arr/services stack arrays configured — skipping deployed stack cleanup"

-    log "Removing owner-deployed containers (auth/arr stacks) from $MIRROR..."

+    log "Removing owner-deployed containers (auth/arr/services stacks) from $MIRROR..."


        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[@]}")

        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 '^$')
        local -a svc_arr
        mapfile -t svc_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_SERVICES_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
        xml_names=("${auth_arr[@]}" "${arr_arr[@]}" "${svc_arr[@]}")


        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[@]}")

        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 '^$')
        local -a svc_arr
        mapfile -t svc_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_SERVICES_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
        xml_names=("${auth_arr[@]}" "${arr_arr[@]}" "${svc_arr[@]}")

+        mapfile -t svc_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

+             printf '%s\n' \"\${PARTNERSHIP_SERVICES_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')


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

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

    log "Removing owner-deployed containers (auth/arr/services stacks) locally..."


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

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

    log "Removing owner-deployed containers (auth/arr/services stacks) locally..."

-        log "Could not read deployed stack from owner — skipping auth/arr cleanup"

+        log "Could not read deployed stack from owner — skipping auth/arr/services cleanup"

-    log "Removing owner-deployed containers (auth/arr stacks) locally..."

+    log "Removing owner-deployed containers (auth/arr/services stacks) locally..."


Now the header doc updates in `partnership_offboard.sh` and `partnership_manager.sh`:


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"

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


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Partnership Manager ============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Manages the full lifecycle of a two-server partnership — onboard, daily health
9	# monitoring, offboard, and ownership transfer. Called by partnership_onboard.sh
10	# during initial setup, and by critical_sync_maintenance.sh every 30 minutes for
11	# the --check mode. All other modes are run manually.
12	#
13	# PARTNERSHIP_OWNER_HOST flips to "HOST2" after a successful --transfer.
14	# AM_OWNER / AM_MIRROR flags (set by detect_hosts) control all routing —
15	# no hostname string comparisons anywhere in this script.
16	#
17	# ==============================================================================================
18	# OPERATIONAL MODEL
19	# ==============================================================================================
20	#
21	# --onboard (owner only)
22	#   Reconfigures mirror auth WebUIs → owner's Tailscale IP
23	#   Mirror operator clicks NPM → gets owner's NPM via Tailscale automatically
24	#   Writes ACTIVE state on both servers
25	#
26	# --offboard (either server)
27	#   Mirror-initiated: reconfigures own WebUIs → localhost, writes INACTIVE state
28	#     Owner finalises on next --check: final sync, Tailscale removal
29	#   Owner-initiated: final sync, reconfigures mirror WebUIs → localhost, removes
30	#     mirror containers + appdata, SSH key revocation, Tailscale removal
31	#   Both leave with clean state ✅
32	#
33	# --transfer (owner only)
34	#   Reconfigures both servers, flips PARTNERSHIP_OWNER_HOST in master.conf
35	#   Requires confirmation string + consecutive health check passes
36	#
37	# --check (called every 30min by critical_sync_maintenance.sh)
38	#   --remote-seen:   rsync succeeded → reset offline counter, read remote state
39	#   --remote-unseen: rsync failed → increment offline counter → auto-offboard at threshold
40	#   Silent when healthy — never noisy on clean runs
41	#
42	# --status (either server)
43	#   Show state files from both servers, blocklist, SSH key status
44	#
45	# ==============================================================================================
46	# DESIGN PRINCIPLES
47	# ==============================================================================================
48	#
49	# Deferred offboard
50	#   Either server can offboard without the other being reachable. The initiating
51	#   server writes its state immediately and becomes independent. The other server
52	#   reads the INACTIVE state on its next --check and finalises automatically.
53	#   No message passing, no coordination required.
54	#
55	# Appdata cleanup on offboard
56	#   Partner containers are stopped and removed. Their appdata bind-mount paths
57	#   (collected via docker inspect before removal) are also deleted. Safety gate:
58	#   only paths matching /mnt/*/appdata* are deleted — media and config shares
59	#   outside the appdata tree are never touched.
60	#
61	# Blocklist is application-layer; Tailscale removal is network-layer
62	#   Both happen on offboard. The blocklist prevents re-onboard until explicitly
63	#   cleared with --unblock. Tailscale removal ends encrypted access at the network
64	#   level. Grace period controls both simultaneously — one var, consistent behaviour.
65	#
66	# ==============================================================================================
67	# OPERATIONAL SAFEGUARDS
68	# ==============================================================================================
69	#
70	# Root check
71	#   All operations require root.
72	#
73	# Role enforcement
74	#   Mirror cannot run --onboard or --transfer. Blocked with a clear error message.
75	#
76	# Version parity check
77	#   --onboard verifies both servers are on compatible unRAID versions.
78	#
79	# SSH_TIMEOUT on all remote calls
80	#   Every ssh/scp call is timeout-protected. No operation hangs on an unreachable peer.
81	#
82	# flock on state writes
83	#   Prevents concurrent state file corruption from overlapping --check cycles.
84	#
85	# SIGTERM trap on grace period sleep
86	#   Offboard grace period is interruptible — Ctrl-C aborts cleanly.
87	#
88	# Silent by default
89	#   --check produces no output when both servers are healthy. Only state changes
90	#   and threshold crossings produce output.
91	#
92	# ==============================================================================================
93	# STATE FILES
94	# ==============================================================================================
95	#
96	#   /boot/config/partnership_HOST1.db  — HOST1 writes, HOST2 reads via SSH
97	#   /boot/config/partnership_HOST2.db  — HOST2 writes, HOST1 reads via SSH
98	#   /boot/config/partnership_blocklist.db  — hostname|timestamp|reason, persists until cleared
99	#
100	# On /boot/config — survives reboots, available before array starts, minimal flash wear.
101	#
102	# ==============================================================================================
103	# CONFIGURATION
104	# ==============================================================================================
105	#
106	# master.conf
107	#
108	#   PARTNERSHIP_ENABLED
109	#       Global gate — set true once both servers are configured (default: false)
110	#
111	#   PARTNERSHIP_OWNER_HOST
112	#       "HOST1" or "HOST2" — flips on --transfer (default: "HOST1")
113	#
114	#   PARTNERSHIP_REMOVE_TAILSCALE
115	#       Remove mirror from tailnet on offboard (default: true)
116	#
117	#   PARTNERSHIP_GRACE_HOURS
118	#       Hours before Tailscale removal after offboard — backup access also expires then (default: 6)
119	#
120	#   PARTNERSHIP_OFFLINE_THRESHOLD

#!/bin/bash
# ==============================================================================================
# ============================= Partnership Manager ============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Manages the full lifecycle of a two-server partnership — onboard, daily health
# monitoring, offboard, and ownership transfer. Called by partnership_onboard.sh
# during initial setup, and by critical_sync_maintenance.sh every 30 minutes for
# the --check mode. All other modes are run manually.
#
# PARTNERSHIP_OWNER_HOST flips to "HOST2" after a successful --transfer.
# AM_OWNER / AM_MIRROR flags (set by detect_hosts) control all routing —
# no hostname string comparisons anywhere in this script.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# --onboard (owner only)
#   Reconfigures mirror auth WebUIs → owner's Tailscale IP
#   Mirror operator clicks NPM → gets owner's NPM via Tailscale automatically
#   Writes ACTIVE state on both servers
#
# --offboard (either server)
#   Mirror-initiated: reconfigures own WebUIs → localhost, writes INACTIVE state
#     Owner finalises on next --check: final sync, Tailscale removal
#   Owner-initiated: final sync, reconfigures mirror WebUIs → localhost, removes
#     mirror containers + appdata, SSH key revocation, Tailscale removal
#   Both leave with clean state ✅
#
# --transfer (owner only)
#   Reconfigures both servers, flips PARTNERSHIP_OWNER_HOST in master.conf
#   Requires confirmation string + consecutive health check passes
#
# --check (called every 30min by critical_sync_maintenance.sh)
#   --remote-seen:   rsync succeeded → reset offline counter, read remote state
#   --remote-unseen: rsync failed → increment offline counter → auto-offboard at threshold
#   Silent when healthy — never noisy on clean runs
#
# --status (either server)
#   Show state files from both servers, blocklist, SSH key status
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Deferred offboard
#   Either server can offboard without the other being reachable. The initiating
#   server writes its state immediately and becomes independent. The other server
#   reads the INACTIVE state on its next --check and finalises automatically.
#   No message passing, no coordination required.
#
# Appdata cleanup on offboard
#   Partner containers are stopped and removed. Their appdata bind-mount paths
#   (collected via docker inspect before removal) are also deleted. Safety gate:
#   only paths matching /mnt/*/appdata* are deleted — media and config shares
#   outside the appdata tree are never touched.
#
# Blocklist is application-layer; Tailscale removal is network-layer
#   Both happen on offboard. The blocklist prevents re-onboard until explicitly
#   cleared with --unblock. Tailscale removal ends encrypted access at the network
#   level. Grace period controls both simultaneously — one var, consistent behaviour.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root check
#   All operations require root.
#
# Role enforcement
#   Mirror cannot run --onboard or --transfer. Blocked with a clear error message.
#
# Version parity check
#   --onboard verifies both servers are on compatible unRAID versions.
#
# SSH_TIMEOUT on all remote calls
#   Every ssh/scp call is timeout-protected. No operation hangs on an unreachable peer.
#
# flock on state writes
#   Prevents concurrent state file corruption from overlapping --check cycles.
#
# SIGTERM trap on grace period sleep
#   Offboard grace period is interruptible — Ctrl-C aborts cleanly.
#
# Silent by default
#   --check produces no output when both servers are healthy. Only state changes
#   and threshold crossings produce output.
#
# ==============================================================================================
# STATE FILES
# ==============================================================================================
#
#   /boot/config/partnership_HOST1.db  — HOST1 writes, HOST2 reads via SSH
#   /boot/config/partnership_HOST2.db  — HOST2 writes, HOST1 reads via SSH
#   /boot/config/partnership_blocklist.db  — hostname|timestamp|reason, persists until cleared
#
# On /boot/config — survives reboots, available before array starts, minimal flash wear.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   PARTNERSHIP_ENABLED
#       Global gate — set true once both servers are configured (default: false)
#
#   PARTNERSHIP_OWNER_HOST
#       "HOST1" or "HOST2" — flips on --transfer (default: "HOST1")
#
#   PARTNERSHIP_REMOVE_TAILSCALE
#       Remove mirror from tailnet on offboard (default: true)
#
#   PARTNERSHIP_GRACE_HOURS
#       Hours before Tailscale removal after offboard — backup access also expires then (default: 6)
#
#   PARTNERSHIP_OFFLINE_THRESHOLD


120	#   PARTNERSHIP_OFFLINE_THRESHOLD
121	#       Days of missed sync cycles before auto-offboard triggers (default: 30)
122	#
123	#   PARTNERSHIP_TRANSFER_CONFIRM
124	#       Exact string required for --transfer (default: "i-understand-this-transfers-ownership")
125	#
126	#   PARTNERSHIP_TRANSFER_STRIKES
127	#       Consecutive health checks required before transfer proceeds (default: 3)
128	#
129	#   PARTNERSHIP_TRANSFER_MAX_ATTEMPTS
130	#       Max health check attempts before giving up (default: 20)
131	#
132	#   PARTNERSHIP_ONBOARD_VERIFY
133	#       Curl-verify each WebUI after reconfigure to confirm Tailscale routing works (default: true)
134	#
135	#   PARTNERSHIP_ONBOARD_NOTIFY
136	#       Notify both servers on successful onboard (default: true)
137	#
138	#   PARTNERSHIP_SYNC_INTERVAL
139	#       Informational — actual schedule is in cron (default: 15)
140	#
141	#   TAILSCALE_API_KEY / TAILSCALE_TAILNET
142	#       Required when PARTNERSHIP_REMOVE_TAILSCALE=true
143	#
144	# host*.conf
145	#
146	#   HOST*_PARTNERSHIP_AUTH_WEBUIS
147	#       Containers reconfigured on onboard/offboard. Format: "ContainerName|WebUIPort"
148	#       Aliased by detect_hosts() → PARTNERSHIP_AUTH_WEBUIS
149	#
150	#   HOST*_PARTNERSHIP_MIRROR_BACKUPS
151	#       Paths accessible to the partner during grace window after offboard.
152	#       Aliased by detect_hosts() → PARTNERSHIP_MIRROR_BACKUPS
153	#
154	#   HOST*_PARTNERSHIP_OWN_CONTAINERS
155	#       Containers parked here during partnership, restarted on offboard.
156	#       Aliased by detect_hosts() → PARTNERSHIP_OWN_CONTAINERS
157	#
158	# ==============================================================================================
159	# RUNTIME MODES
160	# ==============================================================================================
161	#
162	# partnership_manager.sh --onboard
163	#     Establish mirror relationship — owner only
164	#
165	# partnership_manager.sh --offboard
166	#     Clean separation — either server. 10-second countdown before executing.
167	#
168	# partnership_manager.sh --offboard --dry-run
169	#     Show the complete offboard sequence without executing
170	#
171	# partnership_manager.sh --transfer --confirm=i-understand-this-transfers-ownership
172	#     Flip ownership — owner only. Requires exact confirmation string.
173	#
174	# partnership_manager.sh --check --remote-seen|--remote-unseen
175	#     Called by critical_sync_maintenance.sh every 30min — do not run manually
176	#
177	# partnership_manager.sh --status
178	#     Show state files, blocklist, SSH key status from both servers
179	#

#   PARTNERSHIP_OFFLINE_THRESHOLD
#       Days of missed sync cycles before auto-offboard triggers (default: 30)
#
#   PARTNERSHIP_TRANSFER_CONFIRM
#       Exact string required for --transfer (default: "i-understand-this-transfers-ownership")
#
#   PARTNERSHIP_TRANSFER_STRIKES
#       Consecutive health checks required before transfer proceeds (default: 3)
#
#   PARTNERSHIP_TRANSFER_MAX_ATTEMPTS
#       Max health check attempts before giving up (default: 20)
#
#   PARTNERSHIP_ONBOARD_VERIFY
#       Curl-verify each WebUI after reconfigure to confirm Tailscale routing works (default: true)
#
#   PARTNERSHIP_ONBOARD_NOTIFY
#       Notify both servers on successful onboard (default: true)
#
#   PARTNERSHIP_SYNC_INTERVAL
#       Informational — actual schedule is in cron (default: 15)
#
#   TAILSCALE_API_KEY / TAILSCALE_TAILNET
#       Required when PARTNERSHIP_REMOVE_TAILSCALE=true
#
# host*.conf
#
#   HOST*_PARTNERSHIP_AUTH_WEBUIS
#       Containers reconfigured on onboard/offboard. Format: "ContainerName|WebUIPort"
#       Aliased by detect_hosts() → PARTNERSHIP_AUTH_WEBUIS
#
#   HOST*_PARTNERSHIP_MIRROR_BACKUPS
#       Paths accessible to the partner during grace window after offboard.
#       Aliased by detect_hosts() → PARTNERSHIP_MIRROR_BACKUPS
#
#   HOST*_PARTNERSHIP_OWN_CONTAINERS
#       Containers parked here during partnership, restarted on offboard.
#       Aliased by detect_hosts() → PARTNERSHIP_OWN_CONTAINERS
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# partnership_manager.sh --onboard
#     Establish mirror relationship — owner only
#
# partnership_manager.sh --offboard
#     Clean separation — either server. 10-second countdown before executing.
#
# partnership_manager.sh --offboard --dry-run
#     Show the complete offboard sequence without executing
#
# partnership_manager.sh --transfer --confirm=i-understand-this-transfers-ownership
#     Flip ownership — owner only. Requires exact confirmation string.
#
# partnership_manager.sh --check --remote-seen|--remote-unseen
#     Called by critical_sync_maintenance.sh every 30min — do not run manually
#
# partnership_manager.sh --status
#     Show state files, blocklist, SSH key status from both servers
#


#   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.

#   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.
#
#   HOST*_PARTNERSHIP_SERVICES_STACK
#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) — same cleanup logic.


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)

#   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.
#
#   HOST*_PARTNERSHIP_SERVICES_STACK
#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) — same cleanup logic.

+#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) — same cleanup logic.


#   HOST*_PARTNERSHIP_OWN_CONTAINERS
#       Containers parked here during partnership, restarted on offboard.
#       Aliased by detect_hosts() → PARTNERSHIP_OWN_CONTAINERS

#   HOST*_PARTNERSHIP_OWN_CONTAINERS
#       Containers parked here during partnership, restarted on offboard.
#       Aliased by detect_hosts() → PARTNERSHIP_OWN_CONTAINERS
#
#   HOST*_PARTNERSHIP_SERVICES_STACK
#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) deployed on mirror during onboard.
#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_STACK


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)

#   HOST*_PARTNERSHIP_OWN_CONTAINERS
#       Containers parked here during partnership, restarted on offboard.
#       Aliased by detect_hosts() → PARTNERSHIP_OWN_CONTAINERS
#
#   HOST*_PARTNERSHIP_SERVICES_STACK
#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) deployed on mirror during onboard.
#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_STACK

+#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) deployed on mirror during onboard.

+#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_STACK


All six files are done. Let me do a quick sanity check on the key edits:


Bash

grep -n "SERVICES" /boot/config/plugins/varaverk/common.sh | grep "_alias_array"

Verify alias registrations in common.sh


grep -n "SERVICES" /boot/config/plugins/varaverk/Configurations/host1.conf

Verify services arrays in host1.conf


589:    _alias_array "PARTNERSHIP_SERVICES_STACK"
590:    _alias_array "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS"


172:    HOST1_PARTNERSHIP_SERVICES_STACK=(
189:    HOST1_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS=(


grep -n "Step [0-9]" /boot/config/plugins/varaverk/Partnership/partnership_onboard.sh | grep -E "echo|#"

Verify step numbering in partnership_onboard.sh


18:#   Step 1: SSH key setup — generate keypair, copy to owner, update conf
22:#   Step 1:  SSH key setup         — generate keypair, install on mirror, update conf
23:#   Step 2:  Stop mirror auth      — stop mirror's existing auth containers before replacing
24:#   Step 3:  Deploy auth stack     — push XMLs, pull images, create + start on mirror
26:#   Step 4:  Stop mirror arr       — stop mirror's existing arr containers before replacing
27:#   Step 5:  Deploy arr stack      — push arr XMLs, pull images, create + start on mirror
28:#   Step 6:  Stop mirror services  — stop mirror's existing services containers before replacing
29:#   Step 7:  Deploy services stack — push Emby/Jellyfin/Seerr XMLs, pull images, create + start
30:#   Step 8:  Partnership onboard   — configure WebUIs → owner IP, write state, Emby
31:#   Step 9:  Arr bootstrap         — bidirectional library sync (arr_sync.sh)
32:#   Step 10: Conf push             — push master.conf + setup state to all listed hosts
88:#       Read live from the mirror via SSH during Step 3 (sources mirror's load_config.sh at
139:#     Skip arr library bootstrap (Step 9)
291:    echo "━━━ Step 1/2 — SSH Key Setup (Mirror) ━━━"
306:    echo "━━━ Step 2/2 — Notify Owner to Run Phase 2 ━━━"
377:# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
379:echo "━━━ Step 1 — SSH Key Setup ━━━"
516:# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
518:echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"
526:# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
528:echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"
544:# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
546:echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"
557:# ── Step 5: Deploy arr stack on mirror ───────────────────────────────────────────────────────
559:echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"
571:# ── Step 6: Stop mirror's existing services stack ─────────────────────────────────────────────
573:echo "━━━ Step 6 — Stop Mirror Services Stack ━━━"
584:# ── Step 7: Deploy services stack on mirror ───────────────────────────────────────────────────
586:echo "━━━ Step 7 — Deploy Services Stack on Mirror ━━━"
598:# ── Step 8: Partnership onboard ───────────────────────────────────────────────────────────────
600:echo "━━━ Step 8 — Partnership Onboard ━━━"
610:# ── Step 9: Arr library bootstrap ─────────────────────────────────────────────────────────────
612:echo "━━━ Step 9 — Arr Library Bootstrap ━━━"
628:# ── Step 10: Push master.conf to all listed hosts ─────────────────────────────────────────────
632:echo "━━━ $ICON_GEAR Step 10 — master.conf Push ━━━"
679:echo "  Step 1  — SSH keys:         $(_skip "$SKIP_SSH"             "$STEP_SSH_OK")"
680:echo "  Step 2  — Stop auth:        $(_skip "$SKIP_AUTH_STACK"       "$STEP_STOP_AUTH_OK")"
681:echo "  Step 3  — Auth stack:       $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
682:echo "  Step 4  — Stop arr:         $(_skip "$SKIP_ARR_STACK"        "$STEP_STOP_ARR_OK")"
683:echo "  Step 5  — Arr stack:        $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
684:echo "  Step 6  — Stop services:    $(_skip "$SKIP_SERVICES_STACK"   "$STEP_STOP_SERVICES_OK")"
685:echo "  Step 7  — Services stack:   $( [[ "$SKIP_SERVICES_STACK" == true ]] && echo "skipped" || echo "${SERVICES_DEPLOYED} deployed, ${SERVICES_FAILED} failed" )"
686:echo "  Step 8  — Onboard:          $(_ok "$ONBOARD_OK")"
687:echo "  Step 9  — Arr bootstrap:    $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
688:echo "  Step 10 — Conf push:        $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"


grep -n "SERVICES" /boot/config/plugins/varaverk/Plugin/unraid/Partnership/containers.sh

Verify services stack in containers.sh cleanup functions


219:    [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}")
298:             printf '%s\n' \"\${PARTNERSHIP_SERVICES_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')


Everything checks out cleanly. All six files updated:

- **`common.sh`**: `_alias_array` calls for `PARTNERSHIP_SERVICES_STACK` and `PARTNERSHIP_SERVICES_REPLACE_CONTAINERS` added
- **`host1.conf`**: `HOST1_PARTNERSHIP_SERVICES_STACK` (Emby, Jellyfin, Seerr, SeerrFin) and `HOST1_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS` (empty) added
- **`partnership_onboard.sh`**: Steps 6+7 (stop/deploy services) added, old steps renumbered to 8–10, `--skip-services-stack` flag wired up, header and summary updated
- **`containers.sh`**: Both cleanup functions now include `PARTNERSHIP_SERVICES_STACK` — on remote via the existing array, locally via a third SSH read
- **`partnership_offboard.sh`**: Header docs mention `HOST*_PARTNERSHIP_SERVICES_STACK`
- **`partnership_manager.sh`**: Header docs mention `PARTNERSHIP_SERVICES_STACK`


ok so now we have auth,arrs,and shared services that get pushed to mirrors during onboarding


Correct. The onboard sequence now deploys three stacks to the mirror in order:

1. **Auth stack** — Mariadb, Redis, Authelia, NPM, lldap (Steps 2-3)
2. **Arr stack** — Sonarr, Radarr, Lidarr, Prowlarr, Bazarr (Steps 4-5)
3. **Services stack** — Emby, Jellyfin, Seerr, SeerrFin (Steps 6-7)

Each has its own stop-first step (to clear any conflicting containers already on the mirror) and its own `REPLACE_CONTAINERS` array in the mirror's own conf for anything with a different name that needs to get out of the way. Offboard cleanup picks up all three stacks automatically.


now this is in the core code. all bash scipts are core code. i need unraid specific removed and added to the app layer. the app layer calls on the core code and is gbuilt for specif os's. this one is unraid. next might be trunas, who knows


Before touching anything, let me read what's actually Unraid-specific in the core scripts vs what's already in the adapter layer.


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Unraid Platform Adapter ====================================
4	# ==============================================================================================
5	# Sourced by load_config.sh when PLATFORM=unraid.
6	# Provides the platform_*() API — bash scripts call these instead of OS-specific commands.
7	#
8	# ── API CONTRACT ──────────────────────────────────────────────────────────────────────────────
9	# Every function returns 0 on success / 1 on failure unless noted.
10	# Functions that produce output write to stdout; callers capture with $().
11	# No function calls exit — callers decide what failure means for their flow.
12	#
13	# ── ADDING A PLATFORM ─────────────────────────────────────────────────────────────────────────
14	# Create Plugin/truenas/adapter.sh (or ubuntu/adapter.sh) implementing the same function names.
15	# load_config.sh sources Plugin/$PLATFORM/adapter.sh — no other changes needed.
16	#
17	# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
18	#   platform_require_cmd          — verify a platform command exists and is executable
19	#   platform_storage_healthy      — array mounted and shfs active on /mnt/user
20	#   platform_get_disk_states      — raw disks.ini content from emhttp state
21	#   platform_get_temp_thresholds  — disk warn/crit °C from dynamix.cfg
22	#   platform_is_maintenance_running — parity check/sync in progress
23	#   platform_is_service_enabled   — docker or libvirt enabled in boot config
24	#   platform_restart_service      — restart a named service via rc.d
25	#   platform_stop_service         — stop a named service via rc.d
26	#   platform_is_service_running   — check if a named service process is alive
27	#   platform_is_mover_running     — unRAID mover process check
28	#   platform_stop_user_scripts    — kill all user.scripts background processes
29	#   platform_send_os_notification — native unRAID notify (dynamix)
30	# ==============================================================================================
31
32	# ──────────────────────────────────────────────────────────────────────────────────────────────
33	# Internal: map a logical service name → its rc.d script path
34	# ──────────────────────────────────────────────────────────────────────────────────────────────
35	_platform_rc_script() {
36	    case "$1" in
37	        docker)  echo "/etc/rc.d/rc.docker" ;;
38	        sshd)    echo "/etc/rc.d/rc.sshd" ;;
39	        libvirt) echo "/etc/rc.d/rc.libvirt" ;;
40	        rsyslog) echo "/etc/rc.d/rc.rsyslogd" ;;
41	        nginx)   echo "/etc/rc.d/rc.nginx" ;;
42	        php-fpm) echo "/etc/rc.d/rc.php-fpm" ;;
43	        emhttp)  echo "/etc/rc.d/rc.emhttp" ;;
44	        *)       echo "/etc/rc.d/rc.$1" ;;
45	    esac
46	}
47
48	# ──────────────────────────────────────────────────────────────────────────────────────────────
49	# platform_require_cmd <path> [test_arg] [expected_output] [label]
50	#   Replaces platform_require_cmd. Returns 0 if the command exists and (optionally) its output
51	#   matches expected_output. Returns 1 and prints a warning if not.
52	# ──────────────────────────────────────────────────────────────────────────────────────────────
53	platform_require_cmd() {
54	    local cmd="$1"
55	    local test_arg="${2:-}"
56	    local expected="${3:-}"
57	    local label="${4:-$cmd}"
58
59	    if [[ ! -x "$cmd" ]]; then
60	        return 1

#!/bin/bash
# ==============================================================================================
# ================================= Unraid Platform Adapter ====================================
# ==============================================================================================
# Sourced by load_config.sh when PLATFORM=unraid.
# Provides the platform_*() API — bash scripts call these instead of OS-specific commands.
#
# ── API CONTRACT ──────────────────────────────────────────────────────────────────────────────
# Every function returns 0 on success / 1 on failure unless noted.
# Functions that produce output write to stdout; callers capture with $().
# No function calls exit — callers decide what failure means for their flow.
#
# ── ADDING A PLATFORM ─────────────────────────────────────────────────────────────────────────
# Create Plugin/truenas/adapter.sh (or ubuntu/adapter.sh) implementing the same function names.
# load_config.sh sources Plugin/$PLATFORM/adapter.sh — no other changes needed.
#
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
#   platform_require_cmd          — verify a platform command exists and is executable
#   platform_storage_healthy      — array mounted and shfs active on /mnt/user
#   platform_get_disk_states      — raw disks.ini content from emhttp state
#   platform_get_temp_thresholds  — disk warn/crit °C from dynamix.cfg
#   platform_is_maintenance_running — parity check/sync in progress
#   platform_is_service_enabled   — docker or libvirt enabled in boot config
#   platform_restart_service      — restart a named service via rc.d
#   platform_stop_service         — stop a named service via rc.d
#   platform_is_service_running   — check if a named service process is alive
#   platform_is_mover_running     — unRAID mover process check
#   platform_stop_user_scripts    — kill all user.scripts background processes
#   platform_send_os_notification — native unRAID notify (dynamix)
# ==============================================================================================

# ──────────────────────────────────────────────────────────────────────────────────────────────
# Internal: map a logical service name → its rc.d script path
# ──────────────────────────────────────────────────────────────────────────────────────────────
_platform_rc_script() {
    case "$1" in
        docker)  echo "/etc/rc.d/rc.docker" ;;
        sshd)    echo "/etc/rc.d/rc.sshd" ;;
        libvirt) echo "/etc/rc.d/rc.libvirt" ;;
        rsyslog) echo "/etc/rc.d/rc.rsyslogd" ;;
        nginx)   echo "/etc/rc.d/rc.nginx" ;;
        php-fpm) echo "/etc/rc.d/rc.php-fpm" ;;
        emhttp)  echo "/etc/rc.d/rc.emhttp" ;;
        *)       echo "/etc/rc.d/rc.$1" ;;
    esac
}

# ──────────────────────────────────────────────────────────────────────────────────────────────
# platform_require_cmd <path> [test_arg] [expected_output] [label]
#   Replaces platform_require_cmd. Returns 0 if the command exists and (optionally) its output
#   matches expected_output. Returns 1 and prints a warning if not.
# ──────────────────────────────────────────────────────────────────────────────────────────────
platform_require_cmd() {
    local cmd="$1"
    local test_arg="${2:-}"
    local expected="${3:-}"
    local label="${4:-$cmd}"

    if [[ ! -x "$cmd" ]]; then
        return 1


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	# ==============================================================================================

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

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


now this is in the core code. all bash scipts are core code. i need unraid specific removed and added to the app layer. the app layer calls on the core code and is gbuilt for specif os's. this one is…


grep -rn "php\|/usr/local/emhttp\|templates-user\|varaverk_setup\|dynamix\|rc\.d\|emhttp\|TEMPLATES_DIR" \
    /boot/config/plugins/varaverk/Partnership/ \
    /boot/config/plugins/varaverk/Orchestrators/ \
    /boot/config/plugins/varaverk/common.sh \
    /boot/config/plugins/varaverk/load_config.sh \
    2>/dev/null | grep -v "^Binary\|Plugin/unraid" | grep -v "^/boot/config/plugins/varaverk/Plugin"

Find Unraid-specific code in core scripts


/boot/config/plugins/varaverk/Partnership/onboard_cancel.sh:57:STATE_FILE="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
/boot/config/plugins/varaverk/Partnership/onboard_cancel.sh:91:                 sed -i \"/^${MIRROR_ID}_PHASE\|^${MIRROR_ID}_KEY_READY/d\" /boot/config/varaverk_setup.db 2>/dev/null
/boot/config/plugins/varaverk/Partnership/gitea_ssh_setup.sh:139:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Orchestrators/critical_sync_maintenance.sh:63:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:44:#   The owner's templates-user/ XMLs define every container deployed on the mirror.
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:81:#       XML filenames (from this server's templates-user/) to push and deploy on the
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:143:#     Writes HOST2_PHASE1_DONE=true to varaverk_setup.db.
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:148:#     Writes HOST2_PHASE2_DONE=true to varaverk_setup.db.
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:223:    local state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:230:    command -v php &>/dev/null && \
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:231:        php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null || true
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:415:                    local _setup_f="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:468:    elif ! command -v php &>/dev/null; then
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:469:        warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:471:        push_output=$(php -r "
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:472:require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:639:elif ! command -v php &>/dev/null; then
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:640:    warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:642:    push_output=$(php -r "
/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh:643:require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
/boot/config/plugins/varaverk/Partnership/Manual-Partnership.md:70:ls /boot/config/plugins/dockerMan/templates-user/my-NginxProxyManager.xml
/boot/config/plugins/varaverk/Partnership/Manual-Partnership.md:71:ls /boot/config/plugins/dockerMan/templates-user/my-Authelia.xml
/boot/config/plugins/varaverk/Partnership/Manual-Partnership.md:344:1. Create the XML template on HOST1 (`/boot/config/plugins/dockerMan/templates-user/my-NewContainer.xml`)
/boot/config/plugins/varaverk/Partnership/Manual-Partnership.md:450:ls /boot/config/plugins/dockerMan/templates-user/my-Authelia.xml
/boot/config/plugins/varaverk/Partnership/Manual-Partnership.md:474:    /boot/config/plugins/dockerMan/templates-user/my-NginxProxyManager.xml"
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:243:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:390:        "grep -rl '<WebUI>' /boot/config/plugins/dockerMan/templates-user/ 2>/dev/null | \
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:421:            /boot/config/plugins/dockerMan/templates-user/ 2>/dev/null | \
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1241:            if command -v php &>/dev/null; then
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1242:                php -r "
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1243:require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1259:                warn "php not available — set PARTNERSHIP_ENABLED=true in master.conf manually"
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1266:        local_state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1274:            command -v php &>/dev/null && \
/boot/config/plugins/varaverk/Partnership/partnership_manager.sh:1275:                php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null || true
/boot/config/plugins/varaverk/Partnership/partnership_transfer.sh:130:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Orchestrators/daily_sync_maintenance.sh:87:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Orchestrators/intermediate_sync_maintenance.sh:75:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/common.sh:54:#            get_unraid_temp_thresholds() — reads thresholds from dynamix.cfg
/boot/config/plugins/varaverk/common.sh:144:ICON_WEBGUI="💻"        # WebGUI / nginx / emhttp operations
/boot/config/plugins/varaverk/common.sh:233:        local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify"
/boot/config/plugins/varaverk/common.sh:858:# Reads disk temperature thresholds from unRAID's dynamix.cfg.
/boot/config/plugins/varaverk/common.sh:862:    local cfg="/boot/config/plugins/dynamix/dynamix.cfg"
/boot/config/plugins/varaverk/common.sh:869:    # Fall back to master.conf values if dynamix.cfg not found or values empty
/boot/config/plugins/varaverk/common.sh:877:# Reads temps and rotational flag from /var/local/emhttp/disks.ini.
/boot/config/plugins/varaverk/common.sh:878:# Uses unRAID's own thresholds from dynamix.cfg via get_unraid_temp_thresholds().
/boot/config/plugins/varaverk/common.sh:889:    local disks_ini="/var/local/emhttp/disks.ini"
/boot/config/plugins/varaverk/common.sh:988:        "cat /var/local/emhttp/disks.ini 2>/dev/null" 2>/dev/null)
/boot/config/plugins/varaverk/common.sh:1860:#   validate_unraid_cmd #     "/usr/local/emhttp/plugins/dynamix/scripts/notify" #     "--help" #     "Usage" #     "unRAID notify script"
/boot/config/plugins/varaverk/Orchestrators/README-Orchestrators.md:217:    "System_Essentials/php_fpm_max_children.sh"   # WebGUI tuning — before any WebGUI requests
/boot/config/plugins/varaverk/Orchestrators/weekly_sync_maintenance.sh:80:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Partnership/ssh_setup.sh:74:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Orchestrators/array_started.sh:18:#     System_Essentials/php_fpm_max_children.sh — WebGUI performance tuning
/boot/config/plugins/varaverk/Orchestrators/array_started.sh:78:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
/boot/config/plugins/varaverk/Orchestrators/array_stopping.sh:57:    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \


Here's what's actually Unraid-specific sitting in core code right now:

**1. `common.sh` — `notify()`** hardcodes `/usr/local/emhttp/plugins/dynamix/scripts/notify` directly. `platform_send_os_notification()` already exists in the adapter and does exactly this — `notify()` just isn't calling it.

**2. `partnership_onboard.sh` + `partnership_manager.sh` — PHP conf push** — two patterns appearing multiple times:
```bash
php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_master_conf(); vv_push_setup_state();"
```
These are direct Unraid WebGUI PHP calls. Need adapter functions: `platform_push_conf()` and `platform_push_setup_state()`.

**3. `partnership_manager.sh` — WebUI reconfiguration** — hardcodes `/boot/config/plugins/dockerMan/templates-user/` to discover containers with `<WebUI>` tags. That path is already in `TEMPLATES_DIR` in `containers.sh` (the adapter), but the search logic is sitting in core.

**4. `gitea_ssh_setup.sh`, `ssh_setup.sh`, `partnership_transfer.sh`, orchestrators** — all call the dynamix notify path directly, same as #1.

**5. `common.sh` disk functions** (`get_unraid_temp_thresholds`) — reads `/boot/config/plugins/dynamix/dynamix.cfg` and `/var/local/emhttp/disks.ini`. `platform_get_temp_thresholds()` and `platform_get_disk_states()` already exist in the adapter for exactly this.

---

The plan:
- `notify()` in `common.sh` → call `platform_send_os_notification()` (one-line fix, immediate)
- Add `platform_push_conf()` and `platform_push_setup_state()` to `Plugin/unraid/adapter.sh` → update partnership scripts to call them
- Move WebUI template search in `partnership_manager.sh` → new `platform_get_webui_containers()` adapter function
- `get_unraid_temp_thresholds()` and the disk reader in `common.sh` → delegate to `platform_get_temp_thresholds()` / `platform_get_disk_states()`

The `containers.sh` adapter (`Plugin/unraid/Partnership/containers.sh`) is already the right pattern — same thing, just extended to `Plugin/unraid/adapter.sh` for the non-container Unraid calls.

Want to proceed?


gitea is no longer involved. deployment is with github and handled else where


Got it — drop `gitea_ssh_setup.sh` from the list. Should it be deleted, or just left alone?

The rest of the refactor plan stands: `notify()`, the PHP conf/state push calls, the WebUI template search, and the disk reader functions.


leave it alone im using it for dev, but not in release


Understood. Let me read what I need before making changes.


225	    local message="$1"
226	    local subject="${2:-unRAID Notification}"
227	    local severity="${3:-normal}"
228
229	    log "$ICON_NOTIFY Sending notification: $subject — $message"
230
231	    # Guard is built in — scripts do NOT need to validate_unraid_cmd for the notify script.
232	    if [[ "${NOTIFY_UNRAID:-false}" == true ]]; then
233	        local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify"
234	        if [[ -x "$notify_script" ]]; then
235	            "$notify_script" -s "$subject" -d "$message" -i "$severity" 2>/dev/null
236	            log "$ICON_NOTIFY unRAID notification sent"
237	        else
238	            log "$ICON_NOTIFY unRAID notify script not found — skipping"
239	        fi
240	    fi
241
242	    # Uses MY_DISCORD_WEBHOOK — set by detect_hosts() from HOST*_DISCORD_WEBHOOK
243	    if [[ -n "${MY_DISCORD_WEBHOOK:-}" ]]; then
244	        local payload
245	        payload=$(printf '{"content": "%s — **%s**\\n%s"}' \
246	            "$ICON_NOTIFY" "$subject" "$message")
247	        if curl -s -H "Content-Type: application/json" \
248	            -d "$payload" "$MY_DISCORD_WEBHOOK" >/dev/null 2>&1; then
249	            log "$ICON_NOTIFY Discord notification sent"
250	        else
251	            warn "Discord notification failed — check HOST*_DISCORD_WEBHOOK in host*.conf"
252	        fi
253	    fi
254	}

    local message="$1"
    local subject="${2:-unRAID Notification}"
    local severity="${3:-normal}"

    log "$ICON_NOTIFY Sending notification: $subject — $message"

    # Guard is built in — scripts do NOT need to validate_unraid_cmd for the notify script.
    if [[ "${NOTIFY_UNRAID:-false}" == true ]]; then
        local notify_script="/usr/local/emhttp/plugins/dynamix/scripts/notify"
        if [[ -x "$notify_script" ]]; then
            "$notify_script" -s "$subject" -d "$message" -i "$severity" 2>/dev/null
            log "$ICON_NOTIFY unRAID notification sent"
        else
            log "$ICON_NOTIFY unRAID notify script not found — skipping"
        fi
    fi

    # Uses MY_DISCORD_WEBHOOK — set by detect_hosts() from HOST*_DISCORD_WEBHOOK
    if [[ -n "${MY_DISCORD_WEBHOOK:-}" ]]; then
        local payload
        payload=$(printf '{"content": "%s — **%s**\\n%s"}' \
            "$ICON_NOTIFY" "$subject" "$message")
        if curl -s -H "Content-Type: application/json" \
            -d "$payload" "$MY_DISCORD_WEBHOOK" >/dev/null 2>&1; then
            log "$ICON_NOTIFY Discord notification sent"
        else
            warn "Discord notification failed — check HOST*_DISCORD_WEBHOOK in host*.conf"
        fi
    fi
}