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:
Gmer4Lfe
2026-08-14 16:06:02 -04:00
parent 2f8b7276a4
commit a7387c27fb
2 changed files with 88 additions and 10 deletions
+16
View File
@@ -68,6 +68,22 @@ require_once __DIR__ . '/partnership.php';
// until it doesn't and fails silently in the direction of hiding a real strike.
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) ──────────────────────────────────────────
function vv_wd_bash_array(string $raw, string $varname): array {
+72 -10
View File
@@ -45,6 +45,9 @@
// include/ai_chat.php → the shared chat component
require_once dirname(__DIR__) . '/include/confui.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>
.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.err{ color:#ef5350; }
.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 */
.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
// telling me about this container". Everything below tunes what a watchdog does; this is how
// 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' => '',
'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 '
. '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',
'hint' => 'Restarted when it holds more than this. Tier 1 is per container and acts before '
. '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' => '',
'hint' => 'Paused — not stopped — while RAM sits below the medium threshold, and resumed on '
. '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' => '',
'hint' => 'Stopped outright at the hard threshold, and not restarted automatically. The last '
. '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' => '',
'hint' => 'Must be running. Missing is a strike whether or not the container is being '
. '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): ?>
<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>
@@ -306,6 +331,22 @@ function _pill(label, cls) {
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) {
return ['OK', 'SOFT', 'MEDIUM', 'HARD'][level] || '?';
}
@@ -545,28 +586,39 @@ function _configCard(node) {
const stop = cfg.stop_list || [];
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
? '<div style="color:#333;font-size:11px;">none</div>'
: mon.map(([name, mb]) => {
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('');
const reqHtml = req.length === 0
? '<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
? '<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>`;
const pausHtml = paus.length === 0
? '<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
? '<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;">
<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>` : '';
el.innerHTML =
`<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'
? `<input class="vv-wl-val" value="${vvEscAttr(r.value)}" inputmode="numeric">`
+ `<span class="vv-wl-unit">${vvEscHtml(def.unit)}</span>${gb}`
@@ -872,7 +925,16 @@ setInterval(vvWdLoad, 30000);
+ (r.gone ? '↺' : '×') + '</button>';
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(); });
// 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.