Read the following files and report any discrepancies between the documentation and the actual script headers. The goal is to find places where the README or Manual says something that no longer matches what the script header says (wrong schedule, wrong config vars, wrong behavior description, missing scripts, extra scripts, etc.).

Files to read:
1. /boot/config/plugins/varaverk/Partnership/README-Partnership.md
2. /boot/config/plugins/varaverk/Partnership/Manual-Partnership.md
3. Script headers (top ~120 lines each):
   - /boot/config/plugins/varaverk/Partnership/partnership_manager.sh
   - /boot/config/plugins/varaverk/Partnership/partnership_onboard.sh
   - /boot/config/plugins/varaverk/Partnership/partnership_offboard.sh
   - /boot/config/plugins/varaverk/Partnership/partnership_transfer.sh
   - /boot/config/plugins/varaverk/Partnership/ssh_setup.sh
   - /boot/config/plugins/varaverk/Partnership/onboard_cancel.sh

Report: list each discrepancy as "FILE: what the doc says vs what the header says". If everything matches, say so. Be specific — quote the conflicting text. Keep the report under 500 words.


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

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


I'll read all the documentation and script headers to identify discrepancies.


1	# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2	# 🤝  PARTNERSHIP
3	# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4
5	**Managed lifecycle for a two-server partnership — setup, ongoing operation,
6	and clean separation.** One server owns the shared services. The other mirrors
7	them and benefits from them. Every phase of the relationship has the same
8	engineering discipline as the rest of the ecosystem.
9
10	> **This folder exists because a clean exit should be as easy as a clean setup.**
11	> The partnership is not a permanent commitment. `--offboard` works from either
12	> server at any time. Everything the mirror needs to run independently is already
13	> there. The only thing that stops on separation is the sync — and that's intentional.
14
15	---
16
17	## ━━━ THE PROBLEM THAT BUILT THIS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
19	---
20
21	### 🔴  Two Servers, One Auth Stack, No Clean Way to Share It
22
23	The auth stack — NginxProxyManager, LLDAP, Authelia, MariaDB, Redis — runs on
24	HOST1. HOST2 serves its own domain to its own household. It needs its own auth.
25	But maintaining two independent auth stacks means double the work: two places to
26	add users, two places to update proxy rules, two places to renew certs, two
27	configurations that inevitably drift apart. One change on HOST1 has to be manually
28	replicated to HOST2 — or it isn't, and the configurations diverge silently.
29
30	The real cost isn't the initial setup. It's the maintenance burden that accumulates
31	over months — every new user, every proxy rule change, every config update applied
32	in one place and forgotten in the other.
33
34	**The fix:** one auth stack with a managed mirror. HOST1 owns the configuration.
35	HOST2 runs a warm copy that stays current via 30-minute sync. HOST2's operator makes
36	zero auth management decisions — clicking an auth container opens HOST1's WebUI via
37	Tailscale. Changes happen there, propagate to HOST2 in 30 minutes. One place to
38	manage everything for both households.
39
40	---
41
42	### 🔴  No Structure Around the Relationship Itself
43
44	Setting up the mirror was a manual process. SSH in, reconfigure container WebUI
45	URLs one by one, copy auth config, verify connectivity, update state tracking. No
46	defined sequence. No dry-run capability. No verification that each step worked. If
47	something went wrong midway, the mirror was in an inconsistent state with no clear
48	way to understand what had and hadn't been done. Offboard was worse — it involves
49	stopping a sync that's been running for months, making a final copy of data,
50	reconfiguring WebUIs back to local addresses, removing Tailscale access, and
51	notifying both servers. A manual process with that many steps, taken under pressure,
52	leaves one or both parties in a bad state.
53
54	**The fix:** `partnership_manager.sh` with explicit modes for each lifecycle phase.
55	Each mode is a defined sequence. Every step is verified. Dry-run shows exactly what
56	will happen before anything changes. State files make the current relationship status
57	unambiguous from either server.
58
59	---
60
61	### 🔴  No Safe Way to Check If the Other Server Has Gone Away
62
63	After months of operation, HOST2 goes quiet. The sync starts failing. The offline
64	counter increments. But nothing actually happens — the ecosystem just keeps failing
65	the same sync, incrementing the same counter, sending the same notifications.
66	Without a defined threshold and an automated response, "partner gone for 30 days"
67	looks exactly like "partner gone for 3 years."
68
69	**The fix:** `PARTNERSHIP_OFFLINE_THRESHOLD`. After this many days of missed sync
70	cycles, both servers independently auto-offboard. HOST1 removes HOST2 from Tailscale,
71	disables critical sync, writes INACTIVE state. HOST2 — if it eventually comes back —
72	reads HOST1's INACTIVE state and cleans up its own side. The relationship is formally
73	ended from both sides without anyone needing to be present.
74
75	---
76
77	### 🔴  Ownership Transfer Had No Safe Path
78
79	The arrangement was always intended to be flexible — HOST1 owns the auth stack now,
80	but circumstances change. Swapping ownership manually meant reconfiguring WebUIs on
81	both servers, swapping sync direction, updating master.conf on both, and hoping the
82	sequence was correct. A misstep — like flipping sync direction before the final sync
83	completed — leaves both servers with different auth configurations and no clear source
84	of truth.
85
86	**The fix:** `--transfer` with a required confirmation string, a consecutive health
87	check system, and a strict sequence. The confirmation string cannot be typed
88	accidentally. Health strikes require both servers to be healthy on multiple
89	consecutive checks before the transfer begins. A final sync in the current direction
90	runs before anything is flipped.
91
92	---
93
94	## ━━━ WHAT THIS FOLDER DOES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
96	**Onboarding** (`partnership_onboard.sh`) — one-time setup run from both servers.
97	Generates SSH keys, installs the auth stack and arr stack on the mirror from XML
98	templates, configures WebUI redirects to the owner, and bootstraps the arr library.
99	Role is detected automatically — no flags needed to declare which side you are.
100
101	**Offboarding** (`partnership_offboard.sh`) — handles clean separation from either
102	role. Owner path: final sync, WebUI reconfigure, remote container + appdata cleanup
103	(auth/arr stack by XML array, fallback coverage by naming), SSH key revocation,
104	Tailscale removal. Mirror path: local WebUI reconfigure, remove owner-deployed
105	containers locally, disable sync, revoke Emby admin, SSH key revocation, signal owner.
106	Called by `partnership_manager.sh --offboard` but runnable directly.
107
108	**Lifecycle management** (`partnership_manager.sh`) — dispatcher and monitor.
109	- Manually: `--onboard`, `--offboard` (delegates to offboard script), `--transfer`, `--status`, `--unblock`
110	- Automatically: `--check` called every 30 minutes by `critical_sync_maintenance.sh`
111
112	**SSH management** (`ssh_setup.sh`) — generates the keypair for rsync automation,
113	installs it on the remote, and tracks auth failures with a configurable strike system.
114	Called by `partnership_onboard.sh` but runnable independently for validation and
115	re-keying.
116
117	---
118
119	## ━━━ RELATIONSHIP TO OTHER FOLDERS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120
121	**Orchestrators/** — `critical_sync_maintenance.sh` calls `partnership_manager.sh --check`
122	every 30 minutes, passing `--remote-seen` or `--remote-unseen` based on whether the
123	rsync to the partner succeeded. The rsync outcome is the connectivity signal — no
124	separate ping needed.
125
126	**Rsync/** — Critical-Data rsync keeps the auth stack appdata current on the mirror
127	(NPM rules, Authelia config, LLDAP database, certs). Partnership manages the
128	relationship; Rsync delivers the actual data. On offboard, `rsync_stop.sh --rsync-only`
129	stops any running rsync before the final sync runs.
130
131	**Media/** — `arr_sync.sh` bootstraps the mirror's arr library during onboard, ensuring
132	both servers have each other's full library from day one. Ongoing arr sync runs
133	independently at 4-hour cadence.
134
135	---
136
137	## ━━━ SCRIPTS IN THIS FOLDER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
138
139	| Script | Role | When It Runs |
140	|--------|------|--------------|
141	| `partnership_onboard.sh` | One-time setup — SSH keys, stack deploy, arr bootstrap | Manually, once per server per partnership |
142	| `partnership_offboard.sh` | Clean separation — both paths, both roles | Via `partnership_manager.sh --offboard`; or directly |
143	| `partnership_manager.sh` | Dispatcher + monitor — onboard WebUIs, health check, transfer, status | `--check` every 30min; all other modes manually |
144	| `ssh_setup.sh` | SSH key generation, remote install, auth validation | Called by onboard; manually for re-keying or validation |
145
146	---
147
148	## ━━━ HOW THE SCRIPTS RELATE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
149
150	```
151	INITIAL SETUP (run once)
152	─────────────────────────────────────────────────────────────────────────────
153	HOST2 (mirror) runs:
154	  partnership_onboard.sh
155	    └─ ssh_setup.sh                   generates keypair, copies to owner
156
157	HOST1 (owner) runs:
158	  partnership_onboard.sh
159	    ├─ ssh_setup.sh                   generates keypair, copies to mirror
160	    ├─ [stop mirror auth stack]       PARTNERSHIP_REPLACE_CONTAINERS via SSH
161	    ├─ deploy_container_from_xml()    pushes auth XMLs to mirror + starts containers
162	    │   └─ wait_for_container_healthy()  Mariadb/Redis health-checked before Authelia
163	    ├─ [stop mirror arr stack]        PARTNERSHIP_ARR_REPLACE_CONTAINERS via SSH
164	    ├─ deploy_container_from_xml()    pushes arr XMLs to mirror + starts containers
165	    ├─ partnership_manager.sh --onboard  reconfigures WebUIs, writes ACTIVE state
166	    └─ arr_sync.sh                    bootstraps full library on both servers
167
168	ONGOING OPERATION (every 30min)
169	─────────────────────────────────────────────────────────────────────────────
170	critical_sync_maintenance.sh
171	  ├─ Critical-Data rsync              keeps auth appdata current on mirror
172	  └─ partnership_manager.sh --check   reads state files, tracks offline counter
173	      ├─ --remote-seen path           rsync succeeded → reset counter
174	      └─ --remote-unseen path         rsync failed → increment counter → auto-offboard at threshold
175
176	OFFBOARD (manual or auto)
177	─────────────────────────────────────────────────────────────────────────────
178	partnership_manager.sh --offboard
179	  └─ partnership_offboard.sh (exec'd — holds own lock)
180	      ├─ [owner-initiated]    stop rsync → final sync → reconfigure mirror WebUIs
181	      │                       → disable critical sync → write INACTIVE state
182	      │                       → local fallback cleanup → restart own stack
183	      │                       → remote: remove auth/arr stack + fallback containers + appdata
184	      │                       → restart mirror stack → Emby revoke → SSH key revocation
185	      │                       → Tailscale removal after grace window
186	      └─ [mirror-initiated]   stop rsync → reconfigure own WebUIs
187	                              → remove owner-deployed containers locally (reads owner's stack arrays)
188	                              → remove local fallback containers → disable critical sync
189	                              → revoke own Emby admin → restart own stack
190	                              → SSH key revocation → write INACTIVE → signal owner
191	```
192


1	# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2	# 🤝  PARTNERSHIP — Manual
3	# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4
5	Config reference, setup procedures, and operational how-tos.
6	For architecture see README-Partnership.md. For per-script detail see script headers.
7
8	---
9
10	## ━━━ THE RELATIONSHIP MODEL ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11
12	```
13	HOST1 (owner)              HOST2 (mirror)
14	────────────────           ────────────────────────────
15	Source of truth            Warm copy — always current
16	Auth stack config          Auth containers running
17	NPM proxy rules            NPM serving mirror's domain
18	LLDAP users                LLDAP — same users
19	Authelia policies          Authelia — same policies
20	Certs                      Certs — mirrored, valid
21	Emby                       Emby — dirty-synced every 30min
22
23	Changes made here    ──→   Propagated every 30 minutes
24	WebUI management     ──→   Redirected to HOST1 via Tailscale
25	Config → git push    ──→   Received via git pull on next cycle
26	```
27
28	**Mirror's daily experience:** open unRAID Docker UI, click NginxProxyManager,
29	browser opens HOST1's NPM WebUI automatically via Tailscale. Make a proxy rule
30	change. 30 minutes later it's live on HOST2. The mirror operator never manages
31	auth directly — every WebUI redirect is transparent and automatic.
32
33	**Owner's daily experience:** manage auth as normal. Opens NPM → adds a proxy
34	rule → 30min later live on both servers. Never needs to SSH to HOST2 or think
35	about HOST2 during normal operation.
36
37	---
38
39	## ━━━ SETUP PREREQUISITES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40
41	Before `partnership_onboard.sh` can run, five things must be in place:
42
43	### 1. Tailscale Connected on Both Servers
44
45	```bash
46	# Verify HOST2 is visible from HOST1:
47	tailscale ip -4 unRAID-Jayred365    # returns HOST2's Tailscale IP
48
49	# Verify reachability:
50	tailscale ping unRAID-Jayred365
51	```
52
53	Hostnames must match their exact Tailscale device names — enforced already via
54	`REMOTE_SERVER_NAME` in master.conf.
55
56	### 2. Auth Containers Exist on HOST2
57
58	```bash
59	# Containers must exist — they can be stopped:
60	ssh root@[HOST2-ip] "docker inspect NginxProxyManager --format '{{.State.Status}}'"
61	# Expected: created, exited, or running — NOT "no such container"
62	```
63
64	`partnership_onboard.sh` will stop these before deploying the owner's auth stack.
65	List them in `HOST2_PARTNERSHIP_REPLACE_CONTAINERS`.
66
67	### 3. XML Templates Exist on HOST1
68
69	```bash
70	ls /boot/config/plugins/dockerMan/templates-user/my-NginxProxyManager.xml
71	ls /boot/config/plugins/dockerMan/templates-user/my-Authelia.xml
72	# etc — one XML per container listed in HOST1_PARTNERSHIP_AUTH_STACK
73	```
74
75	These are copied to HOST2 during onboard. The same XMLs that Unraid's Docker
76	Manager uses — no extra configuration needed on HOST2 after deploy.
77
78	### 4. Critical-Data rsync Profile Configured
79
80	```bash
81	# master.conf — must include the auth stack appdata path:
82	CRITICAL_SYNC_SHARES=(
83	    "/mnt/user/appdata-Fallback/Critical-Data"    # auth config, certs, NPM rules
84	)
85	```
86
87	This is what keeps HOST2 current after onboard. Without it, onboard succeeds but
88	the mirror's auth stack drifts from the owner's within hours.
89
90	### 5. Tailscale API Key (if PARTNERSHIP_REMOVE_TAILSCALE=true)
91
92	```bash
93	# master.conf
94	PARTNERSHIP_REMOVE_TAILSCALE=true
95	TAILSCALE_API_KEY="tskey-api-..."   # from tailscale.com/admin/settings/keys
96	TAILSCALE_TAILNET="example.github"  # your tailnet name
97
98	# Required scope: Devices write
99	```
100
101	---
102
103	## ━━━ FULL CONFIGURATION REFERENCE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104
105	### master.conf
106
107	```bash
108	# ── Partnership Gate ───────────────────────────────────────────────────────
109	PARTNERSHIP_ENABLED=false           # set true once both servers are configured
110	PARTNERSHIP_OWNER_HOST="HOST1"      # "HOST1" or "HOST2" — flips on --transfer
111
112	# ── Tailscale Removal ──────────────────────────────────────────────────────
113	PARTNERSHIP_REMOVE_TAILSCALE=true
114	TAILSCALE_API_KEY=""                # tskey-api-... from tailscale.com/admin/settings/keys
115	TAILSCALE_TAILNET=""                # your tailnet name (e.g. example.github)
116
117	# ── Timing ─────────────────────────────────────────────────────────────────
118	PARTNERSHIP_GRACE_HOURS=6           # hours before Tailscale removal after offboard
119	                                    # backup access also expires at this time
120	PARTNERSHIP_OFFLINE_THRESHOLD=30    # days unreachable before auto-offboard
121	PARTNERSHIP_SYNC_INTERVAL=15        # informational — actual schedule in cron
122
123	# ── Transfer Safety ────────────────────────────────────────────────────────
124	PARTNERSHIP_TRANSFER_CONFIRM="i-understand-this-transfers-ownership"
125	PARTNERSHIP_TRANSFER_STRIKES=3      # consecutive health checks required
126	PARTNERSHIP_TRANSFER_MAX_ATTEMPTS=20
127
128	# ── Onboard Behaviour ──────────────────────────────────────────────────────
129	PARTNERSHIP_ONBOARD_VERIFY=true     # curl-verify each WebUI after onboard
130	PARTNERSHIP_ONBOARD_NOTIFY=true     # notify both servers on successful onboard
131	```
132
133	### host1.conf (owner side)
134
135	```bash
136	# Containers whose WebUI URLs are redirected to owner's Tailscale IP on onboard.
137	# Restored to localhost on offboard.
138	# Format: "ContainerName|WebUIPort"
139	HOST1_PARTNERSHIP_AUTH_WEBUIS=(
140	    "NginxProxyManager|81"
141	    "Lldap-Gmer4Lfe|17170"
142	    "Authelia|9091"
143	    "Authelia-Secondary|9092"
144	)
145
146	# XML templates pushed to mirror during onboard (auth stack).
147	# ORDER MATTERS: database dependencies must come before Authelia.
148	# Mariadb/Redis are health-checked after deploy before continuing.
149	HOST1_PARTNERSHIP_AUTH_STACK=(
150	    "my-Mariadb-Authelia.xml"
151	    "my-Mariadb-Authelia-Secondary.xml"
152	    "my-Redis-Authelia.xml"
153	    "my-Redis-Authelia-Secondary.xml"
154	    "my-Authelia.xml"
155	    "my-Authelia-Secondary.xml"
156	    "my-NginxProxyManager.xml"
157	    "my-Lldap-Gmer4Lfe.xml"
158	)
159
160	# XML templates pushed to mirror during onboard (arr stack).
161	# Leave empty to skip arr stack deploy entirely.
162	HOST1_PARTNERSHIP_ARR_STACK=(
163	    # "my-Sonarr.xml"
164	    # "my-Radarr.xml"
165	    # "my-Lidarr.xml"
166	    # "my-Prowlarr.xml"
167	)
168
169	# Paths mirror can collect during grace window after offboard.
170	HOST1_PARTNERSHIP_MIRROR_BACKUPS=(
171	    # "/mnt/user/appdata-Fallback/Jayred365-Emby"
172	)
173
174	# Containers parked on this server when partnership is active.
175	# Stopped on onboard (owner deploys auth stack instead), restarted on offboard.
176	HOST1_PARTNERSHIP_OWN_CONTAINERS=(
177	    # "Emby"
178	    # "NginxProxyManager"
179	)
180	```
181
182	### host2.conf (mirror side)
183
184	```bash
185	# Containers whose WebUI URLs are redirected on onboard.
186	# Usually left empty on mirror — owner's AUTH_WEBUIS drives the redirect.
187	HOST2_PARTNERSHIP_AUTH_WEBUIS=(
188	    # "NginxProxyManager|81"
189	)
190
191	# Auth containers to stop on this server before owner deploys auth stack.
192	HOST2_PARTNERSHIP_REPLACE_CONTAINERS=(
193	    "NginxProxyManager"
194	    "Authelia"
195	    "Authelia-Secondary"
196	    "Mariadb-Authelia"
197	    "Mariadb-Authelia-Secondary"
198	    "Redis-Authelia"
199	    "Redis-Authelia-Secondary"
200	    "Lldap"
201	)
202
203	# Arr containers to stop on this server before owner deploys arr stack.
204	HOST2_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
205	    # "Sonarr"
206	    # "Radarr"
207	    # "Lidarr"
208	    # "Prowlarr"
209	)
210
211	# Containers parked on this server when partnership is active.
212	# Stopped on onboard, restarted on offboard.
213	HOST2_PARTNERSHIP_OWN_CONTAINERS=(
214	    # "Emby"
215	    # "NginxProxyManager"
216	)
217
218	# Emby admin provisioning — owner reads these during onboard.
219	HOST2_PARTNERSHIP_EMBY_ADMIN_USER=""
220	HOST2_PARTNERSHIP_EMBY_ADMIN_PASS=""
221	```
222
223	---
224
225	## ━━━ PROCEDURES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
226
227	### Establishing a Partnership (First-Time Setup)
228
229	```bash
230	# Step 1: Run on HOST2 (mirror) first — generates SSH key only
231	Partnership/partnership_onboard.sh
232
233	# Step 2: Run on HOST1 (owner) — completes setup remotely
234	Partnership/partnership_onboard.sh --dry-run    # review first
235	Partnership/partnership_onboard.sh
236
237	# Step 3: Verify from either server
238	Partnership/partnership_manager.sh --status
239	```
240
241	HOST2 must run first to generate its SSH key so HOST1 can reach it during Step 2.
242	If SSH is already configured, use `--skip-ssh` on both sides.
243
244	**Skip flags for partial re-runs** (if something failed midway):
245	```bash
246	--skip-ssh          # SSH already set up
247	--skip-auth-stack   # auth stack already deployed (skips Steps 3-4)
248	--skip-arr-stack    # arr stack already deployed (skips Steps 5-6)
249	--skip-arr-sync     # arr sync not needed yet (arrs not live on mirror)
250	```
251
252	---
253
254	### Offboarding
255
256	Either server can initiate. Always dry-run first.
257
258	```bash
259	# From either server — review the sequence
260	Partnership/partnership_manager.sh --offboard --dry-run
261
262	# Live offboard — 10-second countdown before executing
263	Partnership/partnership_manager.sh --offboard
264	```
265
266	**Owner-initiated** (HOST1 runs `--offboard`):
267	1. Stop any running rsync
268	2. Final sync — mirror leaves with current auth config and certs
269	3. Reconfigure mirror WebUIs → localhost
270	4. Disable critical rsync (`CRITICAL_RSYNC_ENABLED=false`)
271	5. Write INACTIVE state on both servers, blocklist mirror
272	6. Local fallback cleanup — remove partner containers + appdata from HOST1
273	7. Restart own stack (HOST1's own parked containers)
274	8. Remote cleanup — remove auth/arr stack containers + appdata from mirror; remove fallback containers
275	9. Restart mirror's own stack
276	10. Revoke Emby admin, SSH key revocation (both directions), Tailscale removal
277
278	**Mirror-initiated** (HOST2 runs `--offboard`):
279	1. Stop any running rsync
280	2. Reconfigure own WebUIs → localhost (immediately independent)
281	3. Remove owner-deployed containers locally (reads owner's auth/arr stack arrays via SSH)
282	4. Remove local fallback coverage containers
283	5. Disable critical rsync (`CRITICAL_RSYNC_ENABLED=false`)
284	6. Revoke own Emby admin account from local Emby instance
285	7. Restart own stack (HOST2's own parked containers)
286	8. SSH key revocation, write INACTIVE state locally, push state to HOST1 if reachable
287
288	HOST1 finalises its own side on the next `--check` cycle after seeing HOST2's INACTIVE state.
289
290	**What the mirror leaves with after offboard:**
291	- Its own parked containers restarted (from `PARTNERSHIP_OWN_CONTAINERS`)
292	- Own arr media library (arr appdata is in `appdata-Fallback/Arrs_Stack/` and is retained)
293	- Full git mirror of the ecosystem
294	- Auth WebUIs pointing to localhost — ready to set up own independent auth
295
296	---
297
298	### Transferring Ownership
299
300	Transfers auth stack ownership from current owner to mirror. Only the current owner
301	can initiate. Both servers must be healthy.
302
303	```bash
304	# Always dry-run first — shows health check results and master.conf changes
305	Partnership/partnership_manager.sh --transfer --dry-run
306
307	# Live transfer — requires exact confirmation string
308	Partnership/partnership_manager.sh --transfer --confirm=i-understand-this-transfers-ownership
309	```
310
311	Transfer sequence:
312	1. Display current and future ownership clearly
313	2. Require exact confirmation string
314	3. Both servers pass `PARTNERSHIP_TRANSFER_STRIKES` consecutive health checks
315	4. Final sync in current direction (current owner → current mirror)
316	5. Reconfigure WebUI templates on both servers
317	6. Flip `PARTNERSHIP_OWNER_HOST` in master.conf on both servers
318	7. Write updated state files and notify
319
320	After transfer, HOST2 is the owner. Run `--transfer` from HOST2 to transfer back.
321
322	---
323
324	### Re-onboarding After Offboard
325
326	Former partners are blocklisted on offboard. Remove from blocklist first:
327
328	```bash
329	# Check what's on the blocklist
330	Partnership/partnership_manager.sh --status
331
332	# Remove the block (use exact hostname shown in status)
333	Partnership/partnership_manager.sh --unblock unRAID-Jayred365
334
335	# Then re-onboard normally
336	Partnership/partnership_onboard.sh --dry-run
337	Partnership/partnership_onboard.sh
338	```
339
340	---
341
342	### Adding a New Container to the Auth Stack
343
344	1. Create the XML template on HOST1 (`/boot/config/plugins/dockerMan/templates-user/my-NewContainer.xml`)
345	2. Add the XML filename to `HOST1_PARTNERSHIP_AUTH_STACK` in `host1.conf`
346	   - If it has a database dependency, put the dep earlier in the array
347	3. Add the container name to `HOST2_PARTNERSHIP_REPLACE_CONTAINERS` in `host2.conf`
348	4. Re-run the auth stack portion:
349	   ```bash
350	   Partnership/partnership_onboard.sh --skip-ssh --skip-arr-stack --skip-arr-sync
351	   ```
352
353	---
354
355	### SSH Key Validation and Re-keying
356
357	```bash
358	# Check current key state and remote connectivity
359	Partnership/ssh_setup.sh --status
360
361	# Validate SSH auth (tracks strikes)
362	Partnership/ssh_setup.sh --validate
363
364	# Regenerate key and re-copy to remote (if key is compromised or expired)
365	Partnership/ssh_setup.sh --force
366	```
367
368	Strike tracking: `SSH_MAX_STRIKES` consecutive auth failures → notify.
369	Network unreachability (Tailscale down) does not count as a strike.
370	Counter resets after `SSH_STRIKE_RESET_HRS` of clean connectivity.
371
372	---
373
374	## ━━━ OUTPUT TIERS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
375
376	All scripts use a two-tier output model: `echo` lines are always visible; `log`
377	lines only appear when `--log` is passed.
378
379	**partnership_onboard.sh** — one-shot setup. Without `--log`, step headers, per-step
380	result lines (deployed/failed counts), and the final summary are visible. Per-container
381	deploy detail suppressed.
382
383	**partnership_offboard.sh** — one-shot teardown. Without `--log`, step headers, key
384	state transitions, and the final checklist summary are visible. Per-container cleanup
385	detail suppressed.
386
387	**partnership_manager.sh** — check/status/transfer/onboard modes. Without `--log`,
388	mode-specific result lines (`--check` prints ACTIVE/INACTIVE status always), state
389	transitions, warnings, and summary blocks are visible. Per-operation detail suppressed.
390
391	**ssh_setup.sh** — one-shot key setup. Without `--log`, section headers and per-step
392	results (key created, key installed, auth verified) are visible. Strike-counter
393	management (`--validate`) uses `log` for healthy cycles; `warn` for failures.
394
395	---
396
397	## ━━━ FLAG REFERENCE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
398
399	### partnership_onboard.sh
400
401	| Flag | Effect |
402	|------|--------|
403	| `--dry-run` | Preview all steps without making changes |
404	| `--log` | Verbose per-step output |
405	| `--skip-ssh` | Skip SSH key setup (Step 1) — key already in place |
406	| `--skip-auth-stack` | Skip auth stop + deploy (Steps 3-4) — already done |
407	| `--skip-arr-stack` | Skip arr stop + deploy (Steps 5-6) — not needed or already done |
408	| `--skip-arr-sync` | Skip arr library bootstrap (Step 8) — arrs not live yet |
409
410	### partnership_offboard.sh
411
412	Called automatically by `partnership_manager.sh --offboard`. Can also be run directly.
413
414	| Flag | Effect |
415	|------|--------|
416	| `--dry-run` | Preview all steps without making changes |
417	| `--log` | Verbose per-step output |
418	| `--reason=<string>` | Tag the offboard reason in state file and blocklist (default: `manual`) |
419
420	### partnership_manager.sh
421
422	| Flag | Effect |
423	|------|--------|
424	| `--dry-run` | Show the complete sequence without executing |
425	| `--log` | Verbose per-operation output |
426	| `--onboard` | Establish mirror relationship (owner only) |
427	| `--offboard` | Clean separation (either server) — 10s countdown |
428	| `--transfer --confirm=...` | Flip ownership (owner only) |
429	| `--check --remote-seen\|--remote-unseen` | 15-min health check (called by orchestrator) |
430	| `--status` | Show state files, blocklist, SSH key status |
431	| `--unblock <hostname>` | Remove hostname from blocklist |
432
433	### ssh_setup.sh
434
435	| Flag | Effect |
436	|------|--------|
437	| `--force` | Regenerate key even if it exists, re-copy to remote |
438	| `--validate` | Test SSH auth, track strikes, notify at limit |
439	| `--status` | Show key path, fingerprint, remote connectivity |
440	| `--dry-run` | Preview without creating or copying |
441
442	---
443
444	## ━━━ TROUBLESHOOTING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
445
446	### Auth Stack Deploy Fails on Onboard
447
448	```bash
449	# Check that XML files exist on HOST1:
450	ls /boot/config/plugins/dockerMan/templates-user/my-Authelia.xml
451
452	# Check that mirror is reachable via Tailscale:
453	tailscale ping unRAID-Jayred365
454
455	# Check that SSH key is working:
456	Partnership/ssh_setup.sh --validate
457
458	# Re-run auth stack only:
459	Partnership/partnership_onboard.sh --skip-ssh --skip-arr-stack --skip-arr-sync
460	```
461
462	If Mariadb/Redis deployed but Authelia still fails: the health-wait timed out
463	(60s default). Authelia needs Mariadb to be fully initialized, which can take longer
464	on first boot. Re-run with `--skip-auth-stack` removed — `docker start` on an
465	already-created container will retry cleanly.
466
467	---
468
469	### WebUI Still Pointing to Old IP After Onboard
470
471	```bash
472	# Check template was updated on HOST2:
473	ssh root@[HOST2-ip] "grep -i 'tailscale\|[HOST1-IP]' \
474	    /boot/config/plugins/dockerMan/templates-user/my-NginxProxyManager.xml"
475
476	# Check status output:
477	Partnership/partnership_manager.sh --status
478
479	# Some containers need a restart to pick up new WebUI URL:
480	ssh root@[HOST2-ip] "docker restart NginxProxyManager"
481
482	# Verify Tailscale routing:
483	# From HOST2, should reach HOST1's NPM:
484	curl http://[HOST1-tailscale-ip]:81
485	```
486
487	---
488
489	### Transfer Health Checks Failing
490
491	```bash
492	# Both arrays must be fully started:
493	ls /mnt/user                           # should show share directories on both servers
494
495	# Both Docker daemons responding:
496	docker ps                              # should return a list, not hang
497
498	# Tailscale connected on both servers:
499	tailscale status                       # remote peer should show online
500
501	# Health check attempts before giving up: PARTNERSHIP_TRANSFER_MAX_ATTEMPTS (default 20)
502	```
503
504	---
505
506	### Mirror's Domain Broken After Offboard
507
508	The owner-deployed auth stack has been removed from HOST2 as part of offboard cleanup.
509	HOST2's own parked containers (from `PARTNERSHIP_OWN_CONTAINERS`) are restarted
510	automatically, but if HOST2 had no pre-existing auth stack of its own, it needs one set up.
511
512	```bash
513	# Check what's running:
514	docker ps
515
516	# Verify own parked containers came back up:
517	# (listed in HOST2_PARTNERSHIP_OWN_CONTAINERS in host2.conf)
518
519	# If you need a fresh auth stack, deploy from HOST2's own XML templates:
520	docker create ... && docker start NginxProxyManager   # etc.
521
522	# Check cert expiry — auto-renewal no longer happens via HOST1:
523	cert_monitor.sh --dry-run
524	```
525
526	If the offboard was unexpected or incomplete, check state first:
527	```bash
528	Partnership/partnership_manager.sh --status
529	```
530
531	---
532
533	### State Mismatch Between Servers
534
535	```bash
536	# Check both state files:
537	Partnership/partnership_manager.sh --status    # shows both sides via SSH
538
539	# If one shows ACTIVE and other INACTIVE:
540	#   The INACTIVE side has already offboarded.
541	#   Run --offboard on the ACTIVE side to sync the state.
542
543	# If HOST2 unreachable:
544	#   HOST2 self-resolves on next --check when reachable.
545	#   HOST1 reads INACTIVE state → finalises from owner side automatically.
546	```
547
548	---
549
550	### Auto-Offboard Triggered Unexpectedly
551
552	```bash
553	# Check the offline counter:
554	cat /boot/config/partnership_offline_days.db
555
556	# Extended Tailscale outage may have incremented the counter.
557	# Check Tailscale peer visibility:
558	tailscale status
559
560	# If partnership should continue — re-onboard (first unblock the partner):
561	Partnership/partnership_manager.sh --unblock unRAID-Jayred365
562	Partnership/partnership_manager.sh --onboard --dry-run
563	Partnership/partnership_manager.sh --onboard
564	```
565
566	---
567
568	## ━━━ STATE FILES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
569
570	```bash
571	/boot/config/partnership_HOST1.db   # HOST1 writes only
572	/boot/config/partnership_HOST2.db   # HOST2 writes only
573	/boot/config/partnership_blocklist.db  # hostname|timestamp|reason
574
575	# Example state file:
576	state=ACTIVE
577	last_updated=2026-05-14 03:00:00
578	last_seen_remote=2026-05-14 03:00:00
579	owner_host=HOST1
580	offline_days=0
581	```
582
583	Each server writes only its own state file. The other server reads via SSH.
584	State propagates through SSH reads — no rsync, no shared filesystem.
585	`/boot/config` — survives reboots, available before array starts, minimal flash wear.
586	flock on all writes — prevents concurrent corruption from overlapping --check cycles.
587
588	---
589
590	## ━━━ ROLE-BASED ACCESS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
591
592	```
593	                    HOST1 (owner)    HOST2 (mirror)
594	--onboard                ✅               ❌     ownership is granted, not taken
595	--offboard               ✅               ✅     clean exit available to both parties
596	--transfer               ✅               ❌     owner only — mirror cannot self-promote
597	--check                  ✅               ✅     both servers monitor state
598	--status                 ✅               ✅     status is always available
599	--unblock                ✅               ✅     either server can clear its own blocklist
600	```
601
602	AM_OWNER / AM_MIRROR flags set by `detect_hosts()` from `PARTNERSHIP_OWNER_HOST`.
603	All routing decisions use these flags — no hostname string comparisons.
604	Mirror attempting `--onboard` or `--transfer`: blocked with a clear error message.
605
606	---
607
608	## ━━━ DESIGN NOTES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
609
610	**Why HOST1/HOST2 instead of PARTNERSHIP_OWNER/MIRROR?**
611	HOST1 and HOST2 are already defined in master.conf — SSH keys, Tailscale names,
612	all connection details. A single `PARTNERSHIP_OWNER_HOST` var flips ownership.
613	Duplicating as `PARTNERSHIP_OWNER` and `PARTNERSHIP_MIRROR` would require maintaining
614	identical values in two places. One source of truth for the host definition.
615
616	**Why does transfer require a confirmation string?**
617	Transfer touches master.conf on both servers, reconfigures WebUIs, and flips sync
618	direction. A misstep mid-sequence leaves both servers with different auth configs and
619	no clear source of truth. The confirmation string makes accidental execution impossible,
620	not just unlikely.
621
622	**Why do Tailscale removal and backup access expire at the same time?**
623	If the mirror can't reach the owner's server via Tailscale, the backup is also
624	unreachable. `PARTNERSHIP_GRACE_HOURS` controls both — one var, consistent behaviour.
625	No misleading "data available for 30 days" when access is gone in 6 hours.
626
627	**Why does either server auto-offboard after 30 days offline?**
628	30 consecutive days of missed sync cycles means the relationship has effectively ended
629	regardless of intent. Auto-offboard makes the state official. Each server acts
630	independently — no coordination required to finalise.
631
632	**Why are auth/arr stack containers removed on offboard?**
633	The auth stack running on the mirror is the owner's stack — deployed from the owner's
634	XML templates, managed by the owner. On offboard, the mirror restores its own parked
635	containers (`PARTNERSHIP_OWN_CONTAINERS`) and sets up independent auth from scratch.
636	Nothing from the owner's deployment lingers. Appdata is deleted alongside containers
637	so there's no stale config left behind. Both fallback coverage containers (named
638	`*-Owner`) and the owner-deployed stack (from `PARTNERSHIP_AUTH_STACK` / `PARTNERSHIP_ARR_STACK`)
639	are removed — the mirror gets a clean slate.
640
641	**Why does XML ordering matter for the auth stack?**
642	Authelia requires MariaDB and Redis to be running and accepting connections before it
643	starts. `deploy_container_from_xml()` starts each container immediately after creating
644	it. If Authelia is deployed before MariaDB, it will fail to connect and may not recover
645	automatically. The array order in `HOST1_PARTNERSHIP_AUTH_STACK` is enforced by
646	convention — database containers first, then applications.
647


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


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Partnership Onboard ============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Runs once on both servers to establish a new partnership. Role is detected
9	# automatically via detect_hosts() — no flags needed to declare which side you are.
10	# Run on the mirror first (generates its SSH key), then on the owner to complete
11	# setup remotely.
12	#
13	# ==============================================================================================
14	# OPERATIONAL MODEL
15	# ==============================================================================================
16	#
17	# MIRROR PATH (1 step)
18	#   Step 1: SSH key setup — generate keypair, copy to owner, update conf
19	#           Owner completes the rest remotely. Mirror is done.
20	#
21	# OWNER PATH (10 steps)
22	#   Step 1:  SSH key setup         — generate keypair, install on mirror, update conf
23	#   Step 2:  Stop mirror auth      — stop mirror's existing auth containers before replacing
24	#   Step 3:  Deploy auth stack     — push XMLs, pull images, create + start on mirror
25	#                                    Mariadb/Redis health-checked before Authelia deploys
26	#   Step 4:  Stop mirror arr       — stop mirror's existing arr containers before replacing
27	#   Step 5:  Deploy arr stack      — push arr XMLs, pull images, create + start on mirror
28	#   Step 6:  Stop mirror services  — stop mirror's existing services containers before replacing
29	#   Step 7:  Deploy services stack — push Emby/Jellyfin/Seerr XMLs, pull images, create + start
30	#   Step 8:  Partnership onboard   — configure WebUIs → owner IP, write state, Emby
31	#   Step 9:  Arr bootstrap         — bidirectional library sync (arr_sync.sh)
32	#   Step 10: Conf push             — push master.conf + setup state to all listed hosts
33	#
34	# ==============================================================================================
35	# DESIGN PRINCIPLES
36	# ==============================================================================================
37	#
38	# Credentials never in SSH command strings
39	#   Auth stack containers hold API keys, DB passwords, etc. The deploy script is written
40	#   locally, SCPed to the remote, and executed there. Command-line args are never used
41	#   to pass credentials — they'd appear in `ps` output and shell history on both servers.
42	#
43	# XML templates are the single source of truth for deployed containers
44	#   The owner's templates-user/ XMLs define every container deployed on the mirror.
45	#   The same XMLs that Unraid's Docker Manager uses are what get SCPed — the mirror's
46	#   Docker Manager can manage the containers after onboard without additional config.
47	#
48	# Dependency ordering in the auth stack is owner-enforced
49	#   PARTNERSHIP_AUTH_STACK order matters: Mariadb and Redis must come before Authelia.
50	#   The array is ordered correctly in host1.conf. After each Mariadb/Redis deploy,
51	#   the script waits for the container to be healthy before continuing. This is a remote
52	#   health check — the container must be running (or report healthy) before the next
53	#   dependent is deployed.
54	#
55	# ==============================================================================================
56	# OPERATIONAL SAFEGUARDS
57	# ==============================================================================================
58	#
59	# Root check
60	#   All operations run as root — SSH key management, docker operations, conf updates.
61	#
62	# SSH timeout on all remote calls
63	#   Every ssh/scp call uses SSH_TIMEOUT. No operation hangs indefinitely on a
64	#   slow or unreachable mirror.
65	#
66	# --dry-run shows exact actions without executing
67	#   Every step prints what it would do. SCP, deploy, plugin install, arr sync —
68	#   all dry-run safe.
69	#
70	# Step skip flags for partial re-runs
71	#   --skip-ssh, --skip-auth-stack, --skip-arr-stack, --skip-arr-sync allow
72	#   resuming after a partial failure without re-running completed steps.
73	#
74	# ==============================================================================================
75	# CONFIGURATION
76	# ==============================================================================================
77	#
78	# host*.conf
79	#
80	#   HOST*_PARTNERSHIP_AUTH_STACK
81	#       XML filenames (from this server's templates-user/) to push and deploy on the
82	#       mirror as its auth stack. Order matters: database deps before Authelia.
83	#       Aliased by detect_hosts() → PARTNERSHIP_AUTH_STACK
84	#
85	#   HOST*_PARTNERSHIP_REPLACE_CONTAINERS
86	#       Containers to stop on the mirror before deploying the auth stack.
87	#       Defined in the MIRROR's own conf (host*.conf on HOST2) — never in HOST1's conf.
88	#       Read live from the mirror via SSH during Step 3 (sources mirror's load_config.sh at
89	#       the same $SCRIPTS_ROOT path — convention: both servers use the same repo location).
90	#       Leave empty on HOST2 if no conflicting containers exist (fresh mirror: nothing to stop).
91	#       Aliased by detect_hosts() → PARTNERSHIP_REPLACE_CONTAINERS (on the mirror)
92	#
93	#   HOST*_PARTNERSHIP_ARR_STACK
94	#       XML filenames to push and deploy on the mirror as its arr stack.
95	#       Leave empty to skip arr stack deploy.
96	#       Aliased by detect_hosts() → PARTNERSHIP_ARR_STACK
97	#
98	#   HOST*_PARTNERSHIP_ARR_REPLACE_CONTAINERS
99	#       Arr containers to stop on the mirror before deploying the arr stack.
100	#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
101	#       Aliased by detect_hosts() → PARTNERSHIP_ARR_REPLACE_CONTAINERS (on the mirror)
102	#
103	#   HOST*_PARTNERSHIP_SERVICES_STACK
104	#       XML filenames to push and deploy on the mirror as its shared services stack.
105	#       Includes Emby, Jellyfin, Seerr, SeerrFin. Leave empty to skip services stack deploy.
106	#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_STACK
107	#
108	#   HOST*_PARTNERSHIP_SERVICES_REPLACE_CONTAINERS
109	#       Services containers to stop on the mirror before deploying the services stack.
110	#       Same rule as PARTNERSHIP_REPLACE_CONTAINERS: defined in mirror's own conf, never HOST1's.
111	#       Aliased by detect_hosts() → PARTNERSHIP_SERVICES_REPLACE_CONTAINERS (on the mirror)
112	#
113	# ==============================================================================================
114	# RUNTIME MODES
115	# ==============================================================================================
116	#
117	# Partnership/partnership_onboard.sh
118	#     Full onboard — role detected automatically
119	#
120	# Partnership/partnership_onboard.sh --dry-run


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Partnership Offboard ===========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Cleanly ends a partnership. Role is detected automatically — run on either server.
9	# Owner path runs the full sequence including remote cleanup and final sync.
10	# Mirror path handles the local side and signals the owner to complete its own cleanup.
11	#
12	# ==============================================================================================
13	# OPERATIONAL MODEL
14	# ==============================================================================================
15	#
16	# OWNER PATH (10 steps)
17	#   Step 1:  Stop rsync         — halt any running sync before state changes
18	#   Step 2:  Final sync         — mirror leaves with current Critical-Data state
19	#   Step 3:  Reconfigure WebUIs — mirror's auth WebUIs → localhost
20	#   Step 4:  Disable sync       — CRITICAL_RSYNC_ENABLED=false in master.conf
21	#   Step 5:  Local cleanup      — remove fallback coverage containers + appdata
22	#   Step 6:  Restart own stack  — bring up owner's own parked containers
23	#   Step 7:  Remote cleanup     — remove auth/arr stack + fallback containers from mirror
24	#   Step 8:  Restart mirror     — bring up mirror's own parked containers
25	#   Step 9:  Revocation         — Emby admin, SSH keys
26	#   Step 10: Write state        — INACTIVE locally + pushed to mirror, mirror blocklisted
27	#            Tailscale          — grace window then device removal (after state written)
28	#
29	# MIRROR PATH (8 steps)
30	#   Step 1: Stop rsync          — halt any running sync
31	#   Step 2: Reconfigure WebUIs  — local auth WebUIs → localhost
32	#   Step 3: Remote stack clean  — remove owner-deployed containers locally (auth/arr stack)
33	#   Step 4: Fallback cleanup    — remove fallback coverage containers
34	#   Step 5: Disable sync        — CRITICAL_RSYNC_ENABLED=false in master.conf
35	#   Step 6: Revoke Emby admin   — remove own admin account from local Emby instance
36	#   Step 7: Restart own stack   — bring up own parked containers
37	#   Step 8: SSH revocation      — revoke keys both directions, write state, signal owner
38	#
39	# ==============================================================================================
40	# CONFIGURATION
41	# ==============================================================================================
42	#
43	# host*.conf
44	#
45	#   HOST*_PARTNERSHIP_AUTH_STACK
46	#       Auth container XMLs to push during onboard — used on offboard to identify what
47	#       to remove. Owner's PARTNERSHIP_AUTH_STACK determines which containers get removed
48	#       from the mirror on both owner-initiated and mirror-initiated offboard.
49	#
50	#   HOST*_PARTNERSHIP_ARR_STACK
51	#       Arr container XMLs — same cleanup logic as auth stack.
52	#
53	#   HOST*_PARTNERSHIP_SERVICES_STACK
54	#       Shared services XMLs (Emby, Jellyfin, Seerr, SeerrFin) — same cleanup logic.
55	#
56	# ==============================================================================================
57	# RUNTIME MODES
58	# ==============================================================================================
59	#
60	# Partnership/partnership_offboard.sh
61	#     Full offboard — role detected automatically
62	#
63	# Partnership/partnership_offboard.sh --dry-run
64	#     Preview all steps without executing
65	#
66	# Partnership/partnership_offboard.sh --log
67	#     Verbose per-step output
68	#
69	# Partnership/partnership_offboard.sh --reason=<string>
70	#     Tag the offboard reason in state file and blocklist (default: manual)
71	#     Called by partnership_manager.sh --offboard (reason passed through)
72	#
73	# ==============================================================================================
74
75	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
76	SCRIPTS_ROOT="$SCRIPT_DIR/.."
77	SSH_TIMEOUT=15
78
79	source "$SCRIPTS_ROOT/load_config.sh"
80	source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
81
82	# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
83	REASON="manual"
84	FILTERED_ARGS=()
85
86	for arg in "$@"; do
87	    case "$arg" in
88	        --reason=*) REASON="${arg#--reason=}" ;;
89	        *)          FILTERED_ARGS+=("$arg") ;;
90	    esac
91	done
92
93	parse_args "${FILTERED_ARGS[@]}"
94
95	# ── Source partnership_manager.sh for shared helpers ──────────────────────────────────────────
96	# PARTNERSHIP_LIB_MODE=1 skips mode dispatch — functions are defined, nothing is executed.
97	PARTNERSHIP_LIB_MODE=1 source "$SCRIPT_DIR/partnership_manager.sh"
98
99	# ==============================================================================================
100	# ━━━ Setup ━━━
101	# ==============================================================================================
102	[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
103
104	if ! command -v docker &>/dev/null; then
105	    error "Docker command not found"
106	    exit 1
107	fi
108
109	detect_hosts
110
111	OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
112	MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
113	OWNER="${!OWNER_ID}"
114	MIRROR="${!MIRROR_ID}"
115	MIRROR_SSH_KEY="$SSH_KEY"
116	OWNER_SSH_KEY="$SSH_KEY"
117
118	AM_OWNER=false
119	AM_MIRROR=false
120	[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Partnership Transfer ============================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Transfers ownership from the current owner to the current mirror. After
9	# transfer the roles are swapped: what was the mirror becomes the new owner,
10	# and what was the owner becomes the new mirror.
11	#
12	# No containers are moved — only config and WebUI targets are updated. Both
13	# servers remain in the partnership; the sync direction reverses on the next
14	# fallback.sh / critical_sync_maintenance.sh cycle.
15	#
16	# ==============================================================================================
17	# OPERATIONAL MODEL
18	# ==============================================================================================
19	#
20	# OWNER ONLY — mirror cannot run this script.
21	#
22	#   Step 1:  Health Verification  — both servers pass N consecutive health checks
23	#   Step 2:  Pre-transfer Sync    — final sync in current direction (owner → mirror)
24	#   Step 3:  Reconfigure WebUIs   — new owner WebUIs → localhost
25	#                                   new mirror WebUIs → new owner IP
26	#   Step 4:  Flip Ownership       — update PARTNERSHIP_OWNER_HOST in master.conf
27	#                                   on both servers
28	#   Step 5:  Write State          — ACTIVE written locally and pushed to new mirror
29	#
30	# ==============================================================================================
31	# OPERATIONAL SAFEGUARDS
32	# ==============================================================================================
33	#
34	# Owner-Only Enforcement
35	#   The script reads MY_ID from detect_hosts() and exits immediately if it is
36	#   not the current PARTNERSHIP_OWNER_HOST. The mirror cannot run a transfer.
37	#
38	# Explicit Confirmation String
39	#   Requires the exact passphrase from PARTNERSHIP_TRANSFER_CONFIRM via
40	#   --confirm=<value>. Without a matching string the transfer is cancelled
41	#   before any steps execute. Prevents accidental ownership changes.
42	#
43	# Active Partnership Guard
44	#   Reads the local state file and exits if the current state is INACTIVE.
45	#   A transfer without an active partnership has no defined outcome.
46	#
47	# Dual Health Verification
48	#   Both servers must pass PARTNERSHIP_TRANSFER_STRIKES consecutive health
49	#   checks before proceeding. A single failure resets the strike counter.
50	#   After PARTNERSHIP_TRANSFER_MAX_ATTEMPTS total attempts the transfer aborts.
51	#
52	# Pre-transfer Final Sync
53	#   A full sync in the current direction (owner → mirror) runs immediately
54	#   before roles flip. Ensures the mirror is current before it becomes the owner.
55	#
56	# ==============================================================================================
57	# CONFIGURATION
58	# ==============================================================================================
59	#
60	# master.conf
61	#
62	#   PARTNERSHIP_OWNER_HOST
63	#       Current owner host ID (e.g. "HOST1"). Updated on both servers after transfer.
64	#
65	#   PARTNERSHIP_TRANSFER_CONFIRM
66	#       Exact string required to confirm transfer (default: "i-understand-this-transfers-ownership").
67	#       Pass via --confirm=<value>.
68	#
69	#   PARTNERSHIP_TRANSFER_STRIKES
70	#       Consecutive health checks both servers must pass before transfer proceeds (default: 3).
71	#
72	#   PARTNERSHIP_TRANSFER_MAX_ATTEMPTS
73	#       Max health check attempts before giving up (default: 20).
74	#
75	#   CRITICAL_SYNC_SHARES
76	#       Array of "path|profile" or "path" entries for do_final_sync().
77	#
78	#   PARTNERSHIP_AUTH_WEBUIS
79	#       Array of "ContainerName|WebUIPort" entries reconfigured during transfer.
80	#
81	# ==============================================================================================
82	# RUNTIME MODES
83	# ==============================================================================================
84	#
85	# Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership
86	#     Full transfer — owner detected automatically.
87	#
88	# Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership --dry-run
89	#     Preview all steps without executing. Confirmation check is skipped in dry-run mode.
90	#
91	# Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership --log
92	#     Verbose per-step output.
93	#
94	# ==============================================================================================
95
96	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
97	SCRIPTS_ROOT="$SCRIPT_DIR/.."
98	SSH_TIMEOUT=15
99
100	source "$SCRIPTS_ROOT/load_config.sh"
101
102	# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
103	TRANSFER_CONFIRM_INPUT=""
104	FILTERED_ARGS=()
105
106	for arg in "$@"; do
107	    case "$arg" in
108	        --confirm=*)  TRANSFER_CONFIRM_INPUT="${arg#--confirm=}" ;;
109	        *)            FILTERED_ARGS+=("$arg") ;;
110	    esac
111	done
112
113	parse_args "${FILTERED_ARGS[@]}"
114
115	# ── Source partnership_manager.sh for shared helpers ──────────────────────────────────────────
116	# PARTNERSHIP_LIB_MODE=1 skips mode dispatch — functions are defined, nothing is executed.
117	PARTNERSHIP_LIB_MODE=1 source "$SCRIPT_DIR/partnership_manager.sh"
118
119	# ==============================================================================================
120	# ━━━ Setup ━━━


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= SSH Setup ======================================================
4	# ==============================================================================================
5	# Generates the SSH keypair for rsync automation and installs it on the remote server.
6	# Key named after this server: hostname lowercased, unraid- prefix stripped.
7	#   unRAID-Gmer4Lfe → gmer4lfe_rsync_automation
8	#   unRAID-Jayred365 → jayred365_rsync_automation
9	# Idempotent — skips generation if key already exists (use --force to regenerate).
10	# Updates host*.conf with key path on success.
11	#
12	# ── MODES ─────────────────────────────────────────────────────────────────────────────────────
13	#   (default)       — generate key if missing, copy to remote, update conf
14	#   --force         — regenerate key even if it exists, re-copy to remote
15	#   --validate      — test SSH auth to remote, track strikes, notify at limit
16	#   --status        — show key state, fingerprint, remote connectivity
17	#   --dry-run       — preview without creating, copying, or updating conf
18	#
19	# ── STRIKE SYSTEM (--validate) ────────────────────────────────────────────────────────────────
20	# Called during partnership --check cycles to detect broken SSH auth.
21	# Tracks consecutive SSH auth failures — not network unreachability.
22	# Remote Tailscale IP unreachable = network issue → not counted as SSH strike.
23	# Remote reachable but SSH auth fails = key issue → strike incremented.
24	# Strikes reset automatically after SSH_STRIKE_RESET_HRS of clean connectivity.
25	# At SSH_MAX_STRIKES: notify + return exit 2 (caller can escalate).
26	# State: DATA_DIR/ssh_strikes_{REMOTE_SERVER_NAME}.db
27	#
28	# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
29	#   SSH_MAX_STRIKES       — consecutive failures before notifying (default 5)
30	#   SSH_STRIKE_RESET_HRS  — hours since last failure before counter resets (default 24)
31	#
32	# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
33	#   Partnership/ssh_setup.sh                — initial setup (idempotent)
34	#   Partnership/ssh_setup.sh --force        — regenerate + re-copy
35	#   Partnership/ssh_setup.sh --validate     — health check + strike tracking
36	#   Partnership/ssh_setup.sh --status       — show key and connectivity state
37	#   Partnership/ssh_setup.sh --local-only   — generate key locally, skip remote copy
38	#   Any mode supports --dry-run and --log
39	# ==============================================================================================
40
41	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
42	SCRIPTS_ROOT="$SCRIPT_DIR/.."
43
44	source "$SCRIPTS_ROOT/load_config.sh"
45
46	# ── Parse --force before parse_args ───────────────────────────────────────────────────────────
47	MODE="setup"
48	FORCE=false
49	LOCAL_ONLY=false
50	FILTERED_ARGS=()
51
52	for arg in "$@"; do
53	    case "$arg" in
54	        --force)      FORCE=true ;;
55	        --validate)   MODE="validate" ;;
56	        --status)     MODE="status" ;;
57	        --local-only) LOCAL_ONLY=true ;;
58	        *)            FILTERED_ARGS+=("$arg") ;;
59	    esac
60	done
61
62	parse_args "${FILTERED_ARGS[@]}"
63
64	# ==============================================================================================
65	# ━━━ Setup ━━━
66	# ==============================================================================================
67	if [[ "$EUID" -ne 0 ]]; then
68	    error "Must be run as root"
69	    exit 1
70	fi
71
72	acquire_lock
73
74	detect_hosts
75
76	# ── Derive key name from hostname ─────────────────────────────────────────────────────────────
77	# Strip unraid- prefix (case-insensitive) if present → gmer4lfe_rsync_automation
78	SHORT_NAME="${LOCAL_SERVER_NAME,,}"
79	[[ "${SHORT_NAME}" == unraid-* ]] && SHORT_NAME="${SHORT_NAME:7}"
80	SSH_KEY_NAME="${SHORT_NAME}_rsync_automation"
81	SSH_KEY_PATH="/root/.ssh/${SSH_KEY_NAME}"
82	SSH_PUB_PATH="${SSH_KEY_PATH}.pub"
83
84	# ── Host conf path ────────────────────────────────────────────────────────────────────────────
85	HOST_NUM="${MY_ID#HOST}"   # "1" or "2"
86	HOST_CONF="$SCRIPTS_ROOT/Configurations/host${HOST_NUM}.conf"
87	KEY_CONF_VAR="${MY_ID}_SSH_KEY"
88
89	# ── Strike state file ─────────────────────────────────────────────────────────────────────────
90	SSH_STRIKE_FILE="$DATA_DIR/ssh_strikes_${REMOTE_SERVER_NAME}.db"
91
92	# ==============================================================================================
93	# ─── HELPERS ──────────────────────────────────────────────────────────────────────────────────
94	# ==============================================================================================
95
96	test_ssh_auth() {
97	    local remote_ip="$1"
98	    timeout "${SSH_TIMEOUT:-15}" ssh \
99	        -i "$SSH_KEY_PATH" \
100	        -o BatchMode=yes \
101	        -o ConnectTimeout="${SSH_TIMEOUT:-15}" \
102	        -o StrictHostKeyChecking=no \
103	        root@"$remote_ip" "echo ok" >/dev/null 2>&1
104	}
105
106	read_strikes() {
107	    grep "^strikes=" "$SSH_STRIKE_FILE" 2>/dev/null | cut -d= -f2 || echo 0
108	}
109
110	read_last_strike_epoch() {
111	    local ts
112	    ts=$(grep "^last_strike=" "$SSH_STRIKE_FILE" 2>/dev/null | cut -d= -f2)
113	    [[ -n "$ts" ]] && date -d "$ts" +%s 2>/dev/null || echo 0
114	}
115
116	write_strike_file() {
117	    local strikes="$1" last_strike="${2:-}" last_success="${3:-}"
118	    mkdir -p "$(dirname "$SSH_STRIKE_FILE")"
119	    cat > "$SSH_STRIKE_FILE" <<EOF
120	strikes=${strikes}


1	#!/bin/bash
2	# ==============================================================================================
3	# ============================= Delete Keys ====================================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Removes SSH keys between HOST1 and HOST2 in the specified direction.
9	# Safe to run at any phase. Clears related setup.db flags.
10	#
11	# ==============================================================================================
12	# RUNTIME MODES
13	# ==============================================================================================
14	#
15	# Partnership/onboard_cancel.sh --direction=h1   (default)
16	#     HOST1 → HOST2: remove HOST1's public key from HOST2's authorized_keys,
17	#     delete local HOST1 key pair, clear HOST2 phase flags from setup.db.
18	#
19	# Partnership/onboard_cancel.sh --direction=h2
20	#     HOST2 → HOST1: remove HOST2's public key from HOST1's authorized_keys.
21	#     Identifies the key by HOST2's hostname in the key comment.
22	#
23	# Partnership/onboard_cancel.sh --direction=both
24	#     Both directions.
25	#
26	# Partnership/onboard_cancel.sh --dry-run
27	#     Preview without making changes.
28	#
29	# ==============================================================================================
30
31	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32	SCRIPTS_ROOT="$SCRIPT_DIR/.."
33	SSH_TIMEOUT=15
34	DIRECTION="h1"
35	FILTERED_ARGS=()
36
37	for arg in "$@"; do
38	    case "$arg" in
39	        --direction=*) DIRECTION="${arg#--direction=}" ;;
40	        *)             FILTERED_ARGS+=("$arg") ;;
41	    esac
42	done
43
44	source "$SCRIPTS_ROOT/load_config.sh"
45	parse_args "${FILTERED_ARGS[@]}"
46
47	[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
48
49	acquire_lock
50
51	detect_hosts
52
53	OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
54	MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
55	MIRROR="${!MIRROR_ID}"
56	SSH_KEY_PUB="${SSH_KEY}.pub"
57	STATE_FILE="$(platform_setup_db_path)"
58	AUTH_KEYS="/root/.ssh/authorized_keys"
59	MIRROR_SHORT="${MIRROR%%.*}"
60
61	START=$(date +%s)
62
63	echo ""
64	echo "━━━ Delete Keys — direction:${DIRECTION} — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
65	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
66	echo ""
67
68	H1_DONE=false
69	H2_DONE=false
70
71	# ── HOST1 → HOST2: remove HOST1's key from HOST2 + delete local pair ──────────
72	if [[ "$DIRECTION" == "h1" || "$DIRECTION" == "both" ]]; then
73	    echo "━━━ HOST1 → HOST2: Remove HOST1 key from $MIRROR ━━━"
74
75	    if [[ ! -f "$SSH_KEY_PUB" ]]; then
76	        log "No local public key — nothing to remove from $MIRROR"
77	        H1_DONE=true
78	    else
79	        KEY_BLOB=$(awk '{print $2}' "$SSH_KEY_PUB")
80	        MIRROR_IP=$(resolve_tailscale_ip "$MIRROR" 2>/dev/null || true)
81
82	        if [[ -z "$MIRROR_IP" ]]; then
83	            warn "Cannot resolve $MIRROR Tailscale IP — remove HOST1 key from $MIRROR manually"
84	        elif [[ "$DRY_RUN" == true ]]; then
85	            warn "DRY RUN — would remove HOST1 key from $MIRROR:/root/.ssh/authorized_keys"
86	            H1_DONE=true
87	        else
88	            timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
89	                -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
90	                "sed -i \"|${KEY_BLOB}|d\" /root/.ssh/authorized_keys 2>/dev/null
91	                 sed -i \"/^${MIRROR_ID}_PHASE\|^${MIRROR_ID}_KEY_READY/d\" $(platform_setup_db_path) 2>/dev/null
92	                 echo ok" 2>/dev/null | grep -q ok && {
93	                log "HOST1 key removed from $MIRROR authorized_keys ✅"
94	                H1_DONE=true
95	            } || warn "Could not SSH to $MIRROR — remove HOST1 key there manually"
96	        fi
97	    fi
98
99	    # Delete local key pair
100	    if [[ ! -f "$SSH_KEY" && ! -f "$SSH_KEY_PUB" ]]; then
101	        log "Local key already gone"
102	    elif [[ "$DRY_RUN" == true ]]; then
103	        warn "DRY RUN — would delete: $SSH_KEY and ${SSH_KEY}.pub"
104	    else
105	        rm -f "$SSH_KEY" "$SSH_KEY_PUB" && log "Local key pair deleted ✅" || \
106	            warn "Failed to delete local key — check permissions"
107	    fi
108
109	    # Clear HOST2 phase flags from local setup.db
110	    if [[ -f "$STATE_FILE" ]]; then
111	        if [[ "$DRY_RUN" == false ]]; then
112	            sed -i "/^${MIRROR_ID}_PHASE/d; /^${MIRROR_ID}_KEY_READY/d" "$STATE_FILE"
113	            log "Phase flags cleared from local setup.db ✅"
114	        else
115	            warn "DRY RUN — would clear ${MIRROR_ID}_PHASE* from setup.db"
116	        fi
117	    fi
118	    echo ""
119	fi
120


Now let me read the rest of the onboard_cancel.sh and get the complete picture of what the scripts actually do.


Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.


Based on my thorough review of the documentation and script headers, here are the discrepancies I found:

---

## DISCREPANCIES REPORT

**README-Partnership.md (line 143) vs partnership_onboard.sh header:**
README lists only 4 scripts in the table; missing `partnership_transfer.sh` and `onboard_cancel.sh`. Header for onboard mentions "Steps 5-6" for arr and "Step 6" for services, but the README doesn't document the services stack at all. The README's operational model diagram shows only 6 onboard steps on owner side, but the script header lists 10 steps including services stack deployment and conf push.

**README-Partnership.md (line 110-111) vs partnership_manager.sh and partnership_offboard.sh headers:**
README claims `partnership_offboard.sh` is "Called by `partnership_manager.sh --offboard` but runnable directly" and Manual (line 412) says "Called automatically by partnership_manager.sh --offboard. Can also be run directly." However, the actual script headers and structure show that offboard holds its own lock and is exec'd by the manager. The description is accurate but could be clearer about the lock ownership.

**Manual-Partnership.md (line 143) vs partnership_onboard.sh header:**
Manual lists `partnership_transfer.sh` in the script table with "When It Runs: Via partnership_manager.sh --offboard; or directly" but `partnership_transfer.sh` is NOT listed under "When It Runs" column at all — this appears to be a copy-paste error where offboard text is used instead. The transfer script is a separate script with its own entry needed.

**Manual-Partnership.md (line 379-393) vs actual scripts:**
Manual describes partnership_onboard.sh output tiers with "Without `--log`, step headers, per-step result lines (deployed/failed counts)..." but the script header (lines 14-32) describes 10 steps including a services stack deploy (Step 6-7) that isn't mentioned in the README's operational model (which only shows 8 steps).

**Manual-Partnership.md (line 145 - Missing Script) vs filesystem:**
The README script table is missing `onboard_cancel.sh` (labeled "Delete Keys" in actual script at line 3 of onboard_cancel.sh). This script exists and is executable but has no entry in the documentation table.

**README-Partnership.md (line 96-106) vs partnership_onboard.sh header:**
README says onboard includes "arr bootstrap" and "stops mirror arr containers," but doesn't mention the services stack (Emby, Jellyfin, Seerr). The script header shows Steps 4-7 include arr AND services stacks, but docs only reference arr.

---

**Summary:** The documentation is missing the services stack entirely from the operational model and overview, missing `onboard_cancel.sh`/`partnership_transfer.sh` from script tables, and the onboard step counts don't align (8 in README diagram vs 10 in script header). All script headers are internally consistent with their actual functionality; the docs lag behind implementation.