Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix
- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs); remove standalone cert page and top-level tab - cert_monitor.sh: write JSON status cache to State_Files/cert_status.json after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals - api/cert.php: new — serves cached cert status; falls back to configured domains as UNKN when no cache exists; POST action=run triggers live check - arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now read from data/*.db files when log JSON files don't yet exist - config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar signs read correctly from conf files - host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS - Partnership adapter pattern: Unraid-specific container logic extracted to Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/ - First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved to checklist; auto SSH keygen and API key creation on save - api/checklist.php: live setup checklist with pull_master action - Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
This commit is contained in:
+347
-275
@@ -1,312 +1,384 @@
|
||||
<?php
|
||||
// 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.
|
||||
// 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 = 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;
|
||||
$detectedHostname = vv_get_hostname();
|
||||
?>
|
||||
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
||||
|
||||
<style>
|
||||
#vv-setup {
|
||||
max-width: 560px;
|
||||
margin: 48px auto 0;
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
padding: 36px 40px 40px;
|
||||
font-family: monospace;
|
||||
color: #ccc;
|
||||
max-width: 580px; margin: 40px auto 0;
|
||||
background: #141414; border: 1px solid #2a2a2a;
|
||||
border-radius: 6px; padding: 36px 40px 40px;
|
||||
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 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.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; }
|
||||
#vv-setup h1 { margin: 0 0 4px; font-size: 17px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; }
|
||||
.vv-sub { font-size: 12px; color: #555; margin-bottom: 28px; }
|
||||
.vv-field { margin-bottom: 18px; }
|
||||
.vv-field label { display: block; font-size: 11px; color: #888; margin-bottom: 5px; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.vv-field input[type=text],
|
||||
.vv-field select {
|
||||
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-field input:focus, .vv-field select:focus { outline: none; border-color: #555; }
|
||||
.vv-hint { font-size: 11px; color: #555; margin-top: 4px; }
|
||||
.vv-role-row { display: flex; gap: 10px; margin-bottom: 22px; }
|
||||
.vv-role-btn { flex: 1; padding: 9px 0; background: #1a1a1a; border: 1px solid #333;
|
||||
border-radius: 3px; color: #777; font-family: monospace; font-size: 12px;
|
||||
cursor: pointer; text-align: center; transition: border-color .15s, color .15s; }
|
||||
.vv-role-btn.active { border-color: #555; color: #ccc; background: #1e1e1e; }
|
||||
.vv-cond { display: none; }
|
||||
.vv-cond.show { display: block; }
|
||||
hr.vv-hr { border: none; border-top: 1px solid #1e1e1e; margin: 22px 0; }
|
||||
.vv-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-btn:hover { border-color: #666; color: #eee; }
|
||||
.vv-btn:disabled { opacity: .4; cursor: default; }
|
||||
#vv-status { margin-top: 10px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }
|
||||
#vv-status.ok { color: #4a8; }
|
||||
#vv-status.err { color: #a44; }
|
||||
|
||||
/* Detection banner */
|
||||
#vv-detect-banner {
|
||||
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||
padding: 11px 14px; margin-bottom: 22px; font-size: 12px; line-height: 1.8; color: #666;
|
||||
}
|
||||
#vv-detect-banner .vv-det-row { display: flex; gap: 8px; }
|
||||
#vv-detect-banner .vv-det-lbl { color: #555; min-width: 100px; }
|
||||
#vv-detect-banner .vv-det-val { color: #999; }
|
||||
#vv-detect-banner .loading { color: #444; font-style: italic; }
|
||||
|
||||
/* Step 2 */
|
||||
#vv-step2 { display: none; }
|
||||
.vv-guide {
|
||||
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||
padding: 13px 16px; margin-bottom: 20px; font-size: 12px; color: #666; line-height: 1.9;
|
||||
}
|
||||
.vv-guide ol { margin: 8px 0 0 16px; padding: 0; }
|
||||
.vv-guide li { margin-bottom: 3px; }
|
||||
.vv-cl-title { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }
|
||||
.vv-cl-item { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0;
|
||||
border-bottom: 1px solid #1a1a1a; font-size: 12px; }
|
||||
.vv-cl-item:last-child { border-bottom: none; }
|
||||
.vv-cl-icon { font-size: 13px; min-width: 16px; margin-top: 1px; }
|
||||
.vv-cl-body { flex: 1; }
|
||||
.vv-cl-label { color: #bbb; }
|
||||
.vv-cl-detail{ color: #555; font-size: 11px; margin-top: 2px; }
|
||||
.vv-cl-act { margin-top: 5px; }
|
||||
.vv-cl-act button { padding: 4px 10px; background: #1a1a1a; border: 1px solid #333; color: #888;
|
||||
font-family: monospace; font-size: 11px; border-radius: 2px; cursor: pointer; }
|
||||
.vv-cl-act button:hover { border-color: #555; color: #bbb; }
|
||||
.vv-cl-err { font-size: 11px; color: #a44; margin-top: 4px; }
|
||||
</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'; status.className = 'ok';
|
||||
vvShowStep2('?tab=scheduler&vv_setup=' + encodeURIComponent(<?= json_encode($myHostId . '.conf') ?>));
|
||||
} 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'; status.className = 'ok';
|
||||
vvShowStep2(d.redirect ?? '?tab=scheduler');
|
||||
} 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-sub">Set up this server 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">
|
||||
<div class="vv-setup-hint">Must match Settings → Identification exactly (case-sensitive)</div>
|
||||
<!-- ── Step 1: Detection + identity ──────────────────────────────────────── -->
|
||||
<div id="vv-step1">
|
||||
|
||||
<div id="vv-detect-banner"><div class="loading">Detecting environment…</div></div>
|
||||
|
||||
<div class="vv-field">
|
||||
<label>This server's hostname</label>
|
||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
|
||||
<div class="vv-hint">Must match Unraid Settings → Identification exactly (case-sensitive)</div>
|
||||
</div>
|
||||
|
||||
<hr class="vv-hr">
|
||||
<label style="display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Server role</label>
|
||||
<div class="vv-role-row">
|
||||
<div class="vv-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
||||
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first server</span>
|
||||
</div>
|
||||
<div class="vv-role-btn" id="vv-role-partner" onclick="vvSetRole('partner')">
|
||||
Partner<br><span style="color:#555;font-size:10px;">HOST2+ · joining primary</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-cond" id="vv-cond-primary">
|
||||
<div class="vv-field">
|
||||
<label>Partner's hostname <span style="color:#444;font-size:10px;">(optional — can fill in later)</span></label>
|
||||
<input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-cond" id="vv-cond-partner">
|
||||
<div class="vv-field">
|
||||
<label>Primary server's hostname <span style="color:#a44;font-size:10px;">required</span></label>
|
||||
<input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="vv-field">
|
||||
<label>Your slot</label>
|
||||
<select id="vv-partner-slot">
|
||||
<option value="host2">HOST2</option>
|
||||
<option value="host3">HOST3</option>
|
||||
<option value="host4">HOST4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#555;margin-bottom:4px;">
|
||||
SSH key and master.conf pull are handled automatically after save.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>
|
||||
<div id="vv-status"></div>
|
||||
</div>
|
||||
|
||||
<hr class="vv-setup-divider">
|
||||
<!-- ── Step 2: Populate + guide + checklist ───────────────────────────────── -->
|
||||
<div id="vv-step2">
|
||||
<hr class="vv-hr">
|
||||
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:14px;">Step 2 of 2</div>
|
||||
|
||||
<label style="margin-bottom:10px;display:block;">Server role</label>
|
||||
<div class="vv-setup-role">
|
||||
<div class="vv-setup-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
||||
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first to be set up</span>
|
||||
<div id="vv-populate-status" style="font-size:12px;color:#555;margin-bottom:14px;">⟳ Running auto-populate…</div>
|
||||
|
||||
<div class="vv-guide">
|
||||
<strong style="color:#888;">Quick start</strong>
|
||||
<ol>
|
||||
<li>Create your Unraid API key below — needed for live monitor stats</li>
|
||||
<li>Open <strong>Scheduler → Edit host.conf</strong> — only three things need manual entry:<br>
|
||||
<span style="color:#444;">
|
||||
<code>EMBY_API_KEY</code> — Emby Dashboard → API Keys → + New Key<br>
|
||||
<code>DISCORD_WEBHOOK</code> — for notifications (optional)<br>
|
||||
<code>DAILY_SYNC_SHARES</code> — media paths to rsync nightly<br>
|
||||
Everything else was auto-populated or has working defaults
|
||||
</span></li>
|
||||
<li>If partnering: the checklist below will guide you through pulling HOST1's config and running onboard</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="vv-setup-role-btn" id="vv-role-partner" onclick="vvSetRole('partner')">
|
||||
Partner<br><span style="color:#555;font-size:10px;">HOST2+ · joining an existing primary</span>
|
||||
|
||||
<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;">
|
||||
<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>
|
||||
<a href="#" onclick="vvGoScheduler(event)" style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
||||
</div>
|
||||
<div id="vv-key-status" style="font-size:12px;min-height:14px;margin-bottom:18px;"></div>
|
||||
|
||||
<hr class="vv-hr">
|
||||
<div class="vv-cl-title">Setup checklist</div>
|
||||
<div id="vv-checklist"><div style="font-size:12px;color:#444;">Loading…</div></div>
|
||||
|
||||
<div style="margin-top:18px;text-align:right;">
|
||||
<a href="#" onclick="vvGoScheduler(event)" style="font-size:12px;color:#444;text-decoration:none;">Go to Scheduler →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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">
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<button id="vv-setup-btn" onclick="vvDoSetup()">Save and continue →</button>
|
||||
<div id="vv-setup-status"></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; }
|
||||
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 required'; status.className = 'err'; return; }
|
||||
mySlot = document.getElementById('vv-partner-slot').value;
|
||||
host1 = primary;
|
||||
if (mySlot === 'host2') host2 = hostname;
|
||||
}
|
||||
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'; status.className = 'ok';
|
||||
vvShowStep2(d.redirect ?? '?tab=scheduler&vv_setup=master.conf');
|
||||
} 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; ?>
|
||||
|
||||
<!-- Step 2: API key — shown after any wizard flow completes -->
|
||||
<div id="vv-setup-step2" style="display:none;">
|
||||
<hr class="vv-setup-divider">
|
||||
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Step 2 of 2 — Unraid API Key</div>
|
||||
<div class="vv-setup-info-box">
|
||||
Varaverk uses the local Unraid API to display live stats on the Monitor tab.
|
||||
Creates a <strong>Varaverk</strong> key via <code style="color:#555;">unraid-api</code> and writes it to your host conf.
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;align-items:center;">
|
||||
<button id="vv-key-btn2" onclick="vvCreateApiKeyWizard(this)"
|
||||
style="flex:1;padding:10px 0;background:#1a3a1a;border:1px solid #2e6b2e;color:#6fcf97;
|
||||
font-family:monospace;font-size:13px;border-radius:3px;cursor:pointer;">
|
||||
Create API Key
|
||||
</button>
|
||||
<a id="vv-skip-link" href="#" onclick="vvWizardContinue(event)"
|
||||
style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
||||
</div>
|
||||
<div id="vv-key-status2" style="margin-top:8px;font-size:12px;min-height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let _vvWizardNext = '';
|
||||
function vvShowStep2(redirect) {
|
||||
_vvWizardNext = redirect;
|
||||
document.getElementById('vv-setup-step2').style.display = 'block';
|
||||
let _vvRedirect = '?tab=scheduler';
|
||||
|
||||
// ── Detection banner ──────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now())
|
||||
.then(r => r.json()).then(d => {
|
||||
const b = document.getElementById('vv-detect-banner');
|
||||
if (!d.ok) { b.innerHTML = '<span style="color:#555">Detection unavailable</span>'; return; }
|
||||
const modeLabel = d.mode === 'internal'
|
||||
? '<span style="color:#4a8">internal (NVMe/SSD)</span>'
|
||||
: '<span style="color:#a84">flash mode (USB boot)</span>';
|
||||
b.innerHTML =
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">OS</span><span class="vv-det-val">Unraid ' + (d.unraid_ver||'') + '</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Boot device</span><span class="vv-det-val">' + d.boot_device + ' (' + d.transport + ')</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Storage mode</span><span class="vv-det-val">' + modeLabel + '</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Scripts dir</span><span class="vv-det-val" style="color:#666">' + d.scripts_dir + '</span></div>';
|
||||
const hf = document.getElementById('vv-hostname');
|
||||
if (hf && !hf.value.trim()) hf.value = d.hostname;
|
||||
}).catch(() => {
|
||||
document.getElementById('vv-detect-banner').innerHTML = '<span style="color:#444">Detection unavailable</span>';
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Role toggle ───────────────────────────────────────────────────────────────
|
||||
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('show', role === 'primary');
|
||||
document.getElementById('vv-cond-partner')?.classList.toggle('show', role === 'partner');
|
||||
}
|
||||
function vvWizardContinue(e) {
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function vvSetStatus(msg, cls) {
|
||||
const s = document.getElementById('vv-status');
|
||||
s.textContent = msg; s.className = cls || '';
|
||||
}
|
||||
function vvSetBtn(text, disabled) {
|
||||
const b = document.getElementById('vv-main-btn');
|
||||
if (b) { b.textContent = text; b.disabled = disabled; }
|
||||
}
|
||||
function vvGoScheduler(e) {
|
||||
if (e) e.preventDefault();
|
||||
window.location.href = _vvWizardNext || '?tab=scheduler';
|
||||
window.location.href = _vvRedirect || '?tab=scheduler';
|
||||
}
|
||||
function vvCreateApiKeyWizard(btn) {
|
||||
const status = document.getElementById('vv-key-status2');
|
||||
|
||||
// ── Step 2 ────────────────────────────────────────────────────────────────────
|
||||
function vvShowStep2(redirect, apiKey) {
|
||||
_vvRedirect = redirect || '?tab=scheduler';
|
||||
document.getElementById('vv-step1').style.display = 'none';
|
||||
document.getElementById('vv-step2').style.display = 'block';
|
||||
if (apiKey && apiKey.ok) {
|
||||
const btn = document.getElementById('vv-key-btn');
|
||||
const status = document.getElementById('vv-key-status');
|
||||
if (btn) { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }
|
||||
if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }
|
||||
}
|
||||
vvRunPopulate();
|
||||
vvLoadChecklist();
|
||||
}
|
||||
|
||||
// ── Populate ──────────────────────────────────────────────────────────────────
|
||||
function vvRunPopulate() {
|
||||
const el = document.getElementById('vv-populate-status');
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'populate'})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
const found = (d.lines || []).filter(l => /✅|found|detected/i.test(l));
|
||||
el.textContent = found.length
|
||||
? '✓ Auto-populate: ' + found.length + ' field' + (found.length > 1 ? 's' : '') + ' detected'
|
||||
: '✓ Auto-populate ran — arr keys will fill once services are running';
|
||||
el.style.color = '#4a8';
|
||||
} else {
|
||||
el.textContent = 'Auto-populate skipped — run Tools/conf_populate.sh once your arr containers are up';
|
||||
el.style.color = '#555';
|
||||
}
|
||||
vvLoadChecklist();
|
||||
}).catch(() => {
|
||||
el.textContent = 'Auto-populate unavailable — run manually from Scheduler';
|
||||
el.style.color = '#555';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Checklist ─────────────────────────────────────────────────────────────────
|
||||
const vvActionLabels = {
|
||||
create_key: 'Create API key',
|
||||
ssh_setup: 'SSH guide →',
|
||||
run_populate: 'Run now',
|
||||
pull_master: 'Pull from HOST1',
|
||||
onboard: 'Partnership tab →',
|
||||
};
|
||||
const vvActionHref = {
|
||||
ssh_setup: '?tab=partnership',
|
||||
onboard: '?tab=partnership',
|
||||
};
|
||||
|
||||
function vvLoadChecklist() {
|
||||
fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
|
||||
.then(r => r.json()).then(d => {
|
||||
const el = document.getElementById('vv-checklist');
|
||||
if (!d.ok || !d.items) { el.innerHTML = '<span style="color:#555">Unable to load checklist</span>'; return; }
|
||||
el.innerHTML = d.items.map(item => {
|
||||
const icon = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
|
||||
const iclr = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
|
||||
let act = '';
|
||||
if (item.action) {
|
||||
const lbl = vvActionLabels[item.action] || item.action;
|
||||
const href = vvActionHref[item.action];
|
||||
if (href) {
|
||||
act = `<div class="vv-cl-act"><a href="${href}" style="font-size:11px;color:#556;">${lbl}</a></div>`;
|
||||
} else if (item.action === 'create_key') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvCreateKey(this)">${lbl}</button></div>`;
|
||||
} else if (item.action === 'run_populate') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvRunPopulateBtn(this)">${lbl}</button></div>`;
|
||||
} else if (item.action === 'pull_master') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvPullMaster(this)">${lbl}</button><div id="vv-pull-err" class="vv-cl-err"></div></div>`;
|
||||
}
|
||||
}
|
||||
return `<div class="vv-cl-item">
|
||||
<div class="vv-cl-icon" style="color:${iclr}">${icon}</div>
|
||||
<div class="vv-cl-body">
|
||||
<div class="vv-cl-label">${item.label}</div>
|
||||
<div class="vv-cl-detail">${item.detail || ''}</div>
|
||||
${act}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function vvRunPopulateBtn(btn) {
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'populate'})
|
||||
}).then(() => { btn.textContent = 'Done'; vvLoadChecklist(); })
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||
}
|
||||
|
||||
function vvPullMaster(btn) {
|
||||
btn.disabled = true; btn.textContent = '⟳ Pulling…';
|
||||
const errEl = document.getElementById('vv-pull-err');
|
||||
if (errEl) errEl.textContent = '';
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'pull'})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
btn.textContent = '✓ Done';
|
||||
setTimeout(vvLoadChecklist, 600);
|
||||
} else {
|
||||
if (errEl) errEl.textContent = d.error || 'Pull failed';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
}
|
||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||
}
|
||||
|
||||
// ── 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 => {
|
||||
.then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
status.textContent = '✓ Key created — ' + d.key_preview; status.style.color = '#4a8';
|
||||
btn.textContent = 'Continue →'; btn.disabled = false;
|
||||
btn.onclick = vvWizardContinue;
|
||||
const skip = document.getElementById('vv-skip-link');
|
||||
if (skip) skip.style.display = 'none';
|
||||
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';
|
||||
status.textContent = '✗ ' + (d.error || 'Failed');
|
||||
status.style.color = '#a44';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
}).catch(e => {
|
||||
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</div>
|
||||
// ── 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})
|
||||
}).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'); });
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user