From af89258bbdfe8bd4d09a8c95f11fea6db11895ab Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sat, 30 May 2026 15:43:04 -0400 Subject: [PATCH] Setup wizard: full first-run flow for HOST1 and HOST2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Varaverk.page: also shows wizard when local host.conf is missing (handles master.conf pushed by HOST1 before HOST2 installs plugin). pages/setup.php: three wizard flows - Standard: blank master.conf, fill hostnames, redirect to scheduler - Host2/state file: state file detected, pull master.conf from HOST1 via SSH - Conf-only: master.conf already filled (was pushed), just create local host.conf api/setup.php: - save action: writes master.conf + host.conf, creates varaverk_setup.db state file, redirects to ?tab=scheduler&vv_setup=master.conf - pull action: resolves HOST1 Tailscale IP, queries HOST1 SCRIPTS_DIR, SCPs master.conf, creates local host.conf, redirects to ?tab=scheduler&vv_setup=hostN.conf include/config.php: vv_setup_state_read/write/push helpers. vv_push_setup_state() pushes varaverk_setup.db to /boot/config/ on all known remotes — no plugin-readiness probe needed (flash is always accessible). api/rawconf.php: calls vv_push_setup_state() alongside master.conf push. pages/scheduler.php: setup mode via ?vv_setup= URL param. Auto-opens the specified conf file on page load (DOMContentLoaded). vvSaveRawConf: in setup mode, skips confirm dialog and forces sequence: master.conf save → auto-open hostN.conf hostN.conf save → redirect to Monitor (setup complete) --- Plugin/unraid/Varaverk.page | 10 +- Plugin/unraid/api/rawconf.php | 6 +- Plugin/unraid/api/setup.php | 151 ++++++++---- Plugin/unraid/include/config.php | 51 ++++ Plugin/unraid/pages/scheduler.php | 45 +++- Plugin/unraid/pages/setup.php | 375 +++++++++++++++--------------- 6 files changed, 396 insertions(+), 242 deletions(-) diff --git a/Plugin/unraid/Varaverk.page b/Plugin/unraid/Varaverk.page index 05bcbad..5a39a9d 100644 --- a/Plugin/unraid/Varaverk.page +++ b/Plugin/unraid/Varaverk.page @@ -9,14 +9,18 @@ $pluginDir = "$docroot/plugins/$plugin"; require_once "$pluginDir/include/config.php"; -// First-run check — show setup wizard if HOST1 is not configured +// First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing $_master = vv_read_conf_raw('master.conf'); preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m); -if (empty(trim($_h1m[1] ?? ''))) { +$_host1_blank = empty(trim($_h1m[1] ?? '')); +$_my_hostid = vv_detect_host(); +$_conf_missing = $_my_hostid !== 'unknown' + && !file_exists(CONF_DIR . '/' . $_my_hostid . '.conf'); +if ($_host1_blank || $_conf_missing) { include "$pluginDir/pages/setup.php"; return; } -unset($_master, $_h1m); +unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing); // Determine active tab $tab = $_GET['tab'] ?? 'monitor'; diff --git a/Plugin/unraid/api/rawconf.php b/Plugin/unraid/api/rawconf.php index 5671f9d..6bc3f62 100644 --- a/Plugin/unraid/api/rawconf.php +++ b/Plugin/unraid/api/rawconf.php @@ -23,7 +23,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } $written = vv_write_conf_raw($file, $content); - $push = ($written && $file === 'master.conf') ? vv_push_master_conf() : []; + $push = []; + if ($written && $file === 'master.conf') { + $push = vv_push_master_conf(); + vv_push_setup_state(); + } echo json_encode(['ok' => $written, 'push' => $push]); exit; } diff --git a/Plugin/unraid/api/setup.php b/Plugin/unraid/api/setup.php index 8aaec5e..b7f719b 100644 --- a/Plugin/unraid/api/setup.php +++ b/Plugin/unraid/api/setup.php @@ -7,58 +7,119 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { exit; } +$action = trim($_POST['action'] ?? 'save'); + +// ── 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] ?? '/mnt/user/appdata/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) { + $hostname = $myHostname ?: vv_get_hostname(); + $sshKeyPath = $sshKey; + $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); + 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'); // 'host1', 'host2', 'host3', ... +$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; } - -// Validate slot — must be host\d+ if (!preg_match('/^host\d+$/', $mySlot)) { echo json_encode(['ok' => false, 'error' => 'Invalid slot']); exit; } -// ── Write HOST1 / HOST2 into master.conf ───────────────────────────────────────────────────── +// 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; } -// Replace HOST1 and HOST2 lines (preserving indentation) -$master = preg_replace( - '/^(\s*HOST1\s*=\s*).*$/m', - '${1}"' . addslashes($host1) . '"', - $master -); -$master = preg_replace( - '/^(\s*HOST2\s*=\s*).*$/m', - '${1}"' . addslashes($host2) . '"', - $master -); +$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master); +$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master); -// For partner slots beyond HOST2 — ensure the slot line exists in master.conf $slotNum = (int) preg_replace('/\D/', '', $mySlot); if ($slotNum > 2 && !empty($myHostname)) { $hostKey = 'HOST' . $slotNum; if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) { - // Append after HOST2 line - $master = preg_replace( - '/^(\s*HOST2\s*=.*$)/m', - '$1' . "\n {$hostKey}=\"" . addslashes($myHostname) . '"', - $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 - ); + $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m', + '${1}"' . addslashes($myHostname) . '"', $master); } } @@ -67,38 +128,32 @@ if (!vv_write_conf_raw('master.conf', $master)) { exit; } -// ── Create host*.conf from template ────────────────────────────────────────────────────────── -$hostId = strtoupper($mySlot); // HOST1, HOST2, HOST3 ... -$hostIdLow = strtolower($mySlot); // host1, host2, host3 ... +// Create host*.conf from template +$hostId = strtoupper($mySlot); +$hostIdLow = strtolower($mySlot); $confFile = $hostIdLow . '.conf'; -// Only create if it doesn't already exist — never overwrite an existing conf if (!file_exists(CONF_DIR . '/' . $confFile)) { - $template = @file_get_contents(CONF_DIR . '/host.conf.template'); - if ($template === false) { - $template = @file_get_contents(SCRIPTS_DIR . '/Configurations/host.conf.template'); - } - if ($template !== false) { - // Derive SSH key path from hostname convention - // unRAID-MyServer → /root/.ssh/myserver_rsync_automation + $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); - // Pre-fill the SSH key path - $conf = preg_replace( - '/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m', - '${1}"' . $sshKeyPath . '"', - $conf - ); - + $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m', + '${1}"' . $sshKeyPath . '"', $conf); if (!vv_write_conf_raw($confFile, $conf)) { echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]); exit; } } - // Template missing is non-fatal — user can create the conf manually } -echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile]); +// 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', +]); diff --git a/Plugin/unraid/include/config.php b/Plugin/unraid/include/config.php index dbf1635..4826720 100644 --- a/Plugin/unraid/include/config.php +++ b/Plugin/unraid/include/config.php @@ -10,6 +10,57 @@ define('CONF_DIR', SCRIPTS_DIR . '/Configurations'); define('LOG_DIR', '/var/log/varaverk'); unset($_vv_cfg); +const VV_SETUP_STATE_FILE = '/boot/config/varaverk_setup.db'; + +// Read the setup state file into a key=>value array. +function vv_setup_state_read(): array { + $out = []; + foreach (file(VV_SETUP_STATE_FILE) ?: [] as $line) { + [$k, $v] = explode('=', trim($line), 2) + ['', '']; + if ($k !== '') $out[$k] = $v; + } + return $out; +} + +// Write the setup state file (creates or overwrites). +function vv_setup_state_write(array $data): void { + $content = ''; + foreach ($data as $k => $v) $content .= "$k=$v\n"; + file_put_contents(VV_SETUP_STATE_FILE, $content); +} + +// Push the setup state file to all remote hosts via scp. +// Unlike master.conf push, this does NOT require the plugin to be installed on the remote — +// it only needs SSH to be reachable, and pushes to /boot/config/ (always available). +function vv_push_setup_state(): void { + if (!file_exists(VV_SETUP_STATE_FILE)) return; + $myHostId = vv_detect_host(); + $vars = vv_conf_vars(); + $sshKey = $vars[strtoupper($myHostId) . '_SSH_KEY'] ?? ''; + if (!$sshKey || !file_exists($sshKey)) return; + + $master = vv_read_conf_raw('master.conf'); + preg_match_all('/^\s*(HOST\d+)(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m); + $seen = []; + foreach ($m[1] as $i => $hostKey) { + $hostId = strtolower($hostKey); + if ($hostId === $myHostId || isset($seen[$hostId])) continue; + $seen[$hostId] = true; + $hostname = trim($m[2][$i]); + if (!$hostname) continue; + $ip = vv_resolve_tailscale_ip($hostname); + if (!$ip) continue; + $sshBase = 'ssh -i ' . escapeshellarg($sshKey) + . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip; + // Ensure the target dir exists (it always should on Unraid, but be safe) + shell_exec($sshBase . ' "mkdir -p /boot/config" 2>/dev/null'); + $dest = escapeshellarg('root@' . $ip . ':/boot/config/varaverk_setup.db'); + exec('scp -i ' . escapeshellarg($sshKey) + . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no' + . ' ' . escapeshellarg(VV_SETUP_STATE_FILE) . ' ' . $dest . ' 2>&1'); + } +} + // Push master.conf to all remote hosts via scp after a local save. // Returns one result entry per remote found in master.conf. // Silently returns [] on non-owner hosts (no SSH key, no remote access). diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index 52ce1e6..5e4fe3a 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -1,5 +1,12 @@ ; +const vvLocalHostConf = ; + +if (vvSetupConf) { + // Auto-open the setup conf file once the page is ready + document.addEventListener('DOMContentLoaded', () => { + setTimeout(() => vvLoadRawConf(vvSetupConf), 150); + }); +} + function vvEscHtml(s) { return s.replace(/&/g,'&').replace(//g,'>'); } @@ -2585,7 +2603,7 @@ function vvShowRawConfMode(title) { function vvSaveRawConf() { if (!vvRawConfFile) return; - if (!confirm('Save changes to "' + vvRawConfFile + '"?')) return; + if (!vvSetupConf && !confirm('Save changes to "' + vvRawConfFile + '"?')) return; const content = document.getElementById('vv-editor-body').value; const btn = document.getElementById('vv-save-rawconf-btn'); btn.disabled = true; @@ -2598,15 +2616,32 @@ function vvSaveRawConf() { alert('Save failed: ' + (d.error ?? 'Unknown error')); return; } + + // Setup mode: forced editing sequence + if (vvSetupConf) { + if (vvRawConfFile === 'master.conf') { + // master.conf saved → open host conf next + btn.textContent = '✓ Saved — opening ' + vvLocalHostConf + '…'; + vvSetupConf = vvLocalHostConf; + history.replaceState(null, '', location.pathname + '?tab=scheduler&vv_setup=' + encodeURIComponent(vvLocalHostConf)); + setTimeout(() => vvLoadRawConf(vvLocalHostConf), 400); + } else { + // Host conf saved → setup complete + btn.textContent = '✓ Setup complete — loading plugin…'; + vvSetupConf = ''; + setTimeout(() => { window.location.href = location.pathname + '?tab=monitor'; }, 800); + } + return; + } + + // Normal save: show push status const push = d.push ?? []; if (push.length === 0) { btn.textContent = '✓ Saved'; } else if (push.every(p => p.ok)) { - const hosts = push.map(p => p.host).join(', '); - btn.textContent = '✓ Saved · synced to ' + hosts; + btn.textContent = '✓ Saved · synced to ' + push.map(p => p.host).join(', '); } else { - const failed = push.filter(p => !p.ok).map(p => p.host).join(', '); - btn.textContent = '✓ Saved · push failed: ' + failed; + btn.textContent = '✓ Saved · push failed: ' + push.filter(p => !p.ok).map(p => p.host).join(', '); } setTimeout(() => { btn.textContent = 'Save Conf'; }, 3500); }) diff --git a/Plugin/unraid/pages/setup.php b/Plugin/unraid/pages/setup.php index 2fe7cf2..8383c13 100644 --- a/Plugin/unraid/pages/setup.php +++ b/Plugin/unraid/pages/setup.php @@ -1,6 +1,27 @@ @@ -15,116 +36,139 @@ $detectedHostname = trim(shell_exec('hostname -s') ?: ''); font-family: monospace; color: #ccc; } -#vv-setup h1 { - margin: 0 0 6px; - font-size: 18px; - color: #e0e0e0; - font-weight: normal; - letter-spacing: .04em; -} -#vv-setup .vv-setup-sub { - font-size: 12px; - color: #555; - margin-bottom: 32px; -} -#vv-setup .vv-setup-field { - margin-bottom: 20px; -} -#vv-setup label { - display: block; - font-size: 11px; - color: #888; - margin-bottom: 6px; - text-transform: uppercase; - letter-spacing: .06em; -} -#vv-setup input[type=text] { - width: 100%; - box-sizing: border-box; - background: #0d0d0d; - border: 1px solid #333; - color: #ddd; - padding: 7px 10px; - border-radius: 3px; - font-family: monospace; - font-size: 13px; -} -#vv-setup input[type=text]:focus { - outline: none; - border-color: #555; -} -#vv-setup .vv-setup-hint { - font-size: 11px; - color: #555; - margin-top: 5px; -} -#vv-setup .vv-setup-role { - display: flex; - gap: 10px; - margin-bottom: 24px; -} -#vv-setup .vv-setup-role-btn { - flex: 1; - padding: 10px 0; - background: #1a1a1a; - border: 1px solid #333; - border-radius: 3px; - color: #888; - font-family: monospace; - font-size: 12px; - cursor: pointer; - text-align: center; - transition: border-color .15s, color .15s; -} -#vv-setup .vv-setup-role-btn.active { - border-color: #555; - color: #ccc; - background: #222; -} -#vv-setup .vv-setup-conditional { - display: none; -} -#vv-setup .vv-setup-conditional.visible { - display: block; -} -#vv-setup .vv-setup-divider { - border: none; - border-top: 1px solid #222; - margin: 24px 0; -} -#vv-setup-btn { - width: 100%; - padding: 10px; - background: #1e1e1e; - border: 1px solid #444; - color: #ccc; - font-family: monospace; - font-size: 13px; - border-radius: 3px; - cursor: pointer; - letter-spacing: .03em; -} +#vv-setup h1 { margin: 0 0 6px; font-size: 18px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; } +#vv-setup .vv-setup-sub { font-size: 12px; color: #555; margin-bottom: 32px; } +#vv-setup .vv-setup-field { margin-bottom: 20px; } +#vv-setup label { display: block; font-size: 11px; color: #888; margin-bottom: 6px; text-transform: uppercase; letter-spacing: .06em; } +#vv-setup input[type=text] { width: 100%; box-sizing: border-box; background: #0d0d0d; border: 1px solid #333; color: #ddd; padding: 7px 10px; border-radius: 3px; font-family: monospace; font-size: 13px; } +#vv-setup input[type=text]:focus { outline: none; border-color: #555; } +#vv-setup .vv-setup-hint { font-size: 11px; color: #555; margin-top: 5px; } +#vv-setup .vv-setup-role { display: flex; gap: 10px; margin-bottom: 24px; } +#vv-setup .vv-setup-role-btn { flex: 1; padding: 10px 0; background: #1a1a1a; border: 1px solid #333; border-radius: 3px; color: #888; font-family: monospace; font-size: 12px; cursor: pointer; text-align: center; transition: border-color .15s, color .15s; } +#vv-setup .vv-setup-role-btn.active { border-color: #555; color: #ccc; background: #222; } +#vv-setup .vv-setup-conditional { display: none; } +#vv-setup .vv-setup-conditional.visible { display: block; } +#vv-setup hr.vv-setup-divider { border: none; border-top: 1px solid #222; margin: 24px 0; } +#vv-setup-btn { width: 100%; padding: 10px; background: #1e1e1e; border: 1px solid #444; color: #ccc; font-family: monospace; font-size: 13px; border-radius: 3px; cursor: pointer; letter-spacing: .03em; } #vv-setup-btn:hover { border-color: #666; color: #eee; } #vv-setup-btn:disabled { opacity: .45; cursor: default; } -#vv-setup-status { - margin-top: 12px; - font-size: 12px; - color: #666; - text-align: center; - min-height: 16px; -} +#vv-setup-status { margin-top: 12px; font-size: 12px; color: #666; text-align: center; min-height: 16px; } #vv-setup-status.ok { color: #4a8; } #vv-setup-status.err { color: #a44; } +.vv-setup-info-box { background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px; padding: 12px 14px; margin-bottom: 24px; font-size: 12px; color: #777; line-height: 1.6; } +.vv-setup-info-box strong { color: #aaa; }
+ + + + +

⬡ Varaverk — Setup

+
master.conf received from HOST1. Create your local configuration to continue.
+ +
+ HOST1:
+ This server:
+ Creating: .conf +
+ + +
+ + + + + +

⬡ Varaverk — Partner Setup

+
HOST1 has been configured. Pull their settings to continue.
+ +
+ HOST1 detected:
+ This server will pull master.conf from HOST1 via Tailscale + SSH.
+ Requires SSH keys to be exchanged first (Partnership/ssh_setup.sh). +
+ +
+ + +
Must match Settings → Identification exactly
+
+ +
+ + +
+ + +
+ + + + +

⬡ Varaverk — First Run

Set up your server identity before the plugin can start.
- +
Must match Settings → Identification exactly (case-sensitive)
@@ -143,111 +187,72 @@ $detectedHostname = trim(shell_exec('hostname -s') ?: '');
- -
Leave blank if setting up standalone or partner isn't ready yet
+
- +
- -
Which slot are you? Ask your primary server admin if unsure — usually HOST2.
-
- + btn.disabled = true; btn.textContent = 'Saving…'; + const params = new URLSearchParams({ + csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', + 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) { + status.textContent = '✓ Saved — loading…'; status.className = 'ok'; + setTimeout(() => { window.location.href = d.redirect ?? '?tab=scheduler&vv_setup=master.conf'; }, 600); + } else { + btn.disabled = false; btn.textContent = 'Save and continue →'; + status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err'; + } + }).catch(() => { btn.disabled = false; btn.textContent = 'Save and continue →'; status.textContent = '✗ Request failed'; status.className = 'err'; }); + } + + + +