diff --git a/Plugin/unraid/api/partnership_lists.php b/Plugin/unraid/api/partnership_lists.php deleted file mode 100644 index 2e2fc03..0000000 --- a/Plugin/unraid/api/partnership_lists.php +++ /dev/null @@ -1,205 +0,0 @@ -_TIER1-4 in this host's own conf — which of this host's containers -// the partner starts when this host goes dark, and in which delay tier. -// services HOST_PARTNERSHIP_SERVICES_STACK — XML templates pushed to the mirror during -// onboard, for services that are neither auth nor arr. -// -// OPERATIONAL MODEL -// GET returns this host's docker containers plus the current membership of both lists. -// POST action=cover tiers= rewrites all four tier arrays -// POST action=services stack= rewrites the services stack -// -// Both write through vv_conf_edit(), the same locked read-modify-write every other conf -// endpoint uses, and both push the result — a partner holding the old list is a partner that -// will act on the old list. -// -// DESIGN PRINCIPLES -// Edit the array fallback.sh actually reads, not a parallel one. -// The coverage tiers already exist and already carry the timing. A second "what to fail -// over" list would be a second answer to the same question, and the two would drift. -// -// The services stack stores XML template filenames, not container names. -// That is what onboard pushes. The picker speaks container names because that is what the -// operator recognises, and the mapping to my-.xml happens here, once. -// -// OPERATIONAL SAFEGUARDS -// POST only for writes, so Unraid's CSRF guard applies. See README-unraid.md. -// -// Names are validated against the containers this host actually runs. A tier or stack entry -// for a container that does not exist here is a line fallback.sh will fail on during an -// outage, which is the worst possible time to discover a typo. -// -// A tier value outside 1-4 is rejected rather than clamped — silently moving a container from -// tier 9 to tier 4 would give it a 24-hour delay nobody asked for. -// -// Writing an empty list is allowed. "Cover nothing" is a legitimate choice and the only way to -// express it. -// -// DEPENDS ON -// include/confform.php vv_conf_edit() -// include/common.php vv_docker_containers() -// include/config.php vv_detect_host(), vv_read_conf_raw(), vv_push_master_conf() -// ═══════════════════════════════════════════════════════════════════════════════════════════════ -header('Content-Type: application/json'); -require_once dirname(__DIR__) . '/include/confform.php'; -require_once dirname(__DIR__) . '/include/common.php'; // vv_docker_containers() - -$hostId = vv_detect_host(); // host1 / host2 -$hostUp = strtoupper($hostId); // HOST1 -$myConf = $hostId . '.conf'; - -$TIERS = [1, 2, 3, 4]; -$tierVar = fn(int $t) => "FALLBACK_{$hostUp}_TIER{$t}"; -$svcVar = "{$hostUp}_PARTNERSHIP_SERVICES_STACK"; - -// Container name ⇄ template filename. Unraid writes my-.xml; the list stores that. -$toXml = fn(string $c) => 'my-' . $c . '.xml'; -$fromXml = fn(string $x) => preg_replace('/^my-|\.xml$/', '', trim($x)); - -// ── Read ───────────────────────────────────────────────────────────────────────────────────── -if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - $raw = vv_read_conf_raw($myConf); - $mRaw = vv_read_conf_raw('master.conf'); - - $cover = []; - foreach ($TIERS as $t) { - // Tiers live in host*.conf on this host; fall back to master.conf for installs that - // kept them there. Reading both costs nothing and avoids an empty card on either shape. - $vals = vv_parse_conf_list($raw, $tierVar($t)) ?: vv_parse_conf_list($mRaw, $tierVar($t)); - foreach ($vals as $c) { $c = trim($c); if ($c !== '') $cover[$c] = $t; } - } - - $services = []; - foreach (vv_parse_conf_list($raw, $svcVar) as $x) { - $n = $fromXml($x); - if ($n !== '') $services[] = $n; - } - - $containers = []; - foreach (vv_docker_containers() as $c) { - $n = is_array($c) ? ($c['name'] ?? '') : (string)$c; - if ($n !== '') $containers[] = $n; - } - sort($containers, SORT_NATURAL | SORT_FLAG_CASE); - - // Templates that exist, so the services picker can say which choices onboard could actually - // push. A container with no XML cannot be deployed to the partner however it is ticked. - $haveXml = []; - foreach (glob('/boot/config/plugins/dockerMan/templates-user/my-*.xml') ?: [] as $p) { - $haveXml[] = $fromXml(basename($p)); - } - - echo json_encode([ - 'ok' => true, - 'host' => $hostUp, - 'containers' => $containers, - 'have_xml' => $haveXml, - 'cover' => (object)$cover, - 'services' => $services, - 'tier_vars' => array_map($tierVar, $TIERS), - 'svc_var' => $svcVar, - ]); - exit; -} - -// ── 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 { - $body = ''; - foreach ($items as $i) $body .= " \"" . $i . "\"\n"; - $pattern = '/^([ \t]*)' . preg_quote($var, '/') . '=\((?:[^)]*)\)/m'; - if (preg_match($pattern, $cur)) { - return preg_replace_callback($pattern, - fn($m) => $m[1] . $var . "=(\n" . $body . $m[1] . ")", $cur, 1); - } - return null; // absent: refuse rather than append into an unknown section -}; - -if ($action === 'cover') { - $map = json_decode((string)($_POST['tiers'] ?? ''), true); - if (!is_array($map)) { echo json_encode(['ok' => false, 'error' => 'tiers must be an object']); exit; } - - $byTier = array_fill_keys($TIERS, []); - foreach ($map as $name => $tier) { - $t = (int)$tier; - if (!in_array($t, $TIERS, true)) { - echo json_encode(['ok' => false, 'error' => "Tier $tier is not 1-4 (for $name)"]); exit; - } - if (!isset($known[strtolower((string)$name)])) { - echo json_encode(['ok' => false, 'error' => "No container named $name on this host"]); exit; - } - $byTier[$t][] = $known[strtolower((string)$name)]; - } - - $ok = vv_conf_edit($myConf, function (string $cur) use ($byTier, $TIERS, $tierVar, $rewrite): ?string { - foreach ($TIERS as $t) { - $next = $rewrite($cur, $tierVar($t), $byTier[$t]); - if ($next === null) return null; - $cur = $next; - } - return $cur; - }, [], array_map($tierVar, $TIERS)); - - if (!$ok) { echo json_encode(['ok' => false, 'error' => vv_conf_last_error() ?: 'Write failed']); exit; } - vv_push_master_conf(); - echo json_encode(['ok' => true, 'counts' => array_map('count', $byTier)]); - exit; -} - -if ($action === 'services') { - $list = json_decode((string)($_POST['stack'] ?? ''), true); - if (!is_array($list)) { echo json_encode(['ok' => false, 'error' => 'stack must be an array']); exit; } - - $xml = []; - foreach ($list as $name) { - if (!isset($known[strtolower((string)$name)])) { - echo json_encode(['ok' => false, 'error' => "No container named $name on this host"]); exit; - } - $real = $known[strtolower((string)$name)]; - $path = '/boot/config/plugins/dockerMan/templates-user/' . $toXml($real); - if (!is_file($path)) { - echo json_encode(['ok' => false, 'error' => "No template " . $toXml($real) . " — the partner could not deploy it"]); exit; - } - $xml[] = $toXml($real); - } - - $ok = vv_conf_edit($myConf, fn(string $cur): ?string => $rewrite($cur, $svcVar, $xml), [], [$svcVar]); - if (!$ok) { echo json_encode(['ok' => false, 'error' => vv_conf_last_error() ?: 'Write failed']); exit; } - vv_push_master_conf(); - echo json_encode(['ok' => true, 'count' => count($xml)]); - exit; -} - -echo json_encode(['ok' => false, 'error' => 'Unknown action']); diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index a0c4895..32e89da 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -120,47 +120,21 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
Loading…
- -
-
-

Data Transferred

-
Loading…
-
-
-

Actions

-
Loading…
-
+ +
+

Data Transferred

+
Loading…
- +

Mirror Sync

Loading…
- - -
-
-

Failover Coverage

- -
-
Loading…
-
- - -
-
-

Shared Services

- -
-
Loading…
+
+

Actions

+
Loading…
@@ -778,157 +752,6 @@ 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 || {}; @@ -1580,7 +1403,6 @@ function _render(data) { _renderOfflineWarn(cfg); // Mirror sync health - vvPtLoadLists(); _renderXfer(data.xfer || {}); document.getElementById('vv-pt-sync-body').innerHTML = _renderSync(data.sync || {});