Mark watchdog list entries that name a container which no longer exists
Half the hard-pressure stop list is containers that are not installed, so at level 3 it sheds half the memory the card implies. Badged on the lists where a dead entry costs something and deliberately not on Scan ignore, where ignoring something already gone is the point — a card of dashed pills by design would teach the eye to skip the badge where it has to be believed.
This commit is contained in:
@@ -68,6 +68,22 @@ require_once __DIR__ . '/partnership.php';
|
|||||||
// until it doesn't and fails silently in the direction of hiding a real strike.
|
// until it doesn't and fails silently in the direction of hiding a real strike.
|
||||||
const VV_WD_SYS_BOOKKEEPING = ['watchdog_cycle'];
|
const VV_WD_SYS_BOOKKEEPING = ['watchdog_cycle'];
|
||||||
|
|
||||||
|
// Every container that exists on this host, running or not, for deciding whether a name in a
|
||||||
|
// watchdog list still refers to anything.
|
||||||
|
//
|
||||||
|
// `docker ps -a` rather than vv_docker_containers() + vv_docker_stopped(): those two are filtered
|
||||||
|
// by status and between them still miss states like restarting, and a container this page called
|
||||||
|
// "not installed" because it happened to be mid-restart would be worse than not checking at all.
|
||||||
|
// The whole point of the badge is that it is only ever shown when it is certainly true.
|
||||||
|
//
|
||||||
|
// Returns an empty array when docker cannot be reached, and callers must treat that as "unknown"
|
||||||
|
// rather than "nothing is installed" — otherwise a docker outage marks every entry dead.
|
||||||
|
function vv_wd_installed_containers(): array {
|
||||||
|
@exec("docker ps -a --format '{{.Names}}' 2>/dev/null", $out, $rc);
|
||||||
|
if ($rc !== 0) return [];
|
||||||
|
return array_values(array_filter(array_map('trim', $out), fn($n) => $n !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Conf array parser (bash arrays) ──────────────────────────────────────────
|
// ── Conf array parser (bash arrays) ──────────────────────────────────────────
|
||||||
|
|
||||||
function vv_wd_bash_array(string $raw, string $varname): array {
|
function vv_wd_bash_array(string $raw, string $varname): array {
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
// include/ai_chat.php → the shared chat component
|
// include/ai_chat.php → the shared chat component
|
||||||
require_once dirname(__DIR__) . '/include/confui.php';
|
require_once dirname(__DIR__) . '/include/confui.php';
|
||||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||||
|
// For vv_wd_installed_containers() only. The state on this page still arrives entirely through
|
||||||
|
// api/watchdog.php — this include buys one function, not a second collection path.
|
||||||
|
require_once dirname(__DIR__) . '/include/watchdog.php';
|
||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
.vv-wd-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; }
|
.vv-wd-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; }
|
||||||
@@ -104,6 +107,13 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
|
|||||||
.vv-wl-sum.ok { color:#4caf50; }
|
.vv-wl-sum.ok { color:#4caf50; }
|
||||||
.vv-wl-sum.err{ color:#ef5350; }
|
.vv-wl-sum.err{ color:#ef5350; }
|
||||||
.vv-wl-dirty { border-color:#3a4a6a !important; }
|
.vv-wl-dirty { border-color:#3a4a6a !important; }
|
||||||
|
/* An entry naming a container that is not installed. Dashed rather than red: it is not an error,
|
||||||
|
it is a line that does nothing — and on the pressure lists that is the difference between what
|
||||||
|
the card implies can be shed and what actually can be. */
|
||||||
|
.vv-wl-gone-t { font-size:9px;color:#7a5020;background:#1a1000;border:1px dashed #3a2800;
|
||||||
|
border-radius:2px;padding:0 4px;flex-shrink:0;white-space:nowrap; }
|
||||||
|
.vv-wd-pill.missing { opacity:.5;border-style:dashed;color:#7a5020;border-color:#3a2800; }
|
||||||
|
.vv-wd-ctr-row.missing .vv-wd-ctr-name { opacity:.5; }
|
||||||
|
|
||||||
/* One host per row — inner grid sizes all cards equally */
|
/* One host per row — inner grid sizes all cards equally */
|
||||||
.vv-wd-host-row {
|
.vv-wd-host-row {
|
||||||
@@ -144,24 +154,28 @@ $_vv_wd_defs = [
|
|||||||
// First, because it is the one people come to this page looking for: the answer to "stop
|
// First, because it is the one people come to this page looking for: the answer to "stop
|
||||||
// telling me about this container". Everything below tunes what a watchdog does; this is how
|
// telling me about this container". Everything below tunes what a watchdog does; this is how
|
||||||
// you take something out of its hands entirely.
|
// you take something out of its hands entirely.
|
||||||
['key' => $_vv_wd_me . '_WATCHDOG_SCAN_IGNORE', 'kind' => 'list',
|
// 'check' — whether a name that matches no installed container is worth marking. True where a
|
||||||
|
// dead entry silently costs something: a Tier 1 limit that guards nothing, or a stop-list slot
|
||||||
|
// that sheds no memory at hard pressure. False here, because ignoring something that no longer
|
||||||
|
// exists is exactly what this list is for.
|
||||||
|
['key' => $_vv_wd_me . '_WATCHDOG_SCAN_IGNORE', 'kind' => 'list', 'check' => false,
|
||||||
'title' => 'Scan ignore', 'unit' => '',
|
'title' => 'Scan ignore', 'unit' => '',
|
||||||
'hint' => 'Left alone completely — no strikes, no restarts, no unhealthy or OOM reports. '
|
'hint' => 'Left alone completely — no strikes, no restarts, no unhealthy or OOM reports. '
|
||||||
. 'For containers you knowingly run broken or stopped. It silences real faults as '
|
. 'For containers you knowingly run broken or stopped. It silences real faults as '
|
||||||
. 'readily as noise, so it is the wrong tool for one that is merely loud.'],
|
. 'readily as noise, so it is the wrong tool for one that is merely loud.'],
|
||||||
['key' => $_vv_wd_me . '_WATCHDOG_CONTAINERS', 'kind' => 'map',
|
['key' => $_vv_wd_me . '_WATCHDOG_CONTAINERS', 'kind' => 'map', 'check' => true,
|
||||||
'title' => 'Memory limits (Tier 1)', 'unit' => 'MB',
|
'title' => 'Memory limits (Tier 1)', 'unit' => 'MB',
|
||||||
'hint' => 'Restarted when it holds more than this. Tier 1 is per container and acts before '
|
'hint' => 'Restarted when it holds more than this. Tier 1 is per container and acts before '
|
||||||
. 'any pressure tier does.'],
|
. 'any pressure tier does.'],
|
||||||
['key' => $_vv_wd_me . '_RW_PAUSE_CONTAINERS', 'kind' => 'list',
|
['key' => $_vv_wd_me . '_RW_PAUSE_CONTAINERS', 'kind' => 'list', 'check' => true,
|
||||||
'title' => 'Pause at medium pressure', 'unit' => '',
|
'title' => 'Pause at medium pressure', 'unit' => '',
|
||||||
'hint' => 'Paused — not stopped — while RAM sits below the medium threshold, and resumed on '
|
'hint' => 'Paused — not stopped — while RAM sits below the medium threshold, and resumed on '
|
||||||
. 'recovery. Safe for anything that can lose a few minutes.'],
|
. 'recovery. Safe for anything that can lose a few minutes.'],
|
||||||
['key' => $_vv_wd_me . '_RW_STOP_CONTAINERS', 'kind' => 'list',
|
['key' => $_vv_wd_me . '_RW_STOP_CONTAINERS', 'kind' => 'list', 'check' => true,
|
||||||
'title' => 'Stop at hard pressure', 'unit' => '',
|
'title' => 'Stop at hard pressure', 'unit' => '',
|
||||||
'hint' => 'Stopped outright at the hard threshold, and not restarted automatically. The last '
|
'hint' => 'Stopped outright at the hard threshold, and not restarted automatically. The last '
|
||||||
. 'step before the memory shutdown.'],
|
. 'step before the memory shutdown.'],
|
||||||
['key' => $_vv_wd_me . '_WATCHDOG_REQUIRED_CONTAINERS', 'kind' => 'list',
|
['key' => $_vv_wd_me . '_WATCHDOG_REQUIRED_CONTAINERS', 'kind' => 'list', 'check' => true,
|
||||||
'title' => 'Required', 'unit' => '',
|
'title' => 'Required', 'unit' => '',
|
||||||
'hint' => 'Must be running. Missing is a strike whether or not the container is being '
|
'hint' => 'Must be running. Missing is a strike whether or not the container is being '
|
||||||
. 'watched for anything else.'],
|
. 'watched for anything else.'],
|
||||||
@@ -181,6 +195,17 @@ foreach ($_vv_wd_defs as $_d) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
<?php
|
||||||
|
// Which container names still refer to something. Emitted once and read by both surfaces that
|
||||||
|
// draw these lists — the editors below and the inventory card in the polled grid — so the two
|
||||||
|
// cannot disagree about whether an entry is dead.
|
||||||
|
//
|
||||||
|
// Empty means "could not ask docker", not "nothing is installed", and every consumer checks the
|
||||||
|
// length before badging anything. A docker outage must not repaint the page as though every list
|
||||||
|
// were full of ghosts.
|
||||||
|
$_vv_wd_installed = vv_wd_installed_containers();
|
||||||
|
?>
|
||||||
|
<script>window.VvWdInstalled = <?= json_encode($_vv_wd_installed, JSON_UNESCAPED_SLASHES) ?>;</script>
|
||||||
<?php if ($_vv_wd_lists): ?>
|
<?php if ($_vv_wd_lists): ?>
|
||||||
<div id="vv-wd-lists" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px;margin-top:12px;"></div>
|
<div id="vv-wd-lists" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px;margin-top:12px;"></div>
|
||||||
<script>window.VvWdLists = <?= json_encode($_vv_wd_lists, JSON_UNESCAPED_SLASHES) ?>;</script>
|
<script>window.VvWdLists = <?= json_encode($_vv_wd_lists, JSON_UNESCAPED_SLASHES) ?>;</script>
|
||||||
@@ -306,6 +331,22 @@ function _pill(label, cls) {
|
|||||||
return `<span class="vv-wd-pill ${cls}">${vvEscHtml(label)}</span>`;
|
return `<span class="vv-wd-pill ${cls}">${vvEscHtml(label)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Does this name still refer to a container? ───────────────────────────────
|
||||||
|
// One resolver for both surfaces. Answers false only when it is certain: an empty install set
|
||||||
|
// means docker could not be asked, and every caller then badges nothing rather than everything.
|
||||||
|
const _installed = new Set(window.VvWdInstalled || []);
|
||||||
|
function _known(name) { return !_installed.size || _installed.has(String(name || '').trim()); }
|
||||||
|
|
||||||
|
// Applied to the lists whose entries are container names, and deliberately NOT to the appdata
|
||||||
|
// ceilings. Those are documented as containers but hold `7dtd`, which is an appdata folder — the
|
||||||
|
// container is 7DaysToDie. Until that rule is pinned down, badging them would mark a working
|
||||||
|
// entry dead, and a wrong badge on this page is worse than a missing one: the whole value of it
|
||||||
|
// is that it is believed.
|
||||||
|
function _pillK(label, cls) {
|
||||||
|
return _known(label) ? _pill(label, cls)
|
||||||
|
: `<span class="vv-wd-pill ${cls} missing" title="No container by this name is installed — this entry does nothing">${vvEscHtml(label)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function _levelLabel(level) {
|
function _levelLabel(level) {
|
||||||
return ['OK', 'SOFT', 'MEDIUM', 'HARD'][level] || '?';
|
return ['OK', 'SOFT', 'MEDIUM', 'HARD'][level] || '?';
|
||||||
}
|
}
|
||||||
@@ -545,28 +586,39 @@ function _configCard(node) {
|
|||||||
const stop = cfg.stop_list || [];
|
const stop = cfg.stop_list || [];
|
||||||
const crit = cfg.critical || [];
|
const crit = cfg.critical || [];
|
||||||
|
|
||||||
|
// Only this host's lists can be checked — VvWdInstalled is what docker on THIS machine reports,
|
||||||
|
// and marking a partner's entry dead against it would be a guess dressed as a fact.
|
||||||
|
const P = node.is_me ? _pillK : _pill;
|
||||||
|
const K = name => node.is_me && !_known(name);
|
||||||
|
|
||||||
const monHtml = mon.length === 0
|
const monHtml = mon.length === 0
|
||||||
? '<div style="color:#333;font-size:11px;">none</div>'
|
? '<div style="color:#333;font-size:11px;">none</div>'
|
||||||
: mon.map(([name, mb]) => {
|
: mon.map(([name, mb]) => {
|
||||||
const gb = (mb / 1024).toFixed(0);
|
const gb = (mb / 1024).toFixed(0);
|
||||||
return `<div class="vv-wd-ctr-row"><span class="vv-wd-ctr-name">${vvEscHtml(name)}</span><span class="vv-wd-ctr-lim">${gb} GB</span></div>`;
|
return `<div class="vv-wd-ctr-row${K(name) ? ' missing' : ''}"`
|
||||||
|
+ `${K(name) ? ' title="No container by this name is installed — this limit does nothing"' : ''}>`
|
||||||
|
+ `<span class="vv-wd-ctr-name">${vvEscHtml(name)}</span>`
|
||||||
|
+ `<span class="vv-wd-ctr-lim">${K(name) ? 'not installed' : gb + ' GB'}</span></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const reqHtml = req.length === 0
|
const reqHtml = req.length === 0
|
||||||
? '<div style="color:#333;font-size:11px;">none</div>'
|
? '<div style="color:#333;font-size:11px;">none</div>'
|
||||||
: `<div class="vv-wd-pill-row">${req.map(c => _pill(c, crit.includes(c) ? '' : '')).join('')}</div>`;
|
: `<div class="vv-wd-pill-row">${req.map(c => P(c, '')).join('')}</div>`;
|
||||||
|
|
||||||
const ignHtml = ign.length === 0
|
const ignHtml = ign.length === 0
|
||||||
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
|
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
|
||||||
|
// Never badged, unlike the lists above. An ignore entry naming a container that is gone costs
|
||||||
|
// nothing — something that does not exist is not being watched either way — and seven dashed
|
||||||
|
// pills here would teach the eye to skip a badge that has to be believed on the stop list.
|
||||||
: `<div class="vv-wd-pill-row">${ign.map(c => _pill(c, '')).join('')}</div>`;
|
: `<div class="vv-wd-pill-row">${ign.map(c => _pill(c, '')).join('')}</div>`;
|
||||||
|
|
||||||
const pausHtml = paus.length === 0
|
const pausHtml = paus.length === 0
|
||||||
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
|
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
|
||||||
: `<div class="vv-wd-pill-row">${paus.map(c => _pill(c, 'warn')).join('')}</div>`;
|
: `<div class="vv-wd-pill-row">${paus.map(c => P(c, 'warn')).join('')}</div>`;
|
||||||
|
|
||||||
const stopHtml = stop.length === 0
|
const stopHtml = stop.length === 0
|
||||||
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
|
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
|
||||||
: `<div class="vv-wd-pill-row">${stop.map(c => _pill(c, 'err')).join('')}</div>`;
|
: `<div class="vv-wd-pill-row">${stop.map(c => P(c, 'err')).join('')}</div>`;
|
||||||
|
|
||||||
return `<div class="vv-wd-card" style="grid-column:span 4;">
|
return `<div class="vv-wd-card" style="grid-column:span 4;">
|
||||||
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
|
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
|
||||||
@@ -864,6 +916,7 @@ setInterval(vvWdLoad, 30000);
|
|||||||
? `<span class="vv-wl-gb">${(+r.value / 1024).toFixed(1)} GB</span>` : '';
|
? `<span class="vv-wl-gb">${(+r.value / 1024).toFixed(1)} GB</span>` : '';
|
||||||
el.innerHTML =
|
el.innerHTML =
|
||||||
`<input class="vv-wl-name" value="${vvEscAttr(r.name)}" spellcheck="false">`
|
`<input class="vv-wl-name" value="${vvEscAttr(r.name)}" spellcheck="false">`
|
||||||
|
+ `<span class="vv-wl-gone-t" style="display:none">not installed</span>`
|
||||||
+ (def.kind === 'map'
|
+ (def.kind === 'map'
|
||||||
? `<input class="vv-wl-val" value="${vvEscAttr(r.value)}" inputmode="numeric">`
|
? `<input class="vv-wl-val" value="${vvEscAttr(r.value)}" inputmode="numeric">`
|
||||||
+ `<span class="vv-wl-unit">${vvEscHtml(def.unit)}</span>${gb}`
|
+ `<span class="vv-wl-unit">${vvEscHtml(def.unit)}</span>${gb}`
|
||||||
@@ -872,7 +925,16 @@ setInterval(vvWdLoad, 30000);
|
|||||||
+ (r.gone ? '↺' : '×') + '</button>';
|
+ (r.gone ? '↺' : '×') + '</button>';
|
||||||
|
|
||||||
const [nameEl, valEl] = [el.querySelector('.vv-wl-name'), el.querySelector('.vv-wl-val')];
|
const [nameEl, valEl] = [el.querySelector('.vv-wl-name'), el.querySelector('.vv-wl-val')];
|
||||||
nameEl.addEventListener('input', () => { r.name = nameEl.value.trim(); mark(); });
|
const tagEl = el.querySelector('.vv-wl-gone-t');
|
||||||
|
// Re-checked on every keystroke, so correcting a stale name clears the badge as soon as it
|
||||||
|
// matches something real — which is the whole workflow this is meant to support. Blank
|
||||||
|
// rows are exempt: a half-typed new entry is not a dead one.
|
||||||
|
const tag = () => {
|
||||||
|
const n = nameEl.value.trim();
|
||||||
|
tagEl.style.display = (def.check && n !== '' && !_known(n)) ? '' : 'none';
|
||||||
|
};
|
||||||
|
tag();
|
||||||
|
nameEl.addEventListener('input', () => { r.name = nameEl.value.trim(); tag(); mark(); });
|
||||||
if (valEl) valEl.addEventListener('input', () => { r.value = valEl.value.trim(); mark(); });
|
if (valEl) valEl.addEventListener('input', () => { r.value = valEl.value.trim(); mark(); });
|
||||||
// Marked rather than spliced out, so a mis-click is one click back and the reason written
|
// Marked rather than spliced out, so a mis-click is one click back and the reason written
|
||||||
// beside the entry is still there to read while deciding.
|
// beside the entry is still there to read while deciding.
|
||||||
|
|||||||
Reference in New Issue
Block a user