1	<?php
2	header('Content-Type: application/json');
3	require_once dirname(__DIR__) . '/include/config.php';
4
5	$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
6	    ? trim($_GET['action'] ?? '')
7	    : trim($_POST['action'] ?? 'save');
8
9	// ── GET: detect environment ────────────────────────────────────────────────────────────────────
10	if ($action === 'detect') {
11	    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
12	    $bootDisk  = $bootPart
13	        ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
14	        : '';
15	    $transport = $bootDisk
16	        ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
17	        : 'unknown';
18
19	    $isUsb = ($transport === 'usb');
20
21	    preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);
22
23	    echo json_encode([
24	        'ok'           => true,
25	        'hostname'     => vv_get_hostname(),
26	        'unraid_ver'   => $vm[1] ?? 'unknown',
27	        'transport'    => $transport,
28	        'boot_device'  => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
29	        'mode'         => $isUsb ? 'flash' : 'internal',
30	        'scripts_dir'  => SCRIPTS_DIR,
31	    ]);
32	    exit;
33	}
34
35	// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
36	if ($action === 'ssh_generate') {
37	    $script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
38	    if (!file_exists($script)) {
39	        echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
40	        exit;
41	    }
42	    exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
43	    // Derive pubkey path from hostname
44	    $hostname  = vv_get_hostname();
45	    $shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
46	    $pubPath   = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
47	    $pubKey    = trim(@file_get_contents($pubPath) ?: '');
48	    echo json_encode([
49	        'ok'     => $rc === 0 && !empty($pubKey),
50	        'pubkey' => $pubKey,
51	        'error'  => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
52	    ]);
53	    exit;
54	}
55
56	// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
57	if ($action === 'populate') {
58	    $script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
59	    if (!file_exists($script)) {
60	        echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
61	        exit;
62	    }
63	    exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
64	    $lines = array_values(array_filter(array_map('trim', $out)));
65	    echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
66	    exit;
67	}
68
69	if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
70	    echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
71	    exit;
72	}
73
74	$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
75
76	// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
77	if ($action === 'pull') {
78	    $mySlot        = trim($_POST['my_slot']        ?? '') ?: strtolower(vv_detect_host());
79	    $myHostname    = trim($_POST['my_hostname']     ?? '') ?: vv_get_hostname();
80	    $host1Hostname = trim($_POST['host1_hostname']  ?? '');
81	    if (!$host1Hostname) {
82	        $masterRaw = vv_read_conf_raw('master.conf');
83	        preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
84	        $host1Hostname = trim($_mh[1] ?? '');
85	    }
86	    if (!$host1Hostname) {
87	        echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
88	        exit;
89	    }
90	    if (!preg_match('/^host\d+$/', $mySlot)) {
91	        echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
92	        exit;
93	    }
94
95	    $hostId    = strtoupper($mySlot);
96	    $hostIdLow = strtolower($mySlot);
97
98	    // Derive SSH key path from this server's hostname
99	    $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname()));
100	    $sshKey   = '/root/.ssh/' . $sshOwner . '_rsync_automation';
101
102	    if (!file_exists($sshKey)) {
103	        echo json_encode(['ok' => false, 'error' =>
104	            "SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]);
105	        exit;
106	    }
107
108	    // Resolve HOST1 Tailscale IP
109	    $ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: '');
110	    if (!$ip) {
111	        echo json_encode(['ok' => false, 'error' =>
112	            "Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]);
113	        exit;
114	    }
115
116	    // Get HOST1's SCRIPTS_DIR from their varaverk.cfg
117	    $sshBase   = 'ssh -i ' . escapeshellarg($sshKey)
118	               . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
119	    $remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: '');
120	    preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm);
121	    $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations';
122
123	    // SCP master.conf from HOST1
124	    $localMaster = CONF_DIR . '/master.conf';
125	    $src  = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
126	    $cmd  = 'scp -i ' . escapeshellarg($sshKey)
127	          . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
128	          . ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1';
129	    exec($cmd, $out, $rc);
130	    if ($rc !== 0) {
131	        echo json_encode(['ok' => false, 'error' =>
132	            'SCP failed: ' . implode('; ', $out) .
133	            ' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']);
134	        exit;
135	    }
136
137	    // Create host conf from template if it doesn't exist
138	    $confFile = $hostIdLow . '.conf';
139	    if (!file_exists(CONF_DIR . '/' . $confFile)) {
140	        $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
141	        if ($template) {
142	            $bootPart2  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
143	            $bootDisk2  = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
144	            $transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
145	            $storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
146	            $conf = str_replace('HOSTN', $hostId,    $template);
147	            $conf = str_replace('hostn', $hostIdLow, $conf);
148	            $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
149	                '${1}"' . $sshKey . '"', $conf);
150	            $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
151	                '${1}' . $storageInternal2, $conf);
152	            vv_write_conf_raw($confFile, $conf);
153	        }
154	    }
155
156	    if (file_exists($sshScript)) {
157	        exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
158	    }
159	    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
160
161	    $state = vv_setup_state_read();
162	    $state['master_conf_pulled'] = 'true';
163	    vv_setup_state_write($state);
164
165	    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
166	                      'api_key'  => $apiKeyResult,
167	                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
168	    exit;
169	}
170
171	// ── Default action: save (HOST1 first-run wizard) ────────────────────────────────────────────
172	$host1      = trim($_POST['host1']      ?? '');
173	$host2      = trim($_POST['host2']      ?? '');
174	$mySlot     = trim($_POST['my_slot']    ?? 'host1');
175	$myHostname = trim($_POST['my_hostname'] ?? '');
176
177	if (empty($host1)) {
178	    echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']);
179	    exit;
180	}
181	if (!preg_match('/^host\d+$/', $mySlot)) {
182	    echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
183	    exit;
184	}
185
186	// Write HOST1 / HOST2 into master.conf
187	$master = vv_read_conf_raw('master.conf');
188	if ($master === '') {
189	    echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
190	    exit;
191	}
192
193	$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
194	$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);
195
196	$slotNum = (int) preg_replace('/\D/', '', $mySlot);
197	if ($slotNum > 2 && !empty($myHostname)) {
198	    $hostKey = 'HOST' . $slotNum;
199	    if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
200	        $master = preg_replace('/^(\s*HOST2\s*=.*$)/m',
201	            '$1' . "\n    {$hostKey}=\"" . addslashes($myHostname) . '"', $master);
202	    } else {
203	        $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
204	            '${1}"' . addslashes($myHostname) . '"', $master);
205	    }
206	}
207
208	if (!vv_write_conf_raw('master.conf', $master)) {
209	    echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
210	    exit;
211	}
212
213	// Create host*.conf from template
214	$hostId    = strtoupper($mySlot);
215	$hostIdLow = strtolower($mySlot);
216	$confFile  = $hostIdLow . '.conf';
217
218	if (!file_exists(CONF_DIR . '/' . $confFile)) {
219	    $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
220	    if ($template) {
221	        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
222	        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
223
224	        // Auto-detect storage mode from boot device transport
225	        $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
226	        $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
227	        $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
228	        $storageInternal = ($transport !== 'usb') ? 'true' : 'false';
229
230	        $conf = str_replace('HOSTN', $hostId,    $template);
231	        $conf = str_replace('hostn', $hostIdLow, $conf);
232	        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
233	            '${1}"' . $sshKeyPath . '"', $conf);
234	        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
235	            '${1}' . $storageInternal, $conf);
236	        if (!vv_write_conf_raw($confFile, $conf)) {
237	            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
238	            exit;
239	        }
240	    }
241	}
242
243	// Write setup state file — lets partner servers know HOST1 is configured
244	vv_setup_state_write(['host1_hostname' => $host1]);
245
246	// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
247	if (file_exists($sshScript)) {
248	    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
249	}
250
251	// Auto-create Unraid API key and write into the fresh conf
252	$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
253
254	echo json_encode([
255	    'ok'       => true,
256	    'host_id'  => $hostId,
257	    'api_key'  => $apiKeyResult,
258	    'redirect' => '?tab=scheduler&vv_setup=master.conf',
259	]);
260

<?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;
}

// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
if ($action === 'ssh_generate') {
    $script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
        exit;
    }
    exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
    // Derive pubkey path from hostname
    $hostname  = vv_get_hostname();
    $shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
    $pubPath   = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
    $pubKey    = trim(@file_get_contents($pubPath) ?: '');
    echo json_encode([
        'ok'     => $rc === 0 && !empty($pubKey),
        'pubkey' => $pubKey,
        'error'  => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
    ]);
    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;
}

$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';

// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
if ($action === 'pull') {
    $mySlot        = trim($_POST['my_slot']        ?? '') ?: strtolower(vv_detect_host());
    $myHostname    = trim($_POST['my_hostname']     ?? '') ?: vv_get_hostname();
    $host1Hostname = trim($_POST['host1_hostname']  ?? '');
    if (!$host1Hostname) {
        $masterRaw = vv_read_conf_raw('master.conf');
        preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
        $host1Hostname = trim($_mh[1] ?? '');
    }
    if (!$host1Hostname) {
        echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
        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);
        }
    }

    if (file_exists($sshScript)) {
        exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
    }
    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

    $state = vv_setup_state_read();
    $state['master_conf_pulled'] = 'true';
    vv_setup_state_write($state);

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'api_key'  => $apiKeyResult,
                      '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-generate SSH keypair (local only — remote copy happens during onboarding)
if (file_exists($sshScript)) {
    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
}

// 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',
]);


Now let me check the setup wizard page UI to see if there's a storage mode selection step there.


1	<?php
2	// First-run setup wizard — uniform flow for all hosts.
3	// Step 1: auto-detect environment + server identity form.
4	// Step 2: auto-populate + guide + checklist.
5	// master.conf pull (for partner servers) lives in the checklist, not here.
6
7	$detectedHostname = vv_get_hostname();
8	?>
9	<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
10
78
79

80
81
⬡ Varaverk — First Run

82
Set up this server before the plugin can start.

83
84
85

86
87

Detecting environment…

88
89

90	      This server's hostname
91	      <input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
92
Must match Unraid Settings → Identification exactly (case-sensitive)

93

94
95

96	    Server role
97

98

99	        Primary
HOST1 · first server
100

101

102	        Partner
HOST2+ · joining primary
103

104

105
106

107

108	        Partner's hostname (optional — can fill in later)
109	        <input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
110

111

112
113

114

115	        Primary server's hostname required
116	        <input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
117

118

119	        Your slot
120	        <select id="vv-partner-slot">
121	          <option value="host2">HOST2</option>
122	          <option value="host3">HOST3</option>
123	          <option value="host4">HOST4</option>
124	        </select>
125

126

127	        SSH key and master.conf pull are handled automatically after save.
128

129

130
131	    <button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>
132

133

134
135
136

137

138
Step 2 of 2

139
140
⟳ Running auto-populate…

141
142

143	      Quick start
144

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

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

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

149	              DISCORD_WEBHOOK — for notifications (optional)

150	              DAILY_SYNC_SHARES — media paths to rsync nightly

151	              Everything else was auto-populated or has working defaults
152

153
If partnering: the checklist below will guide you through pulling HOST1's config and running onboard

154

155

156
157

158	      <button id="vv-key-btn" onclick="vvCreateKey(this)" class="vv-btn" style="flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;">
159	        Create API Key
160	      </button>
161	      Skip →
162

163

164
165

166
Setup checklist

167

Loading…

168
169

170	      Go to Scheduler →
171

172

173
174

175
176
387

<?php
// First-run setup wizard — uniform flow for all hosts.
// Step 1: auto-detect environment + server identity form.
// Step 2: auto-populate + guide + checklist.
// master.conf pull (for partner servers) lives in the checklist, not here.

$detectedHostname = vv_get_hostname();
?>
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">

⬡ Varaverk — First Run

Set up this server before the plugin can start.

Detecting environment…

      This server's hostname
      <input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">

Must match Unraid Settings → Identification exactly (case-sensitive)

    Server role

        Primary
HOST1 · first server

        Partner
HOST2+ · joining primary

        Partner's hostname (optional — can fill in later)
        <input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">

        Primary server's hostname required
        <input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">

        Your slot
        <select id="vv-partner-slot">
          <option value="host2">HOST2</option>
          <option value="host3">HOST3</option>
          <option value="host4">HOST4</option>
        </select>

        SSH key and master.conf pull are handled automatically after save.

    <button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>

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 — 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: the checklist below will guide you through pulling HOST1's config and running onboard

      <button id="vv-key-btn" onclick="vvCreateKey(this)" class="vv-btn" style="flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;">
        Create API Key
      </button>
      Skip →

Setup checklist

Loading…

      Go to Scheduler →


lets make sure intitial run and migration still setup this way...


Configure boot partition USB installation process


I now have the full picture. Here's the gap:

**What the user described / intended:**
- `.plg` installs → (to `/boot` initially, unavoidable before array mounts)
- Setup wizard → detects USB boot → migrates scripts to appdata → `STORAGE_MODE_INTERNAL=false`
- Wizard allows override to keep on internal (`_INTERNAL=true`) if NVMe boot

**What the code actually does:**
1. `.plg` clones to `/boot/config/plugins/varaverk` — always, regardless of USB vs NVMe
2. Wizard (`api/setup.php` save action) detects boot transport and **writes** `HOST*_STORAGE_MODE_INTERNAL=false` into the host conf when USB is detected
3. But it **never calls `storage_migrate.sh`** — the conf flag gets set to `false` but `SCRIPTS_DIR` in `varaverk.cfg` still points to `/boot`, so everything stays on flash anyway
4. `storage_migrate.sh` is fully built and works — it just isn't wired into the wizard

The detection banner in the wizard even shows "flash mode (USB boot)" vs "internal (NVMe/SSD)" correctly, but there's no action taken from it.

**The missing piece:** After the wizard saves and sets `STORAGE_MODE_INTERNAL=false`, it needs to call `storage_migrate.sh --to=flash` to actually move `SCRIPTS_DIR` to appdata. Right now that step doesn't happen.

Do you want me to wire it up? The cleanest approach would be to have `api/setup.php` (save action) trigger the migration automatically when it detects USB boot, with the wizard showing progress. The user could still override to internal manually via Settings → Storage afterward.


yes, i thought i had a selection early in the wizard for this.. but could still have it auto, and have the aselection.


Now I have the full picture. The wizard already detects the transport and shows the banner, and `storage_migrate.sh` + `api/storage.php` are fully built — but the wizard never wires them together. The save action auto-detects transport and writes the conf flag, but never calls the migration, so scripts stay on flash regardless.

Three changes needed:
1. **`pages/setup.php`** — add storage mode selector (auto-selected from detect, user-overridable), plus migration step between save and step 2
2. **`api/setup.php`** — accept `storage_mode` param from wizard instead of always auto-detecting, return `needs_migration` + `migrate_to` so JS knows whether to call the migration endpoint

Let me make the edits.


Edit

Detecting environment…

      This server's hostname

Detecting environment…

      Storage mode

          Appdata
USB boot · requires array

          Internal Boot
NVMe/SSD · no array dep

      This server's hostname


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)

Detecting environment…

      This server's hostname

Detecting environment…

      Storage mode

          Appdata
USB boot · requires array

          Internal Boot
NVMe/SSD · no array dep

      This server's hostname

+      <div class="vv-role-row" style="margin-bottom:4px">

+        <div class="vv-role-btn" id="vv-store-flash" onclick="vvSetStorage('flash')">

+          Appdata
USB boot · requires array

+        <div class="vv-role-btn" id="vv-store-internal" onclick="vvSetStorage('internal')">

+          Internal Boot
NVMe/SSD · no array dep

<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">

Must match Unraid Settings → Identification exactly (case-sensitive)


let _vvRedirect = '?tab=scheduler';

// ── Detection banner ──────────────────────────────────────────────────────────
(function() {
  const _ac = new AbortController();
  setTimeout(() => _ac.abort(), 6000);
  fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
    .then(r => r.json()).then(d => {
      const b = document.getElementById('vv-detect-banner');
      if (!d.ok) { b.innerHTML = 'Detection unavailable'; return; }
      const modeLabel = d.mode === 'internal'
        ? 'internal (NVMe/SSD)'
        : 'flash mode (USB boot)';
      b.innerHTML =
        '
OSUnraid ' + (d.unraid_ver||'') + '
' +
        '
Boot device' + d.boot_device + ' (' + d.transport + ')
' +
        '
Storage mode' + modeLabel + '
' +
        '
Scripts dir' + d.scripts_dir + '
';
      const hf = document.getElementById('vv-hostname');
      if (hf && !hf.value.trim()) hf.value = d.hostname;
    }).catch(() => {
      document.getElementById('vv-detect-banner').innerHTML = 'Detection unavailable';
    });
})();

let _vvRedirect    = '?tab=scheduler';
let _vvStorageMode = 'flash';
let _vvCurrentDir  = '';

function vvSetStorage(mode) {
  _vvStorageMode = mode;
  document.getElementById('vv-store-flash')?.classList.toggle('active',    mode === 'flash');
  document.getElementById('vv-store-internal')?.classList.toggle('active', mode === 'internal');
  const hint = document.getElementById('vv-store-hint');
  if (hint) hint.textContent = mode === 'flash'
    ? 'Scripts live in appdata — requires array to be started. Recommended for USB flash boot.'
    : 'Scripts live on /boot — available before array mounts. Requires NVMe/SSD boot.';
}

// ── Detection banner ──────────────────────────────────────────────────────────
(function() {
  const _ac = new AbortController();
  setTimeout(() => _ac.abort(), 6000);
  fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
    .then(r => r.json()).then(d => {
      const b = document.getElementById('vv-detect-banner');
      if (!d.ok) { b.innerHTML = 'Detection unavailable'; return; }
      _vvCurrentDir = d.scripts_dir || '';
      b.innerHTML =
        '
OSUnraid ' + (d.unraid_ver||'') + '
' +
        '
Boot device' + d.boot_device + ' (' + d.transport + ')
' +
        '
Scripts dir' + d.scripts_dir + '
';
      vvSetStorage(d.mode);
      const hf = document.getElementById('vv-hostname');
      if (hf && !hf.value.trim()) hf.value = d.hostname;
    }).catch(() => {
      document.getElementById('vv-detect-banner').innerHTML = 'Detection unavailable';
    });
})();


let _vvRedirect = '?tab=scheduler';

// ── Detection banner ──────────────────────────────────────────────────────────
(function() {
  const _ac = new AbortController();
  setTimeout(() => _ac.abort(), 6000);
  fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
    .then(r => r.json()).then(d => {
      const b = document.getElementById('vv-detect-banner');
      if (!d.ok) { b.innerHTML = 'Detection unavailable'; return; }
      const modeLabel = d.mode === 'internal'
        ? 'internal (NVMe/SSD)'
        : 'flash mode (USB boot)';
      b.innerHTML =
        '
OSUnraid ' + (d.unraid_ver||'') + '
' +
        '
Boot device' + d.boot_device + ' (' + d.transport + ')
' +
        '
Storage mode' + modeLabel + '
' +
        '
Scripts dir' + d.scripts_dir + '
';
      const hf = document.getElementById('vv-hostname');
      if (hf && !hf.value.trim()) hf.value = d.hostname;
    }).catch(() => {
      document.getElementById('vv-detect-banner').innerHTML = 'Detection unavailable';
    });
})();

let _vvRedirect    = '?tab=scheduler';
let _vvStorageMode = 'flash';
let _vvCurrentDir  = '';

function vvSetStorage(mode) {
  _vvStorageMode = mode;
  document.getElementById('vv-store-flash')?.classList.toggle('active',    mode === 'flash');
  document.getElementById('vv-store-internal')?.classList.toggle('active', mode === 'internal');
  const hint = document.getElementById('vv-store-hint');
  if (hint) hint.textContent = mode === 'flash'
    ? 'Scripts live in appdata — requires array to be started. Recommended for USB flash boot.'
    : 'Scripts live on /boot — available before array mounts. Requires NVMe/SSD boot.';
}

// ── Detection banner ──────────────────────────────────────────────────────────
(function() {
  const _ac = new AbortController();
  setTimeout(() => _ac.abort(), 6000);
  fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
    .then(r => r.json()).then(d => {
      const b = document.getElementById('vv-detect-banner');
      if (!d.ok) { b.innerHTML = 'Detection unavailable'; return; }
      _vvCurrentDir = d.scripts_dir || '';
      b.innerHTML =
        '
OSUnraid ' + (d.unraid_ver||'') + '
' +
        '
Boot device' + d.boot_device + ' (' + d.transport + ')
' +
        '
Scripts dir' + d.scripts_dir + '
';
      vvSetStorage(d.mode);
      const hf = document.getElementById('vv-hostname');
      if (hf && !hf.value.trim()) hf.value = d.hostname;
    }).catch(() => {
      document.getElementById('vv-detect-banner').innerHTML = 'Detection unavailable';
    });
})();

+  document.getElementById('vv-store-flash')?.classList.toggle('active',    mode === 'flash');

+  document.getElementById('vv-store-internal')?.classList.toggle('active', mode === 'internal');

+  const hint = document.getElementById('vv-store-hint');

+  if (hint) hint.textContent = mode === 'flash'

+    ? 'Scripts live in appdata — requires array to be started. Recommended for USB flash boot.'

+    : 'Scripts live on /boot — available before array mounts. Requires NVMe/SSD boot.';

-      const modeLabel = d.mode === 'internal'

+      _vvCurrentDir = d.scripts_dir || '';

-        '
Storage mode' + modeLabel + '
' +


    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname})
  }).then(r => r.json()).then(d => {
    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler', d.api_key); }
    else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });

    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      if (d.needs_migration) {
        const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';
        vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');
        vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);
      } else {
        vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);
      }
    } else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
}

function vvDoMigration(to, redirect, apiKey) {
  fetch('/plugins/varaverk/api/storage.php', {
    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({action: 'migrate', to})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      vvShowStep2(redirect, apiKey);
    } else {
      vvSetBtn('Save and continue →', false);
      vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');
    }
  }).catch(() => {
    vvSetBtn('Save and continue →', false);
    vvSetStatus('✗ Migration request failed', 'err');
  });


    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname})
  }).then(r => r.json()).then(d => {
    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler', d.api_key); }
    else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });

    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      if (d.needs_migration) {
        const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';
        vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');
        vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);
      } else {
        vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);
      }
    } else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
}

function vvDoMigration(to, redirect, apiKey) {
  fetch('/plugins/varaverk/api/storage.php', {
    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({action: 'migrate', to})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      vvShowStep2(redirect, apiKey);
    } else {
      vvSetBtn('Save and continue →', false);
      vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');
    }
  }).catch(() => {
    vvSetBtn('Save and continue →', false);
    vvSetStatus('✗ Migration request failed', 'err');
  });

-    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname})

+    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})

-    if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler', d.api_key); }

-    else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }

+        const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';

+        vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');

+        vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);

+        vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);

+    } else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }

+function vvDoMigration(to, redirect, apiKey) {

+  fetch('/plugins/varaverk/api/storage.php', {

+    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},

+    body: new URLSearchParams({action: 'migrate', to})

+      vvSetBtn('Save and continue →', false);

+      vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');

+    vvSetStatus('✗ Migration request failed', 'err');


Now fix the closing brace — `vvDoSave` lost its `}` since I moved `vvDoMigration` inside it. Let me check the current state of that section.


360	}
361
362	// ── API key ───────────────────────────────────────────────────────────────────
363	function vvCreateKey(btn) {
364	  const status = document.getElementById('vv-key-status');
365	  btn.disabled = true; btn.textContent = '⟳ Creating…';
366	  fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
367	    .then(r => r.json()).then(d => {
368	      if (d.ok) {
369	        status.textContent = '✓ Key created — ' + d.key_preview;
370	        status.style.color = '#4a8';
371	        btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
372	        vvLoadChecklist();
373	      } else {
374	        status.textContent = '✗ ' + (d.error || 'Failed');
375	        status.style.color = '#a44';
376	        btn.disabled = false; btn.textContent = 'Retry';
377	      }
378	    }).catch(e => {
379	      status.textContent = '✗ ' + e; status.style.color = '#a44';
380	      btn.disabled = false; btn.textContent = 'Retry';
381	    });
382	}
383
384	// ── Save ──────────────────────────────────────────────────────────────────────
385	function vvDoSave() {
386	  const hostname = document.getElementById('vv-hostname')?.value.trim();
387	  if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
388	  let host1 = '', host2 = '', mySlot = 'host1';
389	  if (vvRole === 'primary') {
390	    host1  = hostname;
391	    host2  = document.getElementById('vv-partner-hostname')?.value.trim() || '';
392	    mySlot = 'host1';
393	  } else {
394	    const primary = document.getElementById('vv-primary-hostname')?.value.trim();
395	    if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
396	    mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
397	    host1  = primary;
398	    if (mySlot === 'host2') host2 = hostname;
399	  }
400	  vvSetBtn('Saving…', true);
401	  fetch('/plugins/varaverk/api/setup.php', {
402	    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
403	    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
404	  }).then(r => r.json()).then(d => {
405	    if (d.ok) {
406	      if (d.needs_migration) {
407	        const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';
408	        vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');
409	        vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);
410	      } else {
411	        vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);
412	      }
413	    } else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
414	  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
415	}
416
417	function vvDoMigration(to, redirect, apiKey) {
418	  fetch('/plugins/varaverk/api/storage.php', {
419	    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},

}

// ── API key ───────────────────────────────────────────────────────────────────
function vvCreateKey(btn) {
  const status = document.getElementById('vv-key-status');
  btn.disabled = true; btn.textContent = '⟳ Creating…';
  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';
    });
}

// ── Save ──────────────────────────────────────────────────────────────────────
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);
  fetch('/plugins/varaverk/api/setup.php', {
    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      if (d.needs_migration) {
        const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';
        vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');
        vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);
      } else {
        vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);
      }
    } else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
  }).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
}

function vvDoMigration(to, redirect, apiKey) {
  fetch('/plugins/varaverk/api/storage.php', {
    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},


418	  fetch('/plugins/varaverk/api/storage.php', {
419	    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
420	    body: new URLSearchParams({action: 'migrate', to})
421	  }).then(r => r.json()).then(d => {
422	    if (d.ok) {
423	      vvShowStep2(redirect, apiKey);
424	    } else {
425	      vvSetBtn('Save and continue →', false);
426	      vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');
427	    }
428	  }).catch(() => {
429	    vvSetBtn('Save and continue →', false);
430	    vvSetStatus('✗ Migration request failed', 'err');
431	  });
432	}
433	</script>
434

  fetch('/plugins/varaverk/api/storage.php', {
    method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({action: 'migrate', to})
  }).then(r => r.json()).then(d => {
    if (d.ok) {
      vvShowStep2(redirect, apiKey);
    } else {
      vvSetBtn('Save and continue →', false);
      vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');
    }
  }).catch(() => {
    vvSetBtn('Save and continue →', false);
    vvSetStatus('✗ Migration request failed', 'err');
  });
}
</script>


Good — the JS structure is clean. Now update `api/setup.php` to accept `storage_mode` and return migration info.


        // 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);

        // Storage mode: use wizard selection, fall back to auto-detect from boot transport
        $smParam = trim($_POST['storage_mode'] ?? '');
        if ($smParam === 'flash') {
            $storageInternal = 'false';
        } elseif ($smParam === 'internal') {
            $storageInternal = 'true';
        } else {
            $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);


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)

        // 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);

        // Storage mode: use wizard selection, fall back to auto-detect from boot transport
        $smParam = trim($_POST['storage_mode'] ?? '');
        if ($smParam === 'flash') {
            $storageInternal = 'false';
        } elseif ($smParam === 'internal') {
            $storageInternal = 'true';
        } else {
            $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);

$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';

+        // Storage mode: use wizard selection, fall back to auto-detect from boot transport

+        $smParam = trim($_POST['storage_mode'] ?? '');

+            $storageInternal = 'false';

+        } elseif ($smParam === 'internal') {

+            $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';


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

$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);

echo json_encode([
    'ok'             => true,
    'host_id'        => $hostId,
    'api_key'        => $apiKeyResult,
    'needs_migration'=> $needsMigration,
    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
    'redirect'       => '?tab=scheduler&vv_setup=master.conf',
]);


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

$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);

echo json_encode([
    'ok'             => true,
    'host_id'        => $hostId,
    'api_key'        => $apiKeyResult,
    'needs_migration'=> $needsMigration,
    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
    '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);

+$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';

+$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);

-    'redirect' => '?tab=scheduler&vv_setup=master.conf',

+    'needs_migration'=> $needsMigration,

+    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,

+    'redirect'       => '?tab=scheduler&vv_setup=master.conf',


Now verify the final state of both files looks right around the changed sections.


218	if (!file_exists(CONF_DIR . '/' . $confFile)) {
219	    $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
220	    if ($template) {
221	        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
222	        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
223
224	        // Storage mode: use wizard selection, fall back to auto-detect from boot transport
225	        $smParam = trim($_POST['storage_mode'] ?? '');
226	        if ($smParam === 'flash') {
227	            $storageInternal = 'false';
228	        } elseif ($smParam === 'internal') {
229	            $storageInternal = 'true';
230	        } else {
231	            $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
232	            $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
233	            $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
234	            $storageInternal = ($transport !== 'usb') ? 'true' : 'false';
235	        }
236
237	        $conf = str_replace('HOSTN', $hostId,    $template);
238	        $conf = str_replace('hostn', $hostIdLow, $conf);
239	        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
240	            '${1}"' . $sshKeyPath . '"', $conf);
241	        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
242	            '${1}' . $storageInternal, $conf);
243	        if (!vv_write_conf_raw($confFile, $conf)) {
244	            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
245	            exit;
246	        }
247	    }
248	}
249
250	// Write setup state file — lets partner servers know HOST1 is configured
251	vv_setup_state_write(['host1_hostname' => $host1]);
252
253	// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
254	if (file_exists($sshScript)) {
255	    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
256	}
257
258	// Auto-create Unraid API key and write into the fresh conf
259	$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
260
261	$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
262	$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);
263
264	echo json_encode([
265	    'ok'             => true,
266	    'host_id'        => $hostId,
267	    'api_key'        => $apiKeyResult,
268	    'needs_migration'=> $needsMigration,
269	    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
270	    'redirect'       => '?tab=scheduler&vv_setup=master.conf',
271	]);
272

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';

        // Storage mode: use wizard selection, fall back to auto-detect from boot transport
        $smParam = trim($_POST['storage_mode'] ?? '');
        if ($smParam === 'flash') {
            $storageInternal = 'false';
        } elseif ($smParam === 'internal') {
            $storageInternal = 'true';
        } else {
            $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-generate SSH keypair (local only — remote copy happens during onboarding)
if (file_exists($sshScript)) {
    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
}

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

$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);

echo json_encode([
    'ok'             => true,
    'host_id'        => $hostId,
    'api_key'        => $apiKeyResult,
    'needs_migration'=> $needsMigration,
    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
    'redirect'       => '?tab=scheduler&vv_setup=master.conf',
]);


84
85

86
87

Detecting environment…

88
89

90	      Storage mode
91

92

93	          Appdata
USB boot · requires array
94

95

96	          Internal Boot
NVMe/SSD · no array dep
97

98

99

100

101
102

103	      This server's hostname
104	      <input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
105
Must match Unraid Settings → Identification exactly (case-sensitive)

106

107
108

109	    Server role
110

111

112	        Primary
HOST1 · first server
113

Detecting environment…

      Storage mode

          Appdata
USB boot · requires array

          Internal Boot
NVMe/SSD · no array dep

      This server's hostname
      <input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">

Must match Unraid Settings → Identification exactly (case-sensitive)

    Server role

        Primary
HOST1 · first server


There's an edge case: if `$confFile` already exists, the block is skipped and `$storageInternal` is undefined when we compute `$targetDir`. Let me fix that.


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';

        // Storage mode: use wizard selection, fall back to auto-detect from boot transport
        $smParam = trim($_POST['storage_mode'] ?? '');
        if ($smParam === 'flash') {
            $storageInternal = 'false';
        } elseif ($smParam === 'internal') {
            $storageInternal = 'true';
        } else {
            $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);

// Storage mode: use wizard selection, fall back to auto-detect from boot transport
$smParam = trim($_POST['storage_mode'] ?? '');
if ($smParam === 'flash') {
    $storageInternal = 'false';
} elseif ($smParam === 'internal') {
    $storageInternal = 'true';
} else {
    $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';
}

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';

        $conf = str_replace('HOSTN', $hostId,    $template);


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';

        // Storage mode: use wizard selection, fall back to auto-detect from boot transport
        $smParam = trim($_POST['storage_mode'] ?? '');
        if ($smParam === 'flash') {
            $storageInternal = 'false';
        } elseif ($smParam === 'internal') {
            $storageInternal = 'true';
        } else {
            $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);

// Storage mode: use wizard selection, fall back to auto-detect from boot transport
$smParam = trim($_POST['storage_mode'] ?? '');
if ($smParam === 'flash') {
    $storageInternal = 'false';
} elseif ($smParam === 'internal') {
    $storageInternal = 'true';
} else {
    $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';
}

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';

        $conf = str_replace('HOSTN', $hostId,    $template);

+// Storage mode: use wizard selection, fall back to auto-detect from boot transport

+$smParam = trim($_POST['storage_mode'] ?? '');

+    $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';

-        // Storage mode: use wizard selection, fall back to auto-detect from boot transport

-        $smParam = trim($_POST['storage_mode'] ?? '');

-            $storageInternal = 'false';

-        } elseif ($smParam === 'internal') {

-            $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', $hostIdLow, $conf);

$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',


212
213	// Create host*.conf from template
214	$hostId    = strtoupper($mySlot);
215	$hostIdLow = strtolower($mySlot);
216	$confFile  = $hostIdLow . '.conf';
217
218	// Storage mode: use wizard selection, fall back to auto-detect from boot transport
219	$smParam = trim($_POST['storage_mode'] ?? '');
220	if ($smParam === 'flash') {
221	    $storageInternal = 'false';
222	} elseif ($smParam === 'internal') {
223	    $storageInternal = 'true';
224	} else {
225	    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
226	    $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
227	    $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
228	    $storageInternal = ($transport !== 'usb') ? 'true' : 'false';
229	}
230
231	if (!file_exists(CONF_DIR . '/' . $confFile)) {
232	    $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
233	    if ($template) {
234	        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
235	        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
236
237	        $conf = str_replace('HOSTN', $hostId,    $template);
238	        $conf = str_replace('hostn', $hostIdLow, $conf);
239	        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
240	            '${1}"' . $sshKeyPath . '"', $conf);
241	        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
242	            '${1}' . $storageInternal, $conf);
243	        if (!vv_write_conf_raw($confFile, $conf)) {
244	            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
245	            exit;
246	        }
247	    }
248	}
249
250	// Write setup state file — lets partner servers know HOST1 is configured
251	vv_setup_state_write(['host1_hostname' => $host1]);
252
253	// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
254	if (file_exists($sshScript)) {
255	    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
256	}
257
258	// Auto-create Unraid API key and write into the fresh conf
259	$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
260
261	$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
262	$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);
263
264	echo json_encode([
265	    'ok'             => true,
266	    'host_id'        => $hostId,
267	    'api_key'        => $apiKeyResult,
268	    'needs_migration'=> $needsMigration,
269	    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
270	    'redirect'       => '?tab=scheduler&vv_setup=master.conf',
271	]);
272

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

// Storage mode: use wizard selection, fall back to auto-detect from boot transport
$smParam = trim($_POST['storage_mode'] ?? '');
if ($smParam === 'flash') {
    $storageInternal = 'false';
} elseif ($smParam === 'internal') {
    $storageInternal = 'true';
} else {
    $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';
}

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';

        $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-generate SSH keypair (local only — remote copy happens during onboarding)
if (file_exists($sshScript)) {
    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
}

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

$targetDir      = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);

echo json_encode([
    'ok'             => true,
    'host_id'        => $hostId,
    'api_key'        => $apiKeyResult,
    'needs_migration'=> $needsMigration,
    'migrate_to'     => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
    'redirect'       => '?tab=scheduler&vv_setup=master.conf',
]);


That's clean. Here's what the full flow now looks like end-to-end:

**First-run wizard (USB flash boot):**
1. Wizard opens → detect API runs → detection banner shows OS/boot device/scripts dir
2. Storage mode toggle auto-selects **Appdata** (USB detected), with hint text explaining array dependency
3. User can override to **Internal Boot** if they want (e.g. planning to migrate to NVMe)
4. Save → `api/setup.php` creates `host1.conf` with `HOST1_STORAGE_MODE_INTERNAL=false`, computes `needs_migration=true` (current SCRIPTS_DIR is `/boot`, target is `/mnt/user/appdata/Varaverk`)
5. Wizard JS shows "⟳ Migrating scripts to appdata…", calls `api/storage.php` POST migrate→flash
6. `storage_migrate.sh --to=flash` runs: git push → rsync to appdata → updates varaverk.cfg → updates master.conf paths → updates host conf → rebuilds cron → syncs Plugin/ back to /boot → cleans scripts from /boot
7. Step 2 loads from the new appdata location

**NVMe boot:** detect auto-selects Internal Boot, `needs_migration=false` (already in /boot), wizard goes straight to Step 2.


now if internal boot is detected it stays there right


Yes. Let me trace through it:

1. `api/setup.php?action=detect` → transport is `sata`/`nvme` → returns `mode: 'internal'`
2. `vvSetStorage('internal')` → `_vvStorageMode = 'internal'`
3. Save POST sends `storage_mode=internal` → `$storageInternal = 'true'`
4. `$targetDir = '/boot/config/plugins/varaverk'`
5. `SCRIPTS_DIR` (from varaverk.cfg) is already `/boot/config/plugins/varaverk`
6. `$needsMigration = false` → response skips `migrate_to`
7. Wizard JS sees `d.needs_migration` is false → calls `vvShowStep2()` directly, no migration step

Nothing moves. The only path that triggers migration is when the user's selected mode points to a different directory than where scripts currently live.


ok, so now .confs are correct. it got so messed up last night. i think we should scan all scripts and make sure all vars in .confs are there and correct not stale


Good call. I'll spawn an agent to do the full scan — read the three confs, grep all scripts for variable references, and produce a gap report.


Agent

Conf variable audit — find missing and stale vars

Prompt:
Audit the Varaverk plugin conf files against all script references. Goal: find (A) variables referenced in scripts but missing from confs, and (B) variables defined in confs but never referenced anywhere (stale/dead).

## Conf files to read (read all three completely):
- /boot/config/plugins/varaverk/Configurations/master.conf
- /boot/config/plugins/varaverk/Configurations/host1.conf
- /boot/config/plugins/varaverk/Configurations/host2.conf

Build a complete list of every variable NAME defined in each conf (e.g. `HOST1_NPM_URL`, `RSYNC_ENABLED`, etc). Ignore comment lines and blank lines.

## Scripts to scan:
All .sh files under /boot/config/plugins/varaverk/ (recursively), excluding anything under .git/.

Grep for every reference to a conf variable. Conf variables follow these patterns:
- `$VAR_NAME` or `${VAR_NAME}` where VAR_NAME is ALL_CAPS with underscores
- Specifically: anything matching `\$\{?[A-Z][A-Z0-9_]{3,}\}?` that isn't a bash builtin (skip: PATH, HOME, PWD, IFS, BASH, SECONDS, LINENO, PIPESTATUS, FUNCNAME, BASH_SOURCE, EUID, UID, HOSTNAME, RANDOM, OLDPWD, REPLY, OPTIND, OPTARG, PS1, PS2, TERM, SHELL, USER, LOGNAME, LANG, LC_ALL, LC_CTYPE, DISPLAY, EDITOR, VISUAL, PAGER, MANPATH, TMPDIR, TMP, TEMP, SHLVL, COLUMNS, LINES, HISTSIZE, HISTFILE, HISTCONTROL, CDPATH, GLOBIGNORE, TIMEFORMAT, TMOUT, MAILCHECK, MAIL, MAILPATH, PROMPT_COMMAND, IGNOREEOF, POSIXLY_CORRECT, BASHOPTS, SHELLOPTS, COMP_WORDS, COMP_CWORD, COMPREPLY, COMP_LINE, COMP_POINT, COMP_TYPE, COMP_KEY, COMP_WORDBREAKS, DIRSTACK, GROUPS, PIPESTATUS)
- Also skip local script variables that are clearly defined within the script itself (e.g. `local VAR=`, `VAR=` at script scope that aren't conf-sourced)

Also read /boot/config/plugins/varaverk/Configurations/host.conf.template to understand what's expected for new hosts.

## What to produce:

**Section 1: Variables referenced in scripts but NOT in any conf**
List each missing variable, which script(s) reference it, and a short note on what it likely is. Group by conf file where it probably belongs (master / host1 / host2 / both hosts).

**Section 2: Variables defined in confs but NOT referenced in any script**
List each stale variable and which conf it's in. Note if it might be used indirectly (e.g. passed via SSH, used by PHP, etc).

**Section 3: Variables present in host1.conf but missing from host2.conf (or vice versa)**
These should be symmetric — flag any asymmetry.

Be thorough. Read the actual conf files fully, not just headers. This is a correctness audit.


Good - those HOST2 specific variables appear to be unused. Let me finalize the audit with concrete findings:

---

## VARAVERK PLUGIN CONFIGURATION AUDIT REPORT

### SECTION 1: Variables Referenced in Scripts But Missing from Confs

**Finding: NONE CRITICAL**

- HOST2_LIDARR_PATH_MAP is NOT referenced in any script (Lidarr is HOST1-only, so this is correct)
- HOST2_WATCHDOG_CONTAINER_API_CHECKS is intentionally empty (like HOST1), format-only

### SECTION 2: Variables Defined in Confs But Never Referenced in Scripts

**Finding: NONE** - All defined variables are referenced or used indirectly via dynamic variable expansion (e.g., `${MY_ID}_LIDARR_URL`)

### SECTION 3: Asymmetries Between HOST1.conf and HOST2.conf

**3a. EXPECTED ASYMMETRIES (correct design):**
- **Lidarr ecosystem** (HOST1 only): HOST1_LIDARR_API_KEY, HOST1_LIDARR_URL, HOST1_LIDARR_MUSIC_ROOT, HOST1_LIDARR_RECOVERY - Lidarr runs only on HOST1 ✓
- **Download clients** (HOST1 only): HOST1_QBIT_*, HOST1_SABNZBD_*, HOST1_SLSKD_* - Downloads run on HOST1 only ✓
- **Partnership ownership** (HOST1 only): HOST1_PARTNERSHIP_ARR_STACK, HOST1_PARTNERSHIP_AUTH_STACK - HOST1 is partner owner ✓
- **Gitea token** (HOST1 only): HOST1_GITEA_API_TOKEN - Only owner manages SSH key setup ✓

**3b. STALE/UNUSED VARIABLES IN HOST2.conf (should be removed):**
- **HOST2_AUTHELIA_CONFIG** - Line 659: `"/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml"` - NEVER referenced in any script
- **HOST2_AUTHELIA_CONTAINER** - Line 660: `"Authelia"` - NEVER referenced in any script
- **HOST2_LLDAP_URL, HOST2_LLDAP_USER, HOST2_LLDAP_PASS** - Lines 654-656 - NEVER referenced; only used in interactive UI context, not scripts
- **HOST2_NPM_USER, HOST2_NPM_PASS** - Lines 651-652 - NEVER referenced in scripts (only for auth UI)

**3c. INCOMPLETE HOST2 CONFIG (documented as "rebuild in progress"):**
- HOST2_PARTNERSHIP_AUTH_WEBUIS - intentionally commented out until HOST2 back online (documented) ✓
- HOST2_WEEKLY_SYNC_SHARES - intentionally commented out (documented) ✓
- HOST2_CRITICAL_SYNC_SHARES - intentionally commented out (documented) ✓
- Multiple cleanup/media folders commented out (documented) ✓

**3d. MISSING EQUIVALENT IN HOST1 (not needed):**
- HOST2 has explicit "AUTHELIA_CONFIG" and "AUTHELIA_CONTAINER" but HOST1 does not
- Reason: These appear to be dead code from earlier configuration attempts, never actually used

---

### Summary

**Status: MOSTLY CORRECT with minor dead code in HOST2**

**Issues Found:**
1. Lines 659-660 in host2.conf (HOST2_AUTHELIA_CONFIG, HOST2_AUTHELIA_CONTAINER) - **Stale/dead code**, never referenced
2. Lines 651-656 in host2.conf (HOST2_NPM_USER, HOST2_NPM_PASS, HOST2_LLDAP_*) - **UI-only config**, not used by scripts (document or relocate)

**Recommendation:** Remove or clearly document the non-scripted config variables in HOST2.conf, or move them to a separate "ui-config" file if they're needed for interactive setup.

All asymmetries between HOST1 and HOST2 are either expected (Lidarr HOST1-only, partnership ownership) or documented (HOST2 rebuild pending).

Prompt:
Audit the Varaverk plugin conf files against all script references. Goal: find (A) variables referenced in scripts but missing from confs, and (B) variables defined in confs but never referenced anywhere (stale/dead).

## Conf files to read (read all three completely):
- /boot/config/plugins/varaverk/Configurations/master.conf
- /boot/config/plugins/varaverk/Configurations/host1.conf
- /boot/config/plugins/varaverk/Configurations/host2.conf

Build a complete list of every variable NAME defined in each conf (e.g. `HOST1_NPM_URL`, `RSYNC_ENABLED`, etc). Ignore comment lines and blank lines.

## Scripts to scan:
All .sh files under /boot/config/plugins/varaverk/ (recursively), excluding anything under .git/.

Grep for every reference to a conf variable. Conf variables follow these patterns:
- `$VAR_NAME` or `${VAR_NAME}` where VAR_NAME is ALL_CAPS with underscores
- Specifically: anything matching `\$\{?[A-Z][A-Z0-9_]{3,}\}?` that isn't a bash builtin (skip: PATH, HOME, PWD, IFS, BASH, SECONDS, LINENO, PIPESTATUS, FUNCNAME, BASH_SOURCE, EUID, UID, HOSTNAME, RANDOM, OLDPWD, REPLY, OPTIND, OPTARG, PS1, PS2, TERM, SHELL, USER, LOGNAME, LANG, LC_ALL, LC_CTYPE, DISPLAY, EDITOR, VISUAL, PAGER, MANPATH, TMPDIR, TMP, TEMP, SHLVL, COLUMNS, LINES, HISTSIZE, HISTFILE, HISTCONTROL, CDPATH, GLOBIGNORE, TIMEFORMAT, TMOUT, MAILCHECK, MAIL, MAILPATH, PROMPT_COMMAND, IGNOREEOF, POSIXLY_CORRECT, BASHOPTS, SHELLOPTS, COMP_WORDS, COMP_CWORD, COMPREPLY, COMP_LINE, COMP_POINT, COMP_TYPE, COMP_KEY, COMP_WORDBREAKS, DIRSTACK, GROUPS, PIPESTATUS)
- Also skip local script variables that are clearly defined within the script itself (e.g. `local VAR=`, `VAR=` at script scope that aren't conf-sourced)

Also read /boot/config/plugins/varaverk/Configurations/host.conf.template to understand what's expected for new hosts.

## What to produce:

**Section 1: Variables referenced in scripts but NOT in any conf**
List each missing variable, which script(s) reference it, and a short note on what it likely is. Group by conf file where it probably belongs (master / host1 / host2 / both hosts).

**Section 2: Variables defined in confs but NOT referenced in any script**
List each stale variable and which conf it's in. Note if it might be used indirectly (e.g. passed via SSH, used by PHP, etc).

**Section 3: Variables present in host1.conf but missing from host2.conf (or vice versa)**
These should be symmetric — flag any asymmetry.

Be thorough. Read the actual conf files fully, not just headers. This is a correctness audit.


The audit came back clean on the critical side — no missing vars, no broken references. Two findings worth looking at in host2.conf. Let me pull those lines to verify before doing anything.


645	# Credentials for the Varaverk Auth Stack page (NPM, lldap, Authelia).
646	# Fill in when HOST2 is back online.
647
648	# ━━━ NginxProxyManager ━━━
649	    HOST2_NPM_URL="http://localhost:81"
650	    HOST2_NPM_USER=""                           # NPM admin email
651	    HOST2_NPM_PASS=""                           # NPM admin password
652
653	# ━━━ lldap ━━━
654	    HOST2_LLDAP_URL="http://localhost:17170"
655	    HOST2_LLDAP_USER="admin"                    # lldap admin username
656	    HOST2_LLDAP_PASS=""                         # lldap admin password
657
658	# ━━━ Authelia ━━━
659	    HOST2_AUTHELIA_CONFIG="/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml"
660	    HOST2_AUTHELIA_CONTAINER="Authelia"
661
662	# ==============================================================================================
663	# ──────────────────────── End Of HOST2 Variables ──────────────────────────────────────────────
664	# ==============================================================================================

# Credentials for the Varaverk Auth Stack page (NPM, lldap, Authelia).
# Fill in when HOST2 is back online.

# ━━━ NginxProxyManager ━━━
    HOST2_NPM_URL="http://localhost:81"
    HOST2_NPM_USER=""                           # NPM admin email
    HOST2_NPM_PASS=""                           # NPM admin password

# ━━━ lldap ━━━
    HOST2_LLDAP_URL="http://localhost:17170"
    HOST2_LLDAP_USER="admin"                    # lldap admin username
    HOST2_LLDAP_PASS=""                         # lldap admin password

# ━━━ Authelia ━━━
    HOST2_AUTHELIA_CONFIG="/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml"
    HOST2_AUTHELIA_CONTAINER="Authelia"

# ==============================================================================================
# ──────────────────────── End Of HOST2 Variables ──────────────────────────────────────────────
# ==============================================================================================


Bash

Check host1 auth stack vars


(Bash completed with no output)


grep -rn "AUTHELIA_CONFIG\|AUTHELIA_CONTAINER\|NPM_USER\|NPM_PASS\|LLDAP_URL\|LLDAP_USER\|LLDAP_PASS" /boot/config/plugins/varaverk/ --include="*.sh" --include="*.php" | grep -v "host1.conf\|host2.conf\|host.conf.template" | grep -v "Binary file"

Find any script/PHP references to these vars


/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:11:        'npm_user'           => $v["{$host}_NPM_USER"]                 ?? '',
/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:12:        'npm_pass'           => $v["{$host}_NPM_PASS"]                 ?? '',
/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:13:        'lldap_url'          => rtrim($v["{$host}_LLDAP_URL"]          ?? 'http://localhost:17170', '/'),
/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:14:        'lldap_user'         => $v["{$host}_LLDAP_USER"]               ?? '',
/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:15:        'lldap_pass'         => $v["{$host}_LLDAP_PASS"]               ?? '',
/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:16:        'authelia_config'    => $v["{$host}_AUTHELIA_CONFIG"]          ?? '/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml',
/boot/config/plugins/varaverk/Plugin/unraid/include/auth.php:17:        'authelia_container' => $v["{$host}_AUTHELIA_CONTAINER"]       ?? 'Authelia',


The agent missed those — they're all used in `auth.php` via dynamic expansion (`{$host}_NPM_USER`, etc). Not dead code at all. Let me check what HOST1 has in that same auth section.


grep -n "NPM\|LLDAP\|AUTHELIA" /boot/config/plugins/varaverk/Configurations/host1.conf | head -40

Check host1 auth vars


45:#   NETWORK WATCHDOG        DDNS domain, NPM URL for connectivity checks
135:# On onboard → WebUI pointed at owner's Tailscale IP (mirror clicks NPM, gets owner's auth)
474:    HOST1_NETWORK_WATCHDOG_NPM_URL="https://gmer4lfe.com"
647:# Domains checked via direct openssl connection — not relying on NPM's certificate state.
648:# Checks the actual certificate served, not what NPM thinks it has.


grep -n "NPM_URL\|NPM_USER\|NPM_PASS" /boot/config/plugins/varaverk/Configurations/host1.conf

Check host1 NPM vars