Graceful degradation before partnership/SSH is set up
Fix 1 — Monitor partner card (common.php + monitor.php): vv_remote_hosts_stats() no longer skips hosts with no API key. Returns no_api_key:true entry instead. Monitor JS renders "API key not configured — complete Onboard to enable" instead of a blank space. Fix 2 — rawconf push not alarming pre-onboard (config.php + scheduler.php): Probe failures (Tailscale not found, plugin not installed) now carry ready:false. JS treats ready:false results silently — button shows "✓ Saved" not "push failed: HOST2" before SSH is set up. Only genuine post-onboard failures (ready not false) show as errors. Fix 3 — Partnership Onboard button (partnership.php): Disabled with tooltip when no partner hostname is in master.conf. When partner configured but not onboarded, adds explanation note: "Onboard will generate your SSH key, exchange it with the partner..." No partner configured: "Edit master.conf and set HOST2."
This commit is contained in:
@@ -572,7 +572,11 @@ function vv_remote_hosts_stats(): array {
|
|||||||
foreach ($hostIds as $id) {
|
foreach ($hostIds as $id) {
|
||||||
if (strtolower($id) === strtolower($myHost)) continue;
|
if (strtolower($id) === strtolower($myHost)) continue;
|
||||||
$key = $vars[strtoupper($id) . '_UNRAID_API_KEY'] ?? '';
|
$key = $vars[strtoupper($id) . '_UNRAID_API_KEY'] ?? '';
|
||||||
if (!$key) continue;
|
if (!$key) {
|
||||||
|
$results[$id] = ['available' => false, 'no_api_key' => true,
|
||||||
|
'host_id' => $id, 'hostname' => $vars[$id]];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$cacheFile = "/tmp/vv_remote_{$id}.json";
|
$cacheFile = "/tmp/vv_remote_{$id}.json";
|
||||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) {
|
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function vv_push_master_conf(): array {
|
|||||||
$hostname = trim($m[2][$i]);
|
$hostname = trim($m[2][$i]);
|
||||||
$ip = vv_resolve_tailscale_ip($hostname);
|
$ip = vv_resolve_tailscale_ip($hostname);
|
||||||
if (!$ip) {
|
if (!$ip) {
|
||||||
$results[] = ['host' => $hostKey, 'ok' => false, 'error' => 'Tailscale IP not found'];
|
$results[] = ['host' => $hostKey, 'ok' => false, 'ready' => false, 'error' => 'Tailscale IP not found'];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ function vv_push_master_conf(): array {
|
|||||||
) ?: '');
|
) ?: '');
|
||||||
|
|
||||||
if ($probe === '') {
|
if ($probe === '') {
|
||||||
$results[] = ['host' => $hostKey, 'ok' => false,
|
$results[] = ['host' => $hostKey, 'ok' => false, 'ready' => false,
|
||||||
'error' => 'plugin not installed, dir missing, or master.conf absent — skipped'];
|
'error' => 'plugin not installed, dir missing, or master.conf absent — skipped'];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,6 +579,8 @@ function vvPollMonitor() {
|
|||||||
<span style="color:#444;">Uptime</span>
|
<span style="color:#444;">Uptime</span>
|
||||||
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
|
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
|
||||||
</div>${verWarn}`;
|
</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>`;
|
||||||
} else if (rs && !rs.available) {
|
} else if (rs && !rs.available) {
|
||||||
statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API unreachable</div>`;
|
statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API unreachable</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,11 +207,15 @@ function _renderActions(nodes, cfg) {
|
|||||||
const remoteOpts = remotes.map(n =>
|
const remoteOpts = remotes.map(n =>
|
||||||
`<option value="${n.slot}">${n.id} — ${n.hostname}</option>`).join('');
|
`<option value="${n.slot}">${n.id} — ${n.hostname}</option>`).join('');
|
||||||
|
|
||||||
|
const hasPartner = remotes.length > 0 && remotes.some(n => n.hostname);
|
||||||
|
|
||||||
let html = '<div class="vv-pt-actions">';
|
let html = '<div class="vv-pt-actions">';
|
||||||
|
|
||||||
// Onboard
|
// Onboard — disabled until a partner hostname is configured in master.conf
|
||||||
html += `<button class="vv-pt-action-btn run" onclick="vvPtOnboard(this)"
|
html += `<button class="vv-pt-action-btn run" onclick="vvPtOnboard(this)"
|
||||||
title="Run partnership_onboard.sh — auto-detects role (run on mirror first, then owner)">
|
${!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
|
▶ Onboard
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
@@ -223,6 +227,18 @@ function _renderActions(nodes, cfg) {
|
|||||||
|
|
||||||
html += '</div>';
|
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
|
// Transfer — show manual command, too destructive to one-click
|
||||||
if (cfg.enabled && isOwner) {
|
if (cfg.enabled && isOwner) {
|
||||||
const confirmStr = 'i-understand-this-transfers-ownership';
|
const confirmStr = 'i-understand-this-transfers-ownership';
|
||||||
|
|||||||
@@ -2635,13 +2635,16 @@ function vvSaveRawConf() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normal save: show push status
|
// Normal save: show push status
|
||||||
|
// ready:false = partner not set up yet (pre-onboard) — treat as silent, not an error
|
||||||
const push = d.push ?? [];
|
const push = d.push ?? [];
|
||||||
if (push.length === 0) {
|
const synced = push.filter(p => p.ok);
|
||||||
btn.textContent = '✓ Saved';
|
const realFailed = push.filter(p => !p.ok && p.ready !== false);
|
||||||
} else if (push.every(p => p.ok)) {
|
if (realFailed.length > 0) {
|
||||||
btn.textContent = '✓ Saved · synced to ' + push.map(p => p.host).join(', ');
|
btn.textContent = '✓ Saved · push failed: ' + realFailed.map(p => p.host).join(', ');
|
||||||
|
} else if (synced.length > 0) {
|
||||||
|
btn.textContent = '✓ Saved · synced to ' + synced.map(p => p.host).join(', ');
|
||||||
} else {
|
} else {
|
||||||
btn.textContent = '✓ Saved · push failed: ' + push.filter(p => !p.ok).map(p => p.host).join(', ');
|
btn.textContent = '✓ Saved';
|
||||||
}
|
}
|
||||||
setTimeout(() => { btn.textContent = 'Save Conf'; }, 3500);
|
setTimeout(() => { btn.textContent = 'Save Conf'; }, 3500);
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user