Setup wizard: full first-run flow for HOST1 and HOST2
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=<conf> 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)
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+103
-48
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
// Setup mode — auto-open a conf file and force the editing sequence
|
||||
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
|
||||
? $_GET['vv_setup'] : '';
|
||||
$_vv_my_host = vv_detect_host();
|
||||
$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';
|
||||
|
||||
$tree = vv_job_tree();
|
||||
$customs = vv_custom_scripts();
|
||||
$tools = vv_tools_scripts();
|
||||
@@ -941,6 +948,17 @@ let vvSelectedRow = null;
|
||||
let vvCurrentSiId = null;
|
||||
let vvCurrentSiHdr = null;
|
||||
|
||||
// Setup mode — injected by PHP when navigating from the first-run wizard
|
||||
let vvSetupConf = <?= json_encode($vv_setup_conf) ?>;
|
||||
const vvLocalHostConf = <?= json_encode($_vv_local_host_conf) ?>;
|
||||
|
||||
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,'<').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);
|
||||
})
|
||||
|
||||
+190
-185
@@ -1,6 +1,27 @@
|
||||
<?php
|
||||
// First-run setup wizard — shown when HOST1 is blank in master.conf.
|
||||
// First-run setup wizard.
|
||||
// HOST1 path: blank master.conf → fill hostnames → write master.conf + host1.conf → scheduler.
|
||||
// HOST2 path: state file present → pull master.conf from HOST1 → fill host2.conf → scheduler.
|
||||
|
||||
$detectedHostname = trim(shell_exec('hostname -s') ?: '');
|
||||
|
||||
// Check for setup state file pushed by HOST1
|
||||
$setupState = vv_setup_state_read();
|
||||
$host1FromState = $setupState['host1_hostname'] ?? '';
|
||||
|
||||
// Determine if HOST2 scenario: state file present but master.conf has blank HOST1
|
||||
$_master = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
|
||||
$masterHost1 = trim($_h1m[1] ?? '');
|
||||
|
||||
// Determine local host slot (if master.conf has hostnames, we may already know)
|
||||
$isHost2Flow = !empty($host1FromState) && empty($masterHost1);
|
||||
|
||||
// If master.conf has HOST1/HOST2 filled but local host.conf is missing:
|
||||
// This is the "master was pushed, just need the local conf" scenario
|
||||
$myHostId = vv_detect_host(); // may be 'host2' if master.conf was already pushed
|
||||
$confMissing = $myHostId !== 'unknown' && !file_exists(CONF_DIR . '/' . $myHostId . '.conf');
|
||||
$isConfOnlyFlow = !empty($masterHost1) && $confMissing;
|
||||
?>
|
||||
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
||||
|
||||
@@ -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; }
|
||||
</style>
|
||||
|
||||
<div id="vv-setup">
|
||||
|
||||
<?php if ($isConfOnlyFlow): ?>
|
||||
<!-- master.conf was pushed by HOST1, just need local host.conf -->
|
||||
<?php $slotLabel = strtoupper($myHostId); ?>
|
||||
<h1>⬡ Varaverk — <?= htmlspecialchars($slotLabel) ?> Setup</h1>
|
||||
<div class="vv-setup-sub">master.conf received from HOST1. Create your local configuration to continue.</div>
|
||||
|
||||
<div class="vv-setup-info-box">
|
||||
<strong>HOST1:</strong> <?= htmlspecialchars($masterHost1) ?><br>
|
||||
<strong>This server:</strong> <?= htmlspecialchars($detectedHostname) ?> → <?= htmlspecialchars($slotLabel) ?><br>
|
||||
<strong>Creating:</strong> <?= htmlspecialchars($myHostId) ?>.conf
|
||||
</div>
|
||||
|
||||
<button id="vv-setup-btn" onclick="vvDoConfOnly()">Create <?= htmlspecialchars($myHostId) ?>.conf and continue →</button>
|
||||
<div id="vv-setup-status"></div>
|
||||
|
||||
<script>
|
||||
function vvDoConfOnly() {
|
||||
const btn = document.getElementById('vv-setup-btn');
|
||||
const status = document.getElementById('vv-setup-status');
|
||||
btn.disabled = true; btn.textContent = 'Creating…';
|
||||
const params = new URLSearchParams({
|
||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
||||
action: 'save',
|
||||
host1: <?= json_encode($masterHost1) ?>,
|
||||
host2: <?= json_encode(trim(preg_match('/^\s*HOST2\s*=\s*"([^"]*)"/m', $_master, $m2) ? $m2[1] : '')) ?>,
|
||||
my_slot: <?= json_encode($myHostId) ?>,
|
||||
my_hostname: <?= json_encode($detectedHostname) ?>,
|
||||
});
|
||||
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 = '✓ Created — loading…'; status.className = 'ok';
|
||||
setTimeout(() => { window.location.href = '?tab=scheduler&vv_setup=' + encodeURIComponent(<?= json_encode($myHostId . '.conf') ?>); }, 600);
|
||||
} else {
|
||||
btn.disabled = false; btn.textContent = 'Create <?= htmlspecialchars($myHostId) ?>.conf and continue →';
|
||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
||||
}
|
||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Create <?= htmlspecialchars($myHostId) ?>.conf and continue →'; status.textContent = '✗ Request failed'; status.className = 'err'; });
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php elseif ($isHost2Flow): ?>
|
||||
<!-- State file present, master.conf blank — HOST2 pull flow -->
|
||||
<h1>⬡ Varaverk — Partner Setup</h1>
|
||||
<div class="vv-setup-sub">HOST1 has been configured. Pull their settings to continue.</div>
|
||||
|
||||
<div class="vv-setup-info-box">
|
||||
<strong>HOST1 detected:</strong> <?= htmlspecialchars($host1FromState) ?><br>
|
||||
This server will pull master.conf from HOST1 via Tailscale + SSH.<br>
|
||||
<span style="color:#555">Requires SSH keys to be exchanged first (Partnership/ssh_setup.sh).</span>
|
||||
</div>
|
||||
|
||||
<div class="vv-setup-field">
|
||||
<label>This server's hostname</label>
|
||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
|
||||
<div class="vv-setup-hint">Must match Settings → Identification exactly</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-setup-field">
|
||||
<label>Your slot</label>
|
||||
<select id="vv-partner-slot" style="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;">
|
||||
<option value="host2">HOST2</option>
|
||||
<option value="host3">HOST3</option>
|
||||
<option value="host4">HOST4</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="vv-setup-btn" onclick="vvDoPull()">Pull configuration from HOST1 →</button>
|
||||
<div id="vv-setup-status"></div>
|
||||
|
||||
<script>
|
||||
function vvDoPull() {
|
||||
const btn = document.getElementById('vv-setup-btn');
|
||||
const status = document.getElementById('vv-setup-status');
|
||||
const hostname = document.getElementById('vv-hostname').value.trim();
|
||||
const slot = document.getElementById('vv-partner-slot').value;
|
||||
if (!hostname) { status.textContent = '✗ Hostname required'; status.className = 'err'; return; }
|
||||
btn.disabled = true; btn.textContent = 'Pulling…';
|
||||
const params = new URLSearchParams({
|
||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
||||
action: 'pull',
|
||||
host1_hostname: <?= json_encode($host1FromState) ?>,
|
||||
my_slot: slot,
|
||||
my_hostname: hostname,
|
||||
});
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
status.textContent = '✓ Configuration pulled — loading…'; status.className = 'ok';
|
||||
setTimeout(() => { window.location.href = d.redirect ?? '?tab=scheduler'; }, 600);
|
||||
} else {
|
||||
btn.disabled = false; btn.textContent = 'Pull configuration from HOST1 →';
|
||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
||||
}
|
||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Pull configuration from HOST1 →'; status.textContent = '✗ Request failed'; status.className = 'err'; });
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- Standard first-run: no state file, blank master.conf -->
|
||||
<h1>⬡ Varaverk — First Run</h1>
|
||||
<div class="vv-setup-sub">Set up your server identity before the plugin can start.</div>
|
||||
|
||||
<div class="vv-setup-field">
|
||||
<label>This server's hostname</label>
|
||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>"
|
||||
placeholder="unRAID-MyServer" autocomplete="off" spellcheck="false">
|
||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" placeholder="unRAID-MyServer" autocomplete="off" spellcheck="false">
|
||||
<div class="vv-setup-hint">Must match Settings → Identification exactly (case-sensitive)</div>
|
||||
</div>
|
||||
|
||||
@@ -143,111 +187,72 @@ $detectedHostname = trim(shell_exec('hostname -s') ?: '');
|
||||
<div class="vv-setup-conditional" id="vv-cond-primary">
|
||||
<div class="vv-setup-field">
|
||||
<label>Partner's hostname <span style="color:#444">(optional — can fill in later)</span></label>
|
||||
<input type="text" id="vv-partner-hostname" value=""
|
||||
placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
||||
<div class="vv-setup-hint">Leave blank if setting up standalone or partner isn't ready yet</div>
|
||||
<input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-setup-conditional" id="vv-cond-partner">
|
||||
<div class="vv-setup-field">
|
||||
<label>Primary server's hostname <span style="color:#a44">*required</span></label>
|
||||
<input type="text" id="vv-primary-hostname" value=""
|
||||
placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
||||
<input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="vv-setup-field">
|
||||
<label>Your slot</label>
|
||||
<select id="vv-partner-slot" style="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;">
|
||||
<select id="vv-partner-slot" style="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;">
|
||||
<option value="host2">HOST2</option>
|
||||
<option value="host3">HOST3</option>
|
||||
<option value="host4">HOST4</option>
|
||||
</select>
|
||||
<div class="vv-setup-hint">Which slot are you? Ask your primary server admin if unsure — usually HOST2.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="vv-setup-btn" onclick="vvDoSetup()">Save and continue →</button>
|
||||
<div id="vv-setup-status"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let vvRole = 'primary';
|
||||
|
||||
function vvSetRole(role) {
|
||||
vvRole = role;
|
||||
document.getElementById('vv-role-primary').classList.toggle('active', role === 'primary');
|
||||
document.getElementById('vv-role-partner').classList.toggle('active', role === 'partner');
|
||||
document.getElementById('vv-cond-primary').classList.toggle('visible', role === 'primary');
|
||||
document.getElementById('vv-cond-partner').classList.toggle('visible', role === 'partner');
|
||||
}
|
||||
|
||||
function vvDoSetup() {
|
||||
const hostname = document.getElementById('vv-hostname').value.trim();
|
||||
const status = document.getElementById('vv-setup-status');
|
||||
const btn = document.getElementById('vv-setup-btn');
|
||||
|
||||
if (!hostname) {
|
||||
status.textContent = '✗ Hostname is required';
|
||||
status.className = 'err';
|
||||
return;
|
||||
<script>
|
||||
let vvRole = 'primary';
|
||||
function vvSetRole(role) {
|
||||
vvRole = role;
|
||||
document.getElementById('vv-role-primary').classList.toggle('active', role === 'primary');
|
||||
document.getElementById('vv-role-partner').classList.toggle('active', role === 'partner');
|
||||
document.getElementById('vv-cond-primary').classList.toggle('visible', role === 'primary');
|
||||
document.getElementById('vv-cond-partner').classList.toggle('visible', role === 'partner');
|
||||
}
|
||||
|
||||
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) {
|
||||
status.textContent = '✗ Primary hostname is required for partner setup';
|
||||
status.className = 'err';
|
||||
return;
|
||||
}
|
||||
mySlot = document.getElementById('vv-partner-slot').value;
|
||||
host1 = primary;
|
||||
// Place this server in the correct slot
|
||||
if (mySlot === 'host2') host2 = hostname;
|
||||
// host3/host4 handled server-side
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
status.textContent = '';
|
||||
status.className = '';
|
||||
|
||||
const params = new URLSearchParams({
|
||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
||||
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 plugin…';
|
||||
status.className = 'ok';
|
||||
setTimeout(() => { window.location.href = '?tab=monitor'; }, 800);
|
||||
function vvDoSetup() {
|
||||
const hostname = document.getElementById('vv-hostname').value.trim();
|
||||
const status = document.getElementById('vv-setup-status');
|
||||
const btn = document.getElementById('vv-setup-btn');
|
||||
if (!hostname) { status.textContent = '✗ Hostname is required'; status.className = 'err'; return; }
|
||||
let host1 = '', host2 = '', mySlot = 'host1';
|
||||
if (vvRole === 'primary') {
|
||||
host1 = hostname;
|
||||
host2 = document.getElementById('vv-partner-hostname').value.trim();
|
||||
mySlot = 'host1';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Save and continue →';
|
||||
status.textContent = '✗ ' + (d.error ?? 'Unknown error');
|
||||
status.className = 'err';
|
||||
const primary = document.getElementById('vv-primary-hostname').value.trim();
|
||||
if (!primary) { status.textContent = '✗ Primary hostname required'; status.className = 'err'; return; }
|
||||
mySlot = document.getElementById('vv-partner-slot').value;
|
||||
host1 = primary;
|
||||
if (mySlot === 'host2') host2 = hostname;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Save and continue →';
|
||||
status.textContent = '✗ Request failed';
|
||||
status.className = 'err';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
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'; });
|
||||
}
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user