Two-phase onboard + partner card enhancements

partnership_onboard.sh:
- --phase1-only (OWNER): SSH key exchange + conf push only; safe to run
  before HOST2 has Varaverk installed; writes HOST2_PHASE1_DONE to setup.db
- --phase2-only (OWNER): skips SSH, runs Steps 2-9 (containers, arr stack,
  onboard, arr sync, conf push); writes HOST2_PHASE2_DONE to setup.db
- Mirror path: after SSH setup, SSHes OWNER and fires --phase2-only in
  background (nohup); reads OWNER's SCRIPTS_DIR from varaverk.cfg first;
  falls back to manual instruction if SSH fails

partnership.php (include): vv_pt_nodes() reads setup.db and exposes
onboard_phase (0/1/2) per remote node

partnership.php (page): phase-aware Actions section per partner host;
node cards show phase badge (not provisioned / awaiting onboard / done);
Phase 1 and Phase 2 buttons call run.php with extra_args

monitor.php: partner card logo (top-right), CPU thread count, RAM total GB,
unRAID version row in remote stats grid
This commit is contained in:
Gmer4Lfe
2026-05-30 18:12:49 -04:00
parent 3dd867c684
commit a5755921ab
4 changed files with 269 additions and 59 deletions
+18 -11
View File
@@ -108,6 +108,7 @@ function vv_pt_nodes(): array {
$vars = vv_conf_vars();
$tsPeers = vv_pt_ts_peers();
$ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? '');
$setupDb = vv_setup_state_read();
// SSH key for this host
$myId = strtoupper($currentHost);
@@ -150,18 +151,24 @@ function vv_pt_nodes(): array {
? vv_pt_local_system()
: ($ts['online'] && $ts['ip'] && $mySshKey ? vv_pt_remote_system($ts['ip'], $mySshKey) : []);
// Onboard phase from setup.db — 0=not started, 1=SSH+conf done, 2=fully onboarded
$nodeIdUpper = strtoupper($slot);
$onboardPhase = ($setupDb[$nodeIdUpper . '_PHASE2_DONE'] ?? '') === 'true' ? 2
: (($setupDb[$nodeIdUpper . '_PHASE1_DONE'] ?? '') === 'true' ? 1 : 0);
$nodes[] = [
'slot' => $slot,
'id' => strtoupper($slot),
'hostname' => $hostname,
'is_me' => $isMe,
'is_owner' => $isOwner,
'ts_online' => $ts['online'],
'ts_active' => $ts['active'],
'ts_ip' => $ts['ip'],
'fallback' => $fbState,
'partnership' => $ptDb,
'system' => $system,
'slot' => $slot,
'id' => $nodeIdUpper,
'hostname' => $hostname,
'is_me' => $isMe,
'is_owner' => $isOwner,
'ts_online' => $ts['online'],
'ts_active' => $ts['active'],
'ts_ip' => $ts['ip'],
'fallback' => $fbState,
'partnership' => $ptDb,
'system' => $system,
'onboard_phase' => $isMe ? null : $onboardPhase,
];
}
return $nodes;
+5 -2
View File
@@ -82,6 +82,7 @@
<span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.5" y="1.5" width="4" height="7" rx="0.8"/><rect x="9.5" y="1.5" width="4" height="7" rx="0.8"/><line x1="4.5" y1="5" x2="9.5" y2="5"/><circle cx="7" cy="5" r="1" fill="#888" stroke="none"/></svg></span>
Partner
</span>
<img src="/plugins/varaverk/icons/varaverk.png" style="width:16px;height:16px;object-fit:contain;opacity:0.2;flex-shrink:0;" title="Varaverk">
</h3>
<div id="vv-partner-body">Loading...</div>
</div>
@@ -639,8 +640,8 @@ function vvPollMonitor() {
const ramAvail = rs.mem_used_pct > 0;
const cpuHue = cpuAvail ? Math.round(120 * (1 - rs.cpu_load / 100)) : 0;
const memHue = ramAvail ? Math.round(120 * (1 - rs.mem_used_pct / 100)) : 0;
const cpuStr = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${rs.cpu_load}%</span>` : `<span style="color:#333;">—</span>`;
const ramStr = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${rs.mem_used_pct}%</span>` : `<span style="color:#333;">—</span>`;
const cpuStr = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${rs.cpu_load}%${rs.cpu_threads ? `<span style="color:#333;font-weight:400;"> · ${rs.cpu_threads}t</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
const ramStr = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${rs.mem_used_pct}%${rs.mem_total_gb ? `<span style="color:#333;font-weight:400;"> · ${rs.mem_total_gb}G</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
const arrColor = rs.array_state === 'Started' || rs.array_state === 'STARTED' ? '#4caf50' : '#f44336';
const uptimeStr = rs.uptime && rs.uptime !== '—' ? rs.uptime : '—';
@@ -654,6 +655,7 @@ function vvPollMonitor() {
<span style="color:#555;">Script sync ops are gated until versions match</span>
</div>` : '';
const verRow = remoteVer ? `<span style="color:#444;">unRAID</span><span style="color:#3a3a3a;grid-column:span 3;">${remoteVer}</span>` : '';
statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-top:5px;margin-bottom:2px;">
<span style="color:#444;">CPU</span>${cpuStr}
<span style="color:#444;">RAM</span>${ramStr}
@@ -661,6 +663,7 @@ function vvPollMonitor() {
<span style="color:${arrColor};font-weight:600;">${rs.array_state}</span>
<span style="color:#444;">Uptime</span>
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
${verRow}
</div>${verWarn}`;
} else if (rs && rs.no_api_key) {
statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API key not configured — complete Onboard to enable</div>`;
+109 -31
View File
@@ -155,8 +155,17 @@ function _nodeCard(node) {
const [ptCol, ptLbl] = ptStates[ptState] || ['#444', ptState || '—'];
const ptUpdated = pt.updated ? _relTime(parseInt(pt.updated)) : null;
// Onboard phase (null = self, 0 = not started, 1 = SSH done, 2 = fully onboarded)
const phase = node.onboard_phase;
const phaseHtml = phase === null ? '' :
phase === 2 ? '' :
phase === 1 ? `<div style="font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;">⏳ SSH ready · awaiting HOST2 onboard</div>` :
`<div style="font-size:10px;color:#555;margin-bottom:6px;padding:2px 6px;background:#111;border:1px solid #222;border-radius:3px;">○ Not yet provisioned</div>`;
let body = '';
body += phaseHtml;
// Network
body += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
<span class="vv-pt-dot" style="background:${dotCol}"></span>
@@ -199,27 +208,82 @@ function _nodeCard(node) {
// ── Actions ───────────────────────────────────────────────────────────────────
function _renderActions(nodes, cfg) {
// Onboard: only show when partnership is disabled (not yet set up)
// Offboard: only when enabled
// Transfer: only when enabled + we are owner
const isOwner = nodes.some(n => n.is_me && n.is_owner);
const remotes = nodes.filter(n => !n.is_me);
const remoteOpts = remotes.map(n =>
`<option value="${n.slot}">${n.id} — ${n.hostname}</option>`).join('');
const isOwner = nodes.some(n => n.is_me && n.is_owner);
const remotes = nodes.filter(n => !n.is_me);
const hasPartner = remotes.length > 0 && remotes.some(n => n.hostname);
let html = '<div class="vv-pt-actions">';
let html = '';
// Onboard — disabled until a partner hostname is configured in master.conf
html += `<button class="vv-pt-action-btn run" onclick="vvPtOnboard(this)"
${!hasPartner ? 'disabled title="Add partner hostname to master.conf first (Scheduler → master.conf)"' :
'title="Run partnership_onboard.sh — auto-detects role (run on mirror first, then owner)"'}
${!hasPartner ? 'style="opacity:.35;cursor:default;"' : ''}>
▶ Onboard
</button>`;
// ── Per-partner onboard phase actions (owner only) ─────────────────────────
if (isOwner && hasPartner) {
for (const remote of remotes) {
const phase = remote.onboard_phase ?? 0;
html += `<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #1e1e1e;">`;
html += `<div style="font-size:11px;color:#555;margin-bottom:6px;">${remote.id} — ${remote.hostname}</div>`;
html += '<div class="vv-pt-actions">';
if (phase === 0) {
// Not yet provisioned — offer Phase 1 (safe before HOST2 has Varaverk)
html += `<button class="vv-pt-action-btn run" onclick="vvPtPhase1(this, '${remote.id}')"
title="SSH key exchange + push master.conf to ${remote.hostname}. Safe to run before ${remote.id} has Varaverk installed.">
▶ Phase 1: SSH + Conf Push
</button>`;
html += `<button class="vv-pt-action-btn info" onclick="vvPtOnboard(this)"
title="Run full onboard (Phase 1 + 2 together) — requires ${remote.id} to already have Varaverk installed"
style="opacity:.5;">
▶ Full Onboard
</button>`;
} else if (phase === 1) {
// Phase 1 done — waiting for HOST2 to onboard and trigger Phase 2
html += `<span style="font-size:11px;color:#ff9800;align-self:center;">⏳ Waiting for ${remote.id} to onboard…</span>`;
html += `<button class="vv-pt-action-btn info" onclick="vvPtPhase2(this, '${remote.id}')"
title="Manually trigger Phase 2 — deploy containers, arr stack, and establish partnership on ${remote.hostname}. Normally triggered automatically when ${remote.id} completes its onboard.">
▶ Run Phase 2 Manually
</button>`;
} else {
// Phase 2 done — fully onboarded
html += `<span style="font-size:11px;color:#4caf50;align-self:center;">✅ Partnership established</span>`;
html += `<button class="vv-pt-action-btn info" onclick="vvPtPhase2(this, '${remote.id}')"
title="Re-run Phase 2 — redeploy containers and re-establish partnership on ${remote.hostname}"
style="opacity:.5;">
↻ Re-run Phase 2
</button>`;
}
html += '</div>';
if (phase === 0) {
html += `<div style="font-size:10px;color:#444;margin-top:6px;">
Phase 1 pushes SSH keys and master.conf to ${remote.hostname} before it has Varaverk installed.
${remote.id} can then install the plugin — the wizard will detect the conf and complete setup automatically.
</div>`;
} else if (phase === 1) {
html += `<div style="font-size:10px;color:#444;margin-top:6px;">
When ${remote.id} completes its onboard (Mirror path), it will SSH here and trigger Phase 2 automatically.
Use "Run Phase 2 Manually" if that notification didn't arrive.
</div>`;
}
html += '</div>';
}
} else if (!hasPartner) {
html += `<div style="font-size:11px;color:#444;margin-bottom:12px;">
No partner hostname configured. Edit master.conf (Scheduler tab) and set HOST2.
</div>`;
}
// ── Bottom row: Offboard + mirror Onboard ──────────────────────────────────
html += '<div class="vv-pt-actions">';
if (!isOwner) {
// Mirror: just runs its own path (SSH key + notifies owner)
html += `<button class="vv-pt-action-btn run" onclick="vvPtOnboard(this)"
${!hasPartner ? 'disabled style="opacity:.35;cursor:default;" title="Add partner hostname to master.conf first"' :
'title="Run Mirror onboard: SSH key setup + notify owner to run Phase 2"'}>
▶ Onboard (Mirror)
</button>`;
}
// Offboard
html += `<button class="vv-pt-action-btn warn" onclick="vvPtOffboard(this)"
${!cfg.enabled ? 'title="No active partnership to offboard" style="opacity:.35;cursor:default;"' : ''}>
▶ Offboard
@@ -227,18 +291,6 @@ function _renderActions(nodes, cfg) {
html += '</div>';
// Contextual note under actions
if (!hasPartner) {
html += `<div style="font-size:11px;color:#444;margin-top:8px;">
No partner hostname configured. Edit master.conf (Scheduler tab) and set HOST2.
</div>`;
} else if (!cfg.enabled) {
html += `<div style="font-size:11px;color:#555;margin-top:8px;">
Onboard will generate your SSH key, exchange it with the partner, and establish the partnership.
Run on the mirror (HOST2) first, then on the owner (HOST1).
</div>`;
}
// Transfer — show manual command, too destructive to one-click
if (cfg.enabled && isOwner) {
const confirmStr = 'i-understand-this-transfers-ownership';
@@ -290,12 +342,38 @@ function _render(data) {
// ── Actions ───────────────────────────────────────────────────────────────────
function vvPtPhase1(btn, hostId) {
if (!confirm(`Phase 1: SSH key exchange + conf push to ${hostId}?\n\nSafe to run before ${hostId} has Varaverk installed.`)) return;
btn.disabled = true;
btn.textContent = '⟳ Starting…';
fetch('/plugins/varaverk/api/run.php', {
method: 'POST',
body: new URLSearchParams({id: 'Partnership/partnership_onboard.sh', manual: '1', extra_args: '--phase1-only'})
}).then(r => r.json()).then(d => {
if (!d.ok) { alert('Failed: ' + (d.error ?? 'Unknown error')); }
setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Phase 1: SSH + Conf Push'; }, 3000);
});
}
function vvPtPhase2(btn, hostId) {
if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nThis requires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;
btn.disabled = true;
btn.textContent = '⟳ Starting…';
fetch('/plugins/varaverk/api/run.php', {
method: 'POST',
body: new URLSearchParams({id: 'Partnership/partnership_onboard.sh', manual: '1', extra_args: '--phase2-only'})
}).then(r => r.json()).then(d => {
if (!d.ok) { alert('Failed: ' + (d.error ?? 'Unknown error')); }
setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Run Phase 2 Manually'; }, 3000);
});
}
function vvPtOnboard(btn) {
if (!confirm('Run partnership_onboard.sh?\n\nRun on the MIRROR first, then on the OWNER.')) return;
if (!confirm('Run full partnership_onboard.sh?\n\nRun on the MIRROR first, then on the OWNER.\n\nUse Phase 1 + Phase 2 buttons for step-by-step control.')) return;
btn.disabled = true;
btn.textContent = '⟳ Starting…';
vvRunById('Partnership/partnership_onboard.sh');
setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard'; }, 4000);
setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000);
}
function vvPtOffboard(btn) {