Files
Varaverk/Plugin/unraid/pages/watchdog.php
T
Gmer4Lfe fb051b60c1 Varaverk: FallBack + Watchdog tabs; plugin path restructure to Plugin/unraid/
- FallBack tab: per-node tier inventory + active fallback card with duration, tier, handback strikes, running container status
- Watchdog tab: live system health (RAM bar + thresholds, load, uptime, daemon), docker watchdog strikes + skip list + restart history, stability strikes + reboot log, resource pressure alert card, config inventory (mem limits, required, pause/stop lists)
- Swapped partnership/arrs tab order; FallBack between partnership and watchdog
- Plugin source tree moved from Plugin/usr/local/emhttp/plugins/varaverk/ to Plugin/unraid/
- Deployment/ conf templates added
2026-05-28 22:24:50 -04:00

371 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
.vv-wd-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; }
.vv-wd-sec { font-size:10px;font-weight:bold;color:#444;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px; }
.vv-wd-row { display:flex;justify-content:space-between;align-items:baseline;gap:6px;margin:2px 0; }
.vv-wd-lbl { font-size:11px;color:#444;white-space:nowrap; }
.vv-wd-val { font-size:12px;color:#bbb;text-align:right; }
.vv-wd-sep { border:none;border-top:1px solid #1e1e1e;margin:6px 0; }
.vv-wd-pill { font-size:10px;padding:1px 6px;border-radius:2px;background:#1e1e1e;color:#666;border:1px solid #272727; }
.vv-wd-pill.ok { background:#0d1f0d;color:#4caf50;border-color:#1a3a1a; }
.vv-wd-pill.warn { background:#1f1500;color:#ffb74d;border-color:#3a2800; }
.vv-wd-pill.err { background:#200d0d;color:#ef5350;border-color:#3a1a1a; }
.vv-wd-pill-row { display:flex;flex-wrap:wrap;gap:4px;margin-top:4px; }
.vv-wd-bar { height:5px;border-radius:2px;background:#1e1e1e;margin-top:3px;overflow:hidden; }
.vv-wd-bar-fill{ height:100%;border-radius:2px;transition:width .3s; }
.vv-wd-badge { font-size:11px;font-weight:bold;padding:2px 8px;border-radius:3px; }
.vv-wd-badge.ok { background:#0d1f0d;color:#4caf50; }
.vv-wd-badge.soft { background:#1f1f00;color:#cddc39; }
.vv-wd-badge.med { background:#1f1000;color:#ffb74d; }
.vv-wd-badge.hard { background:#200d0d;color:#ef5350; }
.vv-wd-node-h { display:flex;align-items:center;gap:8px;margin-bottom:10px; }
.vv-wd-node-id { font-size:12px;font-weight:bold;color:#666;letter-spacing:.06em;text-transform:uppercase; }
.vv-wd-dot { width:6px;height:6px;border-radius:50%;flex-shrink:0; }
.vv-wd-strike-row { display:flex;align-items:baseline;gap:6px;margin:2px 0; }
.vv-wd-strike-name{ font-size:11px;color:#777;flex:1; }
.vv-wd-strike-cnt { font-size:11px;font-weight:bold;color:#ffb74d; }
.vv-wd-reboot-ts { font-size:11px;color:#555;margin:1px 0; }
.vv-wd-ctr-row { display:flex;justify-content:space-between;align-items:baseline;margin:2px 0; }
.vv-wd-ctr-name{ font-size:11px;color:#888; }
.vv-wd-ctr-lim { font-size:11px;color:#555; }
.vv-wd-pressure{ grid-column:1/-1;border-color:#3a2000;background:#1a1000; }
</style>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;">
<span style="font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;">Watchdog</span>
<span style="font-size:11px;color:#3a3a3a;" id="vv-wd-ts"></span>
</div>
<div id="vv-wd-grid" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;">
<div style="grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
</div>
<script>
(function() {
const GB = 1073741824;
function _relTime(ts) {
if (!ts) return '—';
const d = Math.floor(Date.now() / 1000) - ts;
if (d < 60) return 'just now';
if (d < 3600) return Math.floor(d / 60) + 'm ago';
if (d < 86400)return Math.floor(d / 3600) + 'h ' + Math.floor((d % 3600) / 60) + 'm ago';
return Math.floor(d / 86400) + 'd ago';
}
function _fmtBytes(b) {
if (b >= GB) return (b / GB).toFixed(1) + ' GB';
if (b >= 1048576) return (b / 1048576).toFixed(0) + ' MB';
return (b / 1024).toFixed(0) + ' KB';
}
function _dur(s) {
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
if (d) return d + 'd ' + h + 'h';
if (h) return h + 'h ' + m + 'm';
return m + 'm';
}
function _row(lbl, val) {
return `<div class="vv-wd-row"><span class="vv-wd-lbl">${lbl}</span><span class="vv-wd-val">${val}</span></div>`;
}
function _bar(pct, col) {
return `<div class="vv-wd-bar"><div class="vv-wd-bar-fill" style="width:${Math.min(pct,100)}%;background:${col}"></div></div>`;
}
function _pill(label, cls) {
return `<span class="vv-wd-pill ${cls}">${label}</span>`;
}
function _levelLabel(level) {
return ['OK', 'SOFT', 'MEDIUM', 'HARD'][level] || '?';
}
function _levelCls(level) {
return ['ok', 'soft', 'med', 'hard'][level] || 'ok';
}
// ── Pressure alert card ───────────────────────────────────────────────────────
function _pressureCard(node) {
const st = node.states;
if (!st || st.rw_level === 0) return '';
const level = st.rw_level;
const cls = _levelCls(level);
const label = _levelLabel(level);
const paused = (st.rw_paused || []).filter(Boolean);
const stopped = (st.rw_stopped || []).filter(Boolean);
const pausedHtml = paused.length ? paused.map(c => _pill(c, 'warn')).join('') : '';
const stoppedHtml = stopped.length ? stopped.map(c => _pill(c, 'err')).join('') : '';
return `<div class="vv-wd-card vv-wd-pressure" style="grid-column:1/-1">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
<span class="vv-wd-badge ${cls}">PRESSURE ${label}</span>
<span style="font-size:11px;color:#7a5020;">${node.id} (${node.hostname})</span>
${st.mem_shutdown ? `<span class="vv-wd-badge hard" style="margin-left:auto;">MEM SHUTDOWN ACTIVE</span>` : ''}
</div>
${paused.length ? `<div style="margin-bottom:4px;"><span class="vv-wd-lbl">Paused:</span> <span class="vv-wd-pill-row" style="display:inline-flex;">${pausedHtml}</span></div>` : ''}
${stopped.length ? `<div><span class="vv-wd-lbl">Stopped:</span> <span class="vv-wd-pill-row" style="display:inline-flex;">${stoppedHtml}</span></div>` : ''}
</div>`;
}
// ── System health card ────────────────────────────────────────────────────────
function _systemCard(node, cfg) {
const sys = node.system;
if (!sys) {
return `<div class="vv-wd-card" style="grid-column:span 2;">
<div class="vv-wd-node-h">
<span class="vv-wd-dot" style="background:#444"></span>
<span class="vv-wd-node-id">${node.id}</span>
<span style="font-size:11px;color:#3a3a3a;">${node.hostname}</span>
<span class="vv-wd-badge" style="margin-left:auto;background:#1a1a1a;color:#444">UNREACHABLE</span>
</div>
</div>`;
}
const memGb = sys.mem_avail / GB;
const memTotGb = sys.mem_total / GB;
const usedPct = memTotGb > 0 ? ((memTotGb - memGb) / memTotGb) * 100 : 0;
const memCol = memGb < cfg.sys_mem_gb ? '#ef5350'
: memGb < cfg.rw_hard_gb ? '#ef5350'
: memGb < cfg.rw_medium_gb ? '#ffb74d'
: memGb < cfg.rw_soft_gb ? '#cddc39' : '#4caf50';
const loadPct = sys.cores > 0 ? (sys.load1 / (sys.cores * cfg.rw_load_med)) * 100 : 0;
const loadCol = sys.load1 > sys.cores * cfg.rw_load_med ? '#ef5350'
: sys.load1 > sys.cores * cfg.rw_load_soft ? '#ffb74d'
: '#4caf50';
const daemonDot = sys.daemon_ok ? '#4caf50' : '#ef5350';
const st = node.states || {};
const level = st.rw_level || 0;
const dotCol = level >= 3 ? '#ef5350' : level >= 2 ? '#ffb74d' : level >= 1 ? '#cddc39' : '#4caf50';
return `<div class="vv-wd-card" style="grid-column:span 2;">
<div class="vv-wd-node-h">
<span class="vv-wd-dot" style="background:${dotCol}"></span>
<span class="vv-wd-node-id">${node.id}</span>
<span style="font-size:11px;color:#3a3a3a;">${node.hostname}</span>
<span class="vv-wd-badge ${_levelCls(level)}" style="margin-left:auto;">${_levelLabel(level)}</span>
</div>
<div class="vv-wd-sec">System</div>
${_row('RAM free', `<span style="color:${memCol}">${_fmtBytes(sys.mem_avail)}</span> / ${_fmtBytes(sys.mem_total)}`)}
${_bar(usedPct, memCol)}
<div style="display:flex;justify-content:space-between;margin-top:1px;font-size:10px;color:#333">
<span>free</span>
<span>${cfg.rw_soft_gb}G soft · ${cfg.rw_hard_gb}G hard · ${cfg.sys_mem_gb}G reboot</span>
</div>
<div style="height:5px"></div>
${_row('Load avg', `<span style="color:${loadCol}">${sys.load1.toFixed(2)}</span> / ${sys.cores} cores`)}
${_bar(loadPct, loadCol)}
<div style="height:5px"></div>
${_row('Uptime', _dur(sys.uptime))}
${_row('Docker', `<span style="color:${daemonDot}">${sys.daemon_ok ? 'daemon ok' : 'daemon err'}</span>`)}
${sys.oom_count > 0 ? _row('OOM kills', `<span style="color:#ef5350">${sys.oom_count}</span>`) : _row('OOM kills', '<span style="color:#333">0</span>')}
</div>`;
}
// ── Docker watchdog state card ────────────────────────────────────────────────
function _dockerCard(node, cfg) {
const st = node.states;
if (!st) return `<div class="vv-wd-card" style="grid-column:span 2;"><div class="vv-wd-sec">Docker Watchdog</div><div style="color:#3a3a3a;font-size:11px;padding:8px 0;">No data</div></div>`;
const strikes = Object.entries(st.ctr_strikes || {});
const skiplist = st.skiplist || [];
const restarts = (st.restarts || []).slice(0, 10);
const daemonCls = st.daemon_strikes > 0 ? 'err' : 'ok';
const allOk = strikes.length === 0 && skiplist.length === 0 && st.daemon_strikes === 0;
// Group restarts by container for last-24h summary
const rCounts = {};
for (const r of restarts) {
rCounts[r.name] = (rCounts[r.name] || 0) + 1;
}
let strikesHtml = '';
if (strikes.length === 0 && st.daemon_strikes === 0) {
strikesHtml = '<div style="color:#333;font-size:11px;">0 active strikes</div>';
} else {
if (st.daemon_strikes > 0) {
strikesHtml += `<div class="vv-wd-strike-row"><span class="vv-wd-strike-name">daemon</span><span class="vv-wd-strike-cnt">${st.daemon_strikes}</span></div>`;
}
for (const [name, cnt] of strikes) {
strikesHtml += `<div class="vv-wd-strike-row"><span class="vv-wd-strike-name">${name}</span><span class="vv-wd-strike-cnt">${cnt} / ${cfg.cpu_fail_lim}</span></div>`;
}
}
let skipHtml = '';
if (skiplist.length === 0) {
skipHtml = '<div style="color:#333;font-size:11px;">empty</div>';
} else {
skipHtml = `<div class="vv-wd-pill-row">${skiplist.map(c => _pill(c, 'err')).join('')}</div>`;
}
let restartHtml = '';
const rcEntries = Object.entries(rCounts);
if (rcEntries.length === 0) {
restartHtml = '<div style="color:#333;font-size:11px;">none (24h)</div>';
} else {
restartHtml = rcEntries.map(([n, c]) =>
`<div class="vv-wd-row"><span class="vv-wd-lbl">${n}</span><span class="vv-wd-val" style="color:${c >= cfg.restart_limit ? '#ef5350' : '#ffb74d'}">${c}×</span></div>`
).join('');
}
return `<div class="vv-wd-card" style="grid-column:span 3;">
<div class="vv-wd-sec">Docker Watchdog</div>
<div style="display:flex;gap:6px;margin-bottom:8px;">
${_pill(st.daemon_restart ? 'daemon restarted' : 'daemon ok', daemonCls)}
${allOk ? _pill('all clear', 'ok') : ''}
</div>
<div class="vv-wd-sec">Strikes</div>
${strikesHtml}
<hr class="vv-wd-sep">
<div class="vv-wd-sec">Skip list</div>
${skipHtml}
<hr class="vv-wd-sep">
<div class="vv-wd-sec">Restarts (24h)</div>
${restartHtml}
</div>`;
}
// ── Stability / reboot card ───────────────────────────────────────────────────
function _stabilityCard(node, cfg) {
const st = node.states;
if (!st) return `<div class="vv-wd-card" style="grid-column:span 1;"><div class="vv-wd-sec">Stability</div><div style="color:#3a3a3a;font-size:11px;padding:8px 0;">No data</div></div>`;
const reboots = st.reboots || [];
const sysStr = Object.entries(st.sys_strikes || {});
const rebootCls = reboots.length >= cfg.reboot_limit ? 'err' : reboots.length > 0 ? 'warn' : 'ok';
let sysHtml = '';
if (sysStr.length === 0) {
sysHtml = '<div style="color:#333;font-size:11px;">0 active strikes</div>';
} else {
sysHtml = sysStr.map(([k, v]) =>
`<div class="vv-wd-strike-row"><span class="vv-wd-strike-name" style="font-size:10px;">${k.replace(/_/g,' ')}</span><span class="vv-wd-strike-cnt">${v}</span></div>`
).join('');
}
const rebootHtml = reboots.length === 0
? '<div style="color:#333;font-size:11px;">none (12h)</div>'
: reboots.map(ts => `<div class="vv-wd-reboot-ts">${_relTime(ts)}</div>`).join('');
return `<div class="vv-wd-card" style="grid-column:span 3;">
<div class="vv-wd-sec">Stability</div>
<div style="display:flex;gap:6px;margin-bottom:8px;">
${_pill(`${reboots.length} / ${cfg.reboot_limit} reboots`, rebootCls)}
${_pill(`${cfg.reboot_window}h window`, '')}
</div>
<div class="vv-wd-sec">Strikes</div>
${sysHtml}
<hr class="vv-wd-sep">
<div class="vv-wd-sec">Reboot history</div>
${rebootHtml}
</div>`;
}
// ── Config inventory card ─────────────────────────────────────────────────────
function _configCard(node) {
const cfg = node.config || {};
const mon = Object.entries(cfg.monitored || {});
const req = cfg.required || [];
const ign = cfg.ignore || [];
const paus = cfg.pause_list|| [];
const stop = cfg.stop_list || [];
const crit = cfg.critical || [];
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">${name}</span><span class="vv-wd-ctr-lim">${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>`;
const ignHtml = ign.length === 0
? '<div style="color:#3a3a3a;font-size:11px;">none</div>'
: `<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>`;
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>`;
return `<div class="vv-wd-card" style="grid-column:span 4;">
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
<span class="vv-wd-node-id">${node.id}</span>
<span style="font-size:11px;color:#3a3a3a;">${node.hostname}</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<div class="vv-wd-sec">Mem limits (Tier 1)</div>
${monHtml}
<div style="height:8px"></div>
<div class="vv-wd-sec">Required</div>
${reqHtml}
</div>
<div>
<div class="vv-wd-sec">Pause at medium pressure</div>
${pausHtml}
<div style="height:8px"></div>
<div class="vv-wd-sec">Stop at hard pressure</div>
${stopHtml}
<div style="height:8px"></div>
<div class="vv-wd-sec">Scan ignore</div>
${ignHtml}
</div>
</div>
</div>`;
}
// ── Main render ───────────────────────────────────────────────────────────────
function _render(data) {
const nodes = data.nodes || [];
const cfg = data.cfg || {};
let html = '';
// Pressure alerts (full width, per node)
for (const node of nodes) html += _pressureCard(node);
// System + watchdog state rows per node
for (const node of nodes) {
html += _systemCard(node, cfg);
html += _dockerCard(node, cfg);
html += _stabilityCard(node, cfg);
}
// Config inventory per node
for (const node of nodes) html += _configCard(node);
if (!html) html = '<div style="grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;">No nodes configured.</div>';
document.getElementById('vv-wd-grid').innerHTML = html;
const ts = data.ts
? new Date(data.ts * 1000).toLocaleString([], {
month:'numeric', day:'numeric', year:'numeric',
hour:'2-digit', minute:'2-digit', second:'2-digit'})
: '';
document.getElementById('vv-wd-ts').textContent = ts ? 'Updated: ' + ts : '';
}
function vvWdLoad() {
fetch('/plugins/varaverk/api/watchdog.php')
.then(r => r.json())
.then(_render)
.catch(() => {});
}
vvWdLoad();
setInterval(vvWdLoad, 30000);
})();
</script>