This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Summary:
1. Primary Request and Intent:

**Play State Sync fixes (completed):**
- Investigate why play state sync was re-syncing the same episodes every 30 minutes
- Fix the issue where items were always re-synced regardless of prior successful sync
- Fix Shameless episodes (batch-marked as played in Emby with null LastPlayedDate) not syncing to Jellyfin
- User explicitly requested "total sync, no max limit" — remove all Limit parameters from played/resumable queries

**Partnership — Shared Services Stack (in progress):**
- Add a `PARTNERSHIP_SERVICES_STACK` to the partnership system, alongside existing auth and arr stacks
- Services: Emby, Jellyfin (my-jellyfin.xml), Seerr (my-Seerr.xml), SeerrFin (my-SeerrFin.xml)
- User confirmed "jellyfin and seerr and seerrfin" when asked about ambiguous variants
- User said "we will update and add the shared services first. then when all is good we will discuss on how to move foreward" regarding broader onboarding refactor
- User said "don't do anything yet" about moving Unraid-specific onboarding things to the adapter layer — that's a future discussion

2. Key Technical Concepts:
- Varaverk: self-healing two-server Unraid home media ecosystem (HOST1=unRAID-Gmer4Lfe, HOST2=unRAID-Jayred365)
- Three-file config model: `master.conf` (shared), `host1.conf`, `host2.conf` (per-host credentials)
- `detect_hosts()` in `common.sh` sets `MY_ID`/`REMOTE_ID` and aliases `HOST*_VAR` → `VAR` via `_alias_array`
- Partnership system: owner/mirror model with onboard/offboard/transfer/check lifecycle
- Emby/Jellyfin APIs: `Filters=IsPlayed`, `Filters=IsResumable`, `SortBy=DatePlayed`, `PlayedItems` endpoint, `/UserData` endpoint for ticks
- `MinDateLastSaved` in Emby = item metadata date (NOT user play state date) — was the root cause of re-syncing bug
- Null `LastPlayedDate` in Emby = items batch-marked as played (no date stamp) — sink to end of DatePlayed-sorted queries
- Unraid CA XML templates used for container deployment in partnership onboard
- `Plugin/unraid/Partnership/containers.sh` = platform adapter for container deployment
- `PLATFORM` variable routes to correct adapter layer (`Plugin/$PLATFORM/Partnership/containers.sh`)
- Sparse git checkout: each server only gets its own `host*.conf`

3. Files and Code Sections:

- **`/boot/config/plugins/varaverk/Media/play_state_sync.sh`**
  - Root cause 1: IsPlayed query used `MinDateLastSaved` (metadata date, not play date) + no sort + Limit=5000 → marked items didn't resurface in next query
  - Root cause 2: Resume skip condition was epoch-based, but posting ticks via `/UserData` never sets `LastPlayedDate` → epoch stays 0 forever
  - Root cause 3: 23,170 played items in Emby; with `SortBy=DatePlayed DESC + Limit=5000`, null-date batch-marked items (Shameless etc.) fell off position 5001+

  **Final state of the fetch section (lines ~304-325):**
  ```bash
  # All played items — no limit, covers both date-stamped and batch-marked (null date) entries
  _endpoint="Users/${_uid}/Items?Recursive=true&Fields=ProviderIds,UserData,Type,ParentIndexNumber,IndexNumber,SeriesName&IncludeItemTypes=${SYNC_TYPES}&Filters=IsPlayed&SortBy=DatePlayed&SortOrder=Descending"
  _resp=$(_api_get "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" "$_endpoint")
  if [[ -z "$_resp" ]]; then
      warn "  ${SRV_NAME[$_si]} — failed to fetch items for $lname"
      continue
  fi

  # Resume positions (not yet marked played)
  _endpoint2="Users/${_uid}/Items?Recursive=true&Fields=ProviderIds,UserData,Type,ParentIndexNumber,IndexNumber,SeriesName&IncludeItemTypes=${SYNC_TYPES}&SortBy=DatePlayed&SortOrder=Descending&Filters=IsResumable"
  _resp2=$(_api_get "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" "$_endpoint2")

  # Combine and deduplicate by Id
  [[ -z "$_resp2" ]] && _resp2='{"Items":[]}'
  _combined=$(printf '%s\n%s' "$_resp" "$_resp2" | jq -s \
      '[.[0].Items // [], .[1].Items // []] | add // [] | unique_by(.Id)' 2>/dev/null)
  ```

  **Final state of the skip condition (lines ~424-440):**
  ```bash
  _skip=false
  if [[ "$_auth_played" == "true" ]]; then
      # Played: skip if target is already marked played with same/newer date
      [[ "$_their_epoch" -ge "$_auth_epoch" && "$_their_played" == "true" ]] && _skip=true
  else
      # Resume only: skip if target already has same or more ticks
      # (posting ticks via /UserData doesn't set LastPlayedDate, so epoch comparison is useless here)
      [[ "${E_TICKS[$_si]:-0}" -ge "${_auth_ticks:-0}" && "$_their_played" == "false" ]] && _skip=true
  fi
  if [[ "$_skip" == true ]]; then
      log "  SKIP  $_pkey → ${SRV_NAME[$_si]} already up to date"
      (( TOTAL_SKIPPED++ ))
      continue
  fi
  ```

- **`/boot/config/plugins/varaverk/common.sh`** (lines 582-588)
  - `_alias_array` calls for all partnership arrays
  - **NEEDS EDIT**: Add `_alias_array "PARTNERSHIP_SERVICES_STACK"` and `_alias_array "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS"` after line 588 (`PARTNERSHIP_ARR_REPLACE_CONTAINERS`)
  - Current state:
  ```bash
  _alias_array "PARTNERSHIP_AUTH_WEBUIS"
  _alias_array "PARTNERSHIP_MIRROR_BACKUPS"
  _alias_array "PARTNERSHIP_OWN_CONTAINERS"
  _alias_array "PARTNERSHIP_AUTH_STACK"
  _alias_array "PARTNERSHIP_REPLACE_CONTAINERS"
  _alias_array "PARTNERSHIP_ARR_STACK"
  _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
  ```

- **`/boot/config/plugins/varaverk/Configurations/host1.conf`** (lines 162-183)
  - Has `HOST1_PARTNERSHIP_AUTH_STACK`, `HOST1_PARTNERSHIP_ARR_STACK`, `HOST1_PARTNERSHIP_REPLACE_CONTAINERS`, `HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS`
  - **NEEDS EDIT**: Add after `HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS` (after line 177):
  ```bash
  # XML templates pushed to mirror for shared services during onboard.
      HOST1_PARTNERSHIP_SERVICES_STACK=(
           "my-Emby.xml"
           "my-jellyfin.xml"
           "my-Seerr.xml"
           "my-SeerrFin.xml"
      )

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

- **`/boot/config/plugins/varaverk/Partnership/partnership_onboard.sh`**
  - Current 8-step owner path: SSH, stop auth, deploy auth, stop arr, deploy arr, partnership onboard, arr bootstrap, conf push
  - **NEEDS EDIT**:
    - Add `--skip-services-stack` flag parsing
    - Add tracking vars: `STEP_STOP_SERVICES_OK=true`, `STEP_SERVICES_OK=true`, `SERVICES_DEPLOYED=0`, `SERVICES_FAILED=0`
    - Add Step 6: Stop mirror services stack (using `stop_mirror_stack "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS" "services stack"`)
    - Add Step 7: Deploy services stack (using `deploy_xml_stack PARTNERSHIP_SERVICES_STACK`)
    - Renumber old Steps 6→8, 7→9, 8→10
    - Update header OWNER PATH from 8 steps to 10 steps
    - Update `--skip-services-stack` in RUNTIME MODES docs
    - Update summary to include services steps

- **`/boot/config/plugins/varaverk/Plugin/unraid/Partnership/containers.sh`**
  - `cleanup_deployed_stack_on_remote()` (lines 214-267): currently builds xml_names from AUTH_STACK + ARR_STACK only
  - `cleanup_deployed_stack_locally()` (lines 276-337): same, reads auth+arr from owner via SSH
  - **NEEDS EDIT** in `cleanup_deployed_stack_on_remote()`:
  ```bash
  [[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]]     && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
  [[ ${#PARTNERSHIP_ARR_STACK[@]}  -gt 0 ]]     && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
  [[ ${#PARTNERSHIP_SERVICES_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_SERVICES_STACK[@]}")
  ```
  - **NEEDS EDIT** in `cleanup_deployed_stack_locally()`: add third SSH read for PARTNERSHIP_SERVICES_STACK and include in xml_names

- **`/boot/config/plugins/varaverk/Partnership/partnership_offboard.sh`**
  - **NEEDS EDIT**: Header CONFIGURATION section — add `HOST*_PARTNERSHIP_SERVICES_STACK` docs alongside auth/arr stack docs

- **`/boot/config/plugins/varaverk/Partnership/partnership_manager.sh`**
  - **NEEDS EDIT**: Header CONFIGURATION section — mention PARTNERSHIP_SERVICES_STACK

4. Errors and fixes:

- **Re-syncing episodes bug**: Root cause was `MinDateLastSaved` in the Emby/Jellyfin `Items` API filtering by item *metadata* save date (not user play state date). Items just marked played via the `PlayedItems` endpoint don't update this metadata date, so they don't resurface in subsequent queries. Fixed by removing `_date_filter` entirely from both IsPlayed and IsResumable queries.

- **Resume item perpetual re-sync**: Posting `PlaybackPositionTicks` to `/UserData` endpoint doesn't set `LastPlayedDate`. The old skip condition compared epochs (`_their_epoch >= _auth_epoch`), but target's epoch stayed 0 forever after ticks were set. Fixed by splitting the skip condition: for resume-only items (auth_played=false), compare ticks instead of epoch.

- **Null-date items (Shameless) not syncing**: 23,170 played items in Emby. With `SortBy=DatePlayed DESC + Limit=5000`, batch-marked items with null LastPlayedDate sort to position 5001+ (null sorts last in DESC). Fix progression: (1) Added second ASC query with Limit=3000 to get null-date items first; (2) User requested "no max limit" so simplified to single no-limit query per endpoint.

5. Problem Solving:

- **Confirmed** Jellyfin TVDB IDs match Emby for Shameless (S9E6 = Tvdb=6821201 on both) — provider key matching was correct
- **Confirmed** null dates sort first in ascending order on Emby by querying directly
- **Confirmed** 23,170 total played items is why 5000-item limit was cutting off batch-marked content
- Play state sync now fetches ALL played items (no limit) sorted by DatePlayed DESC, plus ALL resumable items (no limit) — complete coverage

6. All user messages:

- [Pasted large log output of play state sync showing same episodes repeating]
- "play state has been syncing the same episodes?"
- "run it"
- "how far back are we going... i see shameless in jf didnt update even thjough ive watcdhed a bunch in emby"
- "lets allow total sync. no max limit"
- "up for a big task?"
- [Interrupted mid-sentence about shared services location]
- "shared service would go under [PARTNERSHIP section of host1.conf paste], then onboarding is initial connection to the mesh in the partnership connection. it connects the nodes, and installs the auth stack, arrs stack, and i need shared services stack. emby, jellyfin, seerr. etc"
- "so read the patnership folder, we will update and add the shared services first. then when all is good we will discuss on how to move foreward"
- "jellyfin and seerr and seerrfin"

7. Pending Tasks:

- **Add `PARTNERSHIP_SERVICES_STACK` to the partnership system** — this is the active task:
  1. Edit `common.sh`: add `_alias_array "PARTNERSHIP_SERVICES_STACK"` and `_alias_array "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS"` after existing arr aliases (line 588)
  2. Edit `host1.conf`: add `HOST1_PARTNERSHIP_SERVICES_STACK` (Emby, Jellyfin, Seerr, SeerrFin) and `HOST1_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS` after `HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS` (after line 177)
  3. Edit `partnership_onboard.sh`: add Steps 6+7 (stop/deploy services stack), renumber 6→8, 7→9, 8→10, add `--skip-services-stack` flag, update header and summary
  4. Edit `containers.sh`: add PARTNERSHIP_SERVICES_STACK to both `cleanup_deployed_stack_on_remote()` and `cleanup_deployed_stack_locally()`
  5. Edit `partnership_offboard.sh`: update header docs to mention services stack
  6. Edit `partnership_manager.sh`: update header docs to mention services stack

- **Future (deferred)**: Discuss moving Unraid-specific onboarding things to the adapter layer — user said "don't do anything yet" and wants to "discuss on how to move forward" after shared services is done

8. Current Work:

Immediately before the summary request, I had:
- Identified all files needing changes for the PARTNERSHIP_SERVICES_STACK addition
- Read `common.sh` lines 575-591 to confirm `_alias_array` calls need updating
- Read `host1.conf` lines 162-183 to find the exact insertion point for `HOST1_PARTNERSHIP_SERVICES_STACK` (after `HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS` at line 177)
- Was about to begin making all the edits across the 6 files

The services stack confirmed by user: `my-Emby.xml` (Emby), `my-jellyfin.xml` (Jellyfin), `my-Seerr.xml` (Seerr), `my-SeerrFin.xml` (SeerrFin)

9. Optional Next Step:

Begin making all the edits for the `PARTNERSHIP_SERVICES_STACK` addition. Starting with `common.sh` to add the alias registrations, then `host1.conf` for the new config array, then `partnership_onboard.sh` for the new steps, then `containers.sh` for cleanup function updates, then header doc updates in offboard and manager scripts.

Direct quote from user confirming the task: "jellyfin and seerr and seerrfin" (confirming which services go in the stack, after being asked about the ambiguous variants). The overall task was established with: "we will update and add the shared services first. then when all is good we will discuss on how to move foreward"

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /root/.claude/projects/-root/685e6c5b-62c1-40bd-9cfd-2c9f7e15c50f.jsonl
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.


━━━ 🔄 Play State Sync — 2026-06-13 23:51:52 ━━━
⚠  [WARN]  Emby-Jayred365 (unRAID-Jayred365) — unreachable, skipping
⚠  [WARN]  Jellyfin (unRAID-Jayred365) — unreachable, skipping

── User: gmer4lfe (2 server(s)) ──
🏁  [OK]      ✓  tvdb:ep:11818886:s18e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:8083279:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1014s)
🏁  [OK]      ✓  tvdb:ep:11759792:s9e2 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:82592:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (938s)
🏁  [OK]      ✓  tvdb:ep:5292348:s1e7 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1084s)
🏁  [OK]      ✓  tvdb:ep:10355686:s9e6 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:10355684:s9e7 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:7547570:s1e1 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:10724843:s16e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1446s)
🏁  [OK]      ✓  tvdb:ep:226931:s1e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (46s)
🏁  [OK]      ✓  tvdb:ep:11320854:s4e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (2345s)
🏁  [OK]      ✓  tvdb:ep:3279811:s1e2 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:10693981:s2e3 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  imdb:tt14142060 → Jellyfin (unRAID-Gmer4Lfe) resume synced (5601s)
🏁  [OK]      ✓  tvdb:ep:364205:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (880s)
🏁  [OK]      ✓  tvdb:ep:5553405:s4e13 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:8464434:s1e4 → Jellyfin (unRAID-Gmer4Lfe) resume synced (818s)
🏁  [OK]      ✓  tvdb:ep:10635743:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (192s)
🏁  [OK]      ✓  tvdb:ep:11759791:s9e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:7243045:s10e11 → Jellyfin (unRAID-Gmer4Lfe) resume synced (397s)
🏁  [OK]      ✓  tvdb:ep:5618559:s7e2 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:5648795:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (203s)
🏁  [OK]      ✓  tvdb:ep:167594:s5e4 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:179542:s1e3 → Jellyfin (unRAID-Gmer4Lfe) resume synced (608s)
🏁  [OK]      ✓  tvdb:ep:11766619:s10e12 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:5704192:s1e3 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1532s)
🏁  [OK]      ✓  imdb:tt1294226 → Jellyfin (unRAID-Gmer4Lfe) resume synced (146s)
🏁  [OK]      ✓  tvdb:ep:6324409:s2e7 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1186s)
🏁  [OK]      ✓  tvdb:ep:10941539:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (78s)
🏁  [OK]      ✓  imdb:tt0437863 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:1555111:s1e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  imdb:tt0384537 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1356s)
🏁  [OK]      ✓  tvdb:ep:893201:s1e5 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:4383244:s1e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (639s)
🏁  [OK]      ✓  tvdb:ep:4412085:s1e4 → Jellyfin (unRAID-Gmer4Lfe) resume synced (99s)
🏁  [OK]      ✓  tvdb:ep:9387051:s1e22 → Jellyfin (unRAID-Gmer4Lfe) resume synced (381s)
🏁  [OK]      ✓  tvdb:ep:213767:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (840s)
🏁  [OK]      ✓  imdb:tt8750122 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:11493564:s8e11 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:226930:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (52s)
🏁  [OK]      ✓  tvdb:ep:11426444:s1e3 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:962071:s1e9 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1081s)
🏁  [OK]      ✓  tvdb:ep:2861921:s1e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:307647:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (67s)
🏁  [OK]      ✓  tvdb:ep:7055604:s1e3 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1325s)
🏁  [OK]      ✓  tvdb:ep:10978840:s1e13 → Jellyfin (unRAID-Gmer4Lfe) resume synced (85s)

── User: failedproxy (2 server(s)) ──
🏁  [OK]      ✓  tvdb:ep:278848:s1e9 → Jellyfin (unRAID-Gmer4Lfe) resume synced (72s)
🏁  [OK]      ✓  imdb:tt19847976 → Jellyfin (unRAID-Gmer4Lfe) resume synced (3959s)
🏁  [OK]      ✓  tvdb:ep:4220094:s2e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1094s)
🏁  [OK]      ✓  imdb:tt40792117 → Jellyfin (unRAID-Gmer4Lfe) resume synced (146s)
🏁  [OK]      ✓  tvdb:ep:5775445:s8e9 → Jellyfin (unRAID-Gmer4Lfe) resume synced (555s)
🏁  [OK]      ✓  tvdb:ep:5775460:s8e14 → Jellyfin (unRAID-Gmer4Lfe) resume synced (39s)
🏁  [OK]      ✓  tvdb:ep:9087564:s4e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1185s)

━━━━━ 📋 PLAY STATE SYNC SUMMARY ━━━━━
⏱  Duration:  7m53s
🏁  Synced:    53
🏁  [OK]    Done ✅


━━━ 🔄 Play State Sync — 2026-06-13 23:29:15 ━━━
⚠  [WARN]  Emby-Jayred365 (unRAID-Jayred365) — unreachable, skipping
⚠  [WARN]  Jellyfin (unRAID-Jayred365) — unreachable, skipping

── User: gmer4lfe (2 server(s)) ──
🏁  [OK]      ✓  tvdb:ep:5618559:s7e2 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:5648795:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (203s)
🏁  [OK]      ✓  tvdb:ep:10724843:s16e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1446s)
🏁  [OK]      ✓  tvdb:ep:167594:s5e4 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:9387051:s1e22 → Jellyfin (unRAID-Gmer4Lfe) resume synced (381s)
🏁  [OK]      ✓  tvdb:ep:8083279:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1014s)
🏁  [OK]      ✓  tvdb:ep:11818886:s18e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:226931:s1e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (46s)
🏁  [OK]      ✓  tvdb:ep:179542:s1e3 → Jellyfin (unRAID-Gmer4Lfe) resume synced (608s)
🏁  [OK]      ✓  tvdb:ep:11759792:s9e2 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:213767:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (840s)
🏁  [OK]      ✓  tvdb:ep:11766619:s10e12 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:5704192:s1e3 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1532s)
🏁  [OK]      ✓  imdb:tt8750122 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:11320854:s4e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (2345s)
🏁  [OK]      ✓  tvdb:ep:3279811:s1e2 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:11493564:s8e11 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  imdb:tt1294226 → Jellyfin (unRAID-Gmer4Lfe) resume synced (146s)
🏁  [OK]      ✓  tvdb:ep:226930:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (52s)
🏁  [OK]      ✓  tvdb:ep:82592:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (938s)
🏁  [OK]      ✓  tvdb:ep:10693981:s2e3 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:10693998:s2e10 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:11426444:s1e3 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  imdb:tt14142060 → Jellyfin (unRAID-Gmer4Lfe) resume synced (5601s)
🏁  [OK]      ✓  tvdb:ep:6324409:s2e7 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1186s)
🏁  [OK]      ✓  tvdb:ep:5292348:s1e7 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1084s)
🏁  [OK]      ✓  tvdb:ep:10355686:s9e6 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:10355684:s9e7 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:10941539:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (78s)
🏁  [OK]      ✓  tvdb:ep:364205:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (880s)
🏁  [OK]      ✓  imdb:tt0437863 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:7547570:s1e1 → Jellyfin (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:1555111:s1e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  imdb:tt0384537 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1356s)
🏁  [OK]      ✓  tvdb:ep:5553405:s4e13 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:8464434:s1e4 → Jellyfin (unRAID-Gmer4Lfe) resume synced (818s)
🏁  [OK]      ✓  tvdb:ep:962071:s1e9 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1081s)
🏁  [OK]      ✓  tvdb:ep:893201:s1e5 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:2861921:s1e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:307647:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (67s)
🏁  [OK]      ✓  tvdb:ep:7055604:s1e3 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1325s)
🏁  [OK]      ✓  tvdb:ep:10635743:s1e1 → Jellyfin (unRAID-Gmer4Lfe) resume synced (192s)
🏁  [OK]      ✓  tvdb:ep:10978840:s1e13 → Jellyfin (unRAID-Gmer4Lfe) resume synced (85s)
🏁  [OK]      ✓  tvdb:ep:4383244:s1e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (639s)
🏁  [OK]      ✓  tvdb:ep:4412085:s1e4 → Jellyfin (unRAID-Gmer4Lfe) resume synced (99s)
🏁  [OK]      ✓  tvdb:ep:11759791:s9e1 → Emby (unRAID-Gmer4Lfe) marked played
🏁  [OK]      ✓  tvdb:ep:7243045:s10e11 → Jellyfin (unRAID-Gmer4Lfe) resume synced (397s)

── User: failedproxy (2 server(s)) ──
🏁  [OK]      ✓  tvdb:ep:278848:s1e9 → Jellyfin (unRAID-Gmer4Lfe) resume synced (72s)
🏁  [OK]      ✓  imdb:tt19847976 → Jellyfin (unRAID-Gmer4Lfe) resume synced (3959s)
🏁  [OK]      ✓  tvdb:ep:4220094:s2e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1094s)
🏁  [OK]      ✓  imdb:tt40792117 → Jellyfin (unRAID-Gmer4Lfe) resume synced (146s)
🏁  [OK]      ✓  tvdb:ep:5775445:s8e9 → Jellyfin (unRAID-Gmer4Lfe) resume synced (555s)
🏁  [OK]      ✓  tvdb:ep:5775460:s8e14 → Jellyfin (unRAID-Gmer4Lfe) resume synced (39s)
🏁  [OK]      ✓  tvdb:ep:9087564:s4e2 → Jellyfin (unRAID-Gmer4Lfe) resume synced (1185s)

━━━━━ 📋 PLAY STATE SYNC SUMMARY ━━━━━
⏱  Duration:  6m35s
🏁  Synced:    54
🏁  [OK]    Done ✅


Now `common.sh` — need to see current state of the alias block:


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

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

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


Monitor media server sync and backup health


Edit

    _alias_array "PARTNERSHIP_ARR_STACK"
    _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
    _alias_array "RW_PAUSE_CONTAINERS"

    _alias_array "PARTNERSHIP_ARR_STACK"
    _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
    _alias_array "PARTNERSHIP_SERVICES_STACK"
    _alias_array "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS"
    _alias_array "RW_PAUSE_CONTAINERS"


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

    _alias_array "PARTNERSHIP_ARR_STACK"
    _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
    _alias_array "RW_PAUSE_CONTAINERS"

    _alias_array "PARTNERSHIP_ARR_STACK"
    _alias_array "PARTNERSHIP_ARR_REPLACE_CONTAINERS"
    _alias_array "PARTNERSHIP_SERVICES_STACK"
    _alias_array "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS"
    _alias_array "RW_PAUSE_CONTAINERS"

+    _alias_array "PARTNERSHIP_SERVICES_STACK"

+    _alias_array "PARTNERSHIP_SERVICES_REPLACE_CONTAINERS"


Now `partnership_onboard.sh` — read the relevant sections first:


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 (8 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: Partnership onboard  — configure WebUIs → owner IP, write state, Emby
29	#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
30	#   Step 8: Conf push            — push master.conf + setup state to all listed hosts
31	#
32	# ==============================================================================================
33	# DESIGN PRINCIPLES
34	# ==============================================================================================
35	#
36	# Credentials never in SSH command strings
37	#   Auth stack containers hold API keys, DB passwords, etc. The deploy script is written
38	#   locally, SCPed to the remote, and executed there. Command-line args are never used
39	#   to pass credentials — they'd appear in `ps` output and shell history on both servers.
40	#
41	# XML templates are the single source of truth for deployed containers
42	#   The owner's templates-user/ XMLs define every container deployed on the mirror.
43	#   The same XMLs that Unraid's Docker Manager uses are what get SCPed — the mirror's
44	#   Docker Manager can manage the containers after onboard without additional config.
45	#
46	# Dependency ordering in the auth stack is owner-enforced
47	#   PARTNERSHIP_AUTH_STACK order matters: Mariadb and Redis must come before Authelia.
48	#   The array is ordered correctly in host1.conf. After each Mariadb/Redis deploy,
49	#   the script waits for the container to be healthy before continuing. This is a remote
50	#   health check — the container must be running (or report healthy) before the next
51	#   dependent is deployed.
52	#
53	# ==============================================================================================
54	# OPERATIONAL SAFEGUARDS
55	# ==============================================================================================
56	#
57	# Root check
58	#   All operations run as root — SSH key management, docker operations, conf updates.
59	#
60	# SSH timeout on all remote calls
61	#   Every ssh/scp call uses SSH_TIMEOUT. No operation hangs indefinitely on a
62	#   slow or unreachable mirror.
63	#
64	# --dry-run shows exact actions without executing
65	#   Every step prints what it would do. SCP, deploy, plugin install, arr sync —
66	#   all dry-run safe.
67	#
68	# Step skip flags for partial re-runs
69	#   --skip-ssh, --skip-auth-stack, --skip-arr-stack, --skip-arr-sync allow
70	#   resuming after a partial failure without re-running completed steps.
71	#
72	# ==============================================================================================
73	# CONFIGURATION
74	# ==============================================================================================
75	#
76	# host*.conf
77	#
78	#   HOST*_PARTNERSHIP_AUTH_STACK
79	#       XML filenames (from this server's templates-user/) to push and deploy on the
80	#       mirror as its auth stack. Order matters: database deps before Authelia.

#!/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 (8 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: Partnership onboard  — configure WebUIs → owner IP, write state, Emby
#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 8: Conf push            — push master.conf + setup state to all listed hosts
#
# ==============================================================================================
# 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.


80	#       mirror as its auth stack. Order matters: database deps before Authelia.
81	#       Aliased by detect_hosts() → PARTNERSHIP_AUTH_STACK
82	#
83	#   HOST*_PARTNERSHIP_REPLACE_CONTAINERS
84	#       Containers to stop on the mirror before deploying the auth stack.
85	#       Defined in the MIRROR's own conf (host*.conf on HOST2) — never in HOST1's conf.
86	#       Read live from the mirror via SSH during Step 3 (sources mirror's load_config.sh at
87	#       the same $SCRIPTS_ROOT path — convention: both servers use the same repo location).
88	#       Leave empty on HOST2 if no conflicting containers exist (fresh mirror: nothing to stop).
89	#       Aliased by detect_hosts() → PARTNERSHIP_REPLACE_CONTAINERS (on the mirror)
90	#
91	#   HOST*_PARTNERSHIP_ARR_STACK
92	#       XML filenames to push and deploy on the mirror as its arr stack.
93	#       Leave empty to skip arr stack deploy.
94	#       Aliased by detect_hosts() → PARTNERSHIP_ARR_STACK
95	#
96	#   HOST*_PARTNERSHIP_ARR_REPLACE_CONTAINERS
97	#       Arr containers to stop on the mirror before deploying the arr stack.
98	#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
99	#       Aliased by detect_hosts() → PARTNERSHIP_ARR_REPLACE_CONTAINERS (on the mirror)
100	#
101	# ==============================================================================================
102	# RUNTIME MODES
103	# ==============================================================================================
104	#
105	# Partnership/partnership_onboard.sh
106	#     Full onboard — role detected automatically
107	#
108	# Partnership/partnership_onboard.sh --dry-run
109	#     Preview all steps without making changes
110	#
111	# Partnership/partnership_onboard.sh --log
112	#     Verbose per-step output
113	#
114	# Partnership/partnership_onboard.sh --skip-ssh
115	#     Skip SSH key setup (key already in place)
116	#
117	# Partnership/partnership_onboard.sh --skip-auth-stack
118	#     Skip auth stack stop + deploy (Steps 3-4)
119	#
120	# Partnership/partnership_onboard.sh --skip-arr-stack
121	#     Skip arr stack stop + deploy (Steps 5-6)
122	#
123	# Partnership/partnership_onboard.sh --skip-arr-sync
124	#     Skip arr library bootstrap (Step 8)
125	#
126	# Partnership/partnership_onboard.sh --phase1-only
127	#     OWNER only: SSH key exchange + conf push. Safe to run before HOST2 has Varaverk.
128	#     Writes HOST2_PHASE1_DONE=true to varaverk_setup.db.
129	#
130	# Partnership/partnership_onboard.sh --phase2-only
131	#     OWNER only: container deploy + arr + onboard (skips SSH). Triggered automatically
132	#     by HOST2 after it completes its Mirror-path onboard. Can also be run manually.
133	#     Writes HOST2_PHASE2_DONE=true to varaverk_setup.db.
134	#
135	# ==============================================================================================
136
137	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
138	SCRIPTS_ROOT="$SCRIPT_DIR/.."
139	SSH_TIMEOUT=15
140
141	source "$SCRIPTS_ROOT/load_config.sh"
142	source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
143
144	# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
145	SKIP_SSH=false
146	SKIP_AUTH_STACK=false
147	SKIP_ARR_STACK=false
148	SKIP_ARR_SYNC=false
149	PHASE1_ONLY=false   # OWNER: SSH + conf push only (HOST2 not yet installed)
150	PHASE2_ONLY=false   # OWNER: containers/arr/onboard only (triggered by HOST2 after it onboards)
151	FILTERED_ARGS=()
152
153	for arg in "$@"; do
154	    case "$arg" in
155	        --skip-ssh)        SKIP_SSH=true ;;
156	        --skip-auth-stack) SKIP_AUTH_STACK=true ;;
157	        --skip-arr-stack)  SKIP_ARR_STACK=true ;;
158	        --skip-arr-sync)   SKIP_ARR_SYNC=true ;;
159	        --phase1-only)     PHASE1_ONLY=true ;;

#       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)
#
# ==============================================================================================
# 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 5-6)
#
# Partnership/partnership_onboard.sh --skip-arr-sync
#     Skip arr library bootstrap (Step 8)
#
# 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_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-arr-sync)   SKIP_ARR_SYNC=true ;;
        --phase1-only)     PHASE1_ONLY=true ;;


159	        --phase1-only)     PHASE1_ONLY=true ;;
160	        --phase2-only)     PHASE2_ONLY=true; SKIP_SSH=true ;;
161	        *)                 FILTERED_ARGS+=("$arg") ;;
162	    esac
163	done
164
165	parse_args "${FILTERED_ARGS[@]}"
166
167	# ==============================================================================================
168	# ━━━ Setup ━━━
169	# ==============================================================================================
170	[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
171
172	acquire_lock
173
174	if ! command -v docker &>/dev/null; then
175	    error "Docker command not found"
176	    exit 1
177	fi
178
179	detect_hosts
180
181	OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
182	MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
183	OWNER="${!OWNER_ID}"
184	MIRROR="${!MIRROR_ID}"
185	# SSH_KEY (set by detect_hosts) is this server's own private key.
186	# The remote accepts it because this server's PUBLIC key was installed there via ssh_setup.sh.
187	# HOST{N}_SSH_KEY lives in host{N}.conf — with sparse checkout, the other server's
188	# conf is never present here. Always use SSH_KEY (local private key) for outbound SSH.
189	MIRROR_SSH_KEY="$SSH_KEY"
190
191	AM_OWNER=false
192	AM_MIRROR=false
193	[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
194	[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
195
196	EXTRA_FLAGS=()
197	[[ "$DRY_RUN"  == true ]] && EXTRA_FLAGS+=("--dry-run")
198	[[ "$LOG_MODE" == true ]] && EXTRA_FLAGS+=("--log")
199
200	START=$(date +%s)
201
202	# ── Helper: write phase completion flag to setup.db + push to remotes ─────────────────────────
203	write_onboard_phase() {
204	    local target_id="$1" phase="$2"
205	    local key="${target_id}_PHASE${phase}_DONE"
206	    local state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
207	    [[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would write ${key}=true"; return 0; }
208	    if grep -q "^${key}=" "$state_file" 2>/dev/null; then
209	        sed -i "s|^${key}=.*|${key}=true|" "$state_file"
210	    else
211	        echo "${key}=true" >> "$state_file"
212	    fi
213	    command -v php &>/dev/null && \
214	        php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null || true
215	}
216
217	echo ""
218	echo "━━━ $ICON_FALLBACK Partnership Onboard — $MY_ID ($LOCAL_SERVER_NAME) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
219	echo ""
220	echo "  Role:    $( [[ "$AM_OWNER" == true ]] && echo "OWNER" || echo "MIRROR" )"
221	echo "  This:    $MY_ID ($LOCAL_SERVER_NAME)"
222	echo "  Partner: $( [[ "$AM_OWNER" == true ]] && echo "$MIRROR_ID ($MIRROR)" || echo "$OWNER_ID ($OWNER)" )"
223	echo ""
224	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
225
226	# ==============================================================================================
227	# ── HELPER: stop containers on the mirror by reading its own conf via SSH ────────────────────
228	#
229	# SSHes to the mirror, sources its load_config.sh at the same $SCRIPTS_ROOT path (both servers
230	# use the same convention), and reads the named config array from the mirror's own conf.
231	# HOST2's container list stays in HOST2's host2.conf — not duplicated in HOST1's conf.
232	# Fails gracefully if scripts aren't present yet or the array is empty (nothing to stop).
233	#
234	# deploy_container_from_xml() already stops/removes containers with the same name as what's
235	# being deployed. This step handles containers with DIFFERENT names that conflict.
236	# ==============================================================================================
237	stop_mirror_stack() {
238	    local config_var="$1" label="$2"

        --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="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
    [[ "$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
    command -v php &>/dev/null && \
        php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null || true
}

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"


238	    local config_var="$1" label="$2"
239	    local -a to_stop=()
240
241	    mapfile -t to_stop < <(
242	        timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
243	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
244	            "source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
245	             detect_hosts 2>/dev/null
246	             printf '%s\n' \"\${${config_var}[@]:-}\"" 2>/dev/null | grep -v '^$'
247	    )
248
249	    if [[ ${#to_stop[@]} -eq 0 ]]; then
250	        log "No $label containers to stop on $MIRROR — skipping"
251	        return 0
252	    fi
253
254	    log "Stopping $label on $MIRROR: ${to_stop[*]}"
255	    for container in "${to_stop[@]}"; do
256	        if [[ "$DRY_RUN" == true ]]; then
257	            warn "  DRY RUN — would stop + rm $container on $MIRROR"
258	            continue
259	        fi
260	        timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
261	            -o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
262	            "docker stop '$container' 2>/dev/null
263	             docker rm   '$container' 2>/dev/null && echo removed" 2>/dev/null | \
264	            grep -q removed && \
265	            log "  $container removed ✅" || \
266	            log "  $container not found on $MIRROR — skipping"
267	    done
268	}
269
270	# ==============================================================================================
271	# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
272	# ==============================================================================================
273	if [[ "$AM_MIRROR" == true ]]; then
274	    echo "━━━ Step 1/2 — SSH Key Setup (Mirror) ━━━"
275	    echo ""
276	    echo "  Mirror sets up SSH keys, then notifies Owner to run Phase 2."
277	    echo ""
278
279	    if [[ "$SKIP_SSH" == true ]]; then
280	        warn "Skipping SSH setup (--skip-ssh)"
281	    elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
282	        log "SSH key ready ✅"
283	    else
284	        error "SSH key setup failed"
285	        exit 1
286	    fi
287
288	    echo ""
289	    echo "━━━ Step 2/2 — Notify Owner to Run Phase 2 ━━━"
290	    echo ""
291
292	    OWNER_IP=$(resolve_tailscale_ip "$OWNER" 2>/dev/null || true)
293	    PHASE2_TRIGGERED=false
294
295	    if [[ -n "$OWNER_IP" ]]; then
296	        # Read OWNER's SCRIPTS_DIR from their varaverk.cfg — don't assume same path as mirror
297	        OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
298	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
299	            'grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null | cut -d= -f2 | tr -d "\"'"'"'" 2>/dev/null' 2>/dev/null | tr -d '[:space:]')
300	        OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-/boot/config/plugins/varaverk}"
301
302	        if [[ "$DRY_RUN" == true ]]; then
303	            warn "DRY RUN — would SSH to $OWNER ($OWNER_IP) and trigger Phase 2"
304	            PHASE2_TRIGGERED=true
305	        elif timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
306	            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
307	            "nohup bash '${OWNER_SCRIPTS_DIR}/Partnership/partnership_onboard.sh' --phase2-only > /tmp/vv_phase2_onboard.log 2>&1 & echo triggered" \
308	            2>/dev/null | grep -q triggered; then
309	            log "Phase 2 triggered on $OWNER ✅"
310	            log "Watch progress on $OWNER: tail -f /tmp/vv_phase2_onboard.log"
311	            PHASE2_TRIGGERED=true
312	        else
313	            warn "Could not auto-trigger Phase 2 on $OWNER"
314	        fi
315	    else
316	        warn "Cannot resolve $OWNER Tailscale IP"
317	    fi
318
319	    echo ""
320	    echo "━━━━━ $ICON_SUMMARY MIRROR SETUP COMPLETE ━━━━━"
321	    echo "  SSH key:        ready"
322	    echo "  Phase 2 on $OWNER: $( [[ "$PHASE2_TRIGGERED" == true ]] && echo "triggered ✅" || echo "needs manual trigger ⚠" )"
323	    if [[ "$PHASE2_TRIGGERED" == false ]]; then
324	        echo ""
325	        echo "  Run manually on $OWNER:"
326	        echo "    bash Partnership/partnership_onboard.sh --phase2-only"
327	    fi
328	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
329	    exit 0
330	fi
331
332	# ==============================================================================================
333	# ── OWNER PATH ────────────────────────────────────────────────────────────────────────────────
334	# ==============================================================================================
335
336	MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
337	[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; }
338	log "Mirror: $MIRROR ($MIRROR_IP)"
339	[[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)"
340	[[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)"
341	echo ""
342
343	STEP_SSH_OK=false
344	STEP_STOP_AUTH_OK=true
345	STEP_AUTH_OK=true
346	AUTH_DEPLOYED=0
347	AUTH_FAILED=0
348	STEP_STOP_ARR_OK=true
349	STEP_ARR_OK=true
350	ARR_DEPLOYED=0
351	ARR_FAILED=0
352	ONBOARD_OK=false
353	ARR_SYNC_OK=false
354	MASTER_PUSH_OK=false
355
356	# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
357	# Skipped when --phase2-only (SSH was already done in Phase 1).

    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 from their varaverk.cfg — don't assume same path as mirror
        OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
            'grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null | cut -d= -f2 | tr -d "\"'"'"'" 2>/dev/null' 2>/dev/null | tr -d '[:space:]')
        OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-/boot/config/plugins/varaverk}"

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


355
356	# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
357	# Skipped when --phase2-only (SSH was already done in Phase 1).
358	echo "━━━ Step 1 — SSH Key Setup ━━━"
359
360	if [[ "$SKIP_SSH" == true ]]; then
361	    warn "Skipping (--skip-ssh)"
362	    STEP_SSH_OK=true
363	elif [[ "$PHASE1_ONLY" == true ]]; then
364	    # Phase 1 in background: test if SSH already works first — avoids ssh-copy-id
365	    # hanging for a password prompt with no TTY.
366	    if timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
367	        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" exit 0 2>/dev/null; then
368	        log "SSH to $MIRROR already works ✅ — skipping key install"
369	        STEP_SSH_OK=true
370	    else
371	        # Key not yet on HOST2 — try ssh_setup.sh (works interactively, may fail in background)
372	        if bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
373	            log "SSH keys ready ✅"
374	            STEP_SSH_OK=true
375	        else
376	            # Soft-fail: generate key locally if not present, then tell user to install manually
377	            warn "Could not install key on $MIRROR automatically (no terminal for password prompt)"
378	            if [[ -f "$SSH_KEY" ]]; then
379	                log "Local key exists at: $SSH_KEY"
380	            else
381	                bash "$SCRIPT_DIR/ssh_setup.sh" --key-only "${EXTRA_FLAGS[@]}" 2>/dev/null || true
382	            fi
383	            if [[ -f "${SSH_KEY}.pub" ]]; then
384	                echo ""
385	                echo "  Install this key on $MIRROR to complete SSH setup:"
386	                echo "  ┌─────────────────────────────────────────────────────"
387	                cat "${SSH_KEY}.pub" | sed 's/^/  │ /'
388	                echo "  └─────────────────────────────────────────────────────"
389	                echo "  Run on a terminal: ssh-copy-id -i ${SSH_KEY}.pub root@${MIRROR_IP}"
390	                echo "  Then click 'Push Conf' in the Partnership tab."
391	                # Write key-ready flag so UI can show the manual-install state
392	                [[ "$DRY_RUN" == false ]] && {
393	                    local kflag="${MIRROR_ID}_KEY_READY"
394	                    local _setup_f="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
395	                    grep -q "^${kflag}=" "$_setup_f" 2>/dev/null \
396	                        && sed -i "s|^${kflag}=.*|${kflag}=true|" "$_setup_f" \
397	                        || echo "${kflag}=true" >> "$_setup_f"
398	                }
399	            fi
400	            STEP_SSH_OK=false
401	        fi
402	    fi
403	elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
404	    log "SSH keys ready ✅"
405	    STEP_SSH_OK=true
406	else
407	    error "SSH key setup failed — aborting"
408	    error "Re-run or use --skip-ssh if key is already set up"
409	    exit 1
410	fi
411
412	# ── Phase 1 exit point ────────────────────────────────────────────────────────────────────────
413	# --phase1-only: SSH + conf push is all HOST1 needs to do before HOST2 installs Varaverk.
414	# HOST2's wizard will detect the pushed master.conf + state file and take the correct path.
415	if [[ "$PHASE1_ONLY" == true ]]; then
416	    if [[ "$STEP_SSH_OK" == false ]]; then
417	        # SSH key not yet installed on HOST2 — can't push conf, but local setup still runs.
418	        # UI will show "key ready, install manually" state via HOST2_KEY_READY flag.
419	        echo ""
420	        echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"
421	        bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
422	            warn "Local setup had issues — check partnership_manager.sh output above"
423
424	        END=$(date +%s)
425	        echo ""
426	        echo "━━━━━ $ICON_SUMMARY PHASE 1 — SSH PENDING ━━━━━"
427	        echo "  SSH keys:    key generated ✅ — NOT yet installed on $MIRROR ⚠"
428	        echo "  Conf push:   skipped (needs SSH access to $MIRROR)"
429	        echo "  HOST1 setup: done ✅"
430	        echo "  Duration:    $(format_duration $(( END - START )))"
431	        echo ""
432	        echo "  ACTION NEEDED: install the key on $MIRROR:"
433	        echo "    ssh-copy-id -i ${SSH_KEY}.pub root@${MIRROR_IP}"
434	        echo "  Then click 'Push Conf' in Partnership tab, or run:"
435	        echo "    bash Partnership/partnership_onboard.sh --phase1-only --skip-ssh"
436	        echo "━━━━━━━━━━━━━━━━━━━━━━━"
437	        exit 0
438	    fi
439
440	    echo ""
441	    echo "━━━ Phase 1 — Conf Push ━━━"
442
443	    CONF_PUSH_OK=false
444	    if [[ "$DRY_RUN" == true ]]; then
445	        warn "DRY RUN — would push master.conf + state file to $MIRROR"
446	        CONF_PUSH_OK=true
447	    elif ! command -v php &>/dev/null; then
448	        warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
449	    else
450	        push_output=$(php -r "
451	require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
452	\$results = vv_push_master_conf();
453	vv_push_setup_state();
454	if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
455	\$failed = 0;
456	foreach (\$results as \$r) {
457	    echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
458	    if (!\$r['ok']) \$failed++;
459	}
460	exit(\$failed > 0 ? 1 : 0);
461	" 2>/dev/null)
462	        push_rc=$?
463	        echo "$push_output"
464	        if [[ $push_rc -eq 0 ]]; then
465	            log "Conf push complete ✅"
466	            CONF_PUSH_OK=true
467	        else
468	            warn "Conf push had failures — retry via Scheduler → master.conf → Save Conf"
469	        fi
470	    fi
471
472	    # HOST1 local setup — runs immediately without needing HOST2
473	    echo ""
474	    echo "━━━ Phase 1 — HOST1 Local Setup ━━━"
475	    bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
476	        warn "Local setup had issues — check partnership_manager.sh output above"
477
478	    [[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1
479
480	    END=$(date +%s)
481	    echo ""
482	    echo "━━━━━ $ICON_SUMMARY PHASE 1 COMPLETE ━━━━━"
483	    echo "  SSH keys:     $( [[ "$STEP_SSH_OK" == true ]] && echo "ready ✅" || echo "skipped" )"
484	    echo "  Conf push:    $( [[ "$CONF_PUSH_OK" == true ]] && echo "done ✅" || echo "⚠ manual needed" )"
485	    echo "  HOST1 setup:  done ✅"
486	    echo "  Duration:     $(format_duration $(( END - START )))"
487	    echo ""
488	    echo "  HOST1 is fully set up. HOST2 ($MIRROR) can now install the Varaverk plugin."
489	    echo "  The wizard will detect the pushed conf and take the correct path."
490	    echo "  When HOST2 completes its onboard, it will automatically trigger Phase 2 here."
491	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
492	    exit 0
493	fi
494
495	# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
496	echo ""
497	echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"
498
499	if [[ "$SKIP_AUTH_STACK" == true ]]; then
500	    warn "Skipping (--skip-auth-stack)"
501	else
502	    stop_mirror_stack "PARTNERSHIP_REPLACE_CONTAINERS" "auth stack"
503	fi
504
505	# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
506	echo ""
507	echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"
508
509	if [[ "$SKIP_AUTH_STACK" == true ]]; then
510	    warn "Skipping (--skip-auth-stack)"
511	elif [[ ${#PARTNERSHIP_AUTH_STACK[@]} -eq 0 ]]; then
512	    warn "PARTNERSHIP_AUTH_STACK not set in ${MY_ID} conf — skipping auth stack deploy"
513	    warn "Add HOST${MY_ID: -1}_PARTNERSHIP_AUTH_STACK to host${MY_ID: -1}.conf"
514	    STEP_AUTH_OK=false
515	else
516	    deploy_xml_stack PARTNERSHIP_AUTH_STACK
517	    AUTH_DEPLOYED=$_STACK_DEPLOYED
518	    AUTH_FAILED=$_STACK_FAILED
519	    echo "Auth stack: $AUTH_DEPLOYED deployed, $AUTH_FAILED failed"
520	    [[ "$AUTH_FAILED" -gt 0 ]] && STEP_AUTH_OK=false
521	fi
522
523	# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
524	echo ""
525	echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"
526
527	if [[ "$SKIP_ARR_STACK" == true ]]; then
528	    warn "Skipping (--skip-arr-stack)"
529	elif [[ ${#PARTNERSHIP_ARR_STACK[@]} -eq 0 ]]; then
530	    log "PARTNERSHIP_ARR_STACK not configured — skipping arr stack deploy"
531	    SKIP_ARR_STACK=true
532	else
533	    stop_mirror_stack "PARTNERSHIP_ARR_REPLACE_CONTAINERS" "arr stack"
534	fi

# ── 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="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
                    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
    elif ! command -v php &>/dev/null; then
        warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
    else
        push_output=$(php -r "
require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
\$results = vv_push_master_conf();
vv_push_setup_state();
if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
\$failed = 0;
foreach (\$results as \$r) {
    echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
    if (!\$r['ok']) \$failed++;
}
exit(\$failed > 0 ? 1 : 0);
" 2>/dev/null)
        push_rc=$?
        echo "$push_output"
        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


534	fi
535
536	# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
537	echo ""
538	echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"
539
540	if [[ "$SKIP_ARR_STACK" == true ]]; then
541	    warn "Skipping (--skip-arr-stack)"
542	else
543	    deploy_xml_stack PARTNERSHIP_ARR_STACK
544	    ARR_DEPLOYED=$_STACK_DEPLOYED
545	    ARR_FAILED=$_STACK_FAILED
546	    echo "Arr stack: $ARR_DEPLOYED deployed, $ARR_FAILED failed"
547	    [[ "$ARR_FAILED" -gt 0 ]] && STEP_ARR_OK=false
548	fi
549
550	# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
551	echo ""
552	echo "━━━ Step 6 — Partnership Onboard ━━━"
553
554	if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
555	    echo "Partnership onboard complete ✅"
556	    ONBOARD_OK=true
557	else
558	    error "Partnership onboard failed"
559	    ONBOARD_OK=false
560	fi
561
562	# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
563	echo ""
564	echo "━━━ Step 7 — Arr Library Bootstrap ━━━"
565
566	if [[ "$ONBOARD_OK" == false ]]; then
567	    warn "Skipping — onboard did not complete"
568	elif [[ "$SKIP_ARR_SYNC" == true ]]; then
569	    warn "Skipping (--skip-arr-sync)"
570	elif [[ ! -f "$SCRIPTS_ROOT/Media/arr_sync.sh" ]]; then
571	    warn "arr_sync.sh not found — run Media/arr_sync.sh manually once arrs are live"
572	elif bash "$SCRIPTS_ROOT/Media/arr_sync.sh" "${EXTRA_FLAGS[@]}"; then
573	    echo "Arr bootstrap complete ✅"
574	    ARR_SYNC_OK=true
575	else
576	    warn "Arr sync had errors — partnership still valid"
577	    warn "Re-run Media/arr_sync.sh once all arr containers are live"
578	fi
579
580	# ── Step 9: Push master.conf to all listed hosts ──────────────────────────────────────────────
581	# SSH is now established and all partners have the plugin installed.
582	# Push the authoritative master.conf so every listed host is in sync immediately.
583	echo ""
584	echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"
585
586	if [[ "$ONBOARD_OK" == false ]]; then
587	    warn "Skipping — onboard did not complete"
588	elif [[ "$DRY_RUN" == true ]]; then
589	    warn "DRY RUN — would push master.conf to all listed hosts"
590	    MASTER_PUSH_OK=true
591	elif ! command -v php &>/dev/null; then
592	    warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
593	else
594	    push_output=$(php -r "
595	require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
596	\$results = vv_push_master_conf();
597	vv_push_setup_state();
598	if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
599	\$failed = 0;
600	foreach (\$results as \$r) {
601	    echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
602	    if (!\$r['ok']) \$failed++;
603	}
604	exit(\$failed > 0 ? 1 : 0);
605	" 2>/dev/null)
606	    push_rc=$?
607	    echo "$push_output"
608	    if [[ $push_rc -eq 0 ]]; then
609	        echo "master.conf sync complete ✅"
610	        MASTER_PUSH_OK=true
611	    else
612	        warn "master.conf push had failures — retry via Scheduler → master.conf → Save Conf"
613	    fi
614	fi
615
616	# ── Write Phase 2 completion state ────────────────────────────────────────────────────────────
617	[[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2
618
619	# ── Summary ───────────────────────────────────────────────────────────────────────────────────
620	END=$(date +%s)
621	echo ""
622	echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
623	echo "  Owner:    $MY_ID ($LOCAL_SERVER_NAME)"
624	echo "  Mirror:   $MIRROR ($MIRROR_IP)"
625	[[ "$PHASE2_ONLY" == true ]] && echo "  Mode:     Phase 2 (triggered by HOST2 notification)"
626	echo "  Duration: $(format_duration $(( END - START )))"
627	echo ""
628	_ok()   { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
629	_skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
630
631	echo "  Step 1 — SSH keys:      $(_skip "$SKIP_SSH"        "$STEP_SSH_OK")"
632	echo "  Step 2 — Stop auth:     $(_skip "$SKIP_AUTH_STACK"  "$STEP_STOP_AUTH_OK")"
633	echo "  Step 3 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"

fi

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

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

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

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 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
echo ""
echo "━━━ Step 7 — 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 9: Push master.conf to all listed hosts ──────────────────────────────────────────────
# SSH is now established and all partners have the plugin installed.
# Push the authoritative master.conf so every listed host is in sync immediately.
echo ""
echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"

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
elif ! command -v php &>/dev/null; then
    warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
else
    push_output=$(php -r "
require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
\$results = vv_push_master_conf();
vv_push_setup_state();
if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
\$failed = 0;
foreach (\$results as \$r) {
    echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
    if (!\$r['ok']) \$failed++;
}
exit(\$failed > 0 ? 1 : 0);
" 2>/dev/null)
    push_rc=$?
    echo "$push_output"
    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" )"


633	echo "  Step 3 — Auth stack:    $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
634	echo "  Step 4 — Stop arr:      $(_skip "$SKIP_ARR_STACK"   "$STEP_STOP_ARR_OK")"
635	echo "  Step 5 — Arr stack:     $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
636	echo "  Step 6 — Onboard:       $(_ok "$ONBOARD_OK")"
637	echo "  Step 7 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
638	echo "  Step 8 — Conf push:     $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
639	echo ""
640
641	if [[ "$ONBOARD_OK" == true ]]; then
642	    [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \
643	        echo "$ICON_DONE DONE — partnership established ✅"
644	    echo "Verify with: Partnership/partnership_manager.sh --status"
645	else
646	    error "Setup incomplete — resolve errors above and re-run"
647	fi
648	echo "━━━━━━━━━━━━━━━━━━━━━━━"
649	[[ "$ONBOARD_OK" == false ]] && exit 1
650	exit 0
651

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

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 I have the full picture. Making all the changes to `partnership_onboard.sh`:


# OWNER PATH (8 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: Partnership onboard  — configure WebUIs → owner IP, write state, Emby
#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 8: Conf push            — push master.conf + setup state to all listed hosts

# 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


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

# OWNER PATH (8 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: Partnership onboard  — configure WebUIs → owner IP, write state, Emby
#   Step 7: Arr bootstrap        — bidirectional library sync (arr_sync.sh)
#   Step 8: Conf push            — push master.conf + setup state to all listed hosts

# 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

#   Step 1: SSH key setup — generate keypair, copy to owner, update conf

#           Owner completes the rest remotely. Mirror is done.

-#   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: Partnership onboard  — configure WebUIs → owner IP, write state, Emby

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

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

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

+#   Step 2:  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


#   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)
#
# ==============================================================================================
# RUNTIME MODES

#   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


#   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)
#
# ==============================================================================================
# RUNTIME MODES

#   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

+#       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)


# Partnership/partnership_onboard.sh --skip-arr-stack
#     Skip arr stack stop + deploy (Steps 5-6)
#
# Partnership/partnership_onboard.sh --skip-arr-sync
#     Skip arr library bootstrap (Step 8)

# 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 --skip-arr-stack
#     Skip arr stack stop + deploy (Steps 5-6)
#
# Partnership/partnership_onboard.sh --skip-arr-sync
#     Skip arr library bootstrap (Step 8)

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

#     Skip auth stack stop + deploy (Steps 3-4)

-#     Skip arr stack stop + deploy (Steps 5-6)

+#     Skip arr stack stop + deploy (Steps 4-5)

+#     Skip services stack stop + deploy (Steps 6-7)

-#     Skip arr library bootstrap (Step 8)

+#     Skip arr library bootstrap (Step 9)

#     OWNER only: SSH key exchange + conf push. Safe to run before HOST2 has Varaverk.


SKIP_SSH=false
SKIP_AUTH_STACK=false
SKIP_ARR_STACK=false
SKIP_ARR_SYNC=false

SKIP_SSH=false
SKIP_AUTH_STACK=false
SKIP_ARR_STACK=false
SKIP_SERVICES_STACK=false
SKIP_ARR_SYNC=false


SKIP_SSH=false
SKIP_AUTH_STACK=false
SKIP_ARR_STACK=false
SKIP_ARR_SYNC=false

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)


        --skip-arr-stack)  SKIP_ARR_STACK=true ;;
        --skip-arr-sync)   SKIP_ARR_SYNC=true ;;

        --skip-arr-stack)      SKIP_ARR_STACK=true ;;
        --skip-services-stack) SKIP_SERVICES_STACK=true ;;
        --skip-arr-sync)       SKIP_ARR_SYNC=true ;;


        --skip-arr-stack)  SKIP_ARR_STACK=true ;;
        --skip-arr-sync)   SKIP_ARR_SYNC=true ;;

        --skip-arr-stack)      SKIP_ARR_STACK=true ;;
        --skip-services-stack) SKIP_SERVICES_STACK=true ;;
        --skip-arr-sync)       SKIP_ARR_SYNC=true ;;

-        --skip-arr-stack)  SKIP_ARR_STACK=true ;;

-        --skip-arr-sync)   SKIP_ARR_SYNC=true ;;

+        --skip-arr-stack)      SKIP_ARR_STACK=true ;;

+        --skip-services-stack) SKIP_SERVICES_STACK=true ;;

+        --skip-arr-sync)       SKIP_ARR_SYNC=true ;;

--phase2-only)     PHASE2_ONLY=true; SKIP_SSH=true ;;

*)                 FILTERED_ARGS+=("$arg") ;;


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


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