diff --git a/Partnership/partnership_onboard.sh b/Partnership/partnership_onboard.sh index 950b04b..8036364 100755 --- a/Partnership/partnership_onboard.sh +++ b/Partnership/partnership_onboard.sh @@ -315,14 +315,33 @@ if [[ "$AM_MIRROR" == true ]]; then echo " Mirror sets up SSH keys, then notifies Owner to run Phase 2." echo "" + # Tested before attempted, the same guard the owner's phase 1 has had all along. + # + # The key step is a TERMINAL step by design — ssh_setup.sh runs ssh-copy-id, which prompts for + # the owner's root password on a first install. Once the operator has done that in a terminal, + # pressing ▶ Onboard ran the whole thing again: another ssh-copy-id, this time from the WebGUI + # with no TTY to answer the prompt, which fails and aborts the run at Step 1 — so the button + # whose entire job is Step 2 could never reach it. + # + # Working SSH is the actual precondition, not "have we run the setup script". If it already + # works there is nothing to install, whichever route installed it. + OWNER_IP_PRE=$(resolve_tailscale_ip "$OWNER" 2>/dev/null || true) if [[ "$SKIP_SSH" == true ]]; then warn "Skipping SSH setup (--skip-ssh)" + elif [[ -n "$OWNER_IP_PRE" ]] && timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ + -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ + root@"$OWNER_IP_PRE" exit 0 2>/dev/null; then + echo "SSH to $OWNER already works ✅ — key already installed, skipping setup" elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then echo "SSH key ready ✅" else error "SSH key setup failed" + error "Install the key from a terminal on this host — ssh-copy-id needs $OWNER's password," + error "and a WebGUI button has no way to answer that prompt:" + error " bash $SCRIPTS_ROOT/Partnership/partnership_onboard.sh --phase1-only" exit 1 fi + unset OWNER_IP_PRE # Stop after the key when asked. ssh_setup.sh runs ssh-copy-id, which prompts for the # owner's root password on a first install — answerable in a terminal, never from the diff --git a/Plugin/unraid/Varaverk.page b/Plugin/unraid/Varaverk.page index 17ff2c2..f89fa89 100644 --- a/Plugin/unraid/Varaverk.page +++ b/Plugin/unraid/Varaverk.page @@ -203,6 +203,24 @@ function vvPrompt(text, def, opts) { // // Not in js/varaverk.js: that loads below the tab include, and the wizard returns before it. +// Where Varaverk is installed on THIS host, read live rather than baked into the page. +// +// Two reasons it cannot be a render-time constant. The wizard can move it: choosing appdata in +// step 1 triggers a migration and step 2 renders in the same page load, so PHP's value names the +// pre-migration location. And each host chooses independently — the owner may be on flash while +// the mirror is on appdata — so a panel rendered on either side must ask, not assume. +// +// It matters because the value ends up in a command the operator pastes into a root terminal. +// api/setup.php?action=detect re-reads varaverk.cfg. Cached; the panels re-render often. +let _vvScriptsDir = null; +function vvScriptsDir(fallback) { + if (_vvScriptsDir) return Promise.resolve(_vvScriptsDir); + return fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now()) + .then(r => r.json()) + .then(d => (_vvScriptsDir = (d && d.scripts_dir) || fallback)) + .catch(() => (_vvScriptsDir = fallback)); +} + // api/run.php answers when a job is LAUNCHED, not finished. An empty body is never success — // Unraid's CSRF guard exits with one, and so does a PHP fatal. function _vvPtRun(id, extraArgs) { diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index 0a9bd04..21ac704 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -626,6 +626,11 @@ async function vvPtSaveSettings(btn) { // ── Private page logic ───────────────────────────────────────────────────────── (function() { +// Resolve the install path once, up front, and redraw when it lands. termCmd is built +// synchronously inside _renderActions, so without this the first paint would use the +// render-time fallback — right on a flash host, wrong on one that moved to appdata. +vvScriptsDir().then(() => { if (_vvPtReload) _vvPtReload(); }); + // ── Helpers ─────────────────────────────────────────────────────────────────── function _row(lbl, val) { @@ -950,7 +955,11 @@ function _renderActions(nodes, cfg) { const remotes = nodes.filter(n => !n.is_me); const hasPartner = remotes.some(n => n.hostname); const termBase = `https://${window.location.hostname}/webterminal/ttyd/`; - const termCmd = `bash /Partnership/partnership_onboard.sh --phase1-only`; + // _vvScriptsDir is filled by the live probe below and falls back to the render-time value. + // Both matter: a host in appdata must be handed its appdata path, and this panel is rendered + // on whichever side is the mirror — so it cannot assume either layout. The owner and the + // mirror are independently either flash or appdata. + const termCmd = `bash ${_vvScriptsDir || }/Partnership/partnership_onboard.sh --phase1-only`; let html = ''; diff --git a/Plugin/unraid/pages/setup.php b/Plugin/unraid/pages/setup.php index be961af..64c6064 100644 --- a/Plugin/unraid/pages/setup.php +++ b/Plugin/unraid/pages/setup.php @@ -72,6 +72,18 @@ if ($vvMasterRaw !== '' && $detectedHostname !== '') { if ($vvDetected['role'] === 'partner') $vvDetected['primary'] = $vvSlots['host1'] ?? ''; } +// ── How far this host's own onboarding has got ─────────────────────────────────────────────── +// Lets the inline join panel show the SSH step or the join button rather than both. Same flags +// api/checklist.php reads, and the same two spellings, because the state file has been written +// by both bash and PHP over its life. +if ($vvDetected['slot'] !== '') { + $vvSt = vv_setup_state_read(); + $vvSlUp = strtoupper($vvDetected['slot']); + $vvDetected['phase'] = + (!empty($vvSt[$vvSlUp . '_PHASE2_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase2_done'])) ? 2 + : ((!empty($vvSt[$vvSlUp . '_PHASE1_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase1_done'])) ? 1 : 0); +} + // ── Custom docker networks, adopted from the owner ─────────────────────────────────────────── // master.conf names the hosts; it does not name the networks, which live in each host's own // host*.conf. The owner's copy is available here anyway: onboard Phase 1 caches it into @@ -84,17 +96,6 @@ if ($vvMasterRaw !== '' && $detectedHostname !== '') { // // Adopting the owner's list also means docker_network_connect.sh will recreate the network here // after an Unraid update wipes it, which is the whole reason that script exists. -// How far this host's own onboarding has got, so the inline join panel can show the SSH step or -// the join button rather than both. Same flags api/checklist.php reads, and the same two -// spellings, because the state file has been written by both bash and PHP over its life. -if ($vvDetected['slot'] !== '') { - $vvSt = vv_setup_state_read(); - $vvSlUp = strtoupper($vvDetected['slot']); - $vvDetected['phase'] = - (!empty($vvSt[$vvSlUp . '_PHASE2_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase2_done'])) ? 2 - : ((!empty($vvSt[$vvSlUp . '_PHASE1_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase1_done'])) ? 1 : 0); -} - if ($vvDetected['role'] === 'partner' && $vvDetected['slot'] !== '') { $vvOwnerConf = VV_CONF_RAM_CACHE_DIR . '/host1.conf'; if (is_readable($vvOwnerConf)) { @@ -506,26 +507,6 @@ function vvDeferItem(btn, id, defer) { // // Only while it is still outstanding: once the mirror has joined, the partnership row in the // checklist above says so and a second copy of the panel is noise. -// SCRIPTS_DIR is read live rather than baked into the page, because the wizard can MOVE it. -// -// Choosing appdata storage in step 1 triggers a migration, and step 2 renders in the same page -// load — so anything PHP substituted at render time still names the pre-migration location. A -// host that migrated was handed "bash /boot/config/plugins/varaverk/…" to paste, which does not -// exist there. The command is the one thing on this panel that must be right: it is pasted into -// a root terminal. -// -// api/setup.php?action=detect re-reads varaverk.cfg, so it reports wherever the install actually -// ended up. Cached after the first call — the checklist re-renders on every action. -let _vvScriptsDir = null; -function vvScriptsDir() { - if (_vvScriptsDir) return Promise.resolve(_vvScriptsDir); - return fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now()) - .then(r => r.json()) - .then(d => (_vvScriptsDir = (d && d.scripts_dir) || )) - // Falling back to the render-time value is still better than no command at all; it is only - // wrong in the case this function exists to handle, and the panel shows the path either way. - .catch(() => (_vvScriptsDir = )); -} function vvRenderOnboardPanel(d) { const el = document.getElementById('vv-onboard-panel'); @@ -534,7 +515,7 @@ function vvRenderOnboardPanel(d) { const pt = (d.items || []).find(i => i.id === 'partnership'); if (!partner || !pt || pt.ok) { el.innerHTML = ''; return; } - vvScriptsDir().then(dir => { + vvScriptsDir().then(dir => { el.innerHTML = '
' + '
Join the partnership
' + vvRenderMirrorOnboard({