Failover Coverage and Shared Services pickers on the Partnership page
Coverage edits FALLBACK_<me>_TIER1-4 directly — the array fallback.sh reads during an outage — rather than a parallel list that could drift from it.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -778,6 +778,157 @@ function _renderXfer(x) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 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 `<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;
|
||||
padding:3px 0;border-bottom:1px solid #151515;">
|
||||
<span style="font-size:11px;color:${present ? '#888' : '#5a4a2a'};overflow:hidden;
|
||||
text-overflow:ellipsis;white-space:nowrap;" title="${vvEscAttr(name)}${present ? '' : ' — not on this host'}">
|
||||
${vvEscHtml(name)}${present ? '' : ' <span style="color:#5a4a2a;">·absent</span>'}</span>
|
||||
${right}</div>`;
|
||||
}
|
||||
|
||||
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 = `<div style="font-size:9px;color:#3a3a3a;margin-bottom:6px;line-height:1.5;">
|
||||
What the partner starts when this host is down. Tier 1 is immediate; 2–4 wait for
|
||||
HOST*_TIER<n>_DELAY.</div>`;
|
||||
html += names.length
|
||||
? names.map(n => _vvListRow(n, have.has(n),
|
||||
`<select class="vv-pt-cover-sel" data-name="${vvEscAttr(n)}" onchange="vvPtCoverChanged()"
|
||||
style="font-size:10px;background:#0d0d0d;color:#aaa;border:1px solid #2a2a2a;border-radius:2px;">
|
||||
${[1,2,3,4].map(t => `<option value="${t}"${cover[n]===t?' selected':''}>T${t}</option>`).join('')}
|
||||
<option value="0">remove</option>
|
||||
</select>`)).join('')
|
||||
: `<div style="font-size:11px;color:#444;padding:6px 0;">Nothing covered.</div>`;
|
||||
|
||||
const opts = (_vvLists.containers || []).filter(c => !(c in cover));
|
||||
html += `<div style="margin-top:8px;display:flex;gap:6px;align-items:center;">
|
||||
<select id="vv-pt-cover-add" style="flex:1;min-width:0;font-size:10px;background:#0d0d0d;color:#aaa;
|
||||
border:1px solid #2a2a2a;border-radius:2px;">
|
||||
<option value="">+ add container…</option>
|
||||
${opts.map(c => `<option value="${vvEscAttr(c)}">${vvEscHtml(c)}</option>`).join('')}
|
||||
</select>
|
||||
<button class="vv-pt-action-btn info" style="font-size:10px;" onclick="vvPtCoverAdd()">Add</button>
|
||||
</div>`;
|
||||
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 = `<div style="font-size:9px;color:#3a3a3a;margin-bottom:6px;line-height:1.5;">
|
||||
Non-auth, non-arr services deployed to the partner at onboard and run there continuously.
|
||||
Needs a my-<name>.xml template to push.</div>`;
|
||||
html += cur.length
|
||||
? cur.map(n => _vvListRow(n, have.has(n),
|
||||
`<button class="vv-pt-action-btn warn" style="font-size:9px;opacity:.6;"
|
||||
onclick="vvPtSvcRemove('${vvEscAttr(n)}')">✕</button>`)).join('')
|
||||
: `<div style="font-size:11px;color:#444;padding:6px 0;">No shared services.</div>`;
|
||||
|
||||
const opts = (_vvLists.containers || []).filter(c => !cur.includes(c) && xml.has(c));
|
||||
html += `<div style="margin-top:8px;display:flex;gap:6px;align-items:center;">
|
||||
<select id="vv-pt-svc-add" style="flex:1;min-width:0;font-size:10px;background:#0d0d0d;color:#aaa;
|
||||
border:1px solid #2a2a2a;border-radius:2px;">
|
||||
<option value="">+ add service…</option>
|
||||
${opts.map(c => `<option value="${vvEscAttr(c)}">${vvEscHtml(c)}</option>`).join('')}
|
||||
</select>
|
||||
<button class="vv-pt-action-btn info" style="font-size:10px;" onclick="vvPtSvcAdd()">Add</button>
|
||||
</div>`;
|
||||
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 || {});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user