1	#!/bin/bash
2	# ==============================================================================================
3	# ========================== HOSTN CONFIGURATION — (hostname) ==================================
4	# ==============================================================================================
5	# HOSTN-specific variables — credentials, container names, share paths, failover lists.
6	# Sourced after master.conf — values here extend shared profile arrays and add HOSTN-specific
7	# identity, credentials, and container configuration.
8	#
9	# Sparse checkout (git) ensures other hosts never receive this file.
10	#
11	# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
12	# DO NOT put other hosts' variables here — they belong in their own host*.conf files.
13	#
14	# ── HOW TO USE THIS TEMPLATE ──────────────────────────────────────────────────────────────────
15	# This file was generated by the Varaverk first-run wizard.
16	# Fill in the sections that apply to your setup — leave unused sections empty.
17	# All scripts self-guard against empty values — safe to leave sections blank until needed.
18	#
19	# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
20	#
21	#   ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
22	#   IDENTITY                hostname, SSH key
23	#   EMBY                    container name, URL, API key
24	#   NOTIFICATIONS           Discord webhook
25	#   PARTNERSHIP             auth containers, backup paths
26	#
27	#   ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
28	#   DAILY SYNC SHARES       media shares this host owns and pushes
29	#   WEEKLY SYNC SHARES      appdata shares synced weekly
30	#   CRITICAL SYNC SHARES    appdata shares synced every 30 minutes
31	#   HOSTN RSYNC PROFILE     host-specific appdata sync profile
32	#
33	#   ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
34	#   DOCKER DAILY RESTART    containers restarted daily
35	#   DOCKER WEEKLY RESTART   containers restarted weekly
36	#   DOCKER WATCHDOG         memory limits, health URLs, required containers
37	#   DOCKER NETWORK CONNECT  networks and containers for array start
38	#
39	#   ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
40	#   DDNS                    DDNS containers managed by this host
41	#   INTERNET LOSS           containers stopped when internet is lost
42	#   FALLBACK TIERS          what this host runs for the remote per tier
43	#   TIER DELAYS             delays before each tier activates
44	#   RSYNC WRITEBACK         appdata synced back on handback
45	#
46	#   ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
47	#   MEDIA PERMISSIONS       share list for permissions script
48	#   MEDIA CLEANER           folder lists for media_cleaner.sh
49	#
50	#   ── MONITORS ───────────────────────────────────────────────────────────────────────────────
51	#   CERTIFICATE MONITOR     domains checked for SSL expiry
52	#   SMART HEALTH            drives to skip in SMART monitoring
53	#   ZFS REPORT              pools to exclude from ZFS health report
54	#
55	#   ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
56	#   TRANSCODES              ramdisk size, thresholds, SSD path, server array
57	#
58	#   ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
59	#   DOWNLOADERS             slskd, SABnzbd, qBittorrent credentials and URLs
60	#   LIDARR / SONARR / RADARR  URL, API key, path map
61	#   ARR RECOVERY            per-arr recovery toggles
62	#
63	#   ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
64	#   SYSTEM WATCHDOG         per-host check toggles and NIC configuration
65	#
66	#   ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
67	#   RESOURCE MANAGER        containers paused/stopped under memory pressure
68	#
69	# ==============================================================================================
70
71	# ==============================================================================================
72	# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
73	# ==============================================================================================
74
75	# ━━━ Storage mode ━━━
76	# Controls where Varaverk stores scripts, conf, and state files.
77	# true  = internal NVMe/SSD — /boot/config/plugins/varaverk  (write-safe, git-direct)
78	# false = USB flash boot    — /mnt/user/appdata/Varaverk      (preserves flash lifetime)
79	# Auto-detected from boot device transport on first setup.
80	# To change: Settings → Storage → Migrate.
81	    HOSTN_STORAGE_MODE_INTERNAL=true
82
83	# ==============================================================================================
84	# ── IDENTITY & CONNECTIVITY ───────────────────────────────────────────────────────────────────
85	# ==============================================================================================
86
87	# ━━━ Identity ━━━
88	# HOSTN hostname lives in master.conf (not a credential — safe for all servers).
89	# SSH key used for all server-to-server operations — rsync, failover, conf sync.
90	# Convention: /root/.ssh/<hostname-lowercase-no-unraid-prefix>_rsync_automation
91	# Must be in /root/.ssh/ and authorised in the partner's /root/.ssh/authorized_keys.
92	# Run Partnership/ssh_setup.sh to generate the key and copy it to the partner.
93	    HOSTN_SSH_KEY=""    # e.g. /root/.ssh/myserver_rsync_automation
94	    HOSTN_OWNER=""      # short identifier for this server (e.g. myserver)
95	    HOSTN_OWNER_EMAIL=""
96
97	# ━━━ Unraid API ━━━
98	# Used by the Varaverk plugin to query this server's Unraid GraphQL API.
99	# Generate in Unraid: Settings → Management Access → API Keys → + New Key
100	    HOSTN_UNRAID_API_KEY=""
101
102	# ━━━ Emby ━━━
103	    HOSTN_EMBY_CONTAINER="Emby"
104	    HOSTN_EMBY_URL="http://localhost:8096"
105	    HOSTN_EMBY_API_KEY=""   # Emby Dashboard → API Keys → + New Key
106
107	# ━━━ Jellyfin ━━━
108	    HOSTN_JELLYFIN_CONTAINER="Jellyfin"
109	    HOSTN_JELLYFIN_URL="http://localhost:8095"
110	    HOSTN_JELLYFIN_API_KEY=""   # Jellyfin Dashboard → Administration → API Keys
111
112	# ━━━ Gitea ━━━
113	# Personal access token for gitea_ssh_setup.sh.
114	# Create in Gitea: Settings → Applications → Generate Token → scope: write:user
115	    HOSTN_GITEA_API_TOKEN=""
116
117	# ━━━ Notifications ━━━
118	# Discord webhook — leave blank to disable.
119	    HOSTN_DISCORD_WEBHOOK=""
120
121	# ━━━ Partnership ━━━
122
123	# Auth containers reconfigured on onboard/offboard.
124	# Format: "ContainerName|WebUIPort"
125	    HOSTN_PARTNERSHIP_AUTH_WEBUIS=(
126	        # "NginxProxyManager|81"
127	        # "Authelia|9091"
128	    )
129
130	# XML templates pushed to mirror during onboard — auth stack.
131	# Dependencies (databases) must come before apps that depend on them.
132	    HOSTN_PARTNERSHIP_AUTH_STACK=(
133	        # "my-Authelia.xml"
134	        # "my-NginxProxyManager.xml"
135	    )
136
137	# XML templates pushed to mirror during onboard — arr stack.
138	    HOSTN_PARTNERSHIP_ARR_STACK=(
139	        # "my-Sonarr.xml"
140	        # "my-Radarr.xml"
141	    )
142
143	# Paths the partner should collect during the grace window after offboard.
144	    HOSTN_PARTNERSHIP_MIRROR_BACKUPS=(
145	        # "/mnt/user/appdata-Fallback/Partner-Emby"
146	    )
147
148	# Containers parked on this server when partnership is active.
149	    HOSTN_PARTNERSHIP_OWN_CONTAINERS=(
150	        # "Emby"
151	    )
152
153	# Emby admin provisioning — owner controls whether Emby is shared.
154	    HOSTN_PARTNERSHIP_PROVISION_EMBY_ADMIN=false
155	    HOSTN_PARTNERSHIP_EMBY_PORT=8096
156	    HOSTN_PARTNERSHIP_EMBY_ADMIN_USER=""
157	    HOSTN_PARTNERSHIP_EMBY_ADMIN_PASS=""
158
159	# ==============================================================================================
160	# ── RSYNC ─────────────────────────────────────────────────────────────────────────────────────
161	# ==============================================================================================
162
163	# ━━━ Daily Sync Shares ━━━
164	# Media shares this host pushes to all other nodes every night.
165	# Uses DEFAULT_RSYNC_OPTS from master.conf — no profile needed.
166	    HOSTN_DAILY_SYNC_SHARES=(
167	        # /mnt/user/Movies
168	        # /mnt/user/Tv_Shows
169	        # /mnt/user/Music
170	    )
171
172	# ━━━ Weekly Sync Shares ━━━
173	# Appdata shares synced during the weekly maintenance window.
174	# Profiles (emby, critical-data) drive container stops — define in master.conf.
175	    HOSTN_WEEKLY_SYNC_SHARES=(
176	        # "/mnt/user/Media_Server/Emby"              # emby profile
177	        # "/mnt/user/appdata-Fallback/Critical-Data" # critical-data profile
178	    )
179
180	# ━━━ Intermediate Sync Shares ━━━
181	# Shares synced every 4 hours. Leave empty to skip mid-day rsync.
182	    HOSTN_INTERMEDIATE_SYNC_SHARES=(
183	        # Add shares here to enable mid-day rsync
184	    )
185
186	# ━━━ Critical Sync Shares ━━━
187	# Appdata shares synced every 30 minutes.
188	# Format: "/path/to/share" or "/path/to/share|profile-name"
189	    HOSTN_CRITICAL_SYNC_SHARES=(
190	        # "/mnt/user/appdata-Fallback/Critical-Data|critical-fallback"
191	        # "/mnt/user/Media_Server/Emby|emby-fallback"
192	    )
193
194	# ━━━ Backup Verify ━━━
195	# Leave empty to use HOSTN_DAILY_SYNC_SHARES automatically.
196	    HOSTN_BACKUP_VERIFY_SHARES=(
197	        # leave empty to use HOSTN_DAILY_SYNC_SHARES automatically
198	    )
199
200	# ━━━ HOSTN Rsync Profile — hostn-appdata ━━━
201	# Host-specific appdata sync profile.
202	    PROFILE_RSYNC_OPTS[hostn-appdata]="-av --info=progress2 --bwlimit=${PROFILE_BW_LIMIT[hostn-appdata]:-8000}"
203	    PROFILE_BW_LIMIT[hostn-appdata]=8000
204	    PROFILE_RETRY_COUNT[hostn-appdata]=3
205	    PROFILE_SLEEP[hostn-appdata]=300
206	    PROFILE_CRITICAL_CONTAINER_NAMES[hostn-appdata]=""
207	    PROFILE_DELAYED_CONTAINERS[hostn-appdata]=""
208	    PROFILE_CONTAINER_DELAY[hostn-appdata]=5
209	    PROFILE_EXCLUDE_DIRS[hostn-appdata]="logs *.tmp"
210
211	# ==============================================================================================
212	# ── DOCKER ────────────────────────────────────────────────────────────────────────────────────
213	# ==============================================================================================
214
215	# ━━━ Docker Daily Restart ━━━
216	    HOSTN_DAILY_RESTART_CONTAINERS=(
217	        # "NginxProxyManager"
218	        # "Authelia"
219	    )
220
221	# ━━━ Docker Weekly Restart ━━━
222	    HOSTN_WEEKLY_RESTART_CONTAINERS=(
223	        # "NextCloud"
224	        # "AdGuard-Home"
225	    )
226
227	# ━━━ Docker Watchdog ━━━
228
229	# Memory hard limits in MB — immediate restart if exceeded.
230	# 20GB=20480  16GB=16384  8GB=8192  4GB=4096  2GB=2048  1GB=1024
231	    declare -A HOSTN_WATCHDOG_CONTAINERS=(
232	        # ["Emby"]=18432
233	    )
234
235	# HTTP health check URLs — checked every cycle.
236	    declare -A HOSTN_WATCHDOG_CONTAINER_URLS=(
237	        # ["Emby"]="http://localhost:8096"
238	    )
239
240	# Required containers — must always be running.
241	    HOSTN_WATCHDOG_REQUIRED_CONTAINERS=(
242	        # "NginxProxyManager"
243	        # "Authelia"
244	    )
245
246	# Containers to skip in Tier 2 global scan.
247	    HOSTN_WATCHDOG_SCAN_IGNORE=(
248	        # "my-occasional-container"
249	    )
250
251	# Dependency ordering — skip restarting a container if its dependency is also down.
252	    declare -A HOSTN_WATCHDOG_DEPENDENCIES=(
253	        # ["Authelia"]="Mariadb Redis-Authelia"
254	    )
255
256	# Per-container appdata growth suppress ceilings in MB.
257	    declare -A HOSTN_WATCHDOG_APPDATA_SIZES=(
258	        # ["Tdarr"]="25600"
259	    )
260
261	# ━━━ Network Watchdog ━━━
262	    HOSTN_NETWORK_WATCHDOG_DDNS_DOMAIN=""       # e.g. myserver.com
263	    HOSTN_NETWORK_WATCHDOG_DDNS_CONTAINER=""    # e.g. MyServer.com
264	    HOSTN_NETWORK_WATCHDOG_NPM_URL=""           # e.g. https://myserver.com
265
266	# ━━━ Docker Network Connect ━━━
267	    HOSTN_NETWORK_CONNECT_CONTAINERS=(
268	        # "memcached"
269	    )
270
271	    HOSTN_NETWORK_CONNECT_NETWORKS=(
272	        # "high-availability"
273	    )
274
275	# ==============================================================================================
276	# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
277	# ==============================================================================================
278
279	# ━━━ DDNS ━━━
280	# DDNS containers this host manages.
281	    HOSTN_DDNS_CONTAINERS=(
282	        # "MyServer.com"
283	    )
284
285	# ━━━ Internet Loss ━━━
286	# Containers stopped immediately when internet is lost.
287	    FALLBACK_HOSTN_STOP_ON_NO_NET=(
288	        # "MyServer.com"
289	    )
290
291	# ━━━ Fallback Tiers — HOSTN Runs for Partner ━━━
292	# Containers this host starts when the partner goes down.
293	# Replace REMOTE_ID below with the actual remote host ID (HOST1, HOST2, etc.)
294	    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER1=(
295	        # "Partner-DDNS-Container"
296	    )
297
298	    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER2=(
299	        # "container-placeholder"
300	    )
301
302	    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER3=(
303	        # "container-placeholder"
304	    )
305
306	    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER4=(
307	        # "container-placeholder"
308	    )
309
310	# ━━━ Tier Delays — Partner's Containers on this Host ━━━
311	# How long the partner must be down before each tier activates here — in minutes.
312	# Replace REMOTE_ID with the actual remote host ID (HOST1, HOST2, etc.)
313	    REMOTE_ID_TIER2_DELAY=240           # 4 hours
314	    REMOTE_ID_TIER3_DELAY=720           # 12 hours
315	    REMOTE_ID_TIER4_DELAY=1440          # 24 hours
316
317	# ━━━ Rsync Writeback ━━━
318	    HOSTN_TIER1_WRITEBACK_DELAY=60      # skip Emby writeback if outage under 1hr
319
320	    FALLBACK_HOSTN_WRITEBACK_TIER1=(
321	        # "/mnt/user/Media_Server/Emby"
322	    )
323
324	    FALLBACK_HOSTN_WRITEBACK_TIER2=(
325	        # "/mnt/user/appdata-Fallback/Important-Data"
326	    )
327
328	    FALLBACK_HOSTN_WRITEBACK_TIER3=(
329	        # "location-placeholder"
330	    )
331
332	    FALLBACK_HOSTN_WRITEBACK_TIER4=(
333	        # "/mnt/user/appdata-Fallback/Arrs_Stack"
334	    )
335
336	# ==============================================================================================
337	# ── MEDIA ─────────────────────────────────────────────────────────────────────────────────────
338	# ==============================================================================================
339
340	# ━━━ Media Permissions ━━━
341	    HOSTN_MEDIA_PERMISSION_SHARES=(
342	        # /mnt/user/Movies
343	        # /mnt/user/Tv_Shows
344	        # /mnt/user/Music
345	        # /mnt/user/Downloads
346	    )
347
348	# ━━━ Media Cleaner ━━━
349	    HOSTN_ANIME_CLEAN_FOLDERS=(
350	        # /mnt/user/Anime_Movies
351	        # /mnt/user/Anime_Shows
352	    )
353
354	    HOSTN_MEDIA_CLEAN_FOLDERS=(
355	        # /mnt/user/Movies
356	        # /mnt/user/Tv_Shows
357	    )
358
359	# ==============================================================================================
360	# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
361	# ==============================================================================================
362
363	# ━━━ Certificate Monitor ━━━
364	    HOSTN_CERT_MONITOR_DOMAINS=(
365	        # "myserver.com"
366	    )
367
368	# ━━━ SMART Health ━━━
369	    HOSTN_SMART_IGNORE_DRIVES=(
370	        "sda"   # boot USB — SMART not meaningful on flash drives
371	    )
372
373	# ━━━ ZFS Report ━━━
374	    HOSTN_ZFS_REPORT_IGNORE_POOLS=(
375	        # "disk5"
376	    )
377
378	# ==============================================================================================
379	# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
380	# ==============================================================================================
381
382	    HOSTN_RAMDISK_SIZE="10G"
383	    HOSTN_RAMDISK_WARN_GB=8.5
384	    HOSTN_RAMDISK_LOW_GB=7
385	    HOSTN_TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/"
386
387	    HOSTN_TRANSCODE_SERVERS=(
388	        "${HOSTN_EMBY_CONTAINER}|${HOSTN_EMBY_URL}|${HOSTN_EMBY_API_KEY}|emby"
389	    )
390
391	# ==============================================================================================
392	# ── ARR STACK ─────────────────────────────────────────────────────────────────────────────────
393	# ==============================================================================================
394
395	# ━━━ Downloaders ━━━
396	    HOSTN_SLSKD_URL="http://localhost:8980"
397	    HOSTN_SLSKD_API_KEY=""
398	    HOSTN_SLSKD_FAILED_IMPORTS_DIR=""
399
400	    HOSTN_SABNZBD_URL="http://localhost:8180"
401	    HOSTN_SABNZBD_API_KEY=""
402
403	    HOSTN_QBIT_URL="http://localhost:8080"
404	    HOSTN_QBIT_USERNAME="admin"
405	    HOSTN_QBIT_PASSWORD=""
406
407	# ━━━ Lidarr ━━━
408	    HOSTN_LIDARR_URL="http://localhost:8686"
409	    HOSTN_LIDARR_API_KEY=""
410	    HOSTN_LIDARR_MUSIC_ROOT="/mnt/user/Music"
411	    HOSTN_FANART_API_KEY=""
412	    HOSTN_LASTFM_API_KEY=""
413
414	    declare -A HOSTN_LIDARR_PATH_MAP=(
415	        # ["/music"]="/mnt/user/Music"
416	    )
417
418	# ━━━ Sonarr ━━━
419	    HOSTN_SONARR_URL="http://localhost:8989"
420	    HOSTN_SONARR_API_KEY=""
421	    HOSTN_SONARR_TV_ROOT="/mnt/user/Tv_Shows"
422
423	    declare -A HOSTN_SONARR_PATH_MAP=(
424	        # ["/tv"]="/mnt/user/Tv_Shows"
425	    )
426
427	# ━━━ Radarr ━━━
428	    HOSTN_RADARR_URL="http://localhost:7878"
429	    HOSTN_RADARR_API_KEY=""
430	    HOSTN_RADARR_MOVIE_ROOT="/mnt/user/Movies"
431
432	    declare -A HOSTN_RADARR_PATH_MAP=(
433	        # ["/movies"]="/mnt/user/Movies"
434	    )
435
436	# ━━━ Arr Recovery Toggles ━━━
437	    HOSTN_LIDARR_RECOVERY=false
438	    HOSTN_SONARR_RECOVERY=true
439	    HOSTN_RADARR_RECOVERY=true
440
441	# ==============================================================================================
442	# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
443	# ==============================================================================================
444
445	    HOSTN_SYS_WATCHDOG_CHECK_DOCKER=true
446	    HOSTN_SYS_WATCHDOG_CHECK_ROOTFS=true
447	    HOSTN_SYS_WATCHDOG_CHECK_RAM=true
448	    HOSTN_SYS_WATCHDOG_CHECK_LOAD=true
449	    HOSTN_SYS_WATCHDOG_CHECK_TEMP=true
450	    HOSTN_SYS_WATCHDOG_CHECK_ZOMBIES=true
451	    HOSTN_SYS_WATCHDOG_CHECK_LOG=true
452	    HOSTN_SYS_WATCHDOG_CHECK_TMP=true
453	    HOSTN_SYS_WATCHDOG_CHECK_FD=true
454	    HOSTN_SYS_WATCHDOG_CHECK_RUNAWAY=true
455
456	    HOSTN_SYS_WATCHDOG_NIC=""   # e.g. eth0 — for network monitoring
457
458	# ==============================================================================================
459	# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
460	# ==============================================================================================
461
462	    HOSTN_RW_PAUSE_CONTAINERS=(
463	        # "Tdarr"
464	        # "LidaTube"
465	    )
466
467	    HOSTN_RW_STOP_CONTAINERS=(
468	        # "Tdarr"
469	    )
470

#!/bin/bash
# ==============================================================================================
# ========================== HOSTN CONFIGURATION — (hostname) ==================================
# ==============================================================================================
# HOSTN-specific variables — credentials, container names, share paths, failover lists.
# Sourced after master.conf — values here extend shared profile arrays and add HOSTN-specific
# identity, credentials, and container configuration.
#
# Sparse checkout (git) ensures other hosts never receive this file.
#
# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
# DO NOT put other hosts' variables here — they belong in their own host*.conf files.
#
# ── HOW TO USE THIS TEMPLATE ──────────────────────────────────────────────────────────────────
# This file was generated by the Varaverk first-run wizard.
# Fill in the sections that apply to your setup — leave unused sections empty.
# All scripts self-guard against empty values — safe to leave sections blank until needed.
#
# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
#
#   ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
#   IDENTITY                hostname, SSH key
#   EMBY                    container name, URL, API key
#   NOTIFICATIONS           Discord webhook
#   PARTNERSHIP             auth containers, backup paths
#
#   ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
#   DAILY SYNC SHARES       media shares this host owns and pushes
#   WEEKLY SYNC SHARES      appdata shares synced weekly
#   CRITICAL SYNC SHARES    appdata shares synced every 30 minutes
#   HOSTN RSYNC PROFILE     host-specific appdata sync profile
#
#   ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
#   DOCKER DAILY RESTART    containers restarted daily
#   DOCKER WEEKLY RESTART   containers restarted weekly
#   DOCKER WATCHDOG         memory limits, health URLs, required containers
#   DOCKER NETWORK CONNECT  networks and containers for array start
#
#   ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
#   DDNS                    DDNS containers managed by this host
#   INTERNET LOSS           containers stopped when internet is lost
#   FALLBACK TIERS          what this host runs for the remote per tier
#   TIER DELAYS             delays before each tier activates
#   RSYNC WRITEBACK         appdata synced back on handback
#
#   ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
#   MEDIA PERMISSIONS       share list for permissions script
#   MEDIA CLEANER           folder lists for media_cleaner.sh
#
#   ── MONITORS ───────────────────────────────────────────────────────────────────────────────
#   CERTIFICATE MONITOR     domains checked for SSL expiry
#   SMART HEALTH            drives to skip in SMART monitoring
#   ZFS REPORT              pools to exclude from ZFS health report
#
#   ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
#   TRANSCODES              ramdisk size, thresholds, SSD path, server array
#
#   ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
#   DOWNLOADERS             slskd, SABnzbd, qBittorrent credentials and URLs
#   LIDARR / SONARR / RADARR  URL, API key, path map
#   ARR RECOVERY            per-arr recovery toggles
#
#   ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
#   SYSTEM WATCHDOG         per-host check toggles and NIC configuration
#
#   ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
#   RESOURCE MANAGER        containers paused/stopped under memory pressure
#
# ==============================================================================================

# ==============================================================================================
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Storage mode ━━━
# Controls where Varaverk stores scripts, conf, and state files.
# true  = internal NVMe/SSD — /boot/config/plugins/varaverk  (write-safe, git-direct)
# false = USB flash boot    — /mnt/user/appdata/Varaverk      (preserves flash lifetime)
# Auto-detected from boot device transport on first setup.
# To change: Settings → Storage → Migrate.
    HOSTN_STORAGE_MODE_INTERNAL=true

# ==============================================================================================
# ── IDENTITY & CONNECTIVITY ───────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Identity ━━━
# HOSTN hostname lives in master.conf (not a credential — safe for all servers).
# SSH key used for all server-to-server operations — rsync, failover, conf sync.
# Convention: /root/.ssh/<hostname-lowercase-no-unraid-prefix>_rsync_automation
# Must be in /root/.ssh/ and authorised in the partner's /root/.ssh/authorized_keys.
# Run Partnership/ssh_setup.sh to generate the key and copy it to the partner.
    HOSTN_SSH_KEY=""    # e.g. /root/.ssh/myserver_rsync_automation
    HOSTN_OWNER=""      # short identifier for this server (e.g. myserver)
    HOSTN_OWNER_EMAIL=""

# ━━━ Unraid API ━━━
# Used by the Varaverk plugin to query this server's Unraid GraphQL API.
# Generate in Unraid: Settings → Management Access → API Keys → + New Key
    HOSTN_UNRAID_API_KEY=""

# ━━━ Emby ━━━
    HOSTN_EMBY_CONTAINER="Emby"
    HOSTN_EMBY_URL="http://localhost:8096"
    HOSTN_EMBY_API_KEY=""   # Emby Dashboard → API Keys → + New Key

# ━━━ Jellyfin ━━━
    HOSTN_JELLYFIN_CONTAINER="Jellyfin"
    HOSTN_JELLYFIN_URL="http://localhost:8095"
    HOSTN_JELLYFIN_API_KEY=""   # Jellyfin Dashboard → Administration → API Keys

# ━━━ Gitea ━━━
# Personal access token for gitea_ssh_setup.sh.
# Create in Gitea: Settings → Applications → Generate Token → scope: write:user
    HOSTN_GITEA_API_TOKEN=""

# ━━━ Notifications ━━━
# Discord webhook — leave blank to disable.
    HOSTN_DISCORD_WEBHOOK=""

# ━━━ Partnership ━━━

# Auth containers reconfigured on onboard/offboard.
# Format: "ContainerName|WebUIPort"
    HOSTN_PARTNERSHIP_AUTH_WEBUIS=(
        # "NginxProxyManager|81"
        # "Authelia|9091"
    )

# XML templates pushed to mirror during onboard — auth stack.
# Dependencies (databases) must come before apps that depend on them.
    HOSTN_PARTNERSHIP_AUTH_STACK=(
        # "my-Authelia.xml"
        # "my-NginxProxyManager.xml"
    )

# XML templates pushed to mirror during onboard — arr stack.
    HOSTN_PARTNERSHIP_ARR_STACK=(
        # "my-Sonarr.xml"
        # "my-Radarr.xml"
    )

# Paths the partner should collect during the grace window after offboard.
    HOSTN_PARTNERSHIP_MIRROR_BACKUPS=(
        # "/mnt/user/appdata-Fallback/Partner-Emby"
    )

# Containers parked on this server when partnership is active.
    HOSTN_PARTNERSHIP_OWN_CONTAINERS=(
        # "Emby"
    )

# Emby admin provisioning — owner controls whether Emby is shared.
    HOSTN_PARTNERSHIP_PROVISION_EMBY_ADMIN=false
    HOSTN_PARTNERSHIP_EMBY_PORT=8096
    HOSTN_PARTNERSHIP_EMBY_ADMIN_USER=""
    HOSTN_PARTNERSHIP_EMBY_ADMIN_PASS=""

# ==============================================================================================
# ── RSYNC ─────────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Daily Sync Shares ━━━
# Media shares this host pushes to all other nodes every night.
# Uses DEFAULT_RSYNC_OPTS from master.conf — no profile needed.
    HOSTN_DAILY_SYNC_SHARES=(
        # /mnt/user/Movies
        # /mnt/user/Tv_Shows
        # /mnt/user/Music
    )

# ━━━ Weekly Sync Shares ━━━
# Appdata shares synced during the weekly maintenance window.
# Profiles (emby, critical-data) drive container stops — define in master.conf.
    HOSTN_WEEKLY_SYNC_SHARES=(
        # "/mnt/user/Media_Server/Emby"              # emby profile
        # "/mnt/user/appdata-Fallback/Critical-Data" # critical-data profile
    )

# ━━━ Intermediate Sync Shares ━━━
# Shares synced every 4 hours. Leave empty to skip mid-day rsync.
    HOSTN_INTERMEDIATE_SYNC_SHARES=(
        # Add shares here to enable mid-day rsync
    )

# ━━━ Critical Sync Shares ━━━
# Appdata shares synced every 30 minutes.
# Format: "/path/to/share" or "/path/to/share|profile-name"
    HOSTN_CRITICAL_SYNC_SHARES=(
        # "/mnt/user/appdata-Fallback/Critical-Data|critical-fallback"
        # "/mnt/user/Media_Server/Emby|emby-fallback"
    )

# ━━━ Backup Verify ━━━
# Leave empty to use HOSTN_DAILY_SYNC_SHARES automatically.
    HOSTN_BACKUP_VERIFY_SHARES=(
        # leave empty to use HOSTN_DAILY_SYNC_SHARES automatically
    )

# ━━━ HOSTN Rsync Profile — hostn-appdata ━━━
# Host-specific appdata sync profile.
    PROFILE_RSYNC_OPTS[hostn-appdata]="-av --info=progress2 --bwlimit=${PROFILE_BW_LIMIT[hostn-appdata]:-8000}"
    PROFILE_BW_LIMIT[hostn-appdata]=8000
    PROFILE_RETRY_COUNT[hostn-appdata]=3
    PROFILE_SLEEP[hostn-appdata]=300
    PROFILE_CRITICAL_CONTAINER_NAMES[hostn-appdata]=""
    PROFILE_DELAYED_CONTAINERS[hostn-appdata]=""
    PROFILE_CONTAINER_DELAY[hostn-appdata]=5
    PROFILE_EXCLUDE_DIRS[hostn-appdata]="logs *.tmp"

# ==============================================================================================
# ── DOCKER ────────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Docker Daily Restart ━━━
    HOSTN_DAILY_RESTART_CONTAINERS=(
        # "NginxProxyManager"
        # "Authelia"
    )

# ━━━ Docker Weekly Restart ━━━
    HOSTN_WEEKLY_RESTART_CONTAINERS=(
        # "NextCloud"
        # "AdGuard-Home"
    )

# ━━━ Docker Watchdog ━━━

# Memory hard limits in MB — immediate restart if exceeded.
# 20GB=20480  16GB=16384  8GB=8192  4GB=4096  2GB=2048  1GB=1024
    declare -A HOSTN_WATCHDOG_CONTAINERS=(
        # ["Emby"]=18432
    )

# HTTP health check URLs — checked every cycle.
    declare -A HOSTN_WATCHDOG_CONTAINER_URLS=(
        # ["Emby"]="http://localhost:8096"
    )

# Required containers — must always be running.
    HOSTN_WATCHDOG_REQUIRED_CONTAINERS=(
        # "NginxProxyManager"
        # "Authelia"
    )

# Containers to skip in Tier 2 global scan.
    HOSTN_WATCHDOG_SCAN_IGNORE=(
        # "my-occasional-container"
    )

# Dependency ordering — skip restarting a container if its dependency is also down.
    declare -A HOSTN_WATCHDOG_DEPENDENCIES=(
        # ["Authelia"]="Mariadb Redis-Authelia"
    )

# Per-container appdata growth suppress ceilings in MB.
    declare -A HOSTN_WATCHDOG_APPDATA_SIZES=(
        # ["Tdarr"]="25600"
    )

# ━━━ Network Watchdog ━━━
    HOSTN_NETWORK_WATCHDOG_DDNS_DOMAIN=""       # e.g. myserver.com
    HOSTN_NETWORK_WATCHDOG_DDNS_CONTAINER=""    # e.g. MyServer.com
    HOSTN_NETWORK_WATCHDOG_NPM_URL=""           # e.g. https://myserver.com

# ━━━ Docker Network Connect ━━━
    HOSTN_NETWORK_CONNECT_CONTAINERS=(
        # "memcached"
    )

    HOSTN_NETWORK_CONNECT_NETWORKS=(
        # "high-availability"
    )

# ==============================================================================================
# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ DDNS ━━━
# DDNS containers this host manages.
    HOSTN_DDNS_CONTAINERS=(
        # "MyServer.com"
    )

# ━━━ Internet Loss ━━━
# Containers stopped immediately when internet is lost.
    FALLBACK_HOSTN_STOP_ON_NO_NET=(
        # "MyServer.com"
    )

# ━━━ Fallback Tiers — HOSTN Runs for Partner ━━━
# Containers this host starts when the partner goes down.
# Replace REMOTE_ID below with the actual remote host ID (HOST1, HOST2, etc.)
    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER1=(
        # "Partner-DDNS-Container"
    )

    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER2=(
        # "container-placeholder"
    )

    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER3=(
        # "container-placeholder"
    )

    FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER4=(
        # "container-placeholder"
    )

# ━━━ Tier Delays — Partner's Containers on this Host ━━━
# How long the partner must be down before each tier activates here — in minutes.
# Replace REMOTE_ID with the actual remote host ID (HOST1, HOST2, etc.)
    REMOTE_ID_TIER2_DELAY=240           # 4 hours
    REMOTE_ID_TIER3_DELAY=720           # 12 hours
    REMOTE_ID_TIER4_DELAY=1440          # 24 hours

# ━━━ Rsync Writeback ━━━
    HOSTN_TIER1_WRITEBACK_DELAY=60      # skip Emby writeback if outage under 1hr

    FALLBACK_HOSTN_WRITEBACK_TIER1=(
        # "/mnt/user/Media_Server/Emby"
    )

    FALLBACK_HOSTN_WRITEBACK_TIER2=(
        # "/mnt/user/appdata-Fallback/Important-Data"
    )

    FALLBACK_HOSTN_WRITEBACK_TIER3=(
        # "location-placeholder"
    )

    FALLBACK_HOSTN_WRITEBACK_TIER4=(
        # "/mnt/user/appdata-Fallback/Arrs_Stack"
    )

# ==============================================================================================
# ── MEDIA ─────────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Media Permissions ━━━
    HOSTN_MEDIA_PERMISSION_SHARES=(
        # /mnt/user/Movies
        # /mnt/user/Tv_Shows
        # /mnt/user/Music
        # /mnt/user/Downloads
    )

# ━━━ Media Cleaner ━━━
    HOSTN_ANIME_CLEAN_FOLDERS=(
        # /mnt/user/Anime_Movies
        # /mnt/user/Anime_Shows
    )

    HOSTN_MEDIA_CLEAN_FOLDERS=(
        # /mnt/user/Movies
        # /mnt/user/Tv_Shows
    )

# ==============================================================================================
# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Certificate Monitor ━━━
    HOSTN_CERT_MONITOR_DOMAINS=(
        # "myserver.com"
    )

# ━━━ SMART Health ━━━
    HOSTN_SMART_IGNORE_DRIVES=(
        "sda"   # boot USB — SMART not meaningful on flash drives
    )

# ━━━ ZFS Report ━━━
    HOSTN_ZFS_REPORT_IGNORE_POOLS=(
        # "disk5"
    )

# ==============================================================================================
# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

    HOSTN_RAMDISK_SIZE="10G"
    HOSTN_RAMDISK_WARN_GB=8.5
    HOSTN_RAMDISK_LOW_GB=7
    HOSTN_TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/"

    HOSTN_TRANSCODE_SERVERS=(
        "${HOSTN_EMBY_CONTAINER}|${HOSTN_EMBY_URL}|${HOSTN_EMBY_API_KEY}|emby"
    )

# ==============================================================================================
# ── ARR STACK ─────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# ━━━ Downloaders ━━━
    HOSTN_SLSKD_URL="http://localhost:8980"
    HOSTN_SLSKD_API_KEY=""
    HOSTN_SLSKD_FAILED_IMPORTS_DIR=""

    HOSTN_SABNZBD_URL="http://localhost:8180"
    HOSTN_SABNZBD_API_KEY=""

    HOSTN_QBIT_URL="http://localhost:8080"
    HOSTN_QBIT_USERNAME="admin"
    HOSTN_QBIT_PASSWORD=""

# ━━━ Lidarr ━━━
    HOSTN_LIDARR_URL="http://localhost:8686"
    HOSTN_LIDARR_API_KEY=""
    HOSTN_LIDARR_MUSIC_ROOT="/mnt/user/Music"
    HOSTN_FANART_API_KEY=""
    HOSTN_LASTFM_API_KEY=""

    declare -A HOSTN_LIDARR_PATH_MAP=(
        # ["/music"]="/mnt/user/Music"
    )

# ━━━ Sonarr ━━━
    HOSTN_SONARR_URL="http://localhost:8989"
    HOSTN_SONARR_API_KEY=""
    HOSTN_SONARR_TV_ROOT="/mnt/user/Tv_Shows"

    declare -A HOSTN_SONARR_PATH_MAP=(
        # ["/tv"]="/mnt/user/Tv_Shows"
    )

# ━━━ Radarr ━━━
    HOSTN_RADARR_URL="http://localhost:7878"
    HOSTN_RADARR_API_KEY=""
    HOSTN_RADARR_MOVIE_ROOT="/mnt/user/Movies"

    declare -A HOSTN_RADARR_PATH_MAP=(
        # ["/movies"]="/mnt/user/Movies"
    )

# ━━━ Arr Recovery Toggles ━━━
    HOSTN_LIDARR_RECOVERY=false
    HOSTN_SONARR_RECOVERY=true
    HOSTN_RADARR_RECOVERY=true

# ==============================================================================================
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
# ==============================================================================================

    HOSTN_SYS_WATCHDOG_CHECK_DOCKER=true
    HOSTN_SYS_WATCHDOG_CHECK_ROOTFS=true
    HOSTN_SYS_WATCHDOG_CHECK_RAM=true
    HOSTN_SYS_WATCHDOG_CHECK_LOAD=true
    HOSTN_SYS_WATCHDOG_CHECK_TEMP=true
    HOSTN_SYS_WATCHDOG_CHECK_ZOMBIES=true
    HOSTN_SYS_WATCHDOG_CHECK_LOG=true
    HOSTN_SYS_WATCHDOG_CHECK_TMP=true
    HOSTN_SYS_WATCHDOG_CHECK_FD=true
    HOSTN_SYS_WATCHDOG_CHECK_RUNAWAY=true

    HOSTN_SYS_WATCHDOG_NIC=""   # e.g. eth0 — for network monitoring

# ==============================================================================================
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
# ==============================================================================================

    HOSTN_RW_PAUSE_CONTAINERS=(
        # "Tdarr"
        # "LidaTube"
    )

    HOSTN_RW_STOP_CONTAINERS=(
        # "Tdarr"
    )


how much of the host.conf does that take care of


Here's the full breakdown:

**Auto-detected — conf_populate.sh (22 fields)**
```
OWNER                           ✅
RADARR_API_KEY / URL / MOVIE_ROOT  ✅
SONARR_API_KEY / URL / TV_ROOT     ✅
LIDARR_API_KEY / URL / MUSIC_ROOT  ✅
SABNZBD_API_KEY / URL           ✅
SLSKD_API_KEY / URL             ✅
QBIT_URL / USERNAME / PASSWORD  ✅
EMBY_CONTAINER / URL            ✅
JELLYFIN_CONTAINER / URL        ✅
TRANSCODE_SSD                   ✅
SYS_WATCHDOG_NIC                ✅
```

**Setup wizard — setup.php (3 fields)**
```
STORAGE_MODE_INTERNAL   ✅  boot transport detection
SSH_KEY                 ✅  derived from hostname on host.conf creation
UNRAID_API_KEY          ✅  Create API Key button in wizard
```

**Has working defaults — safe to leave alone (~20 fields)**
```
All SYS_WATCHDOG_CHECK_* flags   (all default true)
RAMDISK_SIZE / WARN_GB / LOW_GB
LIDARR/SONARR/RADARR_RECOVERY
TIER1_WRITEBACK_DELAY
PARTNERSHIP_PROVISION_EMBY_ADMIN / EMBY_PORT
Rsync profile settings, tier delays
```

**Must fill — but feature-gated (safe to leave blank until you use that feature)**
```
EMBY_API_KEY / JELLYFIN_API_KEY     media server integration
DISCORD_WEBHOOK                     notifications
OWNER_EMAIL                         email alerts
FANART_API_KEY / LASTFM_API_KEY     Lidarr metadata
SLSKD_FAILED_IMPORTS_DIR            slskd cleanup
GITEA_API_TOKEN                     git automation
PARTNERSHIP_AUTH_WEBUIS/STACK/etc.  only needed for partnership
All sync shares                     only needed for rsync
Docker watchdog/restart arrays      optional tuning
Fallback tier arrays                only needed for failover
Media cleaner/cert/SMART paths      optional monitors
```

**Bottom line:** the ~5 fields you'd actually need to touch on day one for a working system are `EMBY_API_KEY`, `DISCORD_WEBHOOK`, and the sync share arrays if you want rsync running. Everything that can be read from a running service already is.


does this change anything with the wizard


One thing — the guide text in step 2 currently says:

> *"Fill in credentials in host.conf — (Emby key, share paths, Discord webhook)"*

That was written before the expanded detection. Now we know exactly what's left on day one, so the guide can be precise instead of vague. Update it to name the actual remaining fields:


Edit

Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf and fill in your credentials

            (Emby key, share paths, Discord webhook)

If partnering: run Partnership/partnership_onboard.sh once both servers are ready

Arr keys, container names, and media paths auto-populate on next array start

Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf and fill in the remaining fields:

              EMBY_API_KEY — Emby Dashboard → API Keys → + New Key

              DISCORD_WEBHOOK — for notifications (optional)

              DAILY_SYNC_SHARES — media paths you want rsync'd nightly

              Everything else was auto-populated or has working defaults

If partnering: run Partnership/partnership_onboard.sh once both servers are ready


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


230
231

232
233	        Go to Scheduler →
234
235

236

237
238

239
240

Go to Scheduler →


195

196
Step 2 of 2

197
198
199

200

201	        ⟳ Running auto-populate…
202

203

204
205
206

207	      Quick start
208

209
Create your Unraid API key below — needed for live monitor stats

210
Open Scheduler → Edit host.conf and fill in your credentials

211	            (Emby key, share paths, Discord webhook)

212
If partnering: run Partnership/partnership_onboard.sh once both servers are ready

213
Arr keys, container names, and media paths auto-populate on next array start

214

215

216

Step 2 of 2

        ⟳ Running auto-populate…

      Quick start

Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf and fill in your credentials

            (Emby key, share paths, Discord webhook)

If partnering: run Partnership/partnership_onboard.sh once both servers are ready

Arr keys, container names, and media paths auto-populate on next array start


Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf and fill in your credentials

            (Emby key, share paths, Discord webhook)

If partnering: run Partnership/partnership_onboard.sh once both servers are ready

Arr keys, container names, and media paths auto-populate on next array start

Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf — only three things need manual entry:

              EMBY_API_KEY — Emby Dashboard → API Keys → + New Key

              DISCORD_WEBHOOK — for notifications (optional)

              DAILY_SYNC_SHARES — media paths to rsync nightly

              Everything else was auto-populated or has working defaults

If partnering: run Partnership/partnership_onboard.sh once both servers are ready


The file /boot/config/plugins/varaverk/Plugin/unraid/pages/setup.php has been updated successfully. (file state is current in your context — no need to Read it back)

Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf and fill in your credentials

            (Emby key, share paths, Discord webhook)

If partnering: run Partnership/partnership_onboard.sh once both servers are ready

Arr keys, container names, and media paths auto-populate on next array start

Create your Unraid API key below — needed for live monitor stats

Open Scheduler → Edit host.conf — only three things need manual entry:

              EMBY_API_KEY — Emby Dashboard → API Keys → + New Key

              DISCORD_WEBHOOK — for notifications (optional)

              DAILY_SYNC_SHARES — media paths to rsync nightly

              Everything else was auto-populated or has working defaults

If partnering: run Partnership/partnership_onboard.sh once both servers are ready

-
Open Scheduler → Edit host.conf and fill in your credentials

-            (Emby key, share paths, Discord webhook)

+
Open Scheduler → Edit host.conf — only three things need manual entry:

+              EMBY_API_KEY — Emby Dashboard → API Keys → + New Key

+              DISCORD_WEBHOOK — for notifications (optional)

+              DAILY_SYNC_SHARES — media paths to rsync nightly

+              Everything else was auto-populated or has working defaults

-
Arr keys, container names, and media paths auto-populate on next array start


That's the only thing it changes. The rest of the wizard — detection banner, populate call, checklist, API key button — all still work the same. The guide just went from a vague "fill in credentials" prompt to three named fields with instructions, which is accurate now that populate handles everything else.


we could auto create unraid api and push automatically during initial


Let me check what `create_api_key.php` does so we know how to fold it in.


1	<?php
2	header('Content-Type: application/json');
3	header('Cache-Control: no-store, no-cache');
4	require_once dirname(__DIR__) . '/include/config.php';
5
6	$host = vv_detect_host();
7	if (!preg_match('/^host\d+$/', $host)) {
8	    echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']);
9	    exit;
10	}
11
12	$hostUpper = strtoupper($host);
13	$varName   = $hostUpper . '_UNRAID_API_KEY';
14	$confFile  = $host . '.conf';
15
16	// Create/overwrite the Varaverk API key.
17	// --description and --roles are required to suppress interactive prompts.
18	// --overwrite replaces any existing key with the same name (keeps it to one).
19	$dbg = ['ts' => date('H:i:s'), 'user' => trim(shell_exec('whoami'))];
20	$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
21	$dbg['raw'] = $output;
22	file_put_contents('/tmp/vv_apikey_debug.json', json_encode($dbg, JSON_PRETTY_PRINT));
23
24	if (!$output) {
25	    echo json_encode(['ok' => false, 'error' => 'unraid-api returned no output — check /tmp/vv_apikey_debug.json']);
26	    exit;
27	}
28
29	$data = json_decode(trim($output), true);
30	if (!is_array($data)) {
31	    echo json_encode(['ok' => false, 'error' => 'Could not parse unraid-api output', 'raw' => substr($output, 0, 300)]);
32	    exit;
33	}
34
35	$key = $data['key'] ?? null;
36	if (!$key) {
37	    echo json_encode(['ok' => false, 'error' => 'No key in response', 'raw' => substr($output, 0, 300)]);
38	    exit;
39	}
40
41	// Read conf, replace the key value, write back
42	$raw = vv_read_conf_raw($confFile);
43	if ($raw === '') {
44	    echo json_encode(['ok' => false, 'error' => 'Cannot read ' . $confFile]);
45	    exit;
46	}
47
48	// If line is missing (older conf created before this field was added to the template),
49	// insert it after HOST*_OWNER_EMAIL, or after HOST*_SSH_KEY, or append to file.
50	if (!str_contains($raw, $varName)) {
51	    $inserted = false;
52	    foreach ([$hostUpper . '_OWNER_EMAIL', $hostUpper . '_SSH_KEY'] as $anchor) {
53	        if (str_contains($raw, $anchor)) {
54	            $raw = preg_replace(
55	                '/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
56	                '$1' . "\n    " . $varName . '=""',
57	                $raw, 1
58	            );
59	            $inserted = true;
60	            break;
61	        }
62	    }
63	    if (!$inserted) {
64	        $raw = rtrim($raw) . "\n    " . $varName . '=""' . "\n";
65	    }
66	}
67
68	// Replace quoted value in-place
69	$updated = preg_replace(
70	    '/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
71	    '${1}"' . $key . '"',
72	    $raw
73	);
74
75	if (!vv_write_conf_raw($confFile, $updated)) {
76	    echo json_encode(['ok' => false, 'error' => 'Failed to write ' . $confFile]);
77	    exit;
78	}
79
80	echo json_encode([
81	    'ok'          => true,
82	    'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4),
83	    'conf_file'   => $confFile,
84	]);
85

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache');
require_once dirname(__DIR__) . '/include/config.php';

$host = vv_detect_host();
if (!preg_match('/^host\d+$/', $host)) {
    echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']);
    exit;
}

$hostUpper = strtoupper($host);
$varName   = $hostUpper . '_UNRAID_API_KEY';
$confFile  = $host . '.conf';

// Create/overwrite the Varaverk API key.
// --description and --roles are required to suppress interactive prompts.
// --overwrite replaces any existing key with the same name (keeps it to one).
$dbg = ['ts' => date('H:i:s'), 'user' => trim(shell_exec('whoami'))];
$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
$dbg['raw'] = $output;
file_put_contents('/tmp/vv_apikey_debug.json', json_encode($dbg, JSON_PRETTY_PRINT));

if (!$output) {
    echo json_encode(['ok' => false, 'error' => 'unraid-api returned no output — check /tmp/vv_apikey_debug.json']);
    exit;
}

$data = json_decode(trim($output), true);
if (!is_array($data)) {
    echo json_encode(['ok' => false, 'error' => 'Could not parse unraid-api output', 'raw' => substr($output, 0, 300)]);
    exit;
}

$key = $data['key'] ?? null;
if (!$key) {
    echo json_encode(['ok' => false, 'error' => 'No key in response', 'raw' => substr($output, 0, 300)]);
    exit;
}

// Read conf, replace the key value, write back
$raw = vv_read_conf_raw($confFile);
if ($raw === '') {
    echo json_encode(['ok' => false, 'error' => 'Cannot read ' . $confFile]);
    exit;
}

// If line is missing (older conf created before this field was added to the template),
// insert it after HOST*_OWNER_EMAIL, or after HOST*_SSH_KEY, or append to file.
if (!str_contains($raw, $varName)) {
    $inserted = false;
    foreach ([$hostUpper . '_OWNER_EMAIL', $hostUpper . '_SSH_KEY'] as $anchor) {
        if (str_contains($raw, $anchor)) {
            $raw = preg_replace(
                '/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
                '$1' . "\n    " . $varName . '=""',
                $raw, 1
            );
            $inserted = true;
            break;
        }
    }
    if (!$inserted) {
        $raw = rtrim($raw) . "\n    " . $varName . '=""' . "\n";
    }
}

// Replace quoted value in-place
$updated = preg_replace(
    '/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
    '${1}"' . $key . '"',
    $raw
);

if (!vv_write_conf_raw($confFile, $updated)) {
    echo json_encode(['ok' => false, 'error' => 'Failed to write ' . $confFile]);
    exit;
}

echo json_encode([
    'ok'          => true,
    'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4),
    'conf_file'   => $confFile,
]);


Clean improvement. The key creation is self-contained — just run it server-side at the end of the `save` and `pull` actions, write the key into the fresh conf, and return the result. The wizard JS then marks it done on arrival at step 2 instead of showing a button.


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


I have both files in context. Making the changes:


// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);

echo json_encode([
    'ok'       => true,
    'host_id'  => $hostId,
    'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);

// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);

// Auto-create Unraid API key and write into the fresh conf
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

echo json_encode([
    'ok'       => true,
    'host_id'  => $hostId,
    'api_key'  => $apiKeyResult,
    'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);


The file /boot/config/plugins/varaverk/Plugin/unraid/api/setup.php has been updated successfully. (file state is current in your context — no need to Read it back)

// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);

// Auto-create Unraid API key and write into the fresh conf
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

echo json_encode([
    'ok'       => true,
    'host_id'  => $hostId,
    'api_key'  => $apiKeyResult,
    'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
    ? trim($_GET['action'] ?? '')
    : trim($_POST['action'] ?? 'save');

// ── GET: detect environment ────────────────────────────────────────────────────────────────────
if ($action === 'detect') {
    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
    $bootDisk  = $bootPart
        ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
        : '';
    $transport = $bootDisk
        ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
        : 'unknown';

    $isUsb = ($transport === 'usb');

    preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);

    echo json_encode([
        'ok'           => true,
        'hostname'     => vv_get_hostname(),
        'unraid_ver'   => $vm[1] ?? 'unknown',
        'transport'    => $transport,
        'boot_device'  => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'mode'         => $isUsb ? 'flash' : 'internal',
        'scripts_dir'  => SCRIPTS_DIR,
    ]);
    exit;
}

// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
if ($action === 'populate') {
    $script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
        exit;
    }
    exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
    $lines = array_values(array_filter(array_map('trim', $out)));
    echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
    exit;
}

// ── HOST2 pull: pull master.conf from HOST1 via SSH ──────────────────────────────────────────
if ($action === 'pull') {
    $host1Hostname = trim($_POST['host1_hostname'] ?? '');
    $mySlot        = trim($_POST['my_slot'] ?? 'host2');
    $myHostname    = trim($_POST['my_hostname'] ?? '');

    if (!$host1Hostname) {
        echo json_encode(['ok' => false, 'error' => 'HOST1 hostname required']);
        exit;
    }
    if (!preg_match('/^host\d+$/', $mySlot)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
        exit;
    }

    $hostId    = strtoupper($mySlot);
    $hostIdLow = strtolower($mySlot);

    // Derive SSH key path from this server's hostname
    $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname()));
    $sshKey   = '/root/.ssh/' . $sshOwner . '_rsync_automation';

    if (!file_exists($sshKey)) {
        echo json_encode(['ok' => false, 'error' =>
            "SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]);
        exit;
    }

    // Resolve HOST1 Tailscale IP
    $ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: '');
    if (!$ip) {
        echo json_encode(['ok' => false, 'error' =>
            "Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]);
        exit;
    }

    // Get HOST1's SCRIPTS_DIR from their varaverk.cfg
    $sshBase   = 'ssh -i ' . escapeshellarg($sshKey)
               . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
    $remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: '');
    preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm);
    $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations';

    // SCP master.conf from HOST1
    $localMaster = CONF_DIR . '/master.conf';
    $src  = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
    $cmd  = 'scp -i ' . escapeshellarg($sshKey)
          . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
          . ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1';
    exec($cmd, $out, $rc);
    if ($rc !== 0) {
        echo json_encode(['ok' => false, 'error' =>
            'SCP failed: ' . implode('; ', $out) .
            ' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']);
        exit;
    }

    // Create host conf from template if it doesn't exist
    $confFile = $hostIdLow . '.conf';
    if (!file_exists(CONF_DIR . '/' . $confFile)) {
        $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
        if ($template) {
            $bootPart2  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
            $bootDisk2  = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
            $transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
            $storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
            $conf = str_replace('HOSTN', $hostId,    $template);
            $conf = str_replace('hostn', $hostIdLow, $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
                '${1}"' . $sshKey . '"', $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
                '${1}' . $storageInternal2, $conf);
            vv_write_conf_raw($confFile, $conf);
        }
    }

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;
}

// ── Default action: save (HOST1 first-run wizard) ────────────────────────────────────────────
$host1      = trim($_POST['host1']      ?? '');
$host2      = trim($_POST['host2']      ?? '');
$mySlot     = trim($_POST['my_slot']    ?? 'host1');
$myHostname = trim($_POST['my_hostname'] ?? '');

if (empty($host1)) {
    echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']);
    exit;
}
if (!preg_match('/^host\d+$/', $mySlot)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
    exit;
}

// Write HOST1 / HOST2 into master.conf
$master = vv_read_conf_raw('master.conf');
if ($master === '') {
    echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
    exit;
}

$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);

$slotNum = (int) preg_replace('/\D/', '', $mySlot);
if ($slotNum > 2 && !empty($myHostname)) {
    $hostKey = 'HOST' . $slotNum;
    if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
        $master = preg_replace('/^(\s*HOST2\s*=.*$)/m',
            '$1' . "\n    {$hostKey}=\"" . addslashes($myHostname) . '"', $master);
    } else {
        $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
            '${1}"' . addslashes($myHostname) . '"', $master);
    }
}

if (!vv_write_conf_raw('master.conf', $master)) {
    echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
    exit;
}

// Create host*.conf from template
$hostId    = strtoupper($mySlot);
$hostIdLow = strtolower($mySlot);
$confFile  = $hostIdLow . '.conf';

if (!file_exists(CONF_DIR . '/' . $confFile)) {
    $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
    if ($template) {
        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';

        // Auto-detect storage mode from boot device transport
        $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
        $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
        $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
        $storageInternal = ($transport !== 'usb') ? 'true' : 'false';

        $conf = str_replace('HOSTN', $hostId,    $template);
        $conf = str_replace('hostn', $hostIdLow, $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
            '${1}"' . $sshKeyPath . '"', $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
            '${1}' . $storageInternal, $conf);
        if (!vv_write_conf_raw($confFile, $conf)) {
            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
            exit;
        }
    }
}

// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);

echo json_encode([
    'ok'       => true,
    'host_id'  => $hostId,
    'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);

+// Auto-create Unraid API key and write into the fresh conf

+$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);


    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;

    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'api_key'  => $apiKeyResult,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;


    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'api_key'  => $apiKeyResult,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
    ? trim($_GET['action'] ?? '')
    : trim($_POST['action'] ?? 'save');

// ── GET: detect environment ────────────────────────────────────────────────────────────────────
if ($action === 'detect') {
    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
    $bootDisk  = $bootPart
        ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
        : '';
    $transport = $bootDisk
        ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
        : 'unknown';

    $isUsb = ($transport === 'usb');

    preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);

    echo json_encode([
        'ok'           => true,
        'hostname'     => vv_get_hostname(),
        'unraid_ver'   => $vm[1] ?? 'unknown',
        'transport'    => $transport,
        'boot_device'  => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'mode'         => $isUsb ? 'flash' : 'internal',
        'scripts_dir'  => SCRIPTS_DIR,
    ]);
    exit;
}

// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
if ($action === 'populate') {
    $script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
        exit;
    }
    exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
    $lines = array_values(array_filter(array_map('trim', $out)));
    echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
    exit;
}

// ── HOST2 pull: pull master.conf from HOST1 via SSH ──────────────────────────────────────────
if ($action === 'pull') {
    $host1Hostname = trim($_POST['host1_hostname'] ?? '');
    $mySlot        = trim($_POST['my_slot'] ?? 'host2');
    $myHostname    = trim($_POST['my_hostname'] ?? '');

    if (!$host1Hostname) {
        echo json_encode(['ok' => false, 'error' => 'HOST1 hostname required']);
        exit;
    }
    if (!preg_match('/^host\d+$/', $mySlot)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
        exit;
    }

    $hostId    = strtoupper($mySlot);
    $hostIdLow = strtolower($mySlot);

    // Derive SSH key path from this server's hostname
    $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname()));
    $sshKey   = '/root/.ssh/' . $sshOwner . '_rsync_automation';

    if (!file_exists($sshKey)) {
        echo json_encode(['ok' => false, 'error' =>
            "SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]);
        exit;
    }

    // Resolve HOST1 Tailscale IP
    $ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: '');
    if (!$ip) {
        echo json_encode(['ok' => false, 'error' =>
            "Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]);
        exit;
    }

    // Get HOST1's SCRIPTS_DIR from their varaverk.cfg
    $sshBase   = 'ssh -i ' . escapeshellarg($sshKey)
               . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
    $remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: '');
    preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm);
    $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations';

    // SCP master.conf from HOST1
    $localMaster = CONF_DIR . '/master.conf';
    $src  = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
    $cmd  = 'scp -i ' . escapeshellarg($sshKey)
          . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
          . ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1';
    exec($cmd, $out, $rc);
    if ($rc !== 0) {
        echo json_encode(['ok' => false, 'error' =>
            'SCP failed: ' . implode('; ', $out) .
            ' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']);
        exit;
    }

    // Create host conf from template if it doesn't exist
    $confFile = $hostIdLow . '.conf';
    if (!file_exists(CONF_DIR . '/' . $confFile)) {
        $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
        if ($template) {
            $bootPart2  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
            $bootDisk2  = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
            $transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
            $storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
            $conf = str_replace('HOSTN', $hostId,    $template);
            $conf = str_replace('hostn', $hostIdLow, $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
                '${1}"' . $sshKey . '"', $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
                '${1}' . $storageInternal2, $conf);
            vv_write_conf_raw($confFile, $conf);
        }
    }

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;
}

// ── Default action: save (HOST1 first-run wizard) ────────────────────────────────────────────
$host1      = trim($_POST['host1']      ?? '');
$host2      = trim($_POST['host2']      ?? '');
$mySlot     = trim($_POST['my_slot']    ?? 'host1');
$myHostname = trim($_POST['my_hostname'] ?? '');

if (empty($host1)) {
    echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']);
    exit;
}
if (!preg_match('/^host\d+$/', $mySlot)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
    exit;
}

// Write HOST1 / HOST2 into master.conf
$master = vv_read_conf_raw('master.conf');
if ($master === '') {
    echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
    exit;
}

$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);

$slotNum = (int) preg_replace('/\D/', '', $mySlot);
if ($slotNum > 2 && !empty($myHostname)) {
    $hostKey = 'HOST' . $slotNum;
    if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
        $master = preg_replace('/^(\s*HOST2\s*=.*$)/m',
            '$1' . "\n    {$hostKey}=\"" . addslashes($myHostname) . '"', $master);
    } else {
        $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
            '${1}"' . addslashes($myHostname) . '"', $master);
    }
}

if (!vv_write_conf_raw('master.conf', $master)) {
    echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
    exit;
}

// Create host*.conf from template
$hostId    = strtoupper($mySlot);
$hostIdLow = strtolower($mySlot);
$confFile  = $hostIdLow . '.conf';

if (!file_exists(CONF_DIR . '/' . $confFile)) {
    $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
    if ($template) {
        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';

        // Auto-detect storage mode from boot device transport
        $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
        $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
        $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
        $storageInternal = ($transport !== 'usb') ? 'true' : 'false';

        $conf = str_replace('HOSTN', $hostId,    $template);
        $conf = str_replace('hostn', $hostIdLow, $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
            '${1}"' . $sshKeyPath . '"', $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
            '${1}' . $storageInternal, $conf);
        if (!vv_write_conf_raw($confFile, $conf)) {
            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
            exit;
        }
    }
}

// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);

// Auto-create Unraid API key and write into the fresh conf
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

echo json_encode([
    'ok'       => true,
    'host_id'  => $hostId,
    'api_key'  => $apiKeyResult,
    'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);

+    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

+                      'api_key'  => $apiKeyResult,


Now add the shared helper to `config.php`:


358	}
359
360	// Local LAN IP via routing table — static-cached per request.
361	// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
362	function vv_local_ip(): string {
363	    static $ip = null;
364	    if ($ip !== null) return $ip;
365	    $ip = trim(shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? '');
366	    return $ip;
367	}

}

// Local LAN IP via routing table — static-cached per request.
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
function vv_local_ip(): string {
    static $ip = null;
    if ($ip !== null) return $ip;
    $ip = trim(shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? '');
    return $ip;
}


367	}
368


// Local LAN IP via routing table — static-cached per request.
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
function vv_local_ip(): string {

// Create (or overwrite) the Varaverk Unraid API key and write it into host conf.
// Returns ['ok'=>true,'key_preview'=>'...'] or ['ok'=>false,'error'=>'...'].
function vv_auto_create_api_key(string $hostId, string $confFile): array {
    $varName = strtoupper($hostId) . '_UNRAID_API_KEY';
    $output  = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
    if (!$output) {
        return ['ok' => false, 'error' => 'unraid-api returned no output'];
    }
    $data = json_decode(trim($output), true);
    $key  = $data['key'] ?? null;
    if (!$key) {
        return ['ok' => false, 'error' => 'No key in response'];
    }
    $raw = vv_read_conf_raw($confFile);
    if ($raw === '') {
        return ['ok' => false, 'error' => 'Cannot read ' . $confFile];
    }
    if (!str_contains($raw, $varName)) {
        foreach ([strtoupper($hostId) . '_OWNER_EMAIL', strtoupper($hostId) . '_SSH_KEY'] as $anchor) {
            if (str_contains($raw, $anchor)) {
                $raw = preg_replace('/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
                    '$1' . "\n    " . $varName . '=""', $raw, 1);
                break;
            }
        }
    }
    $raw = preg_replace('/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
        '${1}"' . $key . '"', $raw);
    vv_write_conf_raw($confFile, $raw);
    return ['ok' => true, 'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4)];
}

// Local LAN IP via routing table — static-cached per request.
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
function vv_local_ip(): string {


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

// Create (or overwrite) the Varaverk Unraid API key and write it into host conf.
// Returns ['ok'=>true,'key_preview'=>'...'] or ['ok'=>false,'error'=>'...'].
function vv_auto_create_api_key(string $hostId, string $confFile): array {
    $varName = strtoupper($hostId) . '_UNRAID_API_KEY';
    $output  = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
    if (!$output) {
        return ['ok' => false, 'error' => 'unraid-api returned no output'];
    }
    $data = json_decode(trim($output), true);
    $key  = $data['key'] ?? null;
    if (!$key) {
        return ['ok' => false, 'error' => 'No key in response'];
    }
    $raw = vv_read_conf_raw($confFile);
    if ($raw === '') {
        return ['ok' => false, 'error' => 'Cannot read ' . $confFile];
    }
    if (!str_contains($raw, $varName)) {
        foreach ([strtoupper($hostId) . '_OWNER_EMAIL', strtoupper($hostId) . '_SSH_KEY'] as $anchor) {
            if (str_contains($raw, $anchor)) {
                $raw = preg_replace('/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
                    '$1' . "\n    " . $varName . '=""', $raw, 1);
                break;
            }
        }
    }
    $raw = preg_replace('/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
        '${1}"' . $key . '"', $raw);
    vv_write_conf_raw($confFile, $raw);
    return ['ok' => true, 'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4)];
}

// Local LAN IP via routing table — static-cached per request.
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
function vv_local_ip(): string {

+// Create (or overwrite) the Varaverk Unraid API key and write it into host conf.

+// Returns ['ok'=>true,'key_preview'=>'...'] or ['ok'=>false,'error'=>'...'].

+function vv_auto_create_api_key(string $hostId, string $confFile): array {

+    $varName = strtoupper($hostId) . '_UNRAID_API_KEY';

+    $output  = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');

+        return ['ok' => false, 'error' => 'unraid-api returned no output'];

+    $data = json_decode(trim($output), true);

+        return ['ok' => false, 'error' => 'No key in response'];

+    $raw = vv_read_conf_raw($confFile);

+        return ['ok' => false, 'error' => 'Cannot read ' . $confFile];

+    if (!str_contains($raw, $varName)) {

+        foreach ([strtoupper($hostId) . '_OWNER_EMAIL', strtoupper($hostId) . '_SSH_KEY'] as $anchor) {

+            if (str_contains($raw, $anchor)) {

+    vv_write_conf_raw($confFile, $raw);

+    return ['ok' => true, 'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4)];


Now update `create_api_key.php` to use the shared function, and update the wizard JS to handle the auto-result:


Write

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache');
require_once dirname(__DIR__) . '/include/config.php';

$host = vv_detect_host();
if (!preg_match('/^host\d+$/', $host)) {
    echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']);
    exit;
}

echo json_encode(vv_auto_create_api_key($host, $host . '.conf'));


The file /boot/config/plugins/varaverk/Plugin/unraid/api/create_api_key.php has been updated successfully. (file state is current in your context — no need to Read it back)

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache');
require_once dirname(__DIR__) . '/include/config.php';

$host = vv_detect_host();
if (!preg_match('/^host\d+$/', $host)) {
    echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']);
    exit;
}

echo json_encode(vv_auto_create_api_key($host, $host . '.conf'));

-$varName   = $hostUpper . '_UNRAID_API_KEY';

-// Create/overwrite the Varaverk API key.

-// --description and --roles are required to suppress interactive prompts.

-// --overwrite replaces any existing key with the same name (keeps it to one).

-$dbg = ['ts' => date('H:i:s'), 'user' => trim(shell_exec('whoami'))];

-$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');

-    echo json_encode(['ok' => false, 'error' => 'unraid-api returned no output — check /tmp/vv_apikey_debug.json']);

-$data = json_decode(trim($output), true);

-    echo json_encode(['ok' => false, 'error' => 'Could not parse unraid-api output', 'raw' => substr($output, 0, 300)]);

-    echo json_encode(['ok' => false, 'error' => 'No key in response', 'raw' => substr($output, 0, 300)]);

-// Read conf, replace the key value, write back

-    echo json_encode(['ok' => false, 'error' => 'Cannot read ' . $confFile]);

-// If line is missing (older conf created before this field was added to the template),

-// insert it after HOST*_OWNER_EMAIL, or after HOST*_SSH_KEY, or append to file.

-    foreach ([$hostUpper . '_OWNER_EMAIL', $hostUpper . '_SSH_KEY'] as $anchor) {

-        if (str_contains($raw, $anchor)) {

-if (!vv_write_conf_raw($confFile, $updated)) {

-    echo json_encode(['ok' => false, 'error' => 'Failed to write ' . $confFile]);

-    'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4),

+echo json_encode(vv_auto_create_api_key($host, $host . '.conf'));

<?php
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache');
require_once dirname(__DIR__) . '/include/config.php';

$host = vv_detect_host();
if (!preg_match('/^host\d+$/', $host)) {
    echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']);
    exit;
}

$hostUpper = strtoupper($host);
$varName   = $hostUpper . '_UNRAID_API_KEY';
$confFile  = $host . '.conf';

// Create/overwrite the Varaverk API key.
// --description and --roles are required to suppress interactive prompts.
// --overwrite replaces any existing key with the same name (keeps it to one).
$dbg = ['ts' => date('H:i:s'), 'user' => trim(shell_exec('whoami'))];
$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
$dbg['raw'] = $output;
file_put_contents('/tmp/vv_apikey_debug.json', json_encode($dbg, JSON_PRETTY_PRINT));

if (!$output) {
    echo json_encode(['ok' => false, 'error' => 'unraid-api returned no output — check /tmp/vv_apikey_debug.json']);
    exit;
}

$data = json_decode(trim($output), true);
if (!is_array($data)) {
    echo json_encode(['ok' => false, 'error' => 'Could not parse unraid-api output', 'raw' => substr($output, 0, 300)]);
    exit;
}

$key = $data['key'] ?? null;
if (!$key) {
    echo json_encode(['ok' => false, 'error' => 'No key in response', 'raw' => substr($output, 0, 300)]);
    exit;
}

// Read conf, replace the key value, write back
$raw = vv_read_conf_raw($confFile);
if ($raw === '') {
    echo json_encode(['ok' => false, 'error' => 'Cannot read ' . $confFile]);
    exit;
}

// If line is missing (older conf created before this field was added to the template),
// insert it after HOST*_OWNER_EMAIL, or after HOST*_SSH_KEY, or append to file.
if (!str_contains($raw, $varName)) {
    $inserted = false;
    foreach ([$hostUpper . '_OWNER_EMAIL', $hostUpper . '_SSH_KEY'] as $anchor) {
        if (str_contains($raw, $anchor)) {
            $raw = preg_replace(
                '/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
                '$1' . "\n    " . $varName . '=""',
                $raw, 1
            );
            $inserted = true;
            break;
        }
    }
    if (!$inserted) {
        $raw = rtrim($raw) . "\n    " . $varName . '=""' . "\n";
    }
}

// Replace quoted value in-place
$updated = preg_replace(
    '/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
    '${1}"' . $key . '"',
    $raw
);

if (!vv_write_conf_raw($confFile, $updated)) {
    echo json_encode(['ok' => false, 'error' => 'Failed to write ' . $confFile]);
    exit;
}

echo json_encode([
    'ok'          => true,
    'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4),
    'conf_file'   => $confFile,
]);


Now update the wizard JS — `vvShowStep2` accepts the api_key result, and all three callers pass it through:


280	}
281
282	// ── Status helper ─────────────────────────────────────────────────────────────
283	function vvSetStatus(msg, cls) {
284	  const s = document.getElementById('vv-status');
285	  s.textContent = msg; s.className = cls || '';
286	}
287
288	function vvSetBtn(text, disabled) {
289	  const b = document.getElementById('vv-main-btn');
290	  if (b) { b.textContent = text; b.disabled = disabled; }
291	}
292
293	// ── Advance to step 2 ─────────────────────────────────────────────────────────
294	function vvShowStep2(redirect) {
295	  _vvRedirect = redirect || '?tab=scheduler';
296	  document.getElementById('vv-step1').style.display = 'none';
297	  document.getElementById('vv-step2').style.display = 'block';
298	  vvRunPopulate();
299	  vvLoadChecklist();
300	}
301
302	// ── Populate ──────────────────────────────────────────────────────────────────
303	function vvRunPopulate() {
304	  const el = document.getElementById('vv-populate-status');
305	  fetch('/plugins/varaverk/api/setup.php', {
306	    method: 'POST',
307	    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
308	    body: new URLSearchParams({action: 'populate'})
309	  }).then(r => r.json()).then(d => {
310	    if (d.ok) {
311	      const found = (d.lines || []).filter(l => /✅|found|detected/i.test(l));
312	      el.textContent = found.length
313	        ? '✓ Auto-populate: ' + found.length + ' field' + (found.length > 1 ? 's' : '') + ' detected'
314	        : '✓ Auto-populate ran — arr keys will fill once services are running';
315	      el.style.color = '#4a8';
316	    } else {
317	      el.textContent = 'Auto-populate skipped — run Tools/conf_populate.sh once your arr containers are up';
318	      el.style.color = '#555';
319	    }
320	    vvLoadChecklist();
321	  }).catch(() => {
322	    el.textContent = 'Auto-populate unavailable — run manually from Scheduler';
323	    el.style.color = '#555';
324	  });
325	}
326
327	// ── Checklist ─────────────────────────────────────────────────────────────────
328	const vvActionLabels = {
329	  create_key:   'Create API key',
330	  ssh_setup:    'SSH guide →',
331	  run_populate: 'Run now',
332	  onboard:      'Partnership tab →',
333	};
334	const vvActionHref = {
335	  ssh_setup: '?tab=partnership',
336	  onboard:   '?tab=partnership',
337	};
338
339	function vvLoadChecklist() {
340	  fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
341	    .then(r => r.json()).then(d => {
342	      const el = document.getElementById('vv-checklist');
343	      if (!d.ok || !d.items) { el.innerHTML = 'Unable to load checklist'; return; }
344	      el.innerHTML = d.items.map(item => {
345	        const icon  = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
346	        const iclr  = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
347	        let   act   = '';
348	        if (item.action) {
349	          const lbl  = vvActionLabels[item.action] || item.action;
350	          const href = vvActionHref[item.action];
351	          if (href) {
352	            act = `<div class="vv-cl-act"><a href="${href}" style="font-size:11px;color:#556;">${lbl}</a></div>`;
353	          } else if (item.action === 'create_key') {
354	            act = `<div class="vv-cl-act"><button onclick="vvCreateKey(this)">${lbl}</button></div>`;
355	          } else if (item.action === 'run_populate') {
356	            act = `<div class="vv-cl-act"><button onclick="vvRunPopulateBtn(this)">${lbl}</button></div>`;
357	          }
358	        }
359	        return `

}

// ── Status helper ─────────────────────────────────────────────────────────────
function vvSetStatus(msg, cls) {
  const s = document.getElementById('vv-status');
  s.textContent = msg; s.className = cls || '';
}

function vvSetBtn(text, disabled) {
  const b = document.getElementById('vv-main-btn');
  if (b) { b.textContent = text; b.disabled = disabled; }
}

// ── Advance to step 2 ─────────────────────────────────────────────────────────
function vvShowStep2(redirect) {
  _vvRedirect = redirect || '?tab=scheduler';
  document.getElementById('vv-step1').style.display = 'none';
  document.getElementById('vv-step2').style.display = 'block';
  vvRunPopulate();
  vvLoadChecklist();
}

// ── Populate ──────────────────────────────────────────────────────────────────
function vvRunPopulate() {
  const el = document.getElementById('vv-populate-status');
  fetch('/plugins/varaverk/api/setup.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({action: 'populate'})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      const found = (d.lines || []).filter(l => /✅|found|detected/i.test(l));
      el.textContent = found.length
        ? '✓ Auto-populate: ' + found.length + ' field' + (found.length > 1 ? 's' : '') + ' detected'
        : '✓ Auto-populate ran — arr keys will fill once services are running';
      el.style.color = '#4a8';
    } else {
      el.textContent = 'Auto-populate skipped — run Tools/conf_populate.sh once your arr containers are up';
      el.style.color = '#555';
    }
    vvLoadChecklist();
  }).catch(() => {
    el.textContent = 'Auto-populate unavailable — run manually from Scheduler';
    el.style.color = '#555';
  });
}

// ── Checklist ─────────────────────────────────────────────────────────────────
const vvActionLabels = {
  create_key:   'Create API key',
  ssh_setup:    'SSH guide →',
  run_populate: 'Run now',
  onboard:      'Partnership tab →',
};
const vvActionHref = {
  ssh_setup: '?tab=partnership',
  onboard:   '?tab=partnership',
};

function vvLoadChecklist() {
  fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
    .then(r => r.json()).then(d => {
      const el = document.getElementById('vv-checklist');
      if (!d.ok || !d.items) { el.innerHTML = 'Unable to load checklist'; return; }
      el.innerHTML = d.items.map(item => {
        const icon  = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
        const iclr  = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
        let   act   = '';
        if (item.action) {
          const lbl  = vvActionLabels[item.action] || item.action;
          const href = vvActionHref[item.action];
          if (href) {
            act = `<div class="vv-cl-act"><a href="${href}" style="font-size:11px;color:#556;">${lbl}</a></div>`;
          } else if (item.action === 'create_key') {
            act = `<div class="vv-cl-act"><button onclick="vvCreateKey(this)">${lbl}</button></div>`;
          } else if (item.action === 'run_populate') {
            act = `<div class="vv-cl-act"><button onclick="vvRunPopulateBtn(this)">${lbl}</button></div>`;
          }
        }
        return `


Edit

// ── Advance to step 2 ─────────────────────────────────────────────────────────
function vvShowStep2(redirect) {
  _vvRedirect = redirect || '?tab=scheduler';
  document.getElementById('vv-step1').style.display = 'none';
  document.getElementById('vv-step2').style.display = 'block';
  vvRunPopulate();
  vvLoadChecklist();
}

// ── Advance to step 2 ─────────────────────────────────────────────────────────
function vvShowStep2(redirect, apiKey) {
  _vvRedirect = redirect || '?tab=scheduler';
  document.getElementById('vv-step1').style.display = 'none';
  document.getElementById('vv-step2').style.display = 'block';
  if (apiKey && apiKey.ok) {
    const btn    = document.getElementById('vv-key-btn');
    const status = document.getElementById('vv-key-status');
    if (btn)    { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }
    if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }
  }
  vvRunPopulate();
  vvLoadChecklist();
}


// ── Advance to step 2 ─────────────────────────────────────────────────────────
function vvShowStep2(redirect) {
  _vvRedirect = redirect || '?tab=scheduler';
  document.getElementById('vv-step1').style.display = 'none';
  document.getElementById('vv-step2').style.display = 'block';
  vvRunPopulate();
  vvLoadChecklist();
}

// ── Advance to step 2 ─────────────────────────────────────────────────────────
function vvShowStep2(redirect, apiKey) {
  _vvRedirect = redirect || '?tab=scheduler';
  document.getElementById('vv-step1').style.display = 'none';
  document.getElementById('vv-step2').style.display = 'block';
  if (apiKey && apiKey.ok) {
    const btn    = document.getElementById('vv-key-btn');
    const status = document.getElementById('vv-key-status');
    if (btn)    { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }
    if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }
  }
  vvRunPopulate();
  vvLoadChecklist();
}

+function vvShowStep2(redirect, apiKey) {

+    const btn    = document.getElementById('vv-key-btn');

+    const status = document.getElementById('vv-key-status');

+    if (btn)    { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }

+    if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }


Now pass `d.api_key` through all three callers:


390	  fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
391	    .then(r => r.json()).then(d => {
392	      if (d.ok) {
393	        status.textContent = '✓ Key created — ' + d.key_preview;
394	        status.style.color = '#4a8';
395	        btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
396	        vvLoadChecklist();
397	      } else {
398	        status.textContent = '✗ ' + (d.error || 'Failed');
399	        status.style.color = '#a44';
400	        btn.disabled = false; btn.textContent = 'Retry';
401	      }
402	    }).catch(e => {
403	      status.textContent = '✗ ' + e; status.style.color = '#a44';
404	      btn.disabled = false; btn.textContent = 'Retry';
405	    });
406	}
407
408	// ── Navigation ────────────────────────────────────────────────────────────────
409	function vvGoScheduler(e) {
410	  if (e) e.preventDefault();
411	  window.location.href = _vvRedirect || '?tab=scheduler';
412	}
413
414	// ── Action handlers ───────────────────────────────────────────────────────────
415	function vvDoSave() {
416	  const hostname = document.getElementById('vv-hostname')?.value.trim();
417	  if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
418	  let host1 = '', host2 = '', mySlot = 'host1';
419	  if (vvRole === 'primary') {
420	    host1 = hostname;
421	    host2 = document.getElementById('vv-partner-hostname')?.value.trim() || '';
422	    mySlot = 'host1';
423	  } else {
424	    const primary = document.getElementById('vv-primary-hostname')?.value.trim();
425	    if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
426	    mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
427	    host1  = primary;
428	    if (mySlot === 'host2') host2 = hostname;
429	  }
430	  vvSetBtn('Saving…', true);
431	  const params = new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname});
432	  fetch('/plugins/varaverk/api/setup.php', {
433	    method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params
434	  }).then(r=>r.json()).then(d => {
435	    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler'); }
436	    else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
437	  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
438	}
439
440	function vvDoPull() {
441	  const hostname = document.getElementById('vv-hostname')?.value.trim();
442	  const slot     = document.getElementById('vv-slot')?.value || 'host2';
443	  if (!hostname) { vvSetStatus('✗ Hostname required', 'err'); return; }
444	  vvSetBtn('Pulling…', true);
445	  const params = new URLSearchParams({
446	    action:'pull', host1_hostname:<?= json_encode($host1FromState) ?>,
447	    my_slot:slot, my_hostname:hostname
448	  });
449	  fetch('/plugins/varaverk/api/setup.php', {
450	    method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params
451	  }).then(r=>r.json()).then(d => {
452	    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler'); }
453	    else { vvSetBtn('Pull configuration from HOST1 →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
454	  }).catch(() => { vvSetBtn('Pull configuration from HOST1 →', false); vvSetStatus('✗ Request failed', 'err'); });
455	}
456
457	function vvDoConfOnly() {
458	  vvSetBtn('Creating…', true);
459	  const params = new URLSearchParams({

  fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
    .then(r => r.json()).then(d => {
      if (d.ok) {
        status.textContent = '✓ Key created — ' + d.key_preview;
        status.style.color = '#4a8';
        btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
        vvLoadChecklist();
      } else {
        status.textContent = '✗ ' + (d.error || 'Failed');
        status.style.color = '#a44';
        btn.disabled = false; btn.textContent = 'Retry';
      }
    }).catch(e => {
      status.textContent = '✗ ' + e; status.style.color = '#a44';
      btn.disabled = false; btn.textContent = 'Retry';
    });
}

// ── Navigation ────────────────────────────────────────────────────────────────
function vvGoScheduler(e) {
  if (e) e.preventDefault();
  window.location.href = _vvRedirect || '?tab=scheduler';
}

// ── Action handlers ───────────────────────────────────────────────────────────
function vvDoSave() {
  const hostname = document.getElementById('vv-hostname')?.value.trim();
  if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
  let host1 = '', host2 = '', mySlot = 'host1';
  if (vvRole === 'primary') {
    host1 = hostname;
    host2 = document.getElementById('vv-partner-hostname')?.value.trim() || '';
    mySlot = 'host1';
  } else {
    const primary = document.getElementById('vv-primary-hostname')?.value.trim();
    if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
    mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
    host1  = primary;
    if (mySlot === 'host2') host2 = hostname;
  }
  vvSetBtn('Saving…', true);
  const params = new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname});
  fetch('/plugins/varaverk/api/setup.php', {
    method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params
  }).then(r=>r.json()).then(d => {
    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler'); }
    else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
}

function vvDoPull() {
  const hostname = document.getElementById('vv-hostname')?.value.trim();
  const slot     = document.getElementById('vv-slot')?.value || 'host2';
  if (!hostname) { vvSetStatus('✗ Hostname required', 'err'); return; }
  vvSetBtn('Pulling…', true);
  const params = new URLSearchParams({
    action:'pull', host1_hostname:<?= json_encode($host1FromState) ?>,
    my_slot:slot, my_hostname:hostname
  });
  fetch('/plugins/varaverk/api/setup.php', {
    method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:params
  }).then(r=>r.json()).then(d => {
    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler'); }
    else { vvSetBtn('Pull configuration from HOST1 →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Pull configuration from HOST1 →', false); vvSetStatus('✗ Request failed', 'err'); });
}

function vvDoConfOnly() {
  vvSetBtn('Creating…', true);
  const params = new URLSearchParams({