diff --git a/Plugin/unraid/api/partnership_lists.php b/Plugin/unraid/api/partnership_lists.php index aa3ecd5..2e2fc03 100644 --- a/Plugin/unraid/api/partnership_lists.php +++ b/Plugin/unraid/api/partnership_lists.php @@ -109,11 +109,31 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { // ── Write ──────────────────────────────────────────────────────────────────────────────────── $action = $_POST['action'] ?? ''; +// Containers this host runs, PLUS whatever the lists already name. The conf legitimately holds +// entries for containers that are not here right now — one removed since it was added, one +// living on a partner, one simply stopped. Validating only against the running set would refuse +// to save a list the operator had not touched: FALLBACK_HOST1_TIER4 still carries "LidaTube", +// which no longer exists here, so opening the card and pressing Save would fail on a line +// nobody had edited. +// +// New names still have to be real. The guard is against inventing containers, not against +// keeping ones already recorded. $known = []; foreach (vv_docker_containers() as $c) { $n = is_array($c) ? ($c['name'] ?? '') : (string)$c; if ($n !== '') $known[strtolower($n)] = $n; } +$existingRaw = vv_read_conf_raw($myConf); +foreach ($TIERS as $t) { + foreach (vv_parse_conf_list($existingRaw, $tierVar($t)) as $n) { + $n = trim($n); + if ($n !== '' && !isset($known[strtolower($n)])) $known[strtolower($n)] = $n; + } +} +foreach (vv_parse_conf_list($existingRaw, $svcVar) as $x) { + $n = $fromXml($x); + if ($n !== '' && !isset($known[strtolower($n)])) $known[strtolower($n)] = $n; +} // Rewrites one `NAME=(` … `)` block in place, preserving the leading indent the conf uses. $rewrite = function (string $cur, string $var, array $items): ?string { diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index 92b916d..a0c4895 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -778,6 +778,157 @@ function _renderXfer(x) { `; } +// ── Failover coverage + shared services ─────────────────────────────────────── +// Both cards read one endpoint and edit real conf arrays: the tiers fallback.sh consults during +// an outage, and the services stack onboard pushes. Loaded once — they are conf, not telemetry, +// and re-fetching them on the 10s poll would fight whatever the operator is mid-way through +// selecting. +let _vvLists = null, _vvListsLoaded = false; + +function vvPtLoadLists() { + if (_vvListsLoaded) return; + _vvListsLoaded = true; + fetch('/plugins/varaverk/api/partnership_lists.php?_=' + Date.now()) + .then(r => r.json()) + .then(d => { if (d.ok) { _vvLists = d; _renderCover(); _renderSvc(); } }) + .catch(() => {}); +} + +function _vvListRow(name, present, right) { + return `
+ + ${vvEscHtml(name)}${present ? '' : ' ·absent'} + ${right}
`; +} + +function _renderCover() { + const el = document.getElementById('vv-pt-cover-body'); + if (!el || !_vvLists) return; + const cover = _vvLists.cover || {}; + const have = new Set(_vvLists.containers || []); + const names = Object.keys(cover).sort((a,b) => (cover[a]-cover[b]) || a.localeCompare(b)); + + let html = `
+ What the partner starts when this host is down. Tier 1 is immediate; 2–4 wait for + HOST*_TIER<n>_DELAY.
`; + html += names.length + ? names.map(n => _vvListRow(n, have.has(n), + ``)).join('') + : `
Nothing covered.
`; + + const opts = (_vvLists.containers || []).filter(c => !(c in cover)); + html += `
+ + +
`; + el.innerHTML = html; +} + +function vvPtCoverAdd() { + const sel = document.getElementById('vv-pt-cover-add'); + if (!sel || !sel.value || !_vvLists) return; + _vvLists.cover = _vvLists.cover || {}; + _vvLists.cover[sel.value] = 1; + _renderCover(); + vvPtCoverChanged(); +} + +function vvPtCoverChanged() { + document.getElementById('vv-pt-cover-save').style.display = ''; +} + +async function vvPtSaveCover(btn) { + const map = {}; + document.querySelectorAll('.vv-pt-cover-sel').forEach(s => { + const t = parseInt(s.value, 10); + if (t >= 1 && t <= 4) map[s.dataset.name] = t; // 0 = remove, simply not sent + }); + btn.disabled = true; btn.textContent = '⟳'; + try { + const r = await fetch('/plugins/varaverk/api/partnership_lists.php', { + method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: new URLSearchParams({ + csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', + action: 'cover', tiers: JSON.stringify(map), + }) + }); + const d = await r.json(); + if (!d.ok) { vvAlert('Save failed: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = 'Save'; return; } + btn.textContent = '✓'; _vvListsLoaded = false; vvPtLoadLists(); + setTimeout(() => { btn.disabled = false; btn.textContent = 'Save'; btn.style.display = 'none'; }, 1500); + } catch (e) { btn.disabled = false; btn.textContent = 'Save'; vvAlert('Error: ' + e); } +} + +function _renderSvc() { + const el = document.getElementById('vv-pt-svc-body'); + if (!el || !_vvLists) return; + const cur = _vvLists.services || []; + const have = new Set(_vvLists.containers || []); + const xml = new Set(_vvLists.have_xml || []); + + let html = `
+ Non-auth, non-arr services deployed to the partner at onboard and run there continuously. + Needs a my-<name>.xml template to push.
`; + html += cur.length + ? cur.map(n => _vvListRow(n, have.has(n), + ``)).join('') + : `
No shared services.
`; + + const opts = (_vvLists.containers || []).filter(c => !cur.includes(c) && xml.has(c)); + html += `
+ + +
`; + el.innerHTML = html; +} + +function vvPtSvcAdd() { + const sel = document.getElementById('vv-pt-svc-add'); + if (!sel || !sel.value || !_vvLists) return; + _vvLists.services = (_vvLists.services || []).concat([sel.value]); + _renderSvc(); + document.getElementById('vv-pt-svc-save').style.display = ''; +} + +function vvPtSvcRemove(name) { + if (!_vvLists) return; + _vvLists.services = (_vvLists.services || []).filter(n => n !== name); + _renderSvc(); + document.getElementById('vv-pt-svc-save').style.display = ''; +} + +async function vvPtSaveSvc(btn) { + btn.disabled = true; btn.textContent = '⟳'; + try { + const r = await fetch('/plugins/varaverk/api/partnership_lists.php', { + method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: new URLSearchParams({ + csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', + action: 'services', stack: JSON.stringify(_vvLists.services || []), + }) + }); + const d = await r.json(); + if (!d.ok) { vvAlert('Save failed: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = 'Save'; return; } + btn.textContent = '✓'; _vvListsLoaded = false; vvPtLoadLists(); + setTimeout(() => { btn.disabled = false; btn.textContent = 'Save'; btn.style.display = 'none'; }, 1500); + } catch (e) { btn.disabled = false; btn.textContent = 'Save'; vvAlert('Error: ' + e); } +} + function _renderSync(sync) { const jobs = sync.jobs || {}; const gates = sync.gates || {}; @@ -1429,6 +1580,7 @@ function _render(data) { _renderOfflineWarn(cfg); // Mirror sync health + vvPtLoadLists(); _renderXfer(data.xfer || {}); document.getElementById('vv-pt-sync-body').innerHTML = _renderSync(data.sync || {});