Rehome the failover coverage picker on the Fallback tab, five days after it was dropped from Partnership for belonging here

This commit is contained in:
Gmer4Lfe
2026-08-22 01:32:08 -04:00
parent 8ce8f0dfbe
commit ce613bcf9d
2 changed files with 306 additions and 0 deletions
+140
View File
@@ -225,6 +225,33 @@ if (vv_ai_ui_on()) vv_ai_chat_assets();
</div>
<?php endif; ?>
<!-- Failover coverage — this host's own tiers -->
<!--
Originally on the Partnership page (e8ee5b0), removed the same day in 1a836da because it
describes what runs during an outage, and never rehomed. This is that card, back where it
belongs.
Deliberately edits THIS host's tiers only. FALLBACK_<host>_TIER* lives in that host's conf and
says what someone ELSE starts for it, so this card configures what the partner does for us —
and the partner's own coverage is configured from the partner, because sparse checkout means
this host holds only a read-only RAM-cache copy of their conf.
-->
<div class="vv-card" id="vv-fb-cov-card" style="margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:4px;">
<h3 style="margin:0;">Failover coverage</h3>
<span style="font-size:10px;color:#444;" id="vv-fb-cov-sub">Loading…</span>
</div>
<div style="font-size:10px;color:#3f3f3f;margin-bottom:9px;line-height:1.45;">
Which of <b style="color:#666;">this host's</b> containers the partner starts when this host goes dark.
Tier 1 is immediate; later tiers wait out the delays set in this host's conf.
To change what <span id="vv-fb-cov-partner" style="color:#666;">the partner</span> hands to us, open this page there.
</div>
<div id="vv-fb-cov-body" style="color:#444;font-size:12px;">Loading…</div>
<div style="display:flex;justify-content:flex-end;align-items:center;gap:10px;margin-top:10px;padding-top:8px;border-top:1px solid #1e1e1e;">
<span id="vv-fb-cov-fb" style="font-size:11px;"></span>
<button class="vv-fb-save-btn" id="vv-fb-cov-save" onclick="vvFbCovSave()">Save coverage</button>
</div>
</div>
<!-- Quick settings -->
<div style="font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;padding:0 2px;">Quick settings</div>
@@ -606,6 +633,10 @@ function _render(data) {
_setToggles(data);
_setInputs(data);
_verdict(data);
// Names the partner in the coverage hint, so "open this page there" points somewhere.
const _pn = (data.nodes || []).find(n => !n.is_me);
const _pe = document.getElementById('vv-fb-cov-partner');
if (_pe && _pn) _pe.textContent = _pn.id + ' (' + _pn.hostname + ')';
const grid = document.getElementById('vv-fb-grid');
@@ -740,8 +771,117 @@ window.vvFbSaveSettings = function() {
});
};
// ── Failover coverage picker ──────────────────────────────────────────────────
// Holds the whole edit in memory and writes all four tiers in one POST. Per-row saves would
// leave the four arrays briefly disagreeing, and fallback.sh reads them as a set.
let _vvFbCov = null; // { containers:[], cover:{name:tier}, missing:[] }
function vvFbCovLoad() {
fetch('/plugins/varaverk/api/fallback_coverage.php')
.then(r => r.json())
.then(d => {
if (!d.ok) throw new Error(d.error || 'load failed');
_vvFbCov = d;
_vvFbCovRender();
})
.catch(e => {
const b = document.getElementById('vv-fb-cov-body');
if (b) b.innerHTML = `<span style="color:#ef5350;font-size:11px;">Could not load coverage — ${vvEscHtml(String(e))}</span>`;
});
}
function _vvFbCovRender() {
const d = _vvFbCov;
const body = document.getElementById('vv-fb-cov-body');
if (!d || !body) return;
const sub = document.getElementById('vv-fb-cov-sub');
const n = Object.keys(d.cover || {}).length;
if (sub) sub.textContent = `${d.host} · ${n} container${n !== 1 ? 's' : ''} covered`;
// Names in a tier that are not installed here. Kept and shown rather than dropped: a tier
// entry for a container that does not exist is a line fallback.sh fails on mid-outage.
const miss = new Set((d.missing || []).map(s => s.toLowerCase()));
const all = [...new Set([...(d.containers || []), ...Object.keys(d.cover || {})])]
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
const rows = all.map(name => {
const cur = d.cover?.[name] ?? 0;
const gone = miss.has(name.toLowerCase());
const opts = [0, 1, 2, 3, 4].map(t =>
`<option value="${t}"${t === cur ? ' selected' : ''}>${t === 0 ? '—' : 'T' + t}</option>`).join('');
return `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;border-bottom:1px solid #171717;">
<select class="vv-fb-set-inp vv-fb-cov-sel" style="width:56px;flex-shrink:0;"
data-cov="${vvEscAttr(name)}">${opts}</select>
<span style="font-size:11px;color:${cur ? '#ccc' : '#555'};min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${vvEscHtml(name)}</span>
${gone ? '<span class="vv-fb-leg no" style="flex-shrink:0;">not installed</span>' : ''}
</div>`;
}).join('');
body.innerHTML = `<div style="max-height:320px;overflow:auto;">${rows}</div>`;
}
// Tier 0 means "not covered" — the picker's way of removing something, since an array the
// operator emptied is a legitimate and otherwise inexpressible choice.
// Delegated off the card, and the name travels in a data attribute rather than inside an
// inline handler's quotes. vvEscAttr() escapes " but not ', and these names come from conf as
// well as from Docker — a hand-typed apostrophe would have broken out of the handler string.
document.getElementById('vv-fb-cov-card')?.addEventListener('change', ev => {
const sel = ev.target.closest('.vv-fb-cov-sel');
if (sel) vvFbCovSet(sel.dataset.cov, sel.value);
});
function vvFbCovSet(name, val) {
if (!_vvFbCov) return;
const t = parseInt(val, 10);
if (t === 0) delete _vvFbCov.cover[name];
else _vvFbCov.cover[name] = t;
const sub = document.getElementById('vv-fb-cov-sub');
const n = Object.keys(_vvFbCov.cover).length;
if (sub) sub.textContent = `${_vvFbCov.host} · ${n} container${n !== 1 ? 's' : ''} covered · unsaved`;
}
window.vvFbCovSave = async function () {
if (!_vvFbCov) return;
const fbEl = document.getElementById('vv-fb-cov-fb');
const btn = document.getElementById('vv-fb-cov-save');
const n = Object.keys(_vvFbCov.cover).length;
// Emptying the list is allowed and is sometimes what is wanted, but it is also what a stray
// click looks like, and the consequence only appears during an outage.
if (n === 0 && !await vvConfirm('Save with NO containers covered?\n\nThe partner would start nothing for this host during an outage.')) return;
btn.disabled = true; btn.textContent = 'Saving…'; fbEl.textContent = '';
const fd = new URLSearchParams();
fd.append('action', 'cover');
fd.append('tiers', JSON.stringify(_vvFbCov.cover));
fetch('/plugins/varaverk/api/fallback_coverage.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
btn.disabled = false; btn.textContent = 'Save coverage';
fbEl.style.color = d.ok ? '#4caf50' : '#ef5350';
if (d.ok) {
const c = d.counts || {};
fbEl.textContent = `Saved ✓ T1 ${c[1]??0} · T2 ${c[2]??0} · T3 ${c[3]??0} · T4 ${c[4]??0}`;
setTimeout(() => { fbEl.textContent = ''; }, 5000);
// Re-read rather than trust the local copy, and refresh the node cards whose tier
// pills and coverage counts this just changed.
vvFbCovLoad();
vvFbLoad();
} else {
fbEl.textContent = d.error || 'Failed';
}
})
.catch(e => {
btn.disabled = false; btn.textContent = 'Save coverage';
fbEl.style.color = '#ef5350'; fbEl.textContent = 'Request failed: ' + e;
});
};
vvFbLoad();
setInterval(vvFbLoad, 30000);
vvFbCovLoad(); // once — this is an editor, not a monitor; polling would fight the operator
})();
</script>