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:
Gmer4Lfe
2026-05-30 15:43:04 -04:00
parent 1e9681a71e
commit af89258bbd
6 changed files with 396 additions and 242 deletions
+190 -185
View File
@@ -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>