<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Partnership tab. The mesh view — which nodes exist, whether they are reachable over
//   Tailscale, their system summary, sync history, and the onboarding controls that deploy
//   stacks to a partner.
//
// DESIGN PRINCIPLES
//   Pure view — all node data comes from api/partnership.php; actions are separate endpoints.
//   Reachability, SSH and system collection live in include/partnership.php.
//
//   Reachability is probed on demand as well as polled, because "is my partner up right now"
//   is the question this page exists to answer.
//
// OPERATIONAL SAFEGUARDS
//   Every action that changes a partner is confirmed first — this page can deploy stacks and
//   provision credentials on a machine 50 miles away, and there is no undo for that.
//
//   Long-running operations go through api/run.php rather than blocking the page, so a deploy
//   that takes minutes does not leave the browser hanging on a request.
//
//   An unreachable partner degrades to its own card only; the rest of the mesh still renders.
//
//   Values from remote nodes are escaped on render — this page displays strings originating
//   on another machine.
//
// RENDERS
//   Node cards with identity, reachability, system summary, sync history
//   Onboarding and stack-deploy controls, API key provisioning, partnership settings
//
// DEPENDS ON
//   api/partnership.php           polled every 10s   → include/partnership.php
//   api/partnership_ping.php      on-demand reachability probe
//   api/partnership_settings.php  settings writes
//   api/create_api_key.php        credential provisioning
//   api/run.php                   long-running deploy operations
//   api/confform.php              inline conf edits
?>
<style>
.vv-pt-grid       { display:grid; gap:12px; }
.vv-pt-node       { background:#161616; border:1px solid #2a2a2a; border-radius:6px; padding:12px; min-width:0; }
.vv-pt-node.me    { border-color:#2a3a2a; }
.vv-pt-node-head  { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px; }
.vv-pt-hostname   { font-size:13px; font-weight:bold; color:#ccc; }
.vv-pt-slot       { font-size:10px; color:#444; margin-top:1px; }
.vv-pt-tags       { display:flex; gap:4px; flex-wrap:wrap; justify-content:flex-end; }
.vv-pt-tag        { font-size:8px; padding:1px 5px; border-radius:3px; white-space:nowrap; }
.vv-pt-tag.me     { background:#1a3a1a; color:#4caf50; }
.vv-pt-tag.owner  { background:#1a2a3a; color:#4a9eff; }
.vv-pt-tag.mirror { background:#2a2a1a; color:#ff9800; }
.vv-pt-row        { display:flex; justify-content:space-between; align-items:baseline; margin:3px 0; }
.vv-pt-lbl        { font-size:11px; color:#555; }
.vv-pt-val        { font-size:11px; color:#bbb; text-align:right; }
.vv-pt-sep        { border:none; border-top:1px solid #222; margin:8px 0; }
.vv-pt-state      { font-size:12px; font-weight:500; }
.vv-pt-dot        { width:8px; height:8px; border-radius:50%; display:inline-block; flex-shrink:0; margin-right:5px; }
.vv-pt-actions    { display:flex; gap:10px; flex-wrap:wrap; margin-top:4px; }
.vv-pt-action-btn { padding:5px 16px; border:none; border-radius:4px; cursor:pointer;
                    font-size:12px; font-weight:500; }
.vv-pt-action-btn.run  { background:#1a3a1a; color:#6fcf97; border:1px solid #2e6b2e; }
.vv-pt-action-btn.warn { background:#3a1a1a; color:#f88; border:1px solid #6b2e2e; }
.vv-pt-action-btn.info { background:#1a2a3a; color:#7ab; border:1px solid #2e4a6b; }
.vv-pt-action-btn:disabled { opacity:0.4; cursor:default; }
.vv-pt-transfer-note { font-size:10px; color:#555; margin-top:6px; font-family:monospace; }
.vv-pt-onboard-highlight {
  box-shadow: 0 0 0 2px #4a8, 0 0 12px rgba(100,200,120,.25) !important;
  animation: vvOnboardPulse 1.4s ease-in-out 4;
}
@keyframes vvOnboardPulse {
  0%,100% { box-shadow: 0 0 0 2px #4a8, 0 0 8px rgba(100,200,120,.2); }
  50%     { box-shadow: 0 0 0 4px #4a8, 0 0 18px rgba(100,200,120,.45); }
}

/* ── Settings panel ─────────────────────────────────────────── */
.vv-set-file       { border:1px solid #222; border-radius:5px; margin-bottom:8px; overflow:hidden; }
.vv-set-file-hdr   { display:flex; justify-content:space-between; align-items:center; padding:8px 12px;
                     background:#161616; cursor:pointer; font-size:12px; font-weight:bold; color:#bbb; font-family:monospace; }
.vv-set-file-hdr:hover { background:#1a1a1a; }
.vv-set-file-body  { padding:6px 10px 10px; }
.vv-set-sec        { border-top:1px solid #1c1c1c; }
.vv-set-sec:first-child { border-top:none; }
.vv-set-sec-hdr    { display:flex; justify-content:space-between; align-items:center; padding:6px 4px;
                     cursor:pointer; font-size:11px; color:#888; text-transform:uppercase; letter-spacing:.04em; }
.vv-set-sec-hdr:hover { color:#ccc; }
.vv-set-sec-body   { padding:4px 0 8px 8px; }
.vv-set-chev       { color:#444; font-size:10px; }
.vv-set-field      { margin-bottom:10px; }
.vv-set-key        { font-size:11px; color:#7ab; font-family:monospace; margin-bottom:2px; }
.vv-set-desc       { font-size:10px; color:#555; margin-bottom:3px; line-height:1.4; }
.vv-set-input      { width:100%; box-sizing:border-box; background:#0d0d0d; border:1px solid #2a2a2a;
                     color:#ddd; padding:5px 8px; border-radius:3px; font-family:monospace; font-size:11px; }
.vv-set-input:focus { outline:none; border-color:#4a8; }
.vv-set-input.changed { border-color:#ff9800; }
textarea.vv-set-input { resize:vertical; white-space:pre; }
</style>

<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;">
  <span style="font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;">Partnership</span>
  <span style="font-size:11px;color:#3a3a3a;" id="vv-pt-ts"></span>
</div>

<!-- Config bar -->
<div class="vv-card" style="margin-bottom:12px;" id="vv-pt-config-card">
  <div id="vv-pt-config-body" style="color:#444;font-size:12px;">Loading…</div>
</div>

<!-- Host Settings -->
<div class="vv-card" id="vv-pt-hosts-card" style="margin-bottom:12px;display:none;">
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
    <h3 style="margin:0;">Host Settings</h3>
    <button id="vv-pt-hosts-save" class="vv-pt-action-btn run"
            onclick="vvPtSaveHosts(this)" style="display:none;">Save</button>
  </div>
  <div id="vv-pt-hosts-body"></div>
</div>

<!-- Offline countdown warning (shown only when partner has been unreachable) -->
<div id="vv-pt-offline-warn" style="display:none;margin-bottom:12px;"></div>

<!-- Node grid -->
<div id="vv-pt-nodes" class="vv-pt-grid" style="margin-bottom:12px;">
  <div style="color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
</div>

<!-- Array settings (above sync/actions row) -->
<div id="vv-pt-arrays-wrap" style="margin-bottom:12px;display:none;"></div>

<!-- Mirror sync + Actions side by side -->
<div style="display:flex;gap:12px;margin-bottom:12px;align-items:stretch;flex-wrap:wrap;">
  <div class="vv-card" id="vv-pt-sync-card" style="flex:1 1 220px;min-width:0;display:flex;flex-direction:column;">
    <h3>Mirror Sync</h3>
    <div id="vv-pt-sync-body" style="flex:1;color:#444;font-size:12px;">Loading…</div>
  </div>
  <div class="vv-card" id="vv-pt-actions-card" style="flex:2 1 300px;min-width:0;">
    <h3>Actions</h3>
    <div id="vv-pt-actions-body" style="color:#444;font-size:12px;">Loading…</div>
  </div>
</div>

<!-- Settings -->
<div class="vv-card" id="vv-pt-settings-card" style="margin-top:12px;">
  <div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer;"
       onclick="vvPtToggleSettings()">
    <h3 style="margin:0;">Settings</h3>
    <div style="display:flex;align-items:center;gap:10px;">
      <button id="vv-pt-settings-save" class="vv-pt-action-btn run"
              onclick="event.stopPropagation();vvPtSaveSettings(this)"
              style="display:none;">Save Changes</button>
      <span id="vv-pt-settings-toggle"
            style="font-size:11px;color:#7ab;background:#1a2a3a;border:1px solid #2e4a6b;
                   border-radius:4px;padding:3px 10px;white-space:nowrap;">▸ Show settings</span>
    </div>
  </div>
  <div id="vv-pt-settings-hint" style="font-size:10px;color:#444;margin-top:4px;">
    Partnership configuration for this server.
  </div>
  <div id="vv-pt-settings-body" style="display:none;margin-top:10px;"></div>
</div>

<script>
// ── Toggle states — persist across 10s poll re-renders ────────────────────────
const _vvOnboarding  = {};  // hostId → true when onboard steps are expanded
const _vvDeleteKeys  = {};  // hostId → true when delete-keys panel is 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;
  return fetch('/plugins/varaverk/api/run.php', {
    method: 'POST',
    body: new URLSearchParams(params)
  }).then(r => {
    if (!r.ok) throw new Error('HTTP ' + r.status);
    return r.text();
  }).then(text => {
    if (!text.trim()) return {ok: true, _empty_response: true};
    return JSON.parse(text);
  });
}

function vvPtStartOnboard(hostId) {
  _vvOnboarding[hostId] = true;
  delete _vvDeleteKeys[hostId];
  if (_vvPtReload) _vvPtReload();
}
function vvPtEndOnboard(hostId) {
  delete _vvOnboarding[hostId];
  if (_vvPtReload) _vvPtReload();
}
function vvPtShowDeleteKeys(hostId) {
  _vvDeleteKeys[hostId] = true;
  delete _vvOnboarding[hostId];
  if (_vvPtReload) _vvPtReload();
}
function vvPtHideDeleteKeys(hostId) {
  delete _vvDeleteKeys[hostId];
  if (_vvPtReload) _vvPtReload();
}

function vvApiKey(btn) {
  const origText = btn.textContent;
  btn.disabled = true;
  btn.textContent = '⟳ Working…';
  fetch('/plugins/varaverk/api/create_api_key.php', { method: 'POST' })
    .then(r => {
      if (!r.ok || r.status === 0) throw new Error('HTTP ' + r.status + ' ' + r.statusText);
      return r.text();
    })
    .then(text => {
      if (!text.trim()) throw new Error('Empty response from server');
      return JSON.parse(text);
    })
    .then(d => {
      if (d.ok) {
        if (_vvPtReload) _vvPtReload();
      } else {
        alert('Failed: ' + (d.error ?? 'Unknown error'));
        btn.disabled = false; btn.textContent = origText;
      }
    })
    .catch(e => {
      alert('Error: ' + e);
      btn.disabled = false; btn.textContent = origText;
    });
}

function vvPtPhase2(btn, hostId) {
  if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nRequires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;
  btn.disabled = true;
  btn.textContent = '⟳ Starting…';
  _vvPtRun('Partnership/partnership_onboard.sh', '--phase2-only')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Run Phase 2 Manually'; }, 3000));
}

function vvPtOnboard(btn) {
  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…';
  _vvPtRun('Partnership/partnership_onboard.sh')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000));
}

function vvPtPushConf(btn, hostId) {
  if (!confirm(`Push conf to ${hostId}?\n\nAssumes SSH key is already installed on ${hostId}.\nSkips key generation/install, goes straight to conf push + local setup.`)) return;
  btn.disabled = true;
  btn.textContent = '⟳ Pushing…';
  _vvPtRun('Partnership/partnership_onboard.sh', '--phase1-only --skip-ssh')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Push Conf (key installed)'; }, 4000));
}

function vvPtLocalSetup(btn) {
  if (!confirm('Complete HOST1 local setup?\n\nRuns FolderView3 integration and marks HOST1 as locally ready.\nDoes not require HOST2 to be online.')) return;
  btn.disabled = true;
  btn.textContent = '⟳ Running…';
  _vvPtRun('Partnership/partnership_manager.sh', '--onboard --local-only')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Complete HOST1 Setup'; }, 4000));
}

function vvPtCancel(btn, hostId) {
  if (!confirm(`Cancel onboard for ${hostId}?\n\nThis will remove keys in both directions and reset all phase state.\n\nContinue?`)) return;
  btn.disabled = true;
  btn.textContent = '⟳ Cancelling…';
  _vvPtRun('Partnership/onboard_cancel.sh', '--direction=both')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Cancel'; }, 4000));
}

function vvPtDeleteH1(btn, hostId) {
  if (!confirm(`Remove HOST1's key from ${hostId}?\n\n• Deletes local SSH key pair\n• Removes it from ${hostId}'s authorized_keys\n• Clears phase state`)) return;
  btn.disabled = true; btn.textContent = '⟳ Removing…';
  _vvPtRun('Partnership/onboard_cancel.sh', '--direction=h1')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Remove HOST1 key'; }, 4000));
}

function vvPtDeleteH2(btn, hostId) {
  if (!confirm(`Remove ${hostId}'s key from HOST1?\n\n• Removes ${hostId}'s public key from HOST1's authorized_keys\n• ${hostId} will no longer be able to SSH into HOST1`)) return;
  btn.disabled = true; btn.textContent = '⟳ Removing…';
  _vvPtRun('Partnership/onboard_cancel.sh', '--direction=h2')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = `✕ Remove ${hostId} key`; }, 4000));
}

function vvPtOffboard(btn) {
  if (btn.style.opacity === '0.35' || btn.style.cursor === 'default') return;
  if (!confirm('Run partnership_offboard.sh?\n\nThis will end the partnership, reconfigure WebUIs, and revoke SSH access.\n\nContinue?')) return;
  btn.disabled = true;
  btn.textContent = '⟳ Starting…';
  _vvPtRun('Partnership/partnership_offboard.sh')
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000));
}

function vvPtTransfer(btn, token) {
  if (!confirm('Transfer ownership to the mirror?\n\nThis promotes the mirror to OWNER and demotes this server. '
    + 'partnership_transfer.sh requires sustained health checks before the switch completes.\n\nContinue?')) return;
  if (prompt('Type YES to confirm ownership transfer:') !== 'YES') return;
  btn.disabled = true;
  btn.textContent = '⟳ Transferring…';
  _vvPtRun('Partnership/partnership_transfer.sh', '--confirm=' + token)
    .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
    .catch(e => alert('Error: ' + e))
    .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '⇄ Transfer Ownership'; }, 4000));
}

function vvPtPing(btn, slot, idUpper) {
  const out = document.getElementById('vv-pt-ping-' + idUpper);
  btn.disabled = true;
  btn.textContent = '⟳';
  if (out) { out.textContent = ''; out.style.color = '#444'; }
  fetch('/plugins/varaverk/api/partnership_ping.php?id=' + encodeURIComponent(slot) + '&_=' + Date.now())
    .then(r => r.json())
    .then(d => {
      if (out) {
        if (d.ok) { out.textContent = '✓ ' + d.latency_ms + 'ms'; out.style.color = '#4caf50'; }
        else      { out.textContent = '✗ ' + (d.error || 'failed'); out.style.color = '#f44336'; }
      }
    })
    .catch(e => { if (out) { out.textContent = '✗ ' + e; out.style.color = '#f44336'; } })
    .finally(() => { btn.disabled = false; btn.textContent = '⇄ Test'; });
}

function vvPtToggleSync(el, varName, enabled) {
  el.style.pointerEvents = 'none';
  el.style.opacity = '0.5';
  const params = new URLSearchParams({
    csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
    name: varName,
    enabled: String(enabled)
  });
  fetch('/plugins/varaverk/api/flag_toggle.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: params
  })
    .then(r => r.json())
    .then(d => {
      if (d.ok) { if (_vvPtReload) _vvPtReload(); }
      else { alert('Failed: ' + (d.error ?? 'Unknown error')); el.style.pointerEvents = ''; el.style.opacity = ''; }
    })
    .catch(e => { alert('Error: ' + e); el.style.pointerEvents = ''; el.style.opacity = ''; });
}

// ── Host Settings ─────────────────────────────────────────────────────────────

function _renderHostSettings(nodes, isOwner) {
  let html = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;">';
  for (const n of nodes) {
    const rdonly = isOwner ? '' : 'readonly style="opacity:.5;cursor:default;"';
    html += `<div>
      <div class="vv-set-key" style="margin-bottom:4px;">${_vvSetEsc(n.id)}</div>
      <input type="text" ${rdonly}
             class="vv-set-input vv-pt-host-inp"
             data-key="${_vvSetEsc(n.id)}" data-file="master.conf" data-type="scalar"
             data-orig="${_vvSetEsc(n.hostname)}"
             value="${_vvSetEsc(n.hostname)}"
             oninput="vvPtHostChanged(this)"
             placeholder="hostname">
    </div>`;
  }
  html += '</div>';
  if (!isOwner) html += '<div style="font-size:10px;color:#333;margin-top:6px;">Editing requires host1.</div>';
  return html;
}

function vvPtHostChanged(el) {
  el.classList.toggle('changed', el.value !== el.dataset.orig);
  const any = document.querySelector('.vv-pt-host-inp.changed');
  document.getElementById('vv-pt-hosts-save').style.display = any ? '' : 'none';
}

function vvPtSaveHosts(btn) {
  const changedEls = document.querySelectorAll('.vv-pt-host-inp.changed');
  if (!changedEls.length) return;
  const changes = [];
  changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
  if (!confirm(`Save ${changes.length} host setting(s)?`)) return;
  btn.disabled = true; btn.textContent = '⟳ Saving…';
  fetch('/plugins/varaverk/api/confform.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({
      csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
      id: '__settings__',
      changes: JSON.stringify(changes)
    })
  })
    .then(r => r.json())
    .then(d => {
      btn.disabled = false;
      if (d.ok) {
        changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });
        btn.textContent = '✓ Saved';
        setTimeout(() => { btn.textContent = 'Save'; btn.style.display = 'none'; }, 2000);
      } else {
        btn.textContent = 'Save';
        alert('Save failed: ' + (d.error ?? 'Unknown error'));
      }
    })
    .catch(e => { btn.disabled = false; btn.textContent = 'Save'; alert('Error: ' + e); });
}

// ── Array settings cards ───────────────────────────────────────────────────────

let _vvArrFiles = null;

function _vvInitArrayCards() {
  if (_vvArrFiles !== null) return;
  _vvArrFiles = false; // loading sentinel
  fetch('/plugins/varaverk/api/partnership_settings.php?_=' + Date.now())
    .then(r => r.json())
    .then(d => {
      _vvArrFiles = (d.ok && d.files) ? d.files : [];
      _vvRenderArrayCards();
    })
    .catch(() => { _vvArrFiles = []; document.getElementById('vv-pt-arrays-wrap').style.display = 'none'; });
}

function _vvRenderArrayCards() {
  const files = _vvArrFiles;
  if (!files || !files.length) { document.getElementById('vv-pt-arrays-wrap').style.display = 'none'; return; }

  let html = '';
  let hasArrays = false;
  for (const f of files) {
    for (const g of f.groups) {
      for (const fld of g.fields) {
        if (fld.type === 'scalar') continue;
        hasArrays = true;
        const rows = Math.min(16, (fld.value.match(/\n/g) || []).length + 2);
        const attrs = `class="vv-set-input" data-key="${_vvSetEsc(fld.key)}" `
                    + `data-file="${_vvSetEsc(fld.file)}" data-type="${_vvSetEsc(fld.type)}" `
                    + `data-orig="${_vvSetEsc(fld.value)}" oninput="vvPtArrChanged(this)"`;
        html += `<div class="vv-set-file">
          <div class="vv-set-file-hdr" onclick="vvPtToggleNode(this)">
            <span>${_vvSetEsc(fld.key)}</span>
            <div style="display:flex;align-items:center;gap:6px;">
              ${fld.desc ? `<span style="font-size:9px;color:#444;font-weight:normal;font-family:inherit;">${_vvSetEsc(fld.desc)}</span>` : ''}
              <span class="vv-set-chev">▸</span>
            </div>
          </div>
          <div class="vv-set-file-body" style="display:none;">
            <textarea ${attrs} rows="${rows}">${_vvSetEsc(fld.value)}</textarea>
          </div>
        </div>`;
      }
    }
  }

  const wrap = document.getElementById('vv-pt-arrays-wrap');
  if (!hasArrays) { wrap.style.display = 'none'; return; }

  wrap.style.display = '';
  wrap.innerHTML = `<div class="vv-card">
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
      <h3 style="margin:0;">Array Settings</h3>
      <button id="vv-pt-arrays-save" class="vv-pt-action-btn run"
              onclick="vvPtSaveArrays(this)" style="display:none;">Save Changes</button>
    </div>
    ${html}
  </div>`;
}

function vvPtArrChanged(el) {
  el.classList.toggle('changed', el.value !== el.dataset.orig);
  const any = document.querySelector('#vv-pt-arrays-wrap .vv-set-input.changed');
  const saveBtn = document.getElementById('vv-pt-arrays-save');
  if (saveBtn) saveBtn.style.display = any ? '' : 'none';
}

function vvPtSaveArrays(btn) {
  const changedEls = document.querySelectorAll('#vv-pt-arrays-wrap .vv-set-input.changed');
  if (!changedEls.length) return;
  const changes = [];
  changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
  if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;
  btn.disabled = true; btn.textContent = '⟳ Saving…';
  fetch('/plugins/varaverk/api/confform.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: new URLSearchParams({
      csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
      id: '__settings__',
      changes: JSON.stringify(changes)
    })
  })
    .then(r => r.json())
    .then(d => {
      btn.disabled = false;
      if (d.ok) {
        changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });
        btn.textContent = '✓ Saved';
        _vvArrFiles = null; // invalidate so next init re-fetches
        setTimeout(() => _vvInitArrayCards(), 600);
      } else {
        btn.textContent = 'Save Changes';
        alert('Save failed: ' + (d.error ?? 'Unknown error'));
      }
    })
    .catch(e => { btn.disabled = false; btn.textContent = 'Save Changes'; alert('Error: ' + e); });
}

// ── Settings panel ─────────────────────────────────────────────────────────────
let _vvSetLoaded = false;

function _vvSetEsc(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function vvPtToggleSettings() {
  const body   = document.getElementById('vv-pt-settings-body');
  const toggle = document.getElementById('vv-pt-settings-toggle');
  const open   = body.style.display !== 'none';
  body.style.display   = open ? 'none' : '';
  toggle.textContent   = open ? '▸ Show settings' : '▾ Hide settings';
  if (!open && !_vvSetLoaded) vvPtLoadSettings();
}

function vvPtLoadSettings() {
  const body = document.getElementById('vv-pt-settings-body');
  body.innerHTML = '<div style="color:#555;font-size:12px;padding:8px 0;">Loading…</div>';
  fetch('/plugins/varaverk/api/partnership_settings.php?_=' + Date.now())
    .then(r => r.json())
    .then(d => {
      if (!d.ok || !(d.files || []).length) {
        body.innerHTML = '<div style="color:#555;font-size:12px;padding:8px 0;">No settings available.</div>';
        return;
      }
      // Host-aware hint: master.conf edits propagate; host config stays local.
      const hasMaster = d.files.some(f => f.file === 'master.conf');
      document.getElementById('vv-pt-settings-hint').textContent = hasMaster
        ? 'Shared (master.conf) edits propagate to partners on save · host config stays local to this server.'
        : 'These settings apply to this host only.';
      body.innerHTML = vvPtRenderSettings(d.files);
      _vvSetLoaded = true;
    })
    .catch(e => { body.innerHTML = '<div style="color:#f88;font-size:12px;padding:8px 0;">Error: ' + e + '</div>'; });
}

function vvPtRenderSettings(files) {
  let html = '';
  for (const f of files) {
    const label = f.file === 'master.conf' ? 'master.conf — shared' : f.file + ' — this host';
    const scalarFields = f.groups.flatMap(g => g.fields.filter(fld => fld.type === 'scalar'));
    if (!scalarFields.length) continue;
    html += `<div class="vv-set-file">
      <div class="vv-set-file-hdr" onclick="vvPtToggleNode(this)">
        <span>${_vvSetEsc(label)}</span><span class="vv-set-chev">▾</span>
      </div>
      <div class="vv-set-file-body">`;
    for (const fld of scalarFields) {
      const attrs = `class="vv-set-input" data-key="${_vvSetEsc(fld.key)}" `
                  + `data-file="${_vvSetEsc(fld.file)}" data-type="${_vvSetEsc(fld.type)}" `
                  + `data-orig="${_vvSetEsc(fld.value)}" oninput="vvPtSetChanged(this)"`;
      html += `<div class="vv-set-field">
        <div class="vv-set-key">${_vvSetEsc(fld.key)}</div>`;
      if (fld.desc) html += `<div class="vv-set-desc">${_vvSetEsc(fld.desc)}</div>`;
      html += `<input type="text" ${attrs} value="${_vvSetEsc(fld.value)}"></div>`;
    }
    html += `</div></div>`;
  }
  return html || '<div style="color:#555;font-size:12px;padding:8px 0;">No scalar settings available.</div>';
}

function vvPtToggleNode(hdr) {
  const body = hdr.nextElementSibling;
  const chev = hdr.querySelector('.vv-set-chev');
  const open = body.style.display !== 'none';
  body.style.display = open ? 'none' : '';
  if (chev) chev.textContent = open ? '▸' : '▾';
}

function vvPtSetChanged(el) {
  const changed = el.value !== el.dataset.orig;
  el.classList.toggle('changed', changed);
  // Show Save button if any field is changed
  const any = document.querySelector('#vv-pt-settings-body .vv-set-input.changed');
  document.getElementById('vv-pt-settings-save').style.display = any ? '' : 'none';
}

function vvPtSaveSettings(btn) {
  const changedEls = document.querySelectorAll('#vv-pt-settings-body .vv-set-input.changed');
  if (!changedEls.length) return;
  const changes = [];
  changedEls.forEach(el => changes.push({
    key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value
  }));
  if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;
  btn.disabled = true; btn.textContent = '⟳ Saving…';
  const params = new URLSearchParams({
    csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
    id: '__settings__',
    changes: JSON.stringify(changes)
  });
  fetch('/plugins/varaverk/api/confform.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: params
  })
    .then(r => r.json())
    .then(d => {
      btn.disabled = false;
      if (d.ok) {
        // Commit new originals, clear highlights
        changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });
        btn.textContent = '✓ Saved';
        setTimeout(() => { btn.textContent = 'Save Changes'; btn.style.display = 'none'; }, 2000);
      } else {
        btn.textContent = 'Save Changes';
        alert('Save failed: ' + (d.error ?? 'Unknown error'));
      }
    })
    .catch(e => { btn.disabled = false; btn.textContent = 'Save Changes'; alert('Error: ' + e); });
}

// ── Private page logic ─────────────────────────────────────────────────────────
(function() {

// ── Helpers ───────────────────────────────────────────────────────────────────

function _row(lbl, val) {
  return `<div class="vv-pt-row"><span class="vv-pt-lbl">${lbl}</span><span class="vv-pt-val">${val}</span></div>`;
}

// ── Offline countdown warning ───────────────────────────────────────────────────

function _renderOfflineWarn(cfg) {
  const el   = document.getElementById('vv-pt-offline-warn');
  const days = cfg.offline_days;
  const thr  = cfg.offline_threshold || 30;
  // Auto-offboard only runs for active partnerships — a stale counter while disabled is meaningless.
  if (!cfg.enabled || !days || days < 1) { el.style.display = 'none'; el.innerHTML = ''; return; }

  const left = Math.max(0, thr - days);
  const crit = left <= 5;
  const col  = crit ? '#f44336' : '#ff9800';
  const bg   = crit ? '#2a1212' : '#1a1200';
  const pct  = Math.min(100, Math.round(days / thr * 100));
  el.style.display = '';
  el.innerHTML = `<div class="vv-card" style="background:${bg};border-color:${col};">
    <div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;">
      <span style="font-size:12px;color:${col};font-weight:600;">⚠ Partner unreachable ${days} day${days===1?'':'s'}</span>
      <span style="font-size:11px;color:#888;">Auto-offboard in ${left} day${left===1?'':'s'} (threshold ${thr}d)</span>
    </div>
    <div style="margin-top:6px;height:5px;background:#000;border-radius:3px;overflow:hidden;">
      <div style="width:${pct}%;height:100%;background:${col};"></div>
    </div>
  </div>`;
}

// ── Mirror sync health ──────────────────────────────────────────────────────────

function _syncToggle(gate, dimmed) {
  if (!gate) return '';
  const on  = gate.on;
  const col = on ? '#4caf50' : '#f44336';
  const bg  = on ? '#0a1a0a' : '#2a1212';
  const op  = dimmed ? 'opacity:.4;' : '';
  return `<span onclick="vvPtToggleSync(this,'${gate.var}',${on ? 0 : 1})"
    style="cursor:pointer;font-size:9px;font-weight:600;padding:2px 9px;border-radius:10px;
           background:${bg};color:${col};border:1px solid ${col};white-space:nowrap;margin-left:8px;${op}"
    title="${gate.var} — click to ${on ? 'disable' : 'enable'}">${on ? 'ON' : 'OFF'}</span>`;
}

function _renderSync(sync) {
  const jobs        = sync.jobs  || {};
  const gates       = sync.gates || {};
  const intervalMin = sync.interval_min || 30;

  const sCol = s => s === 'ok' ? '#4caf50' : s === 'running' ? '#4a9eff'
             : s === 'warn' ? '#ff9800' : s === 'error' ? '#f44336' : '#555';
  const sLbl = s => s === 'ok' ? '✓' : s === 'running' ? '⟳' : s === 'warn' ? '⚠' : s === 'error' ? '✗' : '—';

  function _dur(sec) {
    if (!sec || sec < 0) return '';
    if (sec < 60)   return sec + 's';
    if (sec < 3600) return Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
    return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';
  }

  function _countdown(ts) {
    if (!ts) return null;
    const sec = ts - Math.floor(Date.now() / 1000);
    if (sec <= 0) return 'due';
    if (sec < 60) return 'in ' + sec + 's';
    if (sec < 3600) return 'in ' + Math.floor(sec / 60) + 'm';
    return 'in ' + Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';
  }

  const globalOff = gates.global && !gates.global.on;

  // Wrap everything in a flex-column so the bottom section anchors naturally
  let html = '<div style="display:flex;flex-direction:column;gap:0;height:100%;">';

  // Master toggle row
  if (gates.global) {
    html += `<div class="vv-pt-row" style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #1c1c1c;">
      <span class="vv-pt-lbl" style="font-weight:600;color:#999;">All mirroring</span>
      <span class="vv-pt-val">${_syncToggle(gates.global, false)}</span>
    </div>`;
  }

  // Per-tier rows
  const tierMeta = {
    critical: {label: 'Critical',  sched: 'every 30 min', desc: 'appdata · partnership · play state'},
    daily:    {label: 'Daily',     sched: 'nightly',       desc: 'media shares · arr sync · docker'},
    weekly:   {label: 'Weekly',    sched: 'weekly',        desc: 'bulk shares · updates · cleanup'},
  };

  // Critical next-run estimate
  const critJob = jobs.critical;
  const critNextTs = critJob && critJob.end && critJob.status !== 'running'
    ? critJob.end + intervalMin * 60 : null;

  html += '<div style="display:flex;flex-direction:column;gap:10px;flex:1;">';

  for (const key of ['critical', 'daily', 'weekly']) {
    const j      = jobs[key];
    const meta   = tierMeta[key];
    const status = j ? j.status : null;
    const when   = j && j.start
      ? (j.status === 'running' ? 'started ' + _relTime(j.start) : _relTime(j.end || j.start))
      : null;
    const dur     = j && j.start && j.end && j.status !== 'running' ? _dur(j.end - j.start) : '';
    const summary = j && j.summary ? j.summary : '';

    html += `<div style="background:#0f0f0f;border:1px solid #1e1e1e;border-radius:4px;padding:7px 9px;">
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
        <span style="font-size:11px;font-weight:500;color:#aaa;">${meta.label}
          <span style="font-size:9px;color:#333;font-weight:normal;margin-left:3px;">${meta.sched}</span>
        </span>
        <span style="display:flex;align-items:center;gap:5px;">
          <span style="font-size:11px;color:${sCol(status)};">${sLbl(status)}</span>
          ${dur ? `<span style="font-size:9px;color:#3a3a3a;">${dur}</span>` : ''}
          ${when ? `<span style="font-size:9px;color:#2e2e2e;">${when}</span>` : ''}
          ${_syncToggle(gates[key], globalOff)}
        </span>
      </div>
      <div style="font-size:9px;color:#2a2a2a;">${meta.desc}</div>
      ${summary ? `<div style="font-size:9px;color:#3a5a3a;margin-top:3px;font-family:monospace;">${_vvSetEsc(summary)}</div>` : ''}
    </div>`;
  }

  html += '</div>'; // flex:1

  // Bottom info strip
  const nextLabel = critNextTs ? _countdown(critNextTs) : null;
  html += `<div style="margin-top:10px;padding-top:8px;border-top:1px solid #1a1a1a;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;">
    <span style="font-size:10px;color:#2a2a2a;">interval: ${intervalMin}min</span>
    ${nextLabel ? `<span style="font-size:10px;color:#2e3e2e;">next critical ${nextLabel}</span>` : ''}
  </div>`;

  if (globalOff) {
    html += `<div style="font-size:10px;color:#f44336;margin-top:6px;">⚠ All mirroring is off — per-tier switches have no effect until re-enabled.</div>`;
  }

  html += '</div>'; // outer flex column
  return html;
}

// ── Config bar ────────────────────────────────────────────────────────────────

function _renderConfig(cfg) {
  const dot   = cfg.enabled ? '#4caf50' : '#555';
  const label = cfg.enabled
    ? `<span style="color:#4caf50;">enabled</span>`
    : `<span style="color:#555;">disabled</span>`;
  const owner = cfg.owner_host || '—';
  const items = [
    label,
    `Owner: <span style="color:#ccc;">${owner}</span>`,
    `Sync: ${cfg.sync_min}min`,
    `Grace: ${cfg.grace_hours}h`,
    `Offline threshold: ${cfg.offline_threshold}d`,
    cfg.remove_tailscale ? 'Tailscale removal: on' : 'Tailscale removal: off',
    cfg.tailscale_configured ? '' : '<span style="color:#ff9800;">⚠ Tailscale API key not set</span>',
  ].filter(Boolean);

  return `<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
    <span class="vv-pt-dot" style="background:${dot}"></span>
    ${items.map(i => `<span style="font-size:11px;color:#666;">${i}</span>`)
           .join('<span style="color:#333;">·</span>')}
  </div>`;
}

// ── Node card ─────────────────────────────────────────────────────────────────

const fbColors = {
  NORMAL:      '#4caf50',
  FAILOVER:    '#f44336',
  NO_INTERNET: '#ff9800',
  DARK:        '#9e9e9e',
  UNKNOWN:     '#444',
};
const fbLabels = {
  NORMAL:      '✓ Nominal',
  FAILOVER:    '⚠ Failover',
  NO_INTERNET: '⚡ No internet',
  DARK:        '◌ Dark mode',
  UNKNOWN:     '— Unknown',
};
const ptStates = {
  ACTIVE:   ['#4caf50', 'Active'],
  INACTIVE: ['#555',    'Inactive'],
  PENDING:  ['#ff9800', 'Pending'],
};

function _nodeCard(node) {
  const tsOnline = node.ts_online;
  const dotCol   = tsOnline === null ? '#555' : tsOnline ? '#4caf50' : '#f44336';
  const dotTip   = tsOnline === null ? 'unknown' : tsOnline ? (node.ts_active ? 'active' : 'idle') : 'offline';

  const tags = [
    node.is_me    ? '<span class="vv-pt-tag me">US</span>'     : '',
    node.is_owner ? '<span class="vv-pt-tag owner">OWNER</span>' : '<span class="vv-pt-tag mirror">MIRROR</span>',
  ].join('');

  // System info
  const sys    = node.system || {};
  const ver    = sys.unraid_version || '—';
  const uptime = sys.uptime_sec ? _uptime(sys.uptime_sec) : '—';

  // Fallback
  const fb     = (node.fallback || 'UNKNOWN').toUpperCase();
  const fbCol  = fbColors[fb] || '#444';
  const fbLbl  = fbLabels[fb] || fb;

  // Partnership DB
  const pt      = node.partnership || {};
  const ptState = (pt.state || '').toUpperCase();
  const [ptCol, ptLbl] = ptStates[ptState] || ['#444', ptState || '—'];
  const ptUpdated = pt.updated ? _relTime(parseInt(pt.updated)) : null;

  // Onboard phase badge (remote nodes) or local-done badge (self)
  const phase = node.onboard_phase;
  const phaseHtml = node.is_me
    ? (node.local_done
        ? `<div style="font-size:10px;color:#4caf50;margin-bottom:6px;padding:2px 6px;background:#0a1a0a;border:1px solid #2a5a2a;border-radius:3px;">✓ Local setup complete · waiting for partner</div>`
        : '')
    : (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 onboard</div>` :
       node.key_ready ? `<div style="font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;">🔑 Key generated · install on HOST2 then Push Conf</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>`);

  const apiKeyHtml = node.is_me
    ? (node.api_key_set
        ? `<div style="font-size:10px;color:#2a4a2a;margin-bottom:6px;padding:2px 6px;background:#0a1a0a;
                       border:1px solid #1a3a1a;border-radius:3px;display:flex;justify-content:space-between;align-items:center;">
             <span style="color:#3a7a3a;">🔑 API key: <span style="color:#4caf50;">${node.api_key_preview}</span></span>
             <button onclick="vvApiKey(this)"
               style="font-size:9px;padding:1px 6px;background:#0d0d0d;color:#444;border:1px solid #222;
                      border-radius:2px;cursor:pointer;margin-left:6px;white-space:nowrap;">↻ renew</button>
           </div>`
        : `<div style="font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;
                       border:1px solid #3a2800;border-radius:3px;display:flex;justify-content:space-between;align-items:center;">
             <span>⚡ No API key — Monitor stats unavailable</span>
             <button onclick="vvApiKey(this)"
               style="font-size:9px;padding:1px 8px;background:#1a3a1a;color:#6fcf97;border:1px solid #2e6b2e;
                      border-radius:2px;cursor:pointer;margin-left:6px;white-space:nowrap;">+ Create</button>
           </div>`)
    : '';

  // ── Metrics strip ────────────────────────────────────────────────────────────
  const m = node.metrics || {};
  function _ptCol(v, warn, crit) {
    return v >= crit ? '#f44336' : v >= warn ? '#ff9800' : '#4caf50';
  }
  const metricPairs = [];
  if (m.cpu_pct     != null) metricPairs.push(['CPU',   `<span style="color:${_ptCol(m.cpu_pct,70,90)};">${m.cpu_pct}%</span>`]);
  if (m.ram_used_gb != null) {
    const ramPct = m.ram_total_gb ? Math.round(m.ram_used_gb / m.ram_total_gb * 100) : 0;
    metricPairs.push(['RAM', `<span style="color:${_ptCol(ramPct,75,90)};">${m.ram_used_gb}<span style="color:#444;font-size:9px;">/${m.ram_total_gb}GB</span></span>`]);
  }
  if (m.load_avg    != null) metricPairs.push(['Load',  `<span style="color:${m.load_avg>8?'#f44336':m.load_avg>4?'#ff9800':'#4caf50'};">${m.load_avg}</span>`]);
  if (m.containers  != null) metricPairs.push(['Conts', `<span style="color:#bbb;">${m.containers}</span>`]);
  if (m.array_used_tb != null) metricPairs.push(['Array', `<span style="color:#bbb;">${m.array_used_tb}<span style="color:#444;font-size:9px;">/${m.array_total_tb}TB</span></span>`]);
  if (m.max_disk_temp != null) metricPairs.push(['Temp', `<span style="color:${_ptCol(m.max_disk_temp,55,70)};">${m.max_disk_temp}°C</span>`]);
  if (m.vm_count      != null && m.vm_count > 0) metricPairs.push(['VMs', `<span style="color:#bbb;">${m.vm_count}</span>`]);

  let metricsHtml = '';
  if (metricPairs.length) {
    let cells = '';
    for (const [lbl, val] of metricPairs) {
      cells += `<span style="color:#555;">${lbl}</span>${val}`;
    }
    if (metricPairs.length % 2 !== 0) cells += '<span></span><span></span>';
    metricsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-bottom:6px;">${cells}</div>`;
  }

  let body = '';

  body += phaseHtml;
  body += apiKeyHtml;
  body += metricsHtml;

  // Network
  body += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
    <span class="vv-pt-dot" style="background:${dotCol}"></span>
    <span style="font-size:11px;color:${dotCol};">${dotTip}</span>`;
  if (node.ts_ip) body += `<span style="font-size:10px;color:#444;margin-left:4px;">${node.ts_ip}</span>`;
  body += `</div>`;

  // System
  body += _row('unRAID', ver);
  body += _row('Uptime', uptime);

  body += `<hr class="vv-pt-sep">`;

  // States
  body += `<div class="vv-pt-row">
    <span class="vv-pt-lbl">Fallback</span>
    <span class="vv-pt-state" style="color:${fbCol};">${fbLbl}</span>
  </div>`;
  body += `<div class="vv-pt-row">
    <span class="vv-pt-lbl">Partnership</span>
    <span class="vv-pt-state" style="color:${ptCol};">${ptLbl}${ptUpdated ? `<span style="font-size:9px;color:#444;margin-left:4px;">${ptUpdated}</span>` : ''}</span>
  </div>`;

  if (pt.reason) {
    body += `<div style="font-size:10px;color:#555;margin-top:3px;text-align:right;">${pt.reason}</div>`;
  }

  return `<div class="vv-pt-node${node.is_me ? ' me' : ''}">
    <div class="vv-pt-node-head">
      <div>
        <div class="vv-pt-hostname">${node.hostname}</div>
        <div class="vv-pt-slot">${node.id}</div>
      </div>
      <div class="vv-pt-tags">${tags}</div>
    </div>
    ${body}
  </div>`;
}

// ── Actions ───────────────────────────────────────────────────────────────────

function _renderActions(nodes, cfg) {
  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   = `https://${window.location.hostname}/webterminal/ttyd/`;
  const termCmd    = `bash <?= SCRIPTS_DIR ?>/Partnership/partnership_onboard.sh --phase1-only`;

  let html = '';

  // ── No partner ─────────────────────────────────────────────────────────────
  if (!hasPartner && isOwner) {
    html += `<div style="background:#0f0f0f;border:1px solid #1e1e1e;border-radius:4px;padding:14px 16px;
                         display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;">
      <div style="font-size:11px;color:#555;">No partner host configured</div>
      <div style="font-size:10px;color:#333;">Set HOST2 in master.conf (Host Settings above) to enable partnership.</div>
    </div>`;
  }

  // ── Per-remote host section ────────────────────────────────────────────────
  if (isOwner && hasPartner) {
    remotes.forEach((remote, idx) => {
      const phase        = remote.onboard_phase ?? 0;
      const isOnboarding = !!_vvOnboarding[remote.id];
      const isDeleting   = !!_vvDeleteKeys[remote.id];
      const dotCol       = remote.ts_online === null ? '#444' : 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;

      // Left border colour by phase
      const borderCol = phase === 2 ? '#2a5a2a'
                      : phase === 1 ? '#4a3000'
                      : isOnboarding ? '#0e2a4a' : '#222';

      if (idx > 0) html += '<div style="height:10px;"></div>';

      html += `<div style="background:#0d0d0d;border:1px solid #1e1e1e;border-left:3px solid ${borderCol};
                            border-radius:4px;overflow:hidden;">`;

      // ── Card header ─────────────────────────────────────────────────────────
      html += `<div style="display:flex;justify-content:space-between;align-items:center;
                            padding:8px 10px;background:#111;border-bottom:1px solid #1a1a1a;">
        <div style="display:flex;align-items:center;gap:8px;">
          <span style="font-size:9px;color:#444;font-family:monospace;">${remote.id}</span>
          <span style="font-size:12px;color:#ccc;font-weight:600;">${remote.hostname}</span>
          <span style="font-size:9px;color:${dotCol};">● ${dotLbl}</span>
        </div>
        <div style="display:flex;align-items:center;gap:6px;">
          <span id="vv-pt-ping-${remote.id}" style="font-size:9px;color:#444;"></span>
          <button class="vv-pt-action-btn info" onclick="vvPtPing(this,'${remote.slot}','${remote.id}')"
            style="font-size:9px;padding:2px 7px;" title="SSH echo round-trip">⇄ ping</button>
        </div>
      </div>`;

      html += `<div style="padding:10px 12px;">`;

      // ── Phase 0: not provisioned ───────────────────────────────────────────
      if (phase === 0) {
        if (isOnboarding) {
          html += `<div style="display:flex;flex-direction:column;gap:10px;">
            <div>
              <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
                <span style="font-size:9px;font-weight:700;color:#4a9eff;background:#0a1828;
                             padding:2px 8px;border-radius:10px;border:1px solid #1a3a5a;">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;">
                <a href="${termBase}" target="_blank" class="localURL"
                  style="padding:3px 10px;background:#0e1a2a;color:#7ab;border:1px solid #1e3a5a;
                         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='#444',1500)})"
                  style="font-size:9px;color:#444;background:#080808;padding:3px 8px;border-radius:3px;
                         border:1px solid #181818;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;">Enter ${remote.hostname} root password when prompted · tab auto-updates on completion</div>
            </div>
            <div style="border-top:1px solid #1a1a1a;padding-top:10px;">
              <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
                <span style="font-size:9px;font-weight:700;color:#4caf50;background:#0a1a0a;
                             padding:2px 8px;border-radius:10px;border:1px solid #1a3a1a;">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" style="font-size:11px;">▶ Push Conf</button>
            </div>
          </div>
          <div style="margin-top:10px;padding-top:8px;border-top:1px solid #181818;">
            <button class="vv-pt-action-btn warn" onclick="vvPtEndOnboard('${remote.id}')"
              style="opacity:.5;font-size:11px;">✕ Close</button>
          </div>`;
        } else {
          html += `<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">
            <div>
              <div style="font-size:11px;color:#555;margin-bottom:2px;">Not provisioned</div>
              <div style="font-size:9px;color:#2a2a2a;">SSH keys not installed · conf not pushed</div>
            </div>
            <div style="display:flex;gap:6px;flex-wrap:wrap;">
              <button class="vv-pt-action-btn run" onclick="vvPtStartOnboard('${remote.id}')"
                style="font-size:11px;">▶ Onboard</button>
              <button class="vv-pt-action-btn warn" onclick="vvPtShowDeleteKeys('${remote.id}')"
                style="opacity:.45;font-size:11px;">🗑 Keys</button>
            </div>
          </div>`;
        }

      // ── Phase 1: SSH done, awaiting HOST2 ─────────────────────────────────
      } else if (phase === 1) {
        html += `<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
          <div>
            <div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
              <span style="font-size:10px;font-weight:600;color:#ff9800;background:#1a1200;
                           padding:2px 9px;border-radius:10px;border:1px solid #3a2800;">SSH ready</span>
              ${remote.ts_ip ? `<span style="font-size:9px;color:#333;font-family:monospace;">${remote.ts_ip}</span>` : ''}
            </div>
            <div style="font-size:9px;color:#383838;line-height:1.5;">
              Waiting for ${remote.id} to install Varaverk and complete onboard.<br>Phase 2 triggers automatically.
            </div>
          </div>
          <div style="display:flex;flex-direction:column;gap:5px;align-items:flex-end;">
            <button class="vv-pt-action-btn info" onclick="vvPtPhase2(this,'${remote.id}')"
              style="font-size:11px;" title="Manually trigger Phase 2">▶ Phase 2</button>
            <button class="vv-pt-action-btn warn" onclick="vvPtCancel(this,'${remote.id}')"
              style="font-size:11px;">✕ Cancel</button>
            <button class="vv-pt-action-btn warn" onclick="vvPtShowDeleteKeys('${remote.id}')"
              style="opacity:.45;font-size:10px;">🗑 Keys</button>
          </div>
        </div>`;

      // ── Phase 2: fully onboarded ───────────────────────────────────────────
      } else {
        const ptColor = ptState === 'ACTIVE' ? '#4caf50' : ptState === 'INACTIVE' ? '#f44336' : '#555';
        html += `<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap;">
          <div>
            <div style="display:flex;align-items:center;gap:6px;margin-bottom:5px;">
              <span style="font-size:10px;font-weight:600;color:#4caf50;background:#0a1a0a;
                           padding:2px 9px;border-radius:10px;border:1px solid #1a4a1a;">Active</span>
              ${ptState ? `<span style="font-size:9px;color:${ptColor};font-weight:500;">${ptState}</span>` : ''}
              ${ptDate ? `<span style="font-size:9px;color:#2e2e2e;">since ${ptDate}</span>` : ''}
            </div>
            <div style="display:flex;flex-wrap:wrap;gap:10px;font-size:9px;color:#333;font-family:monospace;">
              ${remote.ts_ip ? `<span>${remote.ts_ip}</span>` : ''}
              ${sysVer      ? `<span>unRAID ${sysVer}</span>` : ''}
              ${sysUptime   ? `<span>up ${sysUptime}</span>` : ''}
            </div>
          </div>
          <div style="display:flex;flex-direction:column;gap:5px;align-items:flex-end;">
            <button class="vv-pt-action-btn info" onclick="vvPtPhase2(this,'${remote.id}')"
              style="font-size:10px;" title="Re-run Phase 2">↻ Re-run</button>
            <button class="vv-pt-action-btn warn" onclick="vvPtShowDeleteKeys('${remote.id}')"
              style="opacity:.4;font-size:10px;">🗑 Keys</button>
          </div>
        </div>`;
      }

      // ── Delete Keys panel ──────────────────────────────────────────────────
      if (isDeleting) {
        html += `<div style="margin-top:10px;padding:10px;background:#0a0a0a;border:1px solid #2a1515;
                              border-radius:3px;">
          <div style="font-size:9px;color:#5a2a2a;font-weight:700;text-transform:uppercase;
                      letter-spacing:.06em;margin-bottom:8px;">Remove SSH access</div>
          <div style="display:flex;flex-direction:column;gap:7px;">
            <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
              <div>
                <div style="font-size:10px;color:#888;">HOST1 → ${remote.id}</div>
                <div style="font-size:9px;color:#333;">HOST1's key on ${remote.hostname} · deletes local pair</div>
              </div>
              <button class="vv-pt-action-btn warn" onclick="vvPtDeleteH1(this,'${remote.id}')"
                style="white-space:nowrap;font-size:10px;">✕ HOST1 key</button>
            </div>
            <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
              <div>
                <div style="font-size:10px;color:#888;">${remote.id} → HOST1</div>
                <div style="font-size:9px;color:#333;">${remote.hostname}'s key from authorized_keys</div>
              </div>
              <button class="vv-pt-action-btn warn" onclick="vvPtDeleteH2(this,'${remote.id}')"
                style="white-space:nowrap;font-size:10px;">✕ ${remote.id} key</button>
            </div>
          </div>
          <div style="margin-top:8px;">
            <button class="vv-pt-action-btn info" onclick="vvPtHideDeleteKeys('${remote.id}')"
              style="font-size:10px;opacity:.6;">✓ Done</button>
          </div>
        </div>`;
      }

      html += `</div></div>`; // padding + card
    });
  }

  // ── HOST1 local setup ──────────────────────────────────────────────────────
  if (isOwner && selfNode && !selfNode.local_done && remotes.some(r => (r.onboard_phase ?? 0) >= 1)) {
    html += `<div style="margin-top:10px;padding:10px 12px;background:#0d0d0d;
                          border:1px solid #1e1e1e;border-left:3px solid #0e2a4a;border-radius:4px;
                          display:flex;align-items:center;justify-content:space-between;gap:10px;">
      <div>
        <div style="font-size:11px;color:#888;margin-bottom:2px;">${selfNode.id} — ${selfNode.hostname}</div>
        <div style="font-size:9px;color:#333;">FolderView3 integration · marks HOST1 ready</div>
      </div>
      <button class="vv-pt-action-btn run" onclick="vvPtLocalSetup(this)"
        style="font-size:11px;white-space:nowrap;"
        title="Does not require HOST2 to be online">▶ Complete Setup</button>
    </div>`;
  }

  // ── Mirror onboard (non-owner) ─────────────────────────────────────────────
  if (!isOwner) {
    html += `<div style="padding:10px 12px;background:#0d0d0d;border:1px solid #1e1e1e;border-radius:4px;">
      <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;">
        <div>
          <div style="font-size:11px;color:#888;margin-bottom:2px;">Join partnership</div>
          <div style="font-size:9px;color:#333;">SSH key setup + notify owner</div>
        </div>
        <button class="vv-pt-action-btn run" onclick="vvPtOnboard(this)"
          ${!hasPartner ? 'disabled style="opacity:.35;cursor:default;"' : ''}
          style="font-size:11px;white-space:nowrap;">▶ Onboard</button>
      </div>
    </div>`;
  }

  // ── Offboard + Transfer ────────────────────────────────────────────────────
  html += `<div style="margin-top:12px;padding-top:10px;border-top:1px solid #181818;">`;

  html += `<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;">
    <div>
      <div style="font-size:11px;color:#666;">Offboard</div>
      <div style="font-size:9px;color:#2a2a2a;">End partnership · revoke SSH · restore WebUIs</div>
    </div>
    <button class="vv-pt-action-btn warn" onclick="vvPtOffboard(this)"
      style="font-size:11px;white-space:nowrap;"
      ${!cfg.enabled ? 'style="opacity:.35;cursor:default;" title="No active partnership"' : ''}>
      ▶ Offboard
    </button>
  </div>`;

  if (cfg.enabled && isOwner) {
    const tok = (cfg.transfer_confirm || 'i-understand-this-transfers-ownership').replace(/'/g, "\\'");
    html += `<div style="margin-top:10px;padding-top:10px;border-top:1px dashed #181818;
                          display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;">
      <div>
        <div style="font-size:11px;color:#666;">Transfer ownership</div>
        <div style="font-size:9px;color:#2a2a2a;">Promotes mirror to owner · requires sustained health checks</div>
      </div>
      <button class="vv-pt-action-btn warn" onclick="vvPtTransfer(this, '${tok}')"
        style="font-size:11px;white-space:nowrap;"
        title="Runs partnership_transfer.sh — requires sustained health checks before switching">
        ⇄ Transfer
      </button>
    </div>
    <div style="margin-top:6px;font-size:9px;color:#252525;font-family:monospace;padding:4px 8px;
                background:#080808;border-radius:3px;">
      bash Partnership/partnership_transfer.sh --confirm=${cfg.transfer_confirm || 'i-understand-this-transfers-ownership'}
    </div>`;
  }

  html += '</div>'; // offboard+transfer section

  return html;
}

// ── Main render ───────────────────────────────────────────────────────────────

function _render(data) {
  const cfg     = data.config || {};
  const nodes   = data.nodes  || [];
  const isOwner = nodes.some(n => n.is_me && n.is_owner);

  // Config bar
  document.getElementById('vv-pt-config-body').innerHTML = _renderConfig(cfg);

  // Host settings — always shown upfront
  const hostsCard = document.getElementById('vv-pt-hosts-card');
  hostsCard.style.display = nodes.length ? '' : 'none';
  document.getElementById('vv-pt-hosts-body').innerHTML = _renderHostSettings(nodes, isOwner);
  document.getElementById('vv-pt-hosts-save').style.display = 'none';

  // Offline countdown warning
  _renderOfflineWarn(cfg);

  // Mirror sync health
  document.getElementById('vv-pt-sync-body').innerHTML = _renderSync(data.sync || {});

  // Node grid — columns based on count
  const cols = nodes.length <= 2 ? nodes.length : nodes.length <= 4 ? 2 : 3;
  const grid = document.getElementById('vv-pt-nodes');
  grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
  grid.innerHTML = nodes.length
    ? nodes.map(_nodeCard).join('')
    : '<div style="color:#444;font-size:12px;padding:12px 0;">No hosts found in master.conf.</div>';

  // Actions
  document.getElementById('vv-pt-actions-body').innerHTML = _renderActions(nodes, cfg);

  // Array settings cards above Actions — load once
  _vvInitArrayCards();

  // Settings panel — host1 only
  document.getElementById('vv-pt-settings-card').style.display = isOwner ? '' : 'none';

  // Highlight Onboard button when arriving from first-run wizard
  if (new URLSearchParams(location.search).get('vv_onboard') === '1') {
    const onboardBtn = document.querySelector('#vv-pt-actions-body .vv-pt-action-btn.run');
    if (onboardBtn) {
      onboardBtn.classList.add('vv-pt-onboard-highlight');
      setTimeout(() => onboardBtn.scrollIntoView({behavior: 'smooth', block: 'center'}), 200);
    }
  }

  // Timestamp
  const ts = data.ts ? new Date(data.ts * 1000).toLocaleString([],
    {month:'numeric',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
  document.getElementById('vv-pt-ts').textContent = ts ? 'Updated: ' + ts : '';
}

// ── Poll ──────────────────────────────────────────────────────────────────────

function vvPtLoad() {
  fetch('/plugins/varaverk/api/partnership.php')
    .then(r => r.json())
    .then(_render)
    .catch(() => {});
}

_vvPtReload = vvPtLoad; // expose to top-level toggle fns

vvPtLoad();
setInterval(vvPtLoad, 10000);

})();
</script>
