diff --git a/Partnership/partnership_onboard.sh b/Partnership/partnership_onboard.sh
index 4381e96..7b11250 100755
--- a/Partnership/partnership_onboard.sh
+++ b/Partnership/partnership_onboard.sh
@@ -124,6 +124,15 @@
# 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)"
@@ -138,6 +147,8 @@ 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
@@ -146,6 +157,8 @@ for arg in "$@"; do
--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 ;;
+ --phase2-only) PHASE2_ONLY=true; SKIP_SSH=true ;;
*) FILTERED_ARGS+=("$arg") ;;
esac
done
@@ -187,6 +200,21 @@ EXTRA_FLAGS=()
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="/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 ""
@@ -443,10 +471,9 @@ deploy_xml_stack() {
# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
if [[ "$AM_MIRROR" == true ]]; then
- echo "━━━ Step 1/1 — SSH Key Setup (Mirror) ━━━"
+ echo "━━━ Step 1/2 — SSH Key Setup (Mirror) ━━━"
echo ""
- echo " Mirror only needs SSH keys ready."
- echo " Owner ($OWNER) completes the rest remotely."
+ echo " Mirror sets up SSH keys, then notifies Owner to run Phase 2."
echo ""
if [[ "$SKIP_SSH" == true ]]; then
@@ -458,10 +485,46 @@ if [[ "$AM_MIRROR" == true ]]; then
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:-/mnt/user/appdata/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 " Next: Run Partnership/partnership_onboard.sh on $OWNER to complete setup"
+ 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
@@ -473,6 +536,8 @@ fi
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
@@ -490,10 +555,11 @@ ARR_SYNC_OK=false
MASTER_PUSH_OK=false
# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
-echo "━━━ Step 1/8 — SSH Key Setup ━━━"
+# 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)"
+ warn "Skipping (--skip-ssh / --phase2-only)"
STEP_SSH_OK=true
elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
log "SSH keys ready ✅"
@@ -504,9 +570,61 @@ else
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
+ 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
+
+ [[ "$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 " Duration: $(format_duration $(( END - START )))"
+ echo ""
+ echo " 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: Plugins ───────────────────────────────────────────────────────────────────────────
echo ""
-echo "━━━ Step 2/8 — Plugin Install on Mirror ━━━"
+echo "━━━ Step 2 — Plugin Install on Mirror ━━━"
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && [[ -n "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
FV3_PRESENT=$(timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
@@ -533,7 +651,7 @@ fi
# ── Step 3: Stop mirror's existing auth stack ─────────────────────────────────────────────────
echo ""
-echo "━━━ Step 3/8 — Stop Mirror Auth Stack ━━━"
+echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━"
if [[ "$SKIP_AUTH_STACK" == true ]]; then
warn "Skipping (--skip-auth-stack)"
@@ -543,7 +661,7 @@ fi
# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
echo ""
-echo "━━━ Step 4/8 — Deploy Auth Stack on Mirror ━━━"
+echo "━━━ Step 4 — Deploy Auth Stack on Mirror ━━━"
if [[ "$SKIP_AUTH_STACK" == true ]]; then
warn "Skipping (--skip-auth-stack)"
@@ -561,7 +679,7 @@ fi
# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
echo ""
-echo "━━━ Step 5/8 — Stop Mirror Arr Stack ━━━"
+echo "━━━ Step 5 — Stop Mirror Arr Stack ━━━"
if [[ "$SKIP_ARR_STACK" == true ]]; then
warn "Skipping (--skip-arr-stack)"
@@ -574,7 +692,7 @@ fi
# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
echo ""
-echo "━━━ Step 6/8 — Deploy Arr Stack on Mirror ━━━"
+echo "━━━ Step 6 — Deploy Arr Stack on Mirror ━━━"
if [[ "$SKIP_ARR_STACK" == true ]]; then
warn "Skipping (--skip-arr-stack)"
@@ -588,7 +706,7 @@ fi
# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
echo ""
-echo "━━━ Step 7/8 — Partnership Onboard ━━━"
+echo "━━━ Step 7 — Partnership Onboard ━━━"
if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
echo "Partnership onboard complete ✅"
@@ -600,7 +718,7 @@ fi
# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
echo ""
-echo "━━━ Step 8/8 — Arr Library Bootstrap ━━━"
+echo "━━━ Step 8 — Arr Library Bootstrap ━━━"
if [[ "$ONBOARD_OK" == false ]]; then
warn "Skipping — onboard did not complete"
@@ -620,7 +738,7 @@ fi
# 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 9/9 — master.conf Push ━━━"
+echo "━━━ $ICON_GEAR Step 9 — master.conf Push ━━━"
if [[ "$ONBOARD_OK" == false ]]; then
warn "Skipping — onboard did not complete"
@@ -652,12 +770,16 @@ exit(\$failed > 0 ? 1 : 0);
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 "❌"; }
diff --git a/Plugin/unraid/include/partnership.php b/Plugin/unraid/include/partnership.php
index 859ee26..55a14fb 100644
--- a/Plugin/unraid/include/partnership.php
+++ b/Plugin/unraid/include/partnership.php
@@ -108,6 +108,7 @@ function vv_pt_nodes(): array {
$vars = vv_conf_vars();
$tsPeers = vv_pt_ts_peers();
$ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? '');
+ $setupDb = vv_setup_state_read();
// SSH key for this host
$myId = strtoupper($currentHost);
@@ -150,18 +151,24 @@ function vv_pt_nodes(): array {
? vv_pt_local_system()
: ($ts['online'] && $ts['ip'] && $mySshKey ? vv_pt_remote_system($ts['ip'], $mySshKey) : []);
+ // Onboard phase from setup.db — 0=not started, 1=SSH+conf done, 2=fully onboarded
+ $nodeIdUpper = strtoupper($slot);
+ $onboardPhase = ($setupDb[$nodeIdUpper . '_PHASE2_DONE'] ?? '') === 'true' ? 2
+ : (($setupDb[$nodeIdUpper . '_PHASE1_DONE'] ?? '') === 'true' ? 1 : 0);
+
$nodes[] = [
- 'slot' => $slot,
- 'id' => strtoupper($slot),
- 'hostname' => $hostname,
- 'is_me' => $isMe,
- 'is_owner' => $isOwner,
- 'ts_online' => $ts['online'],
- 'ts_active' => $ts['active'],
- 'ts_ip' => $ts['ip'],
- 'fallback' => $fbState,
- 'partnership' => $ptDb,
- 'system' => $system,
+ 'slot' => $slot,
+ 'id' => $nodeIdUpper,
+ 'hostname' => $hostname,
+ 'is_me' => $isMe,
+ 'is_owner' => $isOwner,
+ 'ts_online' => $ts['online'],
+ 'ts_active' => $ts['active'],
+ 'ts_ip' => $ts['ip'],
+ 'fallback' => $fbState,
+ 'partnership' => $ptDb,
+ 'system' => $system,
+ 'onboard_phase' => $isMe ? null : $onboardPhase,
];
}
return $nodes;
diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php
index cf4301c..9c2a5c2 100644
--- a/Plugin/unraid/pages/monitor.php
+++ b/Plugin/unraid/pages/monitor.php
@@ -82,6 +82,7 @@
Partner
+