Partnership Actions: full redesign with stepped onboard cards
- Single '▶ Onboard' button per unprovisioned remote → expands that card to show Step 1 (terminal link + click-to-copy command) and Step 2 (Push Conf) - Onboard state persists across 10s poll re-renders via top-level _vvOnboarding map and _vvPtReload hook set by the IIFE - Phase 1 complete: amber 'SSH ready' pill + Tailscale IP + Phase 2 manual button - Phase 2 complete: green 'Active' pill + date + IP + uptime + unRAID version + state - Compact compact '○ Not provisioned' + Reset button when not in onboard mode - Removed vvPtPhase1 (replaced by vvPtStartOnboard toggle) - vvPtStartOnboard / vvPtEndOnboard are top-level functions
This commit is contained in:
+167
-127
@@ -56,7 +56,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Public action functions — defined at top level so onclick= can reach them ──
|
||||
// ── Onboard toggle state — persists across 10s poll re-renders ─────────────────
|
||||
const _vvOnboarding = {}; // hostId → true when steps are expanded
|
||||
let _vvPtReload = null; // set by IIFE so top-level fns can trigger a refresh
|
||||
|
||||
// ── Public action functions — top-level so onclick= attributes can reach them ──
|
||||
function _vvPtRun(id, extraArgs) {
|
||||
const params = {id, manual: '1'};
|
||||
if (extraArgs) params.extra_args = extraArgs;
|
||||
@@ -67,23 +71,18 @@ function _vvPtRun(id, extraArgs) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.text();
|
||||
}).then(text => {
|
||||
if (!text.trim()) {
|
||||
// Empty body — script likely started via nohup before response was sent.
|
||||
// Treat as ok; check Scheduler log for confirmation.
|
||||
return {ok: true, _empty_response: true};
|
||||
}
|
||||
if (!text.trim()) return {ok: true, _empty_response: true};
|
||||
return JSON.parse(text);
|
||||
});
|
||||
}
|
||||
|
||||
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…';
|
||||
_vvPtRun('Partnership/partnership_onboard.sh', '--phase1-only')
|
||||
.then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
|
||||
.catch(e => alert('Error: ' + e))
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Phase 1: SSH + Conf Push'; }, 3000));
|
||||
function vvPtStartOnboard(hostId) {
|
||||
_vvOnboarding[hostId] = true;
|
||||
if (_vvPtReload) _vvPtReload();
|
||||
}
|
||||
function vvPtEndOnboard(hostId) {
|
||||
delete _vvOnboarding[hostId];
|
||||
if (_vvPtReload) _vvPtReload();
|
||||
}
|
||||
|
||||
function vvPtPhase2(btn, hostId) {
|
||||
@@ -304,141 +303,180 @@ function _nodeCard(node) {
|
||||
// ── Actions ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function _renderActions(nodes, cfg) {
|
||||
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);
|
||||
const isOwner = nodes.some(n => n.is_me && n.is_owner);
|
||||
const selfNode = nodes.find(n => n.is_me);
|
||||
const remotes = nodes.filter(n => !n.is_me);
|
||||
const hasPartner = remotes.some(n => n.hostname);
|
||||
const termBase = selfNode && selfNode.ts_ip ? `https://${selfNode.ts_ip}/webterminal/ttyd/` : null;
|
||||
const termCmd = `bash /mnt/user/appdata/Varaverk/Partnership/partnership_onboard.sh --phase1-only`;
|
||||
|
||||
let html = '';
|
||||
|
||||
// ── HOST1 local setup button (owner only, before local_done) ──────────────
|
||||
const selfNode = nodes.find(n => n.is_me);
|
||||
if (!hasPartner && isOwner) {
|
||||
html += `<div style="font-size:11px;color:#444;margin-bottom:12px;">
|
||||
No partner configured — edit master.conf (Scheduler tab) and set HOST2.
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Per-remote host section ────────────────────────────────────────────────
|
||||
if (isOwner && hasPartner) {
|
||||
remotes.forEach(remote => {
|
||||
const phase = remote.onboard_phase ?? 0;
|
||||
const isOnboarding = !!_vvOnboarding[remote.id];
|
||||
const dotCol = remote.ts_online === null ? '#555' : remote.ts_online ? '#4caf50' : '#f44336';
|
||||
const dotLbl = remote.ts_online === null ? 'unknown' : remote.ts_online ? 'online' : 'offline';
|
||||
const pt = remote.partnership || {};
|
||||
const ptState = (pt.state || '').toUpperCase();
|
||||
const ptDate = pt.updated
|
||||
? new Date(parseInt(pt.updated) * 1000).toLocaleDateString([], {month:'short',day:'numeric',year:'numeric'})
|
||||
: null;
|
||||
const sys = remote.system || {};
|
||||
const sysUptime = sys.uptime_sec ? _uptime(sys.uptime_sec) : null;
|
||||
const sysVer = sys.unraid_version || null;
|
||||
|
||||
html += `<div style="margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #1e1e1e;">`;
|
||||
|
||||
// Header: host + online dot
|
||||
html += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||
<div>
|
||||
<span style="font-size:10px;color:#444;">${remote.id}</span>
|
||||
<span style="font-size:12px;color:#bbb;font-weight:500;margin-left:5px;">${remote.hostname}</span>
|
||||
</div>
|
||||
<span style="font-size:10px;color:${dotCol};">● ${dotLbl}</span>
|
||||
</div>`;
|
||||
|
||||
// ── Phase 0: not provisioned ─────────────────────────────────────────
|
||||
if (phase === 0) {
|
||||
if (isOnboarding) {
|
||||
// Expanded step-by-step onboard card
|
||||
html += `<div style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:4px;padding:10px 12px;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:8px;">
|
||||
<span style="font-size:9px;color:#4a9eff;background:#0e1a2a;padding:2px 8px;border-radius:10px;font-weight:600;">Step 1</span>
|
||||
<span style="font-size:11px;color:#888;">Install SSH key on ${remote.hostname}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px;">
|
||||
${termBase ? `<a href="${termBase}" target="_blank"
|
||||
style="padding:3px 10px;background:#1a2a3a;color:#7ab;border:1px solid #2e4a6b;
|
||||
border-radius:3px;text-decoration:none;font-size:10px;white-space:nowrap;">🖥 Open Terminal</a>` : ''}
|
||||
<code onclick="navigator.clipboard.writeText('${termCmd}').then(()=>{this.style.color='#4caf50';setTimeout(()=>this.style.color='#555',1500)})"
|
||||
style="font-size:9px;color:#555;background:#0a0a0a;padding:3px 8px;border-radius:3px;
|
||||
border:1px solid #1a1a1a;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
|
||||
white-space:nowrap;cursor:pointer;" title="Click to copy">${termCmd}</code>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#2a2a2a;margin-bottom:14px;">Enter ${remote.hostname} root password when prompted · this tab auto-updates on completion</div>
|
||||
|
||||
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:8px;">
|
||||
<span style="font-size:9px;color:#4caf50;background:#0a1a0a;padding:2px 8px;border-radius:10px;font-weight:600;">Step 2</span>
|
||||
<span style="font-size:11px;color:#888;">Push conf</span>
|
||||
<span style="font-size:9px;color:#2a2a2a;">if key already installed separately</span>
|
||||
</div>
|
||||
<button class="vv-pt-action-btn info" onclick="vvPtPushConf(this,'${remote.id}')"
|
||||
title="Runs --phase1-only --skip-ssh — use when SSH key is already on ${remote.hostname}">
|
||||
▶ Push Conf
|
||||
</button>
|
||||
</div>
|
||||
<div class="vv-pt-actions" style="margin-top:8px;">
|
||||
<button class="vv-pt-action-btn warn" onclick="vvPtEndOnboard('${remote.id}')" style="opacity:.7;">
|
||||
✕ Close
|
||||
</button>
|
||||
<button class="vv-pt-action-btn warn" onclick="vvPtCancel(this,'${remote.id}')" style="opacity:.6;">
|
||||
✕ Reset keys
|
||||
</button>
|
||||
</div>`;
|
||||
} else {
|
||||
// Compact row
|
||||
html += `<div style="display:flex;align-items:center;gap:10px;">
|
||||
<span style="font-size:10px;color:#444;">○ Not provisioned</span>
|
||||
<button class="vv-pt-action-btn run" onclick="vvPtStartOnboard('${remote.id}')"
|
||||
title="Show initialization steps for ${remote.hostname}">
|
||||
▶ Onboard
|
||||
</button>
|
||||
<button class="vv-pt-action-btn warn" onclick="vvPtCancel(this,'${remote.id}')"
|
||||
style="opacity:.5;font-size:11px;" title="Remove any partial keys and reset state">
|
||||
✕ Reset
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Phase 1: SSH done, awaiting HOST2 install ──────────────────────────
|
||||
} else if (phase === 1) {
|
||||
html += `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;">
|
||||
<span style="font-size:10px;color:#ff9800;background:#1a1200;padding:2px 8px;border-radius:10px;border:1px solid #3a2800;">⏳ SSH ready</span>
|
||||
${remote.ts_ip ? `<span style="font-size:9px;color:#2a2a2a;">${remote.ts_ip}</span>` : ''}
|
||||
</div>
|
||||
<div style="font-size:10px;color:#444;margin-bottom:8px;">
|
||||
Waiting for ${remote.id} to install Varaverk and complete its onboard.
|
||||
Phase 2 triggers automatically when it does.
|
||||
</div>
|
||||
<div class="vv-pt-actions">
|
||||
<button class="vv-pt-action-btn info" onclick="vvPtPhase2(this,'${remote.id}')"
|
||||
title="Manually trigger Phase 2 if ${remote.id} auto-notification did not arrive">
|
||||
▶ Run Phase 2 Manually
|
||||
</button>
|
||||
<button class="vv-pt-action-btn warn" onclick="vvPtCancel(this,'${remote.id}')"
|
||||
style="opacity:.7;">
|
||||
✕ Cancel
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
// ── Phase 2: fully onboarded ───────────────────────────────────────────
|
||||
} else {
|
||||
const ptColor = ptState === 'ACTIVE' ? '#4caf50' : ptState === 'INACTIVE' ? '#f44336' : '#555';
|
||||
html += `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;">
|
||||
<span style="font-size:10px;color:#4caf50;background:#0a1a0a;padding:2px 8px;border-radius:10px;border:1px solid #1a4a1a;">✅ Active</span>
|
||||
${ptDate ? `<span style="font-size:9px;color:#333;">since ${ptDate}</span>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:10px;color:#2a2a2a;margin-bottom:8px;">
|
||||
${remote.ts_ip ? `<span>${remote.ts_ip}</span>` : ''}
|
||||
${sysUptime ? `<span>up ${sysUptime}</span>` : ''}
|
||||
${sysVer ? `<span>unRAID ${sysVer}</span>` : ''}
|
||||
${ptState ? `<span style="color:${ptColor};">${ptState}</span>` : ''}
|
||||
</div>
|
||||
<div class="vv-pt-actions">
|
||||
<button class="vv-pt-action-btn info" onclick="vvPtPhase2(this,'${remote.id}')"
|
||||
style="opacity:.4;" title="Re-run Phase 2 — redeploy containers and re-establish partnership">
|
||||
↻ Re-run Phase 2
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// ── HOST1 local setup (when needed after phase 1+) ────────────────────────
|
||||
if (isOwner && selfNode && !selfNode.local_done && remotes.some(r => (r.onboard_phase ?? 0) >= 1)) {
|
||||
html += `<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #1e1e1e;">
|
||||
<div style="font-size:11px;color:#555;margin-bottom:6px;">${selfNode.id} — ${selfNode.hostname} <span style="color:#333;">(this server)</span></div>
|
||||
<div style="font-size:11px;color:#555;margin-bottom:6px;">
|
||||
${selfNode.id} — ${selfNode.hostname} <span style="color:#2a2a2a;">(this server)</span>
|
||||
</div>
|
||||
<div class="vv-pt-actions">
|
||||
<button class="vv-pt-action-btn run" onclick="vvPtLocalSetup(this)"
|
||||
title="Run HOST1-local onboard steps (FolderView3, setup state) without needing HOST2 to be present">
|
||||
title="FolderView3, setup state — does not require HOST2">
|
||||
▶ Complete HOST1 Setup
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#444;margin-top:6px;">
|
||||
Configures HOST1's local side independently — FolderView3 folder, state flags.
|
||||
Does not require HOST2 to be online.
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 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="Generate SSH key + install on ${remote.hostname} + push conf. First run needs terminal access to ${remote.hostname} for password — subsequent runs auto-detect the installed key.">
|
||||
▶ Phase 1: SSH + Conf Push
|
||||
</button>`;
|
||||
if (remote.key_ready) {
|
||||
html += `<button class="vv-pt-action-btn run" onclick="vvPtPushConf(this, '${remote.id}')"
|
||||
title="Key is already installed on ${remote.hostname} — push conf files and complete Phase 1 without re-running SSH setup">
|
||||
▶ Push Conf (key installed)
|
||||
</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>`;
|
||||
html += `<button class="vv-pt-action-btn warn" onclick="vvPtCancel(this, '${remote.id}')"
|
||||
title="Remove SSH keys from both sides and reset phase state — use if a partial Phase 1 left a stale key"
|
||||
style="opacity:.7;">
|
||||
✕ Cancel
|
||||
</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>`;
|
||||
html += `<button class="vv-pt-action-btn warn" onclick="vvPtCancel(this, '${remote.id}')"
|
||||
title="Remove SSH keys from both sides and reset to phase 0 — ${remote.id}'s authorized_keys will be cleaned up before the key is deleted">
|
||||
✕ Cancel
|
||||
</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) {
|
||||
const termUrl = selfNode && selfNode.ts_ip ? `https://${selfNode.ts_ip}/webterminal/ttyd/` : null;
|
||||
const termCmd = `bash /mnt/user/appdata/Varaverk/Partnership/partnership_onboard.sh --phase1-only`;
|
||||
html += `<div style="margin-top:8px;font-size:10px;color:#555;">
|
||||
<div style="margin-bottom:5px;">First run requires terminal access for SSH key password.</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
${termUrl ? `<a href="${termUrl}" target="_blank"
|
||||
style="font-size:10px;padding:3px 10px;background:#1a2a3a;color:#7ab;border:1px solid #2e4a6b;
|
||||
border-radius:3px;text-decoration:none;white-space:nowrap;">🖥 Open Terminal</a>` : ''}
|
||||
<code style="font-size:9px;color:#666;background:#111;padding:3px 7px;border-radius:3px;
|
||||
border:1px solid #222;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
|
||||
white-space:nowrap;cursor:pointer;"
|
||||
onclick="navigator.clipboard.writeText('${termCmd}').then(()=>{this.style.color='#4caf50';setTimeout(()=>this.style.color='',1200)})"
|
||||
title="Click to copy">${termCmd}</code>
|
||||
</div>
|
||||
<div style="margin-top:4px;color:#333;">Tab auto-updates when complete · or click Push Conf if key already installed.</div>
|
||||
</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">';
|
||||
|
||||
// ── Bottom row ─────────────────────────────────────────────────────────────
|
||||
html += `<div class="vv-pt-actions" style="padding-top:4px;">`;
|
||||
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"'}>
|
||||
${!hasPartner ? 'disabled style="opacity:.35;cursor:default;"' : 'title="Mirror onboard: SSH key setup + notify owner"'}>
|
||||
▶ Onboard (Mirror)
|
||||
</button>`;
|
||||
}
|
||||
|
||||
html += `<button class="vv-pt-action-btn warn" onclick="vvPtOffboard(this)"
|
||||
${!cfg.enabled ? 'title="No active partnership to offboard" style="opacity:.35;cursor:default;"' : ''}>
|
||||
${!cfg.enabled ? 'style="opacity:.35;cursor:default;" title="No active partnership"' : ''}>
|
||||
▶ Offboard
|
||||
</button>`;
|
||||
</button></div>`;
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Transfer — show manual command, too destructive to one-click
|
||||
if (cfg.enabled && isOwner) {
|
||||
const confirmStr = 'i-understand-this-transfers-ownership';
|
||||
html += `<div style="margin-top:14px;padding-top:10px;border-top:1px solid #1e1e1e;">
|
||||
<span style="font-size:11px;color:#555;">Transfer ownership — run manually from the current owner:</span>
|
||||
<div class="vv-pt-transfer-note" style="margin-top:4px;padding:6px 10px;background:#111;border-radius:4px;color:#666;">
|
||||
bash Partnership/partnership_transfer.sh --confirm=${confirmStr}
|
||||
html += `<div style="margin-top:12px;padding-top:10px;border-top:1px solid #1e1e1e;">
|
||||
<span style="font-size:11px;color:#555;">Transfer ownership — run manually:</span>
|
||||
<div class="vv-pt-transfer-note" style="margin-top:4px;padding:6px 10px;background:#111;border-radius:4px;color:#555;">
|
||||
bash Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -490,6 +528,8 @@ function vvPtLoad() {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
_vvPtReload = vvPtLoad; // expose to top-level toggle fns
|
||||
|
||||
vvPtLoad();
|
||||
setInterval(vvPtLoad, 10000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user