585 lines
30 KiB
PHP
585 lines
30 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Setup tab. First-run wizard — walks a fresh install through host identity, credentials,
|
|
// API key creation, storage mode, and the readiness checklist.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// The checklist is derived, never stored. api/checklist.php re-evaluates real state on each
|
|
// load, so a step cannot be marked complete while the thing it checks is actually missing.
|
|
//
|
|
// Setup state is pushed to partners. Once a node knows who it is, that identity is shared
|
|
// through vv_push_setup_state() rather than typed twice.
|
|
//
|
|
// Detection over interrogation. Where a value can be read from a running service, setup
|
|
// reads it instead of asking — the same principle conf_populate.sh follows.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// Existing values are not overwritten by detection. A field already filled in stays as it
|
|
// is; the wizard fills gaps rather than resetting a working install.
|
|
//
|
|
// Ambiguous detection is refused rather than guessed — a container prefix matching two
|
|
// containers is reported for manual resolution, never picked arbitrarily.
|
|
//
|
|
// Credentials entered here are written to host*.conf, which is gitignored. They are never
|
|
// committed and never leave the node except through the explicit partner push.
|
|
//
|
|
// RENDERS
|
|
// Step-by-step wizard, readiness checklist, credential entry, API key creation
|
|
//
|
|
// DEPENDS ON
|
|
// api/setup.php wizard state and writes
|
|
// api/checklist.php live readiness evaluation
|
|
// api/create_api_key.php unraid-api key provisioning
|
|
// api/storage.php storage-mode selection
|
|
// First-run setup wizard — uniform flow for all hosts.
|
|
// Step 1: auto-detect environment + server identity form.
|
|
// Step 2: auto-populate + guide + checklist.
|
|
// master.conf pull (for partner servers) lives in the checklist, not here.
|
|
|
|
$detectedHostname = vv_get_hostname();
|
|
|
|
// ── Identity already on disk? ────────────────────────────────────────────────────────────────
|
|
// A partner that has been sent the owner's master.conf already knows everything this step asks
|
|
// for: the conf names the primary, and it names this machine in one of the HOSTn slots. Asking
|
|
// for it again invites a typo in the one field the form itself warns is case-sensitive, and the
|
|
// answer is sitting in a file two lines away.
|
|
//
|
|
// Only a *populated* conf counts. The installer seeds master.conf from the template, where every
|
|
// HOSTn is empty — that is a fresh node with no identity, not a detected one.
|
|
//
|
|
// Matching is exact and case-insensitive, never a prefix: two hosts called Tower and Tower2 must
|
|
// not resolve to each other, and there is no ambiguity to tolerate when the value was written by
|
|
// the very host it names.
|
|
$vvDetected = ['slot' => '', 'primary' => '', 'role' => ''];
|
|
$vvMasterRaw = vv_read_conf_raw('master.conf');
|
|
if ($vvMasterRaw !== '' && $detectedHostname !== '') {
|
|
preg_match_all('/^\s*(HOST\d+)\s*=\s*"([^"]*)"/m', $vvMasterRaw, $vvHm, PREG_SET_ORDER);
|
|
$vvSlots = [];
|
|
foreach ($vvHm as $m) {
|
|
$val = trim($m[2]);
|
|
if ($val !== '') $vvSlots[strtolower($m[1])] = $val;
|
|
}
|
|
foreach ($vvSlots as $slot => $name) {
|
|
if (strcasecmp($name, $detectedHostname) === 0) {
|
|
$vvDetected['slot'] = $slot;
|
|
$vvDetected['role'] = ($slot === 'host1') ? 'primary' : 'partner';
|
|
break;
|
|
}
|
|
}
|
|
// The primary is whatever HOST1 says, and it is only useful to a host that is not HOST1.
|
|
if ($vvDetected['role'] === 'partner') $vvDetected['primary'] = $vvSlots['host1'] ?? '';
|
|
}
|
|
?>
|
|
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
|
<style>
|
|
#vv-setup {
|
|
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 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">
|
|
|
|
<h1>⬡ Varaverk — First Run</h1>
|
|
<div class="vv-sub">Set up this server before the plugin can start.</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>Storage mode</label>
|
|
<div class="vv-role-row" style="margin-bottom:4px">
|
|
<div class="vv-role-btn" id="vv-store-flash" onclick="vvSetStorage('flash')">
|
|
Appdata<br><span style="color:#555;font-size:10px;">USB boot · requires array</span>
|
|
</div>
|
|
<div class="vv-role-btn" id="vv-store-internal" onclick="vvSetStorage('internal')">
|
|
Internal Boot<br><span style="color:#555;font-size:10px;">NVMe/SSD · no array dep</span>
|
|
</div>
|
|
</div>
|
|
<div id="vv-store-hint" class="vv-hint"></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>
|
|
|
|
<!-- ── 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>
|
|
|
|
<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> — a few things need manual entry:<br>
|
|
<span style="color:#444;">
|
|
<code>HOST1_EMBY_API_KEY</code> — Emby Dashboard → API Keys → + New Key<br>
|
|
<code>HOST1_JELLYFIN_API_KEY</code> — Jellyfin Dashboard → Administration → API Keys (if using Jellyfin)<br>
|
|
<code>HOST1_DISCORD_WEBHOOK</code> — for notifications (optional)<br>
|
|
<code>HOST1_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 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>
|
|
|
|
<script>
|
|
let _vvRedirect = '?tab=scheduler';
|
|
let _vvStorageMode = 'flash';
|
|
let _vvCurrentDir = '';
|
|
|
|
function vvSetStorage(mode) {
|
|
_vvStorageMode = mode;
|
|
document.getElementById('vv-store-flash')?.classList.toggle('active', mode === 'flash');
|
|
document.getElementById('vv-store-internal')?.classList.toggle('active', mode === 'internal');
|
|
const hint = document.getElementById('vv-store-hint');
|
|
if (hint) hint.textContent = mode === 'flash'
|
|
? 'Scripts live in appdata — requires array to be started. Recommended for USB flash boot.'
|
|
: 'Scripts live on /boot — available before array mounts. Requires NVMe/SSD boot.';
|
|
}
|
|
|
|
// ── Detection banner ──────────────────────────────────────────────────────────
|
|
(function() {
|
|
const _ac = new AbortController();
|
|
setTimeout(() => _ac.abort(), 6000);
|
|
fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
|
|
.then(r => r.json()).then(d => {
|
|
const b = document.getElementById('vv-detect-banner');
|
|
if (!d.ok) { b.innerHTML = '<span style="color:#555">Detection unavailable</span>'; return; }
|
|
_vvCurrentDir = d.scripts_dir || '';
|
|
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">Scripts dir</span><span class="vv-det-val" style="color:#666">' + d.scripts_dir + '</span></div>';
|
|
vvSetStorage(d.mode);
|
|
const hf = document.getElementById('vv-hostname');
|
|
if (hf && !hf.value.trim()) hf.value = d.hostname;
|
|
// After the banner is rebuilt, never before — this appends to it, and the line above
|
|
// replaces its innerHTML wholesale.
|
|
vvApplyDetectedIdentity();
|
|
}).catch(() => {
|
|
document.getElementById('vv-detect-banner').innerHTML = '<span style="color:#444">Detection unavailable</span>';
|
|
// Identity comes off the local conf, not from the probe, so it still applies when the
|
|
// environment probe is the thing that failed.
|
|
vvApplyDetectedIdentity();
|
|
});
|
|
})();
|
|
|
|
// ── 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');
|
|
}
|
|
|
|
// ── Identity detected from master.conf ────────────────────────────────────────
|
|
// Applied, not enforced. Every field stays editable: the conf is evidence of what the owner
|
|
// intended, and if it is wrong the person standing at the machine is the one who can say so.
|
|
// What this removes is the need to retype three values that are already known — including the
|
|
// hostname field whose own hint warns it is case-sensitive.
|
|
const vvDetectedIdentity = <?= json_encode($vvDetected) ?>;
|
|
|
|
function vvApplyDetectedIdentity() {
|
|
const d = vvDetectedIdentity;
|
|
if (!d || !d.slot) return;
|
|
|
|
vvSetRole(d.role);
|
|
if (d.role === 'partner') {
|
|
const pn = document.getElementById('vv-primary-hostname');
|
|
if (pn && d.primary) pn.value = d.primary;
|
|
const sel = document.getElementById('vv-partner-slot');
|
|
if (sel && [...sel.options].some(o => o.value === d.slot)) sel.value = d.slot;
|
|
}
|
|
|
|
const banner = document.getElementById('vv-detect-banner');
|
|
if (banner) {
|
|
const note = document.createElement('div');
|
|
note.style.cssText = 'margin-top:8px;padding:7px 10px;border-radius:3px;background:#0d1f0d;'
|
|
+ 'border:1px solid #1a3a1a;color:#4caf50;font-size:11px;line-height:1.5;';
|
|
note.textContent = d.role === 'partner'
|
|
? `Identity read from master.conf — this server is ${d.slot.toUpperCase()}, joining `
|
|
+ `${d.primary}. Change anything below if that is wrong.`
|
|
: 'Identity read from master.conf — this server is HOST1, the primary.';
|
|
banner.appendChild(note);
|
|
}
|
|
}
|
|
|
|
// ── 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 = _vvRedirect || '?tab=scheduler';
|
|
}
|
|
|
|
// ── 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 Deployment/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: 'Generate SSH key',
|
|
run_populate: 'Run now',
|
|
pull_master: 'Pull from HOST1',
|
|
onboard: 'Partnership tab →',
|
|
};
|
|
// ssh_setup used to be a link to the Partnership tab — a guide, on the one row where the reader
|
|
// has no key and therefore cannot do the thing the guide describes. It generates the key here
|
|
// now, through api/setup.php?action=ssh_generate, which already existed and had never been
|
|
// called from anywhere. The partner path needs this: 'Pull from HOST1' authenticates with the
|
|
// key this row creates, so sending someone away to read about it left the next row unusable.
|
|
const vvActionHref = {
|
|
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}" class="localURL" 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>`;
|
|
} else if (item.action === 'ssh_setup') {
|
|
act = `<div class="vv-cl-act"><button onclick="vvSshGenerate(this)">${lbl}</button>`
|
|
+ `<div id="vv-ssh-out" 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(() => {});
|
|
}
|
|
|
|
// ── SSH key ───────────────────────────────────────────────────────────────────
|
|
// Generates this node's keypair and shows the public half. Generating it is the easy part; the
|
|
// key is useless until the other host trusts it, and that is a step no button here can take —
|
|
// so the public key is put on screen, in something selectable, rather than described.
|
|
//
|
|
// A readonly textarea rather than a copy button: the clipboard API needs a secure context and
|
|
// Unraid is routinely reached over plain http on the LAN, where it is simply undefined.
|
|
function vvSshGenerate(btn) {
|
|
btn.disabled = true; btn.textContent = '⟳ Generating…';
|
|
const out = document.getElementById('vv-ssh-out');
|
|
if (out) { out.textContent = ''; out.style.color = ''; }
|
|
fetch('/plugins/varaverk/api/setup.php', {
|
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
body: new URLSearchParams({action: 'ssh_generate'})
|
|
}).then(r => r.json()).then(d => {
|
|
if (d.ok && d.pubkey) {
|
|
btn.textContent = '✓ Created';
|
|
if (out) {
|
|
out.style.color = '#8a8a8a';
|
|
// Built as nodes, not innerHTML — the key and its comment come off the filesystem and
|
|
// land in the page either way, so there is no reason to send them through a parser.
|
|
const p = document.createElement('div');
|
|
p.textContent = 'Key created. It is not trusted until this public key is on the other '
|
|
+ 'host — append it to /root/.ssh/authorized_keys there:';
|
|
p.style.cssText = 'margin-bottom:6px;line-height:1.5;';
|
|
const ta = document.createElement('textarea');
|
|
ta.readOnly = true; ta.value = d.pubkey; ta.rows = 3;
|
|
ta.style.cssText = 'width:100%;font-family:monospace;font-size:10px;background:#111;'
|
|
+ 'color:#8a8a8a;border:1px solid #2a2a2a;border-radius:3px;padding:6px;';
|
|
ta.onclick = () => ta.select();
|
|
out.appendChild(p); out.appendChild(ta);
|
|
}
|
|
setTimeout(vvLoadChecklist, 800);
|
|
} else {
|
|
if (out) { out.style.color = '#ef5350'; out.textContent = d.error || 'Key generation failed'; }
|
|
btn.disabled = false; btn.textContent = 'Retry';
|
|
}
|
|
}).catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
|
}
|
|
|
|
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', { method: 'POST' })
|
|
.then(r => r.json()).then(d => {
|
|
if (d.ok) {
|
|
status.textContent = '✓ Key created — ' + d.key_preview;
|
|
status.style.color = '#4a8';
|
|
btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
|
|
vvLoadChecklist();
|
|
} else {
|
|
status.textContent = '✗ ' + (d.error || 'Failed');
|
|
status.style.color = '#a44';
|
|
btn.disabled = false; btn.textContent = 'Retry';
|
|
}
|
|
}).catch(e => {
|
|
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
|
btn.disabled = false; btn.textContent = 'Retry';
|
|
});
|
|
}
|
|
|
|
// ── Save ──────────────────────────────────────────────────────────────────────
|
|
function vvDoSave() {
|
|
const hostname = document.getElementById('vv-hostname')?.value.trim();
|
|
if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
|
|
let host1 = '', host2 = '', mySlot = 'host1';
|
|
if (vvRole === 'primary') {
|
|
host1 = hostname;
|
|
host2 = document.getElementById('vv-partner-hostname')?.value.trim() || '';
|
|
mySlot = 'host1';
|
|
} else {
|
|
const primary = document.getElementById('vv-primary-hostname')?.value.trim();
|
|
if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
|
|
mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
|
|
host1 = primary;
|
|
if (mySlot === 'host2') host2 = hostname;
|
|
}
|
|
vvSetBtn('Saving…', true);
|
|
fetch('/plugins/varaverk/api/setup.php', {
|
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
|
|
}).then(r => r.json()).then(d => {
|
|
if (d.ok) {
|
|
if (d.needs_migration) {
|
|
const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';
|
|
vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');
|
|
vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);
|
|
} else {
|
|
vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);
|
|
}
|
|
} else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
|
|
}).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
|
|
}
|
|
|
|
function vvDoMigration(to, redirect, apiKey) {
|
|
fetch('/plugins/varaverk/api/storage.php', {
|
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
body: new URLSearchParams({action: 'migrate', to})
|
|
}).then(r => r.json()).then(d => {
|
|
if (d.ok) {
|
|
vvShowStep2(redirect, apiKey);
|
|
} else {
|
|
vvSetBtn('Save and continue →', false);
|
|
vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');
|
|
}
|
|
}).catch(() => {
|
|
vvSetBtn('Save and continue →', false);
|
|
vvSetStatus('✗ Migration request failed', 'err');
|
|
});
|
|
}
|
|
</script>
|