diff --git a/Partnership/partnership_offboard.sh b/Partnership/partnership_offboard.sh index 5580d30..cf60e2c 100755 --- a/Partnership/partnership_offboard.sh +++ b/Partnership/partnership_offboard.sh @@ -173,6 +173,7 @@ STEP_REMOTE_CLEANUP_OK=true # or "skipped" when the mirror is unreachable STEP_MIRROR_STACK_OK=true # or "skipped" STEP_STATE_WRITE_OK=true STEP_STATE_PUSH_OK=false # INACTIVE actually delivered to the mirror, or "skipped" +STEP_SETUP_PUSH_OK=false # cleared phase flags delivered to the mirror, or "skipped" FILTERED_ARGS=() for arg in "$@"; do @@ -255,6 +256,24 @@ echo "" # ============================================================================================== _VV_SYNC_GATES=(RSYNC_ENABLED CRITICAL_RSYNC_ENABLED CONF_SYNC_ENABLED ARR_SYNC_ENABLED) +# ── Stop any running rsync, and say honestly whether it worked ──────────────────────────────── +# +# Both offboard paths called "$SCRIPTS_ROOT/Rsync/rsync_stop.sh". That file has never existed — +# rsync_stop.sh lives in System_Essentials/. With stderr sent to /dev/null the "No such file" +# went unseen, and the line below it printed "Rsync stopped ✅" unconditionally, so every +# offboard ever run reported stopping an rsync it had not touched. On the owner path the failed +# exit also set STEP_STOP_OK=false, which is why the summary said ❌ two lines under a ✅. +# +# One helper, one path, and the outcome is the return value. +stop_rsync_now() { + local script="$SCRIPTS_ROOT/System_Essentials/rsync_stop.sh" + if [[ ! -f "$script" ]]; then + warn "rsync_stop.sh not found at $script" + return 1 + fi + bash "$script" --rsync-only +} + _disarm_sync_gates() { local gate rc=0 conf="$SCRIPTS_ROOT/Configurations/master.conf" for gate in "${_VV_SYNC_GATES[@]}"; do @@ -357,8 +376,12 @@ if [[ "$AM_MIRROR" == true ]]; then echo "━━━ $ICON_STOP Step 1/8 — Stop Rsync ━━━" if [[ "$DRY_RUN" == false ]]; then - bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || true - echo "Rsync stopped ✅" + if stop_rsync_now; then + echo "Rsync stopped ✅" + else + warn "Could not stop rsync — a transfer may still be running into $OWNER" + STEP_STOP_RSYNC_OK=false + fi else warn "DRY RUN — would stop rsync" fi @@ -506,8 +529,14 @@ echo "" echo "━━━ $ICON_STOP Step 1/10 — Stop Rsync ━━━" if [[ "$DRY_RUN" == false ]]; then - bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || STEP_STOP_OK=false - echo "Rsync stopped ✅" + if stop_rsync_now; then + echo "Rsync stopped ✅" + else + warn "Could not stop rsync — a transfer may still be running into $MIRROR" + warn " Everything below changes state while data is still moving, which is the one" + warn " ordering this step exists to prevent. Check: ps -ef | grep rsync" + STEP_STOP_OK=false + fi else warn "DRY RUN — would stop rsync" fi @@ -637,10 +666,21 @@ if [[ "$DRY_RUN" == false ]]; then unset _setup_db _flag if [[ "$MIRROR_REACHABLE" == true ]]; then + # Two separate pushes, and the summary used to report only the second. A run where the + # setup.db push failed and the state-file push succeeded printed "Could not push state + # file to remote" in the body and "Pushed to mirror: ✅" in the summary — describing + # different files with the same words. They are tracked apart now, because they fail + # apart: the mirror can be told the partnership is INACTIVE while keeping the phase + # flags that make its own UI still claim a finished onboard. + # # Pushed after the flags are cleared, so the mirror receives the cleared file rather than # the version that still claimed a finished onboard. - platform_push_setup_state 2>/dev/null || \ - warn "Could not push cleared setup state to $MIRROR" + if platform_push_setup_state 2>/dev/null; then + STEP_SETUP_PUSH_OK=true + else + warn "Could not push cleared phase flags to $MIRROR — its wizard and partnership" + warn " card will keep showing a completed onboard until it is reinstalled or pulled" + fi if push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then STEP_STATE_PUSH_OK=true else @@ -648,10 +688,12 @@ if [[ "$DRY_RUN" == false ]]; then fi else STEP_STATE_PUSH_OK=skipped + STEP_SETUP_PUSH_OK=skipped fi else warn "DRY RUN — would write INACTIVE state, blocklist $MIRROR, push to remote" STEP_STATE_PUSH_OK=true + STEP_SETUP_PUSH_OK=true fi # ── Step 11: SSH key revocation ─────────────────────────────────────────────────────────────── @@ -751,7 +793,8 @@ echo " Step 7 — Remote cleanup: $( [[ "$STEP_REMOTE_CLEANUP_OK" == skipped ] echo " Step 8 — Mirror stack: $( [[ "$STEP_MIRROR_STACK_OK" == skipped ]] && echo "skipped (unreachable)" || { [[ "$STEP_MIRROR_STACK_OK" == true ]] && echo "started ✅" || echo "⚠️ check warnings above"; } )" echo " Step 9 — Emby revoked: $(_ok "${STEP_EMBY_OK:-true}")" echo " Step 10 — State: $( [[ "$STEP_STATE_WRITE_OK" == true ]] && echo "INACTIVE ✅" || echo "⚠️ WRITE FAILED — still looks ACTIVE here" )" -echo " Step 10 — Pushed to mirror: $( [[ "$STEP_STATE_PUSH_OK" == skipped ]] && echo "skipped (unreachable)" || { [[ "$STEP_STATE_PUSH_OK" == true ]] && echo "✅" || echo "⚠️ $MIRROR still reads ACTIVE"; } )" +echo " Step 10 — INACTIVE pushed: $( [[ "$STEP_STATE_PUSH_OK" == skipped ]] && echo "skipped (unreachable)" || { [[ "$STEP_STATE_PUSH_OK" == true ]] && echo "✅" || echo "⚠️ $MIRROR still reads ACTIVE"; } )" +echo " Step 10 — Phase flags pushed: $( [[ "$STEP_SETUP_PUSH_OK" == skipped ]] && echo "skipped (unreachable)" || { [[ "$STEP_SETUP_PUSH_OK" == true ]] && echo "✅" || echo "⚠️ $MIRROR still shows a finished onboard"; } )" echo " Step 11 — Keys revoked: $(_revoke_status)" echo "" echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅" @@ -768,11 +811,35 @@ fi echo " FALLBACK_ENABLED is still true — nothing left to fail over to, turn it off yourself" echo "" echo " $MIRROR leaves with:" -echo " ✓ Current auth config (final sync)" -echo " ✓ Auth WebUIs → localhost" +# Each line reads the step that produced it. "✓ Current auth config (final sync)" was a +# literal, so an offboard whose Step 2 had just warned "Final sync did NOT complete" still +# closed by telling the operator the mirror held current auth data. That is the one claim here +# somebody might act on — it is the difference between a partner that can stand alone and one +# carrying a stale copy of the auth stack. +if [[ "$STEP_SYNC_OK" == true ]]; then + echo " ✓ Current auth config (final sync)" +else + echo " ✗ Auth config NOT synced — it keeps whatever it already had (see Step 2)" +fi +if [[ "${WEBUI_FAILURES:-0}" -eq 0 ]]; then + echo " ✓ Auth WebUIs → localhost" +else + echo " ✗ ${WEBUI_FAILURES} auth WebUI(s) still point at $LOCAL_SERVER_NAME — fix by hand there" +fi echo " ✓ ${PARTNERSHIP_GRACE_HOURS:-6}hr to collect backups" echo "" -[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes made" || \ - warn "$ICON_DONE DONE — clean separation complete ✅" + +# The verdict is derived, never asserted. "DONE — clean separation complete ✅" printed +# unconditionally, under a summary that had already shown two failed steps. +if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — no changes made" +elif [[ "$STEP_STOP_OK" == true && "$STEP_SYNC_OK" == true \ + && "$STEP_STATE_PUSH_OK" != false && "$STEP_SETUP_PUSH_OK" != false \ + && "${WEBUI_FAILURES:-0}" -eq 0 ]]; then + echo "$ICON_DONE DONE — clean separation complete ✅" +else + warn "$ICON_DONE Offboard finished with unresolved steps — the partnership is ended, but" + warn " the ❌ lines above did not happen. Re-read them before re-onboarding." +fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0