during onboard we need to run the docker network script to ensure the custom netork is there before containers get installed


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Fast read-only search agent for locating code. Use it to find files by pattern (eg. "src/components/**/*.tsx"), grep for symbols or keywords (eg. "API endpoints"), or answer "where is X defined / which files reference Y." Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: "quick" for a single targeted lookup, "medium" for moderate exploration, or "very thorough" to search across multiple locations and naming conventions. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


Run docker network script during onboarding


Let me look at the onboard setup and the docker network script.


Bash

find /boot/config/plugins/varaverk -name "*onboard*" -o -name "*docker_network*" | sort

Find onboard and docker network scripts


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Docker Network Connect =====================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Ensures custom Docker networks exist and connects configured containers to
9	# them at every array start.
10	#
11	# Called via ARRAY_START_SCRIPTS — runs early in the array start sequence,
12	# before watchdogs begin their first cycle. Idempotent: safe to re-run at any
13	# time. Silent when everything is already correct. Notifies when a network had
14	# to be created — that only happens after a unRAID update wipes custom networks,
15	# and it is worth knowing when it does.
16	#
17	# ==============================================================================================
18	# OPERATIONAL MODEL
19	# ==============================================================================================
20	#
21	# For each configured network:
22	#
23	#   1. Does the network exist?
24	#        NO  → create it (bridge driver, Docker assigns subnet automatically)
25	#             → send notification — creation is unexpected outside post-update recovery
26	#        YES → skip silently
27	#
28	#   2. For each configured container:
29	#        Already connected  → skip silently
30	#        Not connected      → connect it
31	#        Container missing  → warn and skip — may not be running yet, not fatal
32	#
33	# ==============================================================================================
34	# DESIGN PRINCIPLES
35	# ==============================================================================================
36	#
37	# Idempotent by Design
38	#   Running this script multiple times produces the same result as running it
39	#   once. Skips anything already in the correct state without error or noise.
40	#
41	# Silent When Correct
42	#   Produces no output on a clean run. The absence of output is confirmation
43	#   that everything is already correct.
44	#
45	# Notify on Creation
46	#   Network creation is always notified because it should only happen after a
47	#   unRAID update. If it happens regularly something is misconfigured and the
48	#   operator needs to know.
49	#
50	# ==============================================================================================
51	# OPERATIONAL SAFEGUARDS
52	# ==============================================================================================
53	#
54	# Lock Acquisition
55	#   Prevents concurrent execution via acquire_lock(). Safe to call from
56	#   array start hooks or manually without risk of overlap.
57	#
58	# Host Detection
59	#   detect_hosts() identifies which server is running the script and aliases
60	#   HOST*_NETWORK_CONNECT_* arrays to the correct host's values.
61	#
62	# Docker Daemon Check
63	#   Verifies daemon is responsive before any network operations. Network
64	#   commands against a hung daemon hang indefinitely.
65	#
66	# Timeout Protection
67	#   All docker commands wrapped in timeout. Daemon hangs cannot stall the
68	#   array start sequence.
69	#
70	# Empty Array Guards
71	#   Warns and exits cleanly if NETWORK_CONNECT_NETWORKS or
72	#   NETWORK_CONNECT_CONTAINERS are unconfigured.
73	#
74	# Command Validation
75	#   Validates unRAID notify script before use.
76	#
77	# ==============================================================================================
78	# CONFIGURATION
79	# ==============================================================================================
80	#
81	# host*.conf
82	#
83	#   HOST*_NETWORK_CONNECT_NETWORKS
84	#       Networks to ensure exist at array start. Aliased by detect_hosts() →
85	#       NETWORK_CONNECT_NETWORKS
86	#
87	#   HOST*_NETWORK_CONNECT_CONTAINERS
88	#       Containers to connect to every configured network. Aliased by
89	#       detect_hosts() → NETWORK_CONNECT_CONTAINERS
90	#
91	# ==============================================================================================
92	# RUNTIME MODES
93	# ==============================================================================================
94	#
95	# docker_network_connect.sh
96	#     Ensure all configured networks exist and containers are connected
97	#
98	# docker_network_connect.sh --dry-run
99	#     Preview what would be created or connected without making changes
100	#
101	# docker_network_connect.sh --status
102	#     Show current network state and container connection status
103	#
104	# docker_network_connect.sh --log
105	#     Verbose per-network per-container output
106	#
107	# ==============================================================================================
108
109	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
110
111	source "$SCRIPT_DIR/../load_config.sh"
112
113	parse_args "$@"
114
115	# ==============================================================================================
116	# ━━━ Setup ━━━
117	# ==============================================================================================
118	if [[ "$EUID" -ne 0 ]]; then
119	    error "Must be run as root"
120	    exit 1
121	fi
122
123	acquire_lock
124
125	if ! command -v docker &>/dev/null; then
126	    error "Docker not found — cannot manage networks"
127	    exit 1
128	fi
129
130	# detect_hosts() sets MY_ID and aliases HOST*_NETWORK_CONNECT_* arrays
131	detect_hosts
132
133	# Validate unRAID notify script — used for network creation alerts
134
135	# Docker daemon check — network operations are useless if daemon is hung
136	DOCKER_TIMEOUT=15
137	if ! timeout "$DOCKER_TIMEOUT" docker info >/dev/null 2>&1; then
138	    error "Docker daemon not responding — cannot manage networks"
139	    notify "docker_network_connect failed on $(hostname) — Docker daemon not responding"         "Network Connect" "warning"
140	    exit 1
141	fi
142
143	# Empty array guards
144	if [[ ${#NETWORK_CONNECT_NETWORKS[@]} -eq 0 ]]; then
145	    warn "NETWORK_CONNECT_NETWORKS is empty for $MY_ID — nothing to do"
146	    warn "Check HOST*_NETWORK_CONNECT_NETWORKS in host*.conf"
147	    exit 0
148	fi
149
150	if [[ ${#NETWORK_CONNECT_CONTAINERS[@]} -eq 0 ]]; then
151	    warn "NETWORK_CONNECT_CONTAINERS is empty for $MY_ID — no containers to connect"
152	    warn "Check HOST*_NETWORK_CONNECT_CONTAINERS in host*.conf"
153	    exit 0
154	fi
155
156	# ==============================================================================================
157	# ━━━ Status ━━━
158	# ==============================================================================================
159	if [[ "$SHOW_STATUS" == true ]]; then
160	    echo ""
161	    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
162	    echo "$ICON_HOST        Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
163	    echo "$ICON_DOCKER_NET  Networks:   ${NETWORK_CONNECT_NETWORKS[*]}"
164	    echo "$ICON_CONTAINERS  Containers: ${NETWORK_CONNECT_CONTAINERS[*]}"
165	    echo ""
166	    echo "━━━ Network State ━━━"
167	    for network in "${NETWORK_CONNECT_NETWORKS[@]}"; do
168	        [[ -z "$network" ]] && continue
169	        if timeout "$DOCKER_TIMEOUT" docker network inspect "$network" &>/dev/null; then
170	            echo "  $ICON_SUCCESS $network — exists"
171	            timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
172	                --format '  Subnet: {{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null
173	        else
174	            echo "  $ICON_ERROR $network — missing (will be created on next run)"
175	        fi
176	    done
177	    echo ""
178	    echo "━━━ Container Connections ━━━"
179	    for container in "${NETWORK_CONNECT_CONTAINERS[@]}"; do
180	        [[ -z "$container" ]] && continue
181	        echo "$ICON_CONTAINERS $container:"
182	        timeout "$DOCKER_TIMEOUT" docker inspect "$container" \
183	            --format '{{range $k, $v := .NetworkSettings.Networks}}    {{$k}}{{"\\n"}}{{end}}' \
184	            2>/dev/null || echo "    not found"
185	    done
186	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
187	    exit 0
188	fi
189
190	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
191
192	# ==============================================================================================
193	# ━━━ Network Ensure + Connect ━━━
194	# ==============================================================================================
195	log "Networks:   ${NETWORK_CONNECT_NETWORKS[*]}"
196	log "Containers: ${NETWORK_CONNECT_CONTAINERS[*]}"
197
198	START=$(date +%s)
199	NETWORKS_CREATED=()
200	CONNECTED=()
201	SKIPPED=()
202	FAILED=()
203
204	for network in "${NETWORK_CONNECT_NETWORKS[@]}"; do
205	    [[ -z "$network" ]] && continue
206
207	    log "Processing network: $network"
208
209	    # ── Step 1 — ensure network exists ───────────────────────────────────────────────────────
210	    if timeout "$DOCKER_TIMEOUT" docker network inspect "$network" &>/dev/null; then
211	        local net_subnet net_driver
212	        net_subnet=$(timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
213	            --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null || echo "unknown")
214	        net_driver=$(timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
215	            --format '{{.Driver}}' 2>/dev/null || echo "unknown")
216	        log "$ICON_DOCKER_NET $network exists ✅ — driver: $net_driver subnet: $net_subnet"
217	    else
218	        warn "$ICON_DOCKER_NET $network not found — creating (unRAID update may have wiped networks)"
219	        if [[ "$DRY_RUN" == true ]]; then
220	            warn "DRY RUN — would create: $network (bridge, auto subnet)"
221	        else
222	            if timeout "$DOCKER_TIMEOUT" docker network create \
223	                --driver bridge "$network" >/dev/null 2>&1; then
224	                warn "$ICON_DOCKER_NET $network created"
225	                NETWORKS_CREATED+=("$network")
226	                # Network creation is unexpected — notify so user is aware
227	                notify "Docker network created on $(hostname) — $network (unRAID update likely wiped it)" \
228	                    "Network Connect" "normal"
229	            else
230	                error "$network — failed to create"
231	                FAILED+=("$network:create")
232	                continue
233	            fi
234	        fi
235	    fi
236
237	    # ── Step 2 — connect containers to this network ──────────────────────────────────────────
238	    for container in "${NETWORK_CONNECT_CONTAINERS[@]}"; do
239	        [[ -z "$container" ]] && continue
240
241	        # Container not found — warn and skip, not an error
242	        if ! timeout "$DOCKER_TIMEOUT" docker inspect "$container" &>/dev/null; then
243	            warn "$container not found — skipping (may not be running yet)"
244	            continue
245	        fi
246
247	        # Already connected — skip cleanly and silently
248	        if timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
249	            --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
250	            | grep -qw "$container"; then
251	            log "$container already on $network — skipping"
252	            SKIPPED+=("$container→$network")
253	            continue
254	        fi
255
256	        # Connect
257	        if [[ "$DRY_RUN" == true ]]; then
258	            warn "DRY RUN — would connect $container to $network"
259	            continue
260	        fi
261
262	        log "Connecting $container to $network..."
263	        if timeout "$DOCKER_TIMEOUT" docker network connect "$network" "$container" 2>/dev/null; then
264	            log "$container connected to $network"
265	            CONNECTED+=("$container→$network")
266	        else
267	            error "Failed to connect $container to $network"
268	            FAILED+=("$container:$network")
269	        fi
270	    done
271
272	done
273
274	END=$(date +%s)
275
276	# ==============================================================================================
277	# ━━━ Summary ━━━
278	# ==============================================================================================
279	# Always show summary — this runs at array start and output is useful for diagnostics
280	echo ""
281	echo "━━━━━ $ICON_SUMMARY NETWORK CONNECT SUMMARY ━━━━━"
282	echo "$ICON_HOST       Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
283	echo "$ICON_TIME       Duration:  $(format_duration $((END - START)))"
284	[[ ${#NETWORKS_CREATED[@]} -gt 0 ]] && warn "$ICON_DOCKER_NET Created:   ${NETWORKS_CREATED[*]} (networks were missing)"
285	[[ ${#CONNECTED[@]} -gt 0 ]]        && log "Connected: ${CONNECTED[*]}"
286	[[ ${#SKIPPED[@]} -gt 0 ]]          && log "Already connected: ${SKIPPED[*]}"
287	[[ ${#FAILED[@]} -gt 0 ]]           && echo "$ICON_ERROR      Failed:    ${FAILED[*]}"
288
289	if [[ "$DRY_RUN" == true ]]; then
290	    warn "DRY RUN — no changes made"
291	elif [[ ${#FAILED[@]} -gt 0 ]]; then
292	    echo "$ICON_ERROR Status: SOME OPERATIONS FAILED"
293	    notify "Docker network connect failed on $(hostname) — ${FAILED[*]}"         "Network Connect" "warning"
294	elif [[ ${#NETWORKS_CREATED[@]} -gt 0 ]]; then
295	    warn "Networks recreated — ${NETWORKS_CREATED[*]} — unRAID update likely wiped them"
296	else
297	    echo "All networks healthy — no action needed"
298	fi
299	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
300
301	[[ ${#FAILED[@]} -gt 0 ]] && exit 1
302	exit 0

#!/bin/bash
# ==============================================================================================
# ================================= Docker Network Connect =====================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Ensures custom Docker networks exist and connects configured containers to
# them at every array start.
#
# Called via ARRAY_START_SCRIPTS — runs early in the array start sequence,
# before watchdogs begin their first cycle. Idempotent: safe to re-run at any
# time. Silent when everything is already correct. Notifies when a network had
# to be created — that only happens after a unRAID update wipes custom networks,
# and it is worth knowing when it does.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# For each configured network:
#
#   1. Does the network exist?
#        NO  → create it (bridge driver, Docker assigns subnet automatically)
#             → send notification — creation is unexpected outside post-update recovery
#        YES → skip silently
#
#   2. For each configured container:
#        Already connected  → skip silently
#        Not connected      → connect it
#        Container missing  → warn and skip — may not be running yet, not fatal
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Idempotent by Design
#   Running this script multiple times produces the same result as running it
#   once. Skips anything already in the correct state without error or noise.
#
# Silent When Correct
#   Produces no output on a clean run. The absence of output is confirmation
#   that everything is already correct.
#
# Notify on Creation
#   Network creation is always notified because it should only happen after a
#   unRAID update. If it happens regularly something is misconfigured and the
#   operator needs to know.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Lock Acquisition
#   Prevents concurrent execution via acquire_lock(). Safe to call from
#   array start hooks or manually without risk of overlap.
#
# Host Detection
#   detect_hosts() identifies which server is running the script and aliases
#   HOST*_NETWORK_CONNECT_* arrays to the correct host's values.
#
# Docker Daemon Check
#   Verifies daemon is responsive before any network operations. Network
#   commands against a hung daemon hang indefinitely.
#
# Timeout Protection
#   All docker commands wrapped in timeout. Daemon hangs cannot stall the
#   array start sequence.
#
# Empty Array Guards
#   Warns and exits cleanly if NETWORK_CONNECT_NETWORKS or
#   NETWORK_CONNECT_CONTAINERS are unconfigured.
#
# Command Validation
#   Validates unRAID notify script before use.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_NETWORK_CONNECT_NETWORKS
#       Networks to ensure exist at array start. Aliased by detect_hosts() →
#       NETWORK_CONNECT_NETWORKS
#
#   HOST*_NETWORK_CONNECT_CONTAINERS
#       Containers to connect to every configured network. Aliased by
#       detect_hosts() → NETWORK_CONNECT_CONTAINERS
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_network_connect.sh
#     Ensure all configured networks exist and containers are connected
#
# docker_network_connect.sh --dry-run
#     Preview what would be created or connected without making changes
#
# docker_network_connect.sh --status
#     Show current network state and container connection status
#
# docker_network_connect.sh --log
#     Verbose per-network per-container output
#
# ==============================================================================================

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

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

parse_args "$@"

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

acquire_lock

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

# detect_hosts() sets MY_ID and aliases HOST*_NETWORK_CONNECT_* arrays
detect_hosts

# Validate unRAID notify script — used for network creation alerts

# Docker daemon check — network operations are useless if daemon is hung
DOCKER_TIMEOUT=15
if ! timeout "$DOCKER_TIMEOUT" docker info >/dev/null 2>&1; then
    error "Docker daemon not responding — cannot manage networks"
    notify "docker_network_connect failed on $(hostname) — Docker daemon not responding"         "Network Connect" "warning"
    exit 1
fi

# Empty array guards
if [[ ${#NETWORK_CONNECT_NETWORKS[@]} -eq 0 ]]; then
    warn "NETWORK_CONNECT_NETWORKS is empty for $MY_ID — nothing to do"
    warn "Check HOST*_NETWORK_CONNECT_NETWORKS in host*.conf"
    exit 0
fi

if [[ ${#NETWORK_CONNECT_CONTAINERS[@]} -eq 0 ]]; then
    warn "NETWORK_CONNECT_CONTAINERS is empty for $MY_ID — no containers to connect"
    warn "Check HOST*_NETWORK_CONNECT_CONTAINERS in host*.conf"
    exit 0
fi

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_HOST        Identity:   $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_DOCKER_NET  Networks:   ${NETWORK_CONNECT_NETWORKS[*]}"
    echo "$ICON_CONTAINERS  Containers: ${NETWORK_CONNECT_CONTAINERS[*]}"
    echo ""
    echo "━━━ Network State ━━━"
    for network in "${NETWORK_CONNECT_NETWORKS[@]}"; do
        [[ -z "$network" ]] && continue
        if timeout "$DOCKER_TIMEOUT" docker network inspect "$network" &>/dev/null; then
            echo "  $ICON_SUCCESS $network — exists"
            timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
                --format '  Subnet: {{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null
        else
            echo "  $ICON_ERROR $network — missing (will be created on next run)"
        fi
    done
    echo ""
    echo "━━━ Container Connections ━━━"
    for container in "${NETWORK_CONNECT_CONTAINERS[@]}"; do
        [[ -z "$container" ]] && continue
        echo "$ICON_CONTAINERS $container:"
        timeout "$DOCKER_TIMEOUT" docker inspect "$container" \
            --format '{{range $k, $v := .NetworkSettings.Networks}}    {{$k}}{{"\\n"}}{{end}}' \
            2>/dev/null || echo "    not found"
    done
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

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

# ==============================================================================================
# ━━━ Network Ensure + Connect ━━━
# ==============================================================================================
log "Networks:   ${NETWORK_CONNECT_NETWORKS[*]}"
log "Containers: ${NETWORK_CONNECT_CONTAINERS[*]}"

START=$(date +%s)
NETWORKS_CREATED=()
CONNECTED=()
SKIPPED=()
FAILED=()

for network in "${NETWORK_CONNECT_NETWORKS[@]}"; do
    [[ -z "$network" ]] && continue

    log "Processing network: $network"

    # ── Step 1 — ensure network exists ───────────────────────────────────────────────────────
    if timeout "$DOCKER_TIMEOUT" docker network inspect "$network" &>/dev/null; then
        local net_subnet net_driver
        net_subnet=$(timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
            --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null || echo "unknown")
        net_driver=$(timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
            --format '{{.Driver}}' 2>/dev/null || echo "unknown")
        log "$ICON_DOCKER_NET $network exists ✅ — driver: $net_driver subnet: $net_subnet"
    else
        warn "$ICON_DOCKER_NET $network not found — creating (unRAID update may have wiped networks)"
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would create: $network (bridge, auto subnet)"
        else
            if timeout "$DOCKER_TIMEOUT" docker network create \
                --driver bridge "$network" >/dev/null 2>&1; then
                warn "$ICON_DOCKER_NET $network created"
                NETWORKS_CREATED+=("$network")
                # Network creation is unexpected — notify so user is aware
                notify "Docker network created on $(hostname) — $network (unRAID update likely wiped it)" \
                    "Network Connect" "normal"
            else
                error "$network — failed to create"
                FAILED+=("$network:create")
                continue
            fi
        fi
    fi

    # ── Step 2 — connect containers to this network ──────────────────────────────────────────
    for container in "${NETWORK_CONNECT_CONTAINERS[@]}"; do
        [[ -z "$container" ]] && continue

        # Container not found — warn and skip, not an error
        if ! timeout "$DOCKER_TIMEOUT" docker inspect "$container" &>/dev/null; then
            warn "$container not found — skipping (may not be running yet)"
            continue
        fi

        # Already connected — skip cleanly and silently
        if timeout "$DOCKER_TIMEOUT" docker network inspect "$network" \
            --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
            | grep -qw "$container"; then
            log "$container already on $network — skipping"
            SKIPPED+=("$container→$network")
            continue
        fi

        # Connect
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would connect $container to $network"
            continue
        fi

        log "Connecting $container to $network..."
        if timeout "$DOCKER_TIMEOUT" docker network connect "$network" "$container" 2>/dev/null; then
            log "$container connected to $network"
            CONNECTED+=("$container→$network")
        else
            error "Failed to connect $container to $network"
            FAILED+=("$container:$network")
        fi
    done

done

END=$(date +%s)

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
# Always show summary — this runs at array start and output is useful for diagnostics
echo ""
echo "━━━━━ $ICON_SUMMARY NETWORK CONNECT SUMMARY ━━━━━"
echo "$ICON_HOST       Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME       Duration:  $(format_duration $((END - START)))"
[[ ${#NETWORKS_CREATED[@]} -gt 0 ]] && warn "$ICON_DOCKER_NET Created:   ${NETWORKS_CREATED[*]} (networks were missing)"
[[ ${#CONNECTED[@]} -gt 0 ]]        && log "Connected: ${CONNECTED[*]}"
[[ ${#SKIPPED[@]} -gt 0 ]]          && log "Already connected: ${SKIPPED[*]}"
[[ ${#FAILED[@]} -gt 0 ]]           && echo "$ICON_ERROR      Failed:    ${FAILED[*]}"

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — no changes made"
elif [[ ${#FAILED[@]} -gt 0 ]]; then
    echo "$ICON_ERROR Status: SOME OPERATIONS FAILED"
    notify "Docker network connect failed on $(hostname) — ${FAILED[*]}"         "Network Connect" "warning"
elif [[ ${#NETWORKS_CREATED[@]} -gt 0 ]]; then
    warn "Networks recreated — ${NETWORKS_CREATED[*]} — unRAID update likely wiped them"
else
    echo "All networks healthy — no action needed"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

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


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Partnership Onboard ============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Runs once on both servers to establish a new partnership. Role is detected
9	# automatically via detect_hosts() — no flags needed to declare which side you are.
10	# Run on the mirror first (generates its SSH key), then on the owner to complete
11	# setup remotely.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# MIRROR PATH (1 step)
18	#   Step 1: SSH key setup — generate keypair, copy to owner, update conf
19	#           Owner completes the rest remotely. Mirror is done.
20	#
21	# OWNER PATH (10 steps)
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
25	#                                    Mariadb/Redis health-checked before Authelia deploys
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
33	#
34	# ==============================================================================================
35	# DESIGN PRINCIPLES
36	# ==============================================================================================
37	#
38	# Credentials never in SSH command strings
39	#   Auth stack containers hold API keys, DB passwords, etc. The deploy script is written
40	#   locally, SCPed to the remote, and executed there. Command-line args are never used
41	#   to pass credentials — they'd appear in `ps` output and shell history on both servers.
42	#
43	# XML templates are the single source of truth for deployed containers
44	#   The owner's templates-user/ XMLs define every container deployed on the mirror.
45	#   The same XMLs that Unraid's Docker Manager uses are what get SCPed — the mirror's
46	#   Docker Manager can manage the containers after onboard without additional config.
47	#
48	# Dependency ordering in the auth stack is owner-enforced
49	#   PARTNERSHIP_AUTH_STACK order matters: Mariadb and Redis must come before Authelia.
50	#   The array is ordered correctly in host1.conf. After each Mariadb/Redis deploy,
51	#   the script waits for the container to be healthy before continuing. This is a remote
52	#   health check — the container must be running (or report healthy) before the next
53	#   dependent is deployed.
54	#
55	# ==============================================================================================
56	# OPERATIONAL SAFEGUARDS
57	# ==============================================================================================
58	#
59	# Root check
60	#   All operations run as root — SSH key management, docker operations, conf updates.
61	#
62	# SSH timeout on all remote calls
63	#   Every ssh/scp call uses SSH_TIMEOUT. No operation hangs indefinitely on a
64	#   slow or unreachable mirror.
65	#
66	# --dry-run shows exact actions without executing
67	#   Every step prints what it would do. SCP, deploy, plugin install, arr sync —
68	#   all dry-run safe.
69	#
70	# Step skip flags for partial re-runs
71	#   --skip-ssh, --skip-auth-stack, --skip-arr-stack, --skip-arr-sync allow
72	#   resuming after a partial failure without re-running completed steps.
73	#
74	# ==============================================================================================
75	# CONFIGURATION
76	# ==============================================================================================
77	#
78	# host*.conf
79	#
80	#   HOST*_PARTNERSHIP_AUTH_STACK
81	#       XML filenames (from this server's templates-user/) to push and deploy on the
82	#       mirror as its auth stack. Order matters: database deps before Authelia.
83	#       Aliased by detect_hosts() → PARTNERSHIP_AUTH_STACK
84	#
85	#   HOST*_PARTNERSHIP_REPLACE_CONTAINERS
86	#       Containers to stop on the mirror before deploying the auth stack.
87	#       Defined in the MIRROR's own conf (host*.conf on HOST2) — never in HOST1's conf.
88	#       Read live from the mirror via SSH during Step 3 (sources mirror's load_config.sh at
89	#       the same $SCRIPTS_ROOT path — convention: both servers use the same repo location).
90	#       Leave empty on HOST2 if no conflicting containers exist (fresh mirror: nothing to stop).
91	#       Aliased by detect_hosts() → PARTNERSHIP_REPLACE_CONTAINERS (on the mirror)
92	#
93	#   HOST*_PARTNERSHIP_ARR_STACK
94	#       XML filenames to push and deploy on the mirror as its arr stack.
95	#       Leave empty to skip arr stack deploy.
96	#       Aliased by detect_hosts() → PARTNERSHIP_ARR_STACK
97	#
98	#   HOST*_PARTNERSHIP_ARR_REPLACE_CONTAINERS
99	#       Arr containers to stop on the mirror before deploying the arr stack.
100	#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
101	#       Aliased by detect_hosts() → PARTNERSHIP_ARR_REPLACE_CONTAINERS (on the mirror)
102	#
103	#   HOST*_PARTNERSHIP_SERVICES_STACK
104	#       XML filenames to push and deploy on the mirror as its shared services stack.
105	#       Includes Emby, Jellyfin, Seerr, SeerrFin. Leave empty to skip services stack deploy.
106	#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_STACK
107	#
108	#   HOST*_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS
109	#       Services containers to stop on the mirror before deploying the services stack.
110	#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
111	#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_REPLACE_CONTAINERS (on the mirror)
112	#
113	# ==============================================================================================
114	# RUNTIME MODES
115	# ==============================================================================================
116	#
117	# Partnership/partnership_onboard.sh
118	#     Full onboard — role detected automatically
119	#
120	# Partnership/partnership_onboard.sh --dry-run
121	#     Preview all steps without making changes
122	#
123	# Partnership/partnership_onboard.sh --log
124	#     Verbose per-step output
125	#
126	# Partnership/partnership_onboard.sh --skip-ssh
127	#     Skip SSH key setup (key already in place)
128	#
129	# Partnership/partnership_onboard.sh --skip-auth-stack
130	#     Skip auth stack stop + deploy (Steps 3-4)
131	#
132	# Partnership/partnership_onboard.sh --skip-arr-stack
133	#     Skip arr stack stop + deploy (Steps 4-5)
134	#
135	# Partnership/partnership_onboard.sh --skip-services-stack
136	#     Skip services stack stop + deploy (Steps 6-7)
137	#
138	# Partnership/partnership_onboard.sh --skip-arr-sync
139	#     Skip arr library bootstrap (Step 9)
140	#
141	# Partnership/partnership_onboard.sh --phase1-only
142	#     OWNER only: SSH key exchange + conf push. Safe to run before HOST2 has Varaverk.
143	#     Writes HOST2_PHASE1_DONE=true to varaverk_setup.db.
144	#
145	# Partnership/partnership_onboard.sh --phase2-only
146	#     OWNER only: container deploy + arr + onboard (skips SSH). Triggered automatically
147	#     by HOST2 after it completes its Mirror-path onboard. Can also be run manually.
148	#     Writes HOST2_PHASE2_DONE=true to varaverk_setup.db.
149	#
150	# ==============================================================================================
151
152	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
153	SCRIPTS_ROOT="$SCRIPT_DIR/.."
154	SSH_TIMEOUT=15
155
156	source "$SCRIPTS_ROOT/load_config.sh"
157	source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
158
159	# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
160	SKIP_SSH=false
161	SKIP_AUTH_STACK=false
162	SKIP_ARR_STACK=false
163	SKIP_SERVICES_STACK=false
164	SKIP_ARR_SYNC=false
165	PHASE1_ONLY=false   # OWNER: SSH + conf push only (HOST2 not yet installed)
166	PHASE2_ONLY=false   # OWNER: containers/arr/onboard only (triggered by HOST2 after it onboards)
167	FILTERED_ARGS=()
168
169	for arg in "$@"; do
170	    case "$arg" in
171	        --skip-ssh)        SKIP_SSH=true ;;
172	        --skip-auth-stack) SKIP_AUTH_STACK=true ;;
173	        --skip-arr-stack)      SKIP_ARR_STACK=true ;;
174	        --skip-services-stack) SKIP_SERVICES_STACK=true ;;
175	        --skip-arr-sync)       SKIP_ARR_SYNC=true ;;
176	        --phase1-only)     PHASE1_ONLY=true ;;
177	        --phase2-only)     PHASE2_ONLY=true; SKIP_SSH=true ;;
178	        *)                 FILTERED_ARGS+=("$arg") ;;
179	    esac
180	done
181
182	parse_args "${FILTERED_ARGS[@]}"
183
184	# ==============================================================================================
185	# ━━━ Setup ━━━
186	# ==============================================================================================
187	[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
188
189	acquire_lock
190
191	if ! command -v docker &>/dev/null; then
192	    error "Docker command not found"
193	    exit 1
194	fi
195
196	detect_hosts
197
198	OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
199	MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
200	OWNER="${!OWNER_ID}"
201	MIRROR="${!MIRROR_ID}"
202	# SSH_KEY (set by detect_hosts) is this server's own private key.
203	# The remote accepts it because this server's PUBLIC key was installed there via ssh_setup.sh.
204	# HOST{N}_SSH_KEY lives in host{N}.conf — with sparse checkout, the other server's
205	# conf is never present here. Always use SSH_KEY (local private key) for outbound SSH.
206	MIRROR_SSH_KEY="$SSH_KEY"
207
208	AM_OWNER=false
209	AM_MIRROR=false
210	[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
211	[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
212
213	EXTRA_FLAGS=()
214	[[ "$DRY_RUN"  == true ]] && EXTRA_FLAGS+=("--dry-run")
215	[[ "$LOG_MODE" == true ]] && EXTRA_FLAGS+=("--log")
216
217	START=$(date +%s)
218
219	# ── Helper: write phase completion flag to setup.db + push to remotes ─────────────────────────
220	write_onboard_phase() {
221	    local target_id="$1" phase="$2"
222	    local key="${target_id}_PHASE${phase}_DONE"
223	    local state_file="$(platform_setup_db_path)"
224	    [[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would write ${key}=true"; return 0; }
225	    if grep -q "^${key}=" "$state_file" 2>/dev/null; then
226	        sed -i "s|^${key}=.*|${key}=true|" "$state_file"
227	    else
228	        echo "${key}=true" >> "$state_file"
229	    fi
230	    platform_push_setup_state
231	}
232
233	echo ""
234	echo "━━━ $ICON_FALLBACK Partnership Onboard — $MY_ID ($LOCAL_SERVER_NAME) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
235	echo ""
236	echo "  Role:    $( [[ "$AM_OWNER" == true ]] && echo "OWNER" || echo "MIRROR" )"
237	echo "  This:    $MY_ID ($LOCAL_SERVER_NAME)"
238	echo "  Partner: $( [[ "$AM_OWNER" == true ]] && echo "$MIRROR_ID ($MIRROR)" || echo "$OWNER_ID ($OWNER)" )"
239	echo ""
240	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
241
242	# ==============================================================================================
243	# ── HELPER: stop containers on the mirror by reading its own conf via SSH ────────────────────
244	#
245	# SSHes to the mirror, sources its load_config.sh at the same $SCRIPTS_ROOT path (both servers
246	# use the same convention), and reads the named config array from the mirror's own conf.
247	# HOST2's container list stays in HOST2's host2.conf — not duplicated in HOST1's conf.
248	# Fails gracefully if scripts aren't present yet or the array is empty (nothing to stop).
249	#
250	# deploy_container_from_xml() already stops/removes containers with the same name as what's
251	# being deployed. This step handles containers with DIFFERENT names that conflict.
252	# ==============================================================================================
253	stop_mirror_stack() {
254	    local config_var="$1" label="$2"
255	    local -a to_stop=()
256
257	    mapfile -t to_stop < <(
258	        timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
259	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
260	            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
261	             detect_hosts 2>/dev/null
262	             printf '%s\n' \"\${${config_var}[@]:-}\"" 2>/dev/null | grep -v '^$'
263	    )
264
265	    if [[ ${#to_stop[@]} -eq 0 ]]; then
266	        log "No $label containers to stop on $MIRROR — skipping"
267	        return 0
268	    fi
269
270	    log "Stopping $label on $MIRROR: ${to_stop[*]}"
271	    for container in "${to_stop[@]}"; do
272	        if [[ "$DRY_RUN" == true ]]; then
273	            warn "  DRY RUN — would stop + rm $container on $MIRROR"
274	            continue
275	        fi
276	        timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
277	            -o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
278	            "docker stop '$container' 2>/dev/null
279	             docker rm   '$container' 2>/dev/null && echo removed" 2>/dev/null | \
280	            grep -q removed && \
281	            log "  $container removed ✅" || \
282	            log "  $container not found on $MIRROR — skipping"
283	    done
284	}
285
286	# ==============================================================================================
287	# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
288	# ==============================================================================================
289	if [[ "$AM_MIRROR" == true ]]; then
290	    echo "━━━ Step 1/2 — SSH Key Setup (Mirror) ━━━"
291	    echo ""
292	    echo "  Mirror sets up SSH keys, then notifies Owner to run Phase 2."
293	    echo ""
294
295	    if [[ "$SKIP_SSH" == true ]]; then
296	        warn "Skipping SSH setup (--skip-ssh)"
297	    elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
298	        log "SSH key ready ✅"
299	    else
300	        error "SSH key setup failed"
301	        exit 1
302	    fi
303
304	    echo ""
305	    echo "━━━ Step 2/2 — Notify Owner to Run Phase 2 ━━━"
306	    echo ""
307
308	    OWNER_IP=$(resolve_tailscale_ip "$OWNER" 2>/dev/null || true)
309	    PHASE2_TRIGGERED=false
310
311	    if [[ -n "$OWNER_IP" ]]; then
312	        # Read OWNER's SCRIPTS_DIR via platform probe command — don't assume same path as mirror
313	        _probe_cmd=$(platform_scripts_dir_probe_cmd)
314	        OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
315	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
316	            "$_probe_cmd" 2>/dev/null | tr -d '[:space:]')
317	        OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-$SCRIPTS_DIR}"
318
319	        if [[ "$DRY_RUN" == true ]]; then
320	            warn "DRY RUN — would SSH to $OWNER ($OWNER_IP) and trigger Phase 2"
321	            PHASE2_TRIGGERED=true
322	        elif timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
323	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
324	            "nohup bash '${OWNER_SCRIPTS_DIR}/Partnership/partnership_onboard.sh' --phase2-only > /tmp/vv_phase2_onboard.log 2>&1 & echo triggered" \
325	            2>/dev/null | grep -q triggered; then
326	            log "Phase 2 triggered on $OWNER ✅"
327	            log "Watch progress on $OWNER: tail -f /tmp/vv_phase2_onboard.log"
328	            PHASE2_TRIGGERED=true
329	        else
330	            warn "Could not auto-trigger Phase 2 on $OWNER"
331	        fi
332	    else
333	        warn "Cannot resolve $OWNER Tailscale IP"
334	    fi
335
336	    echo ""
337	    echo "━━━━━ $ICON_SUMMARY MIRROR SETUP COMPLETE ━━━━━"
338	    echo "  SSH key:        ready"
339	    echo "  Phase 2 on $OWNER: $( [[ "$PHASE2_TRIGGERED" == true ]] && echo "triggered ✅" || echo "needs manual trigger ⚠" )"
340	    if [[ "$PHASE2_TRIGGERED" == false ]]; then
341	        echo ""
342	        echo "  Run manually on $OWNER:"
343	        echo "    bash Partnership/partnership_onboard.sh --phase2-only"
344	    fi
345	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
346	    exit 0
347	fi
348
349	# ==============================================================================================
350	# ── OWNER PATH ────────────────────────────────────────────────────────────────────────────────
351	# ==============================================================================================
352
353	MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
354	[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; }
355	log "Mirror: $MIRROR ($MIRROR_IP)"
356	[[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)"
357	[[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)"
358	echo ""
359
360	STEP_SSH_OK=false
361	STEP_STOP_AUTH_OK=true
362	STEP_AUTH_OK=true
363	AUTH_DEPLOYED=0
364	AUTH_FAILED=0
365	STEP_STOP_ARR_OK=true
366	STEP_ARR_OK=true
367	ARR_DEPLOYED=0
368	ARR_FAILED=0
369	STEP_STOP_SERVICES_OK=true
370	STEP_SERVICES_OK=true
371	SERVICES_DEPLOYED=0
372	SERVICES_FAILED=0
373	ONBOARD_OK=false
374	ARR_SYNC_OK=false
375	MASTER_PUSH_OK=false
376
377	# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
378	# Skipped when --phase2-only (SSH was already done in Phase 1).
379	echo "━━━ Step 1 — SSH Key Setup ━━━"
380
381	if [[ "$SKIP_SSH" == true ]]; then
382	    warn "Skipping (--skip-ssh)"
383	    STEP_SSH_OK=true
384	elif [[ "$PHASE1_ONLY" == true ]]; then
385	    # Phase 1 in background: test if SSH already works first — avoids ssh-copy-id
386	    # hanging for a password prompt with no TTY.
387	    if timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
388	        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" exit 0 2>/dev/null; then
389	        log "SSH to $MIRROR already works ✅ — skipping key install"
390	        STEP_SSH_OK=true
391	    else
392	        # Key not yet on HOST2 — try ssh_setup.sh (works interactively, may fail in background)
393	        if bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
394	            log "SSH keys ready ✅"
395	            STEP_SSH_OK=true
396	        else
397	            # Soft-fail: generate key locally if not present, then tell user to install manually
398	            warn "Could not install key on $MIRROR automatically (no terminal for password prompt)"
399	            if [[ -f "$SSH_KEY" ]]; then
400	                log "Local key exists at: $SSH_KEY"
401	            else
402	                bash "$SCRIPT_DIR/ssh_setup.sh" --key-only "${EXTRA_FLAGS[@]}" 2>/dev/null || true
403	            fi
404	            if [[ -f "${SSH_KEY}.pub" ]]; then
405	                echo ""
406	                echo "  Install this key on $MIRROR to complete SSH setup:"
407	                echo "  ┌─────────────────────────────────────────────────────"
408	                cat "${SSH_KEY}.pub" | sed 's/^/  │ /'
409	                echo "  └─────────────────────────────────────────────────────"
410	                echo "  Run on a terminal: ssh-copy-id -i ${SSH_KEY}.pub root@${MIRROR_IP}"
411	                echo "  Then click 'Push Conf' in the Partnership tab."
412	                # Write key-ready flag so UI can show the manual-install state
413	                [[ "$DRY_RUN" == false ]] && {
414	                    local kflag="${MIRROR_ID}_KEY_READY"
415	                    local _setup_f="$(platform_setup_db_path)"
416	                    grep -q "^${kflag}=" "$_setup_f" 2>/dev/null \
417	                        && sed -i "s|^${kflag}=.*|${kflag}=true|" "$_setup_f" \
418	                        || echo "${kflag}=true" >> "$_setup_f"
419	                }
420	            fi
421	            STEP_SSH_OK=false
422	        fi
423	    fi
424	elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
425	    log "SSH keys ready ✅"
426	    STEP_SSH_OK=true
427	else
428	    error "SSH key setup failed — aborting"
429	    error "Re-run or use --skip-ssh if key is already set up"
430	    exit 1
431	fi
432
433	# ── Phase 1 exit point ────────────────────────────────────────────────────────────────────────
434	# --phase1-only: SSH + conf push is all HOST1 needs to do before HOST2 installs Varaverk.
435	# HOST2's wizard will detect the pushed master.conf + state file and take the correct path.
436	if [[ "$PHASE1_ONLY" == true ]]; then
437	    if [[ "$STEP_SSH_OK" == false ]]; then
438	        # SSH key not yet installed on HOST2 — can't push conf, but local setup still runs.
439	        # UI will show "key ready, install manually" state via HOST2_KEY_READY flag.
440	        echo ""
441	        echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"
442	        bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
443	            warn "Local setup had issues — check partnership_manager.sh output above"
444
445	        END=$(date +%s)
446	        echo ""
447	        echo "━━━━━ $ICON_SUMMARY PHASE 1 — SSH PENDING ━━━━━"
448	        echo "  SSH keys:    key generated ✅ — NOT yet installed on $MIRROR ⚠"
449	        echo "  Conf push:   skipped (needs SSH access to $MIRROR)"
450	        echo "  HOST1 setup: done ✅"
451	        echo "  Duration:    $(format_duration $(( END - START )))"
452	        echo ""
453	        echo "  ACTION NEEDED: install the key on $MIRROR:"
454	        echo "    ssh-copy-id -i ${SSH_KEY}.pub root@${MIRROR_IP}"
455	        echo "  Then click 'Push Conf' in Partnership tab, or run:"
456	        echo "    bash Partnership/partnership_onboard.sh --phase1-only --skip-ssh"
457	        echo "━━━━━━━━━━━━━━━━━━━━━━━"
458	        exit 0
459	    fi
460
461	    echo ""
462	    echo "━━━ Phase 1 — Conf Push ━━━"
463
464	    CONF_PUSH_OK=false
465	    if [[ "$DRY_RUN" == true ]]; then
466	        warn "DRY RUN — would push master.conf + state file to $MIRROR"
467	        CONF_PUSH_OK=true
468	    else
469	        push_output=$(platform_push_conf)
470	        push_rc=$?
471	        [[ -n "$push_output" ]] && echo "$push_output"
472	        platform_push_setup_state
473	        if [[ $push_rc -eq 0 ]]; then
474	            log "Conf push complete ✅"
475	            CONF_PUSH_OK=true
476	        else
477	            warn "Conf push had failures — retry via Scheduler → master.conf → Save Conf"
478	        fi
479	    fi
480
481	    # HOST1 local setup — runs immediately without needing HOST2
482	    echo ""
483	    echo "━━━ Phase 1 — HOST1 Local Setup ━━━"
484	    bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
485	        warn "Local setup had issues — check partnership_manager.sh output above"
486
487	    [[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1
488
489	    END=$(date +%s)
490	    echo ""
491	    echo "━━━━━ $ICON_SUMMARY PHASE 1 COMPLETE ━━━━━"
492	    echo "  SSH keys:     $( [[ "$STEP_SSH_OK" == true ]] && echo "ready ✅" || echo "skipped" )"
493	    echo "  Conf push:    $( [[ "$CONF_PUSH_OK" == true ]] && echo "done ✅" || echo "⚠ manual needed" )"
494	    echo "  HOST1 setup:  done ✅"
495	    echo "  Duration:     $(format_duration $(( END - START )))"
496	    echo ""
497	    echo "  HOST1 is fully set up. HOST2 ($MIRROR) can now install the Varaverk plugin."
498	    echo "  The wizard will detect the pushed conf and take the correct path."
499	    echo "  When HOST2 completes its onboard, it will automatically trigger Phase 2 here."
500	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
501	    exit 0
502	fi
503
504	# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
505	echo ""
506	echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"
507
508	if [[ "$SKIP_AUTH_STACK" == true ]]; then
509	    warn "Skipping (--skip-auth-stack)"
510	else
511	    stop_mirror_stack "PARTNERSHIP_REPLACE_CONTAINERS" "auth stack"
512	fi
513
514	# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
515	echo ""
516	echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"
517
518	if [[ "$SKIP_AUTH_STACK" == true ]]; then
519	    warn "Skipping (--skip-auth-stack)"
520	elif [[ ${#PARTNERSHIP_AUTH_STACK[@]} -eq 0 ]]; then
521	    warn "PARTNERSHIP_AUTH_STACK not set in ${MY_ID} conf — skipping auth stack deploy"
522	    warn "Add HOST${MY_ID: -1}_PARTNERSHIP_AUTH_STACK to host${MY_ID: -1}.conf"
523	    STEP_AUTH_OK=false
524	else
525	    deploy_xml_stack PARTNERSHIP_AUTH_STACK
526	    AUTH_DEPLOYED=$_STACK_DEPLOYED
527	    AUTH_FAILED=$_STACK_FAILED
528	    echo "Auth stack: $AUTH_DEPLOYED deployed, $AUTH_FAILED failed"
529	    [[ "$AUTH_FAILED" -gt 0 ]] && STEP_AUTH_OK=false
530	fi
531
532	# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
533	echo ""
534	echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"
535
536	if [[ "$SKIP_ARR_STACK" == true ]]; then
537	    warn "Skipping (--skip-arr-stack)"
538	elif [[ ${#PARTNERSHIP_ARR_STACK[@]} -eq 0 ]]; then
539	    log "PARTNERSHIP_ARR_STACK not configured — skipping arr stack deploy"
540	    SKIP_ARR_STACK=true
541	else
542	    stop_mirror_stack "PARTNERSHIP_ARR_REPLACE_CONTAINERS" "arr stack"
543	fi
544
545	# ── Step 5: Deploy arr stack on mirror ───────────────────────────────────────────────────────
546	echo ""
547	echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"
548
549	if [[ "$SKIP_ARR_STACK" == true ]]; then
550	    warn "Skipping (--skip-arr-stack)"
551	else
552	    deploy_xml_stack PARTNERSHIP_ARR_STACK
553	    ARR_DEPLOYED=$_STACK_DEPLOYED
554	    ARR_FAILED=$_STACK_FAILED
555	    echo "Arr stack: $ARR_DEPLOYED deployed, $ARR_FAILED failed"
556	    [[ "$ARR_FAILED" -gt 0 ]] && STEP_ARR_OK=false
557	fi
558
559	# ── Step 6: Stop mirror's existing services stack ─────────────────────────────────────────────
560	echo ""
561	echo "━━━ Step 6 — Stop Mirror Services Stack ━━━"
562
563	if [[ "$SKIP_SERVICES_STACK" == true ]]; then
564	    warn "Skipping (--skip-services-stack)"
565	elif [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -eq 0 ]]; then
566	    log "PARTNERSHIP_SERVICES_STACK not configured — skipping services stack deploy"
567	    SKIP_SERVICES_STACK=true
568	else
569	    stop_mirror_stack "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS" "services stack"
570	fi
571
572	# ── Step 7: Deploy services stack on mirror ───────────────────────────────────────────────────
573	echo ""
574	echo "━━━ Step 7 — Deploy Services Stack on Mirror ━━━"
575
576	if [[ "$SKIP_SERVICES_STACK" == true ]]; then
577	    warn "Skipping (--skip-services-stack)"
578	else
579	    deploy_xml_stack PARTNERSHIP_SERVICES_STACK
580	    SERVICES_DEPLOYED=$_STACK_DEPLOYED
581	    SERVICES_FAILED=$_STACK_FAILED
582	    echo "Services stack: $SERVICES_DEPLOYED deployed, $SERVICES_FAILED failed"
583	    [[ "$SERVICES_FAILED" -gt 0 ]] && STEP_SERVICES_OK=false
584	fi
585
586	# ── Step 8: Partnership onboard ───────────────────────────────────────────────────────────────
587	echo ""
588	echo "━━━ Step 8 — Partnership Onboard ━━━"
589
590	if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
591	    echo "Partnership onboard complete ✅"
592	    ONBOARD_OK=true
593	else
594	    error "Partnership onboard failed"
595	    ONBOARD_OK=false
596	fi
597
598	# ── Step 9: Arr library bootstrap ─────────────────────────────────────────────────────────────
599	echo ""
600	echo "━━━ Step 9 — Arr Library Bootstrap ━━━"
601
602	if [[ "$ONBOARD_OK" == false ]]; then
603	    warn "Skipping — onboard did not complete"
604	elif [[ "$SKIP_ARR_SYNC" == true ]]; then
605	    warn "Skipping (--skip-arr-sync)"
606	elif [[ ! -f "$SCRIPTS_ROOT/Media/arr_sync.sh" ]]; then
607	    warn "arr_sync.sh not found — run Media/arr_sync.sh manually once arrs are live"
608	elif bash "$SCRIPTS_ROOT/Media/arr_sync.sh" "${EXTRA_FLAGS[@]}"; then
609	    echo "Arr bootstrap complete ✅"
610	    ARR_SYNC_OK=true
611	else
612	    warn "Arr sync had errors — partnership still valid"
613	    warn "Re-run Media/arr_sync.sh once all arr containers are live"
614	fi
615
616	# ── Step 10: Push master.conf to all listed hosts ─────────────────────────────────────────────
617	# SSH is now established and all partners have the plugin installed.
618	# Push the authoritative master.conf so every listed host is in sync immediately.
619	echo ""
620	echo "━━━ $ICON_GEAR Step 10 — master.conf Push ━━━"
621
622	if [[ "$ONBOARD_OK" == false ]]; then
623	    warn "Skipping — onboard did not complete"
624	elif [[ "$DRY_RUN" == true ]]; then
625	    warn "DRY RUN — would push master.conf to all listed hosts"
626	    MASTER_PUSH_OK=true
627	else
628	    push_output=$(platform_push_conf)
629	    push_rc=$?
630	    [[ -n "$push_output" ]] && echo "$push_output"
631	    platform_push_setup_state
632	    if [[ $push_rc -eq 0 ]]; then
633	        echo "master.conf sync complete ✅"
634	        MASTER_PUSH_OK=true
635	    else
636	        warn "master.conf push had failures — retry via Scheduler → master.conf → Save Conf"
637	    fi
638	fi
639
640	# ── Write Phase 2 completion state ────────────────────────────────────────────────────────────
641	[[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2
642
643	# ── Summary ───────────────────────────────────────────────────────────────────────────────────
644	END=$(date +%s)
645	echo ""
646	echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
647	echo "  Owner:    $MY_ID ($LOCAL_SERVER_NAME)"
648	echo "  Mirror:   $MIRROR ($MIRROR_IP)"
649	[[ "$PHASE2_ONLY" == true ]] && echo "  Mode:     Phase 2 (triggered by HOST2 notification)"
650	echo "  Duration: $(format_duration $(( END - START )))"
651	echo ""
652	_ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
653	_skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
654
655	echo "  Step 1  — SSH keys:         $(_skip "$SKIP_SSH"             "$STEP_SSH_OK")"
656	echo "  Step 2  — Stop auth:        $(_skip "$SKIP_AUTH_STACK"       "$STEP_STOP_AUTH_OK")"
657	echo "  Step 3  — Auth stack:       $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
658	echo "  Step 4  — Stop arr:         $(_skip "$SKIP_ARR_STACK"        "$STEP_STOP_ARR_OK")"
659	echo "  Step 5  — Arr stack:        $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
660	echo "  Step 6  — Stop services:    $(_skip "$SKIP_SERVICES_STACK"   "$STEP_STOP_SERVICES_OK")"
661	echo "  Step 7  — Services stack:   $( [[ "$SKIP_SERVICES_STACK" == true ]] && echo "skipped" || echo "${SERVICES_DEPLOYED} deployed, ${SERVICES_FAILED} failed" )"
662	echo "  Step 8  — Onboard:          $(_ok "$ONBOARD_OK")"
663	echo "  Step 9  — Arr bootstrap:    $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
664	echo "  Step 10 — Conf push:        $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
665	echo ""
666
667	if [[ "$ONBOARD_OK" == true ]]; then
668	    [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
669	        echo "$ICON_DONE DONE — partnership established ✅"
670	    echo "Verify with: Partnership/partnership_manager.sh --status"
671	else
672	    error "Setup incomplete — resolve errors above and re-run"
673	fi
674	echo "━━━━━━━━━━━━━━━━━━━━━━━"
675	[[ "$ONBOARD_OK" == false ]] && exit 1
676	exit 0
677

#!/bin/bash
# ==============================================================================================
# ============================= Partnership Onboard ============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Runs once on both servers to establish a new partnership. Role is detected
# automatically via detect_hosts() — no flags needed to declare which side you are.
# Run on the mirror first (generates its SSH key), then on the owner to complete
# setup remotely.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# MIRROR PATH (1 step)
#   Step 1: SSH key setup — generate keypair, copy to owner, update conf
#           Owner completes the rest remotely. Mirror is done.
#
# OWNER PATH (10 steps)
#   Step 1:  SSH key setup         — generate keypair, install on mirror, update conf
#   Step 2:  Stop mirror auth      — stop mirror's existing auth containers before replacing
#   Step 3:  Deploy auth stack     — push XMLs, pull images, create + start on mirror
#                                    Mariadb/Redis health-checked before Authelia deploys
#   Step 4:  Stop mirror arr       — stop mirror's existing arr containers before replacing
#   Step 5:  Deploy arr stack      — push arr XMLs, pull images, create + start on mirror
#   Step 6:  Stop mirror services  — stop mirror's existing services containers before replacing
#   Step 7:  Deploy services stack — push Emby/Jellyfin/Seerr XMLs, pull images, create + start
#   Step 8:  Partnership onboard   — configure WebUIs → owner IP, write state, Emby
#   Step 9:  Arr bootstrap         — bidirectional library sync (arr_sync.sh)
#   Step 10: Conf push             — push master.conf + setup state to all listed hosts
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Credentials never in SSH command strings
#   Auth stack containers hold API keys, DB passwords, etc. The deploy script is written
#   locally, SCPed to the remote, and executed there. Command-line args are never used
#   to pass credentials — they'd appear in `ps` output and shell history on both servers.
#
# XML templates are the single source of truth for deployed containers
#   The owner's templates-user/ XMLs define every container deployed on the mirror.
#   The same XMLs that Unraid's Docker Manager uses are what get SCPed — the mirror's
#   Docker Manager can manage the containers after onboard without additional config.
#
# Dependency ordering in the auth stack is owner-enforced
#   PARTNERSHIP_AUTH_STACK order matters: Mariadb and Redis must come before Authelia.
#   The array is ordered correctly in host1.conf. After each Mariadb/Redis deploy,
#   the script waits for the container to be healthy before continuing. This is a remote
#   health check — the container must be running (or report healthy) before the next
#   dependent is deployed.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root check
#   All operations run as root — SSH key management, docker operations, conf updates.
#
# SSH timeout on all remote calls
#   Every ssh/scp call uses SSH_TIMEOUT. No operation hangs indefinitely on a
#   slow or unreachable mirror.
#
# --dry-run shows exact actions without executing
#   Every step prints what it would do. SCP, deploy, plugin install, arr sync —
#   all dry-run safe.
#
# Step skip flags for partial re-runs
#   --skip-ssh, --skip-auth-stack, --skip-arr-stack, --skip-arr-sync allow
#   resuming after a partial failure without re-running completed steps.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_PARTNERSHIP_AUTH_STACK
#       XML filenames (from this server's templates-user/) to push and deploy on the
#       mirror as its auth stack. Order matters: database deps before Authelia.
#       Aliased by detect_hosts() → PARTNERSHIP_AUTH_STACK
#
#   HOST*_PARTNERSHIP_REPLACE_CONTAINERS
#       Containers to stop on the mirror before deploying the auth stack.
#       Defined in the MIRROR's own conf (host*.conf on HOST2) — never in HOST1's conf.
#       Read live from the mirror via SSH during Step 3 (sources mirror's load_config.sh at
#       the same $SCRIPTS_ROOT path — convention: both servers use the same repo location).
#       Leave empty on HOST2 if no conflicting containers exist (fresh mirror: nothing to stop).
#       Aliased by detect_hosts() → PARTNERSHIP_REPLACE_CONTAINERS (on the mirror)
#
#   HOST*_PARTNERSHIP_ARR_STACK
#       XML filenames to push and deploy on the mirror as its arr stack.
#       Leave empty to skip arr stack deploy.
#       Aliased by detect_hosts() → PARTNERSHIP_ARR_STACK
#
#   HOST*_PARTNERSHIP_ARR_REPLACE_CONTAINERS
#       Arr containers to stop on the mirror before deploying the arr stack.
#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
#       Aliased by detect_hosts() → PARTNERSHIP_ARR_REPLACE_CONTAINERS (on the mirror)
#
#   HOST*_PARTNERSHIP_SERVICES_STACK
#       XML filenames to push and deploy on the mirror as its shared services stack.
#       Includes Emby, Jellyfin, Seerr, SeerrFin. Leave empty to skip services stack deploy.
#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_STACK
#
#   HOST*_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS
#       Services containers to stop on the mirror before deploying the services stack.
#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_REPLACE_CONTAINERS (on the mirror)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# Partnership/partnership_onboard.sh
#     Full onboard — role detected automatically
#
# Partnership/partnership_onboard.sh --dry-run
#     Preview all steps without making changes
#
# Partnership/partnership_onboard.sh --log
#     Verbose per-step output
#
# Partnership/partnership_onboard.sh --skip-ssh
#     Skip SSH key setup (key already in place)
#
# Partnership/partnership_onboard.sh --skip-auth-stack
#     Skip auth stack stop + deploy (Steps 3-4)
#
# Partnership/partnership_onboard.sh --skip-arr-stack
#     Skip arr stack stop + deploy (Steps 4-5)
#
# Partnership/partnership_onboard.sh --skip-services-stack
#     Skip services stack stop + deploy (Steps 6-7)
#
# Partnership/partnership_onboard.sh --skip-arr-sync
#     Skip arr library bootstrap (Step 9)
#
# Partnership/partnership_onboard.sh --phase1-only
#     OWNER only: SSH key exchange + conf push. Safe to run before HOST2 has Varaverk.
#     Writes HOST2_PHASE1_DONE=true to varaverk_setup.db.
#
# Partnership/partnership_onboard.sh --phase2-only
#     OWNER only: container deploy + arr + onboard (skips SSH). Triggered automatically
#     by HOST2 after it completes its Mirror-path onboard. Can also be run manually.
#     Writes HOST2_PHASE2_DONE=true to varaverk_setup.db.
#
# ==============================================================================================

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 ───────────────────────────────────────────────────────────────────────────────
SKIP_SSH=false
SKIP_AUTH_STACK=false
SKIP_ARR_STACK=false
SKIP_SERVICES_STACK=false
SKIP_ARR_SYNC=false
PHASE1_ONLY=false   # OWNER: SSH + conf push only (HOST2 not yet installed)
PHASE2_ONLY=false   # OWNER: containers/arr/onboard only (triggered by HOST2 after it onboards)
FILTERED_ARGS=()

for arg in "$@"; do
    case "$arg" in
        --skip-ssh)        SKIP_SSH=true ;;
        --skip-auth-stack) SKIP_AUTH_STACK=true ;;
        --skip-arr-stack)      SKIP_ARR_STACK=true ;;
        --skip-services-stack) SKIP_SERVICES_STACK=true ;;
        --skip-arr-sync)       SKIP_ARR_SYNC=true ;;
        --phase1-only)     PHASE1_ONLY=true ;;
        --phase2-only)     PHASE2_ONLY=true; SKIP_SSH=true ;;
        *)                 FILTERED_ARGS+=("$arg") ;;
    esac
done

parse_args "${FILTERED_ARGS[@]}"

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

acquire_lock

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}"
# SSH_KEY (set by detect_hosts) is this server's own private key.
# The remote accepts it because this server's PUBLIC key was installed there via ssh_setup.sh.
# HOST{N}_SSH_KEY lives in host{N}.conf — with sparse checkout, the other server's
# conf is never present here. Always use SSH_KEY (local private key) for outbound SSH.
MIRROR_SSH_KEY="$SSH_KEY"

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

EXTRA_FLAGS=()
[[ "$DRY_RUN"  == true ]] && EXTRA_FLAGS+=("--dry-run")
[[ "$LOG_MODE" == true ]] && EXTRA_FLAGS+=("--log")

START=$(date +%s)

# ── Helper: write phase completion flag to setup.db + push to remotes ─────────────────────────
write_onboard_phase() {
    local target_id="$1" phase="$2"
    local key="${target_id}_PHASE${phase}_DONE"
    local state_file="$(platform_setup_db_path)"
    [[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would write ${key}=true"; return 0; }
    if grep -q "^${key}=" "$state_file" 2>/dev/null; then
        sed -i "s|^${key}=.*|${key}=true|" "$state_file"
    else
        echo "${key}=true" >> "$state_file"
    fi
    platform_push_setup_state
}

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

# ==============================================================================================
# ── HELPER: stop containers on the mirror by reading its own conf via SSH ────────────────────
#
# SSHes to the mirror, sources its load_config.sh at the same $SCRIPTS_ROOT path (both servers
# use the same convention), and reads the named config array from the mirror's own conf.
# HOST2's container list stays in HOST2's host2.conf — not duplicated in HOST1's conf.
# Fails gracefully if scripts aren't present yet or the array is empty (nothing to stop).
#
# deploy_container_from_xml() already stops/removes containers with the same name as what's
# being deployed. This step handles containers with DIFFERENT names that conflict.
# ==============================================================================================
stop_mirror_stack() {
    local config_var="$1" label="$2"
    local -a to_stop=()

    mapfile -t to_stop < <(
        timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
             detect_hosts 2>/dev/null
             printf '%s\n' \"\${${config_var}[@]:-}\"" 2>/dev/null | grep -v '^$'
    )

    if [[ ${#to_stop[@]} -eq 0 ]]; then
        log "No $label containers to stop on $MIRROR — skipping"
        return 0
    fi

    log "Stopping $label on $MIRROR: ${to_stop[*]}"
    for container in "${to_stop[@]}"; do
        if [[ "$DRY_RUN" == true ]]; then
            warn "  DRY RUN — would stop + rm $container on $MIRROR"
            continue
        fi
        timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
            "docker stop '$container' 2>/dev/null
             docker rm   '$container' 2>/dev/null && echo removed" 2>/dev/null | \
            grep -q removed && \
            log "  $container removed ✅" || \
            log "  $container not found on $MIRROR — skipping"
    done
}

# ==============================================================================================
# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
if [[ "$AM_MIRROR" == true ]]; then
    echo "━━━ Step 1/2 — SSH Key Setup (Mirror) ━━━"
    echo ""
    echo "  Mirror sets up SSH keys, then notifies Owner to run Phase 2."
    echo ""

    if [[ "$SKIP_SSH" == true ]]; then
        warn "Skipping SSH setup (--skip-ssh)"
    elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
        log "SSH key ready ✅"
    else
        error "SSH key setup failed"
        exit 1
    fi

    echo ""
    echo "━━━ Step 2/2 — Notify Owner to Run Phase 2 ━━━"
    echo ""

    OWNER_IP=$(resolve_tailscale_ip "$OWNER" 2>/dev/null || true)
    PHASE2_TRIGGERED=false

    if [[ -n "$OWNER_IP" ]]; then
        # Read OWNER's SCRIPTS_DIR via platform probe command — don't assume same path as mirror
        _probe_cmd=$(platform_scripts_dir_probe_cmd)
        OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
            "$_probe_cmd" 2>/dev/null | tr -d '[:space:]')
        OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-$SCRIPTS_DIR}"

        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would SSH to $OWNER ($OWNER_IP) and trigger Phase 2"
            PHASE2_TRIGGERED=true
        elif timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
            "nohup bash '${OWNER_SCRIPTS_DIR}/Partnership/partnership_onboard.sh' --phase2-only > /tmp/vv_phase2_onboard.log 2>&1 & echo triggered" \
            2>/dev/null | grep -q triggered; then
            log "Phase 2 triggered on $OWNER ✅"
            log "Watch progress on $OWNER: tail -f /tmp/vv_phase2_onboard.log"
            PHASE2_TRIGGERED=true
        else
            warn "Could not auto-trigger Phase 2 on $OWNER"
        fi
    else
        warn "Cannot resolve $OWNER Tailscale IP"
    fi

    echo ""
    echo "━━━━━ $ICON_SUMMARY MIRROR SETUP COMPLETE ━━━━━"
    echo "  SSH key:        ready"
    echo "  Phase 2 on $OWNER: $( [[ "$PHASE2_TRIGGERED" == true ]] && echo "triggered ✅" || echo "needs manual trigger ⚠" )"
    if [[ "$PHASE2_TRIGGERED" == false ]]; then
        echo ""
        echo "  Run manually on $OWNER:"
        echo "    bash Partnership/partnership_onboard.sh --phase2-only"
    fi
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ── OWNER PATH ────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; }
log "Mirror: $MIRROR ($MIRROR_IP)"
[[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)"
[[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)"
echo ""

STEP_SSH_OK=false
STEP_STOP_AUTH_OK=true
STEP_AUTH_OK=true
AUTH_DEPLOYED=0
AUTH_FAILED=0
STEP_STOP_ARR_OK=true
STEP_ARR_OK=true
ARR_DEPLOYED=0
ARR_FAILED=0
STEP_STOP_SERVICES_OK=true
STEP_SERVICES_OK=true
SERVICES_DEPLOYED=0
SERVICES_FAILED=0
ONBOARD_OK=false
ARR_SYNC_OK=false
MASTER_PUSH_OK=false

# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
# Skipped when --phase2-only (SSH was already done in Phase 1).
echo "━━━ Step 1 — SSH Key Setup ━━━"

if [[ "$SKIP_SSH" == true ]]; then
    warn "Skipping (--skip-ssh)"
    STEP_SSH_OK=true
elif [[ "$PHASE1_ONLY" == true ]]; then
    # Phase 1 in background: test if SSH already works first — avoids ssh-copy-id
    # hanging for a password prompt with no TTY.
    if timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" exit 0 2>/dev/null; then
        log "SSH to $MIRROR already works ✅ — skipping key install"
        STEP_SSH_OK=true
    else
        # Key not yet on HOST2 — try ssh_setup.sh (works interactively, may fail in background)
        if bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
            log "SSH keys ready ✅"
            STEP_SSH_OK=true
        else
            # Soft-fail: generate key locally if not present, then tell user to install manually
            warn "Could not install key on $MIRROR automatically (no terminal for password prompt)"
            if [[ -f "$SSH_KEY" ]]; then
                log "Local key exists at: $SSH_KEY"
            else
                bash "$SCRIPT_DIR/ssh_setup.sh" --key-only "${EXTRA_FLAGS[@]}" 2>/dev/null || true
            fi
            if [[ -f "${SSH_KEY}.pub" ]]; then
                echo ""
                echo "  Install this key on $MIRROR to complete SSH setup:"
                echo "  ┌─────────────────────────────────────────────────────"
                cat "${SSH_KEY}.pub" | sed 's/^/  │ /'
                echo "  └─────────────────────────────────────────────────────"
                echo "  Run on a terminal: ssh-copy-id -i ${SSH_KEY}.pub root@${MIRROR_IP}"
                echo "  Then click 'Push Conf' in the Partnership tab."
                # Write key-ready flag so UI can show the manual-install state
                [[ "$DRY_RUN" == false ]] && {
                    local kflag="${MIRROR_ID}_KEY_READY"
                    local _setup_f="$(platform_setup_db_path)"
                    grep -q "^${kflag}=" "$_setup_f" 2>/dev/null \
                        && sed -i "s|^${kflag}=.*|${kflag}=true|" "$_setup_f" \
                        || echo "${kflag}=true" >> "$_setup_f"
                }
            fi
            STEP_SSH_OK=false
        fi
    fi
elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
    log "SSH keys ready ✅"
    STEP_SSH_OK=true
else
    error "SSH key setup failed — aborting"
    error "Re-run or use --skip-ssh if key is already set up"
    exit 1
fi

# ── Phase 1 exit point ────────────────────────────────────────────────────────────────────────
# --phase1-only: SSH + conf push is all HOST1 needs to do before HOST2 installs Varaverk.
# HOST2's wizard will detect the pushed master.conf + state file and take the correct path.
if [[ "$PHASE1_ONLY" == true ]]; then
    if [[ "$STEP_SSH_OK" == false ]]; then
        # SSH key not yet installed on HOST2 — can't push conf, but local setup still runs.
        # UI will show "key ready, install manually" state via HOST2_KEY_READY flag.
        echo ""
        echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"
        bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
            warn "Local setup had issues — check partnership_manager.sh output above"

        END=$(date +%s)
        echo ""
        echo "━━━━━ $ICON_SUMMARY PHASE 1 — SSH PENDING ━━━━━"
        echo "  SSH keys:    key generated ✅ — NOT yet installed on $MIRROR ⚠"
        echo "  Conf push:   skipped (needs SSH access to $MIRROR)"
        echo "  HOST1 setup: done ✅"
        echo "  Duration:    $(format_duration $(( END - START )))"
        echo ""
        echo "  ACTION NEEDED: install the key on $MIRROR:"
        echo "    ssh-copy-id -i ${SSH_KEY}.pub root@${MIRROR_IP}"
        echo "  Then click 'Push Conf' in Partnership tab, or run:"
        echo "    bash Partnership/partnership_onboard.sh --phase1-only --skip-ssh"
        echo "━━━━━━━━━━━━━━━━━━━━━━━"
        exit 0
    fi

    echo ""
    echo "━━━ Phase 1 — Conf Push ━━━"

    CONF_PUSH_OK=false
    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would push master.conf + state file to $MIRROR"
        CONF_PUSH_OK=true
    else
        push_output=$(platform_push_conf)
        push_rc=$?
        [[ -n "$push_output" ]] && echo "$push_output"
        platform_push_setup_state
        if [[ $push_rc -eq 0 ]]; then
            log "Conf push complete ✅"
            CONF_PUSH_OK=true
        else
            warn "Conf push had failures — retry via Scheduler → master.conf → Save Conf"
        fi
    fi

    # HOST1 local setup — runs immediately without needing HOST2
    echo ""
    echo "━━━ Phase 1 — HOST1 Local Setup ━━━"
    bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
        warn "Local setup had issues — check partnership_manager.sh output above"

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

    END=$(date +%s)
    echo ""
    echo "━━━━━ $ICON_SUMMARY PHASE 1 COMPLETE ━━━━━"
    echo "  SSH keys:     $( [[ "$STEP_SSH_OK" == true ]] && echo "ready ✅" || echo "skipped" )"
    echo "  Conf push:    $( [[ "$CONF_PUSH_OK" == true ]] && echo "done ✅" || echo "⚠ manual needed" )"
    echo "  HOST1 setup:  done ✅"
    echo "  Duration:     $(format_duration $(( END - START )))"
    echo ""
    echo "  HOST1 is fully set up. HOST2 ($MIRROR) can now install the Varaverk plugin."
    echo "  The wizard will detect the pushed conf and take the correct path."
    echo "  When HOST2 completes its onboard, it will automatically trigger Phase 2 here."
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

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

if [[ "$SKIP_AUTH_STACK" == true ]]; then
    warn "Skipping (--skip-auth-stack)"
else
    stop_mirror_stack "PARTNERSHIP_REPLACE_CONTAINERS" "auth stack"
fi

# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
echo ""
echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"

if [[ "$SKIP_AUTH_STACK" == true ]]; then
    warn "Skipping (--skip-auth-stack)"
elif [[ ${#PARTNERSHIP_AUTH_STACK[@]} -eq 0 ]]; then
    warn "PARTNERSHIP_AUTH_STACK not set in ${MY_ID} conf — skipping auth stack deploy"
    warn "Add HOST${MY_ID: -1}_PARTNERSHIP_AUTH_STACK to host${MY_ID: -1}.conf"
    STEP_AUTH_OK=false
else
    deploy_xml_stack PARTNERSHIP_AUTH_STACK
    AUTH_DEPLOYED=$_STACK_DEPLOYED
    AUTH_FAILED=$_STACK_FAILED
    echo "Auth stack: $AUTH_DEPLOYED deployed, $AUTH_FAILED failed"
    [[ "$AUTH_FAILED" -gt 0 ]] && STEP_AUTH_OK=false
fi

# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
echo ""
echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"

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

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

if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
    echo "Partnership onboard complete ✅"
    ONBOARD_OK=true
else
    error "Partnership onboard failed"
    ONBOARD_OK=false
fi

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

if [[ "$ONBOARD_OK" == false ]]; then
    warn "Skipping — onboard did not complete"
elif [[ "$SKIP_ARR_SYNC" == true ]]; then
    warn "Skipping (--skip-arr-sync)"
elif [[ ! -f "$SCRIPTS_ROOT/Media/arr_sync.sh" ]]; then
    warn "arr_sync.sh not found — run Media/arr_sync.sh manually once arrs are live"
elif bash "$SCRIPTS_ROOT/Media/arr_sync.sh" "${EXTRA_FLAGS[@]}"; then
    echo "Arr bootstrap complete ✅"
    ARR_SYNC_OK=true
else
    warn "Arr sync had errors — partnership still valid"
    warn "Re-run Media/arr_sync.sh once all arr containers are live"
fi

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

if [[ "$ONBOARD_OK" == false ]]; then
    warn "Skipping — onboard did not complete"
elif [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would push master.conf to all listed hosts"
    MASTER_PUSH_OK=true
else
    push_output=$(platform_push_conf)
    push_rc=$?
    [[ -n "$push_output" ]] && echo "$push_output"
    platform_push_setup_state
    if [[ $push_rc -eq 0 ]]; then
        echo "master.conf sync complete ✅"
        MASTER_PUSH_OK=true
    else
        warn "master.conf push had failures — retry via Scheduler → master.conf → Save Conf"
    fi
fi

# ── Write Phase 2 completion state ────────────────────────────────────────────────────────────
[[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2

# ── Summary ───────────────────────────────────────────────────────────────────────────────────
END=$(date +%s)
echo ""
echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
echo "  Owner:    $MY_ID ($LOCAL_SERVER_NAME)"
echo "  Mirror:   $MIRROR ($MIRROR_IP)"
[[ "$PHASE2_ONLY" == true ]] && echo "  Mode:     Phase 2 (triggered by HOST2 notification)"
echo "  Duration: $(format_duration $(( END - START )))"
echo ""
_ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
_skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }

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

if [[ "$ONBOARD_OK" == true ]]; then
    [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
        echo "$ICON_DONE DONE — partnership established ✅"
    echo "Verify with: Partnership/partnership_manager.sh --status"
else
    error "Setup incomplete — resolve errors above and re-run"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━"
[[ "$ONBOARD_OK" == false ]] && exit 1
exit 0


Now let me find where the local onboard and container deployment happens.


find /boot/config/plugins/varaverk/Plugin -name "containers.sh" | sort && find /boot/config/plugins/varaverk -name "partnership_manager.sh" | sort

Find containers.sh and partnership_manager.sh


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