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
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<style>
|
||||
.vv-fb-active { background:#1a1200;border:1px solid #5a3800;border-radius:6px;padding:12px 14px; }
|
||||
.vv-fb-active-h { display:flex;align-items:baseline;gap:10px;margin-bottom:8px; }
|
||||
.vv-fb-badge { font-size:11px;font-weight:bold;letter-spacing:.06em;padding:2px 7px;border-radius:3px;flex-shrink:0; }
|
||||
.vv-fb-badge.fb { background:#5a3800;color:#ffb74d; }
|
||||
.vv-fb-badge.norm { background:#1a2a1a;color:#4caf50; }
|
||||
.vv-fb-badge.dark { background:#2a1a2a;color:#9c27b0; }
|
||||
.vv-fb-badge.nonet{ background:#1a1a2a;color:#5c7cfa; }
|
||||
.vv-fb-meta { display:flex;gap:18px;flex-wrap:wrap;margin-bottom:10px; }
|
||||
.vv-fb-meta-item{ display:flex;flex-direction:column;gap:1px; }
|
||||
.vv-fb-meta-val { font-size:17px;font-weight:bold;color:#ffb74d; }
|
||||
.vv-fb-meta-lbl { font-size:10px;color:#5a4020; }
|
||||
.vv-fb-ctrs { display:flex;gap:6px;flex-wrap:wrap;margin-top:6px; }
|
||||
.vv-fb-ctr { font-size:11px;padding:2px 8px;border-radius:3px;background:#2a1e00;color:#ffb74d;border:1px solid #4a2e00; }
|
||||
.vv-fb-ctr.running { background:#1a2a1a;color:#6fcf97;border-color:#2d4a2d; }
|
||||
.vv-fb-ctr.stopped { background:#2a1a1a;color:#e57;border-color:#4a2020;opacity:.7; }
|
||||
.vv-fb-node { grid-column:span 4; }
|
||||
.vv-fb-node-h { display:flex;align-items:baseline;gap:8px;margin-bottom:10px; }
|
||||
.vv-fb-node-id { font-size:12px;font-weight:bold;color:#666;letter-spacing:.06em;text-transform:uppercase; }
|
||||
.vv-fb-node-nm { font-size:11px;color:#3a3a3a; }
|
||||
.vv-fb-arrow { font-size:11px;color:#333; }
|
||||
.vv-fb-covers { font-size:11px;color:#3a3a3a; }
|
||||
.vv-fb-tier { margin-bottom:8px; }
|
||||
.vv-fb-tier-h { display:flex;align-items:baseline;gap:6px;margin-bottom:4px; }
|
||||
.vv-fb-tier-lbl { font-size:11px;font-weight:bold;color:#555;text-transform:uppercase;letter-spacing:.04em; }
|
||||
.vv-fb-tier-delay { font-size:10px;color:#3a3a3a; }
|
||||
.vv-fb-tier-pills { display:flex;gap:5px;flex-wrap:wrap; }
|
||||
.vv-fb-pill { font-size:11px;padding:2px 7px;border-radius:3px;background:#1e1e1e;color:#777;border:1px solid #2a2a2a; }
|
||||
.vv-fb-pill.active-t { background:#1a2010;color:#8bc34a;border-color:#2d3a1d; }
|
||||
.vv-fb-pill.empty { color:#333;font-style:italic; }
|
||||
.vv-fb-state-dot { width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:3px; }
|
||||
.vv-fb-sep { border:none;border-top:1px solid #222;margin:8px 0; }
|
||||
.vv-fb-disabled { grid-column:1/-1;color:#3a3a3a;font-size:12px;padding:20px 0;text-align:center; }
|
||||
</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;">FallBack</span>
|
||||
<span style="font-size:11px;color:#3a3a3a;" id="vv-fb-ts"></span>
|
||||
</div>
|
||||
|
||||
<div id="vv-fb-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() {
|
||||
|
||||
function _dur(startTs) {
|
||||
if (!startTs || startTs === 0) return '—';
|
||||
const s = Math.floor(Date.now() / 1000) - startTs;
|
||||
if (s < 60) return s + 's';
|
||||
if (s < 3600) return Math.floor(s / 60) + 'm';
|
||||
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
|
||||
return m ? h + 'h ' + m + 'm' : h + 'h';
|
||||
}
|
||||
|
||||
function _fmtDelay(min) {
|
||||
if (!min) return 'immediate';
|
||||
if (min < 60) return min + 'min';
|
||||
const h = Math.floor(min / 60), m = min % 60;
|
||||
return m ? h + 'h ' + m + 'm' : h + 'h';
|
||||
}
|
||||
|
||||
function _activeTier(state) {
|
||||
if (!state) return 0;
|
||||
if (state.tier4_started) return 4;
|
||||
if (state.tier3_started) return 3;
|
||||
if (state.tier2_started) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function _stateBadge(st) {
|
||||
const map = {
|
||||
NORMAL: ['norm', 'NORMAL'],
|
||||
FALLBACK: ['fb', 'FALLBACK'],
|
||||
NO_INTERNET: ['nonet', 'NO INTERNET'],
|
||||
DARK: ['dark', 'DARK'],
|
||||
OFFLINE: ['dark', 'OFFLINE'],
|
||||
UNREACHABLE: ['dark', 'UNREACHABLE'],
|
||||
UNKNOWN: ['dark', 'UNKNOWN'],
|
||||
};
|
||||
const [cls, label] = map[st] || ['dark', st];
|
||||
return `<span class="vv-fb-badge ${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
function _stateDot(st) {
|
||||
const col = {
|
||||
NORMAL:'#4caf50', FALLBACK:'#ffb74d',
|
||||
NO_INTERNET:'#5c7cfa', DARK:'#9c27b0',
|
||||
OFFLINE:'#555', UNREACHABLE:'#555', UNKNOWN:'#333',
|
||||
}[st] || '#333';
|
||||
return `<span class="vv-fb-state-dot" style="background:${col}"></span>`;
|
||||
}
|
||||
|
||||
function _activeCard(nodes, handbackReq) {
|
||||
const active = nodes.filter(n => n.state && n.state.state === 'FALLBACK');
|
||||
if (!active.length) return '';
|
||||
|
||||
return active.map(covering => {
|
||||
const st = covering.state;
|
||||
const tier = _activeTier(st);
|
||||
const cov = covering.covers;
|
||||
const covered = cov ? cov.hostname : '?';
|
||||
|
||||
// All containers that should be running at current tier
|
||||
let expected = [...(cov?.tier1 || [])];
|
||||
if (tier >= 2) expected = expected.concat(cov?.tier2 || []);
|
||||
if (tier >= 3) expected = expected.concat(cov?.tier3 || []);
|
||||
if (tier >= 4) expected = expected.concat(cov?.tier4 || []);
|
||||
const runningSet = new Set(covering.running || []);
|
||||
|
||||
const ctrPills = expected.length
|
||||
? expected.map(c => {
|
||||
const cls = runningSet.has(c) ? 'running' : 'stopped';
|
||||
const sym = runningSet.has(c) ? '▲' : '▼';
|
||||
return `<span class="vv-fb-ctr ${cls}">${sym} ${c}</span>`;
|
||||
}).join('')
|
||||
: '<span style="color:#5a4020;font-size:11px;">No containers configured for this tier</span>';
|
||||
|
||||
return `<div class="vv-card vv-fb-active" style="grid-column:1/-1;">
|
||||
<div class="vv-fb-active-h">
|
||||
${_stateBadge('FALLBACK')}
|
||||
<span style="font-size:12px;color:#aa7020;">${covering.id} covering ${cov?.id || '?'} (${covered})</span>
|
||||
</div>
|
||||
<div class="vv-fb-meta">
|
||||
<div class="vv-fb-meta-item">
|
||||
<span class="vv-fb-meta-val">${_dur(st.fallback_start)}</span>
|
||||
<span class="vv-fb-meta-lbl">DURATION</span>
|
||||
</div>
|
||||
<div class="vv-fb-meta-item">
|
||||
<span class="vv-fb-meta-val">Tier ${tier}</span>
|
||||
<span class="vv-fb-meta-lbl">ACTIVE TIER</span>
|
||||
</div>
|
||||
<div class="vv-fb-meta-item">
|
||||
<span class="vv-fb-meta-val">${st.handback_strikes} / ${handbackReq}</span>
|
||||
<span class="vv-fb-meta-lbl">HANDBACK STRIKES</span>
|
||||
</div>
|
||||
<div class="vv-fb-meta-item">
|
||||
<span class="vv-fb-meta-val">${expected.length}</span>
|
||||
<span class="vv-fb-meta-lbl">CONTAINERS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vv-fb-ctrs">${ctrPills}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function _tierSection(tiers, activeTier, delays) {
|
||||
const defs = [
|
||||
{ n: 1, key: 'tier1', label: 'Tier 1', delay: 0 },
|
||||
{ n: 2, key: 'tier2', label: 'Tier 2', delay: delays?.tier2 },
|
||||
{ n: 3, key: 'tier3', label: 'Tier 3', delay: delays?.tier3 },
|
||||
{ n: 4, key: 'tier4', label: 'Tier 4', delay: delays?.tier4 },
|
||||
];
|
||||
return defs.map(({ n, key, label, delay }) => {
|
||||
const containers = tiers[key] || [];
|
||||
const isActive = activeTier >= n;
|
||||
const pills = containers.length
|
||||
? containers.map(c => `<span class="vv-fb-pill${isActive ? ' active-t' : ''}">${c}</span>`).join('')
|
||||
: `<span class="vv-fb-pill empty">none</span>`;
|
||||
const delayStr = n === 1 ? 'immediate' : _fmtDelay(delay);
|
||||
return `<div class="vv-fb-tier">
|
||||
<div class="vv-fb-tier-h">
|
||||
<span class="vv-fb-tier-lbl">${label}</span>
|
||||
<span class="vv-fb-tier-delay">${delayStr}</span>
|
||||
</div>
|
||||
<div class="vv-fb-tier-pills">${pills}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function _nodeCard(node) {
|
||||
const st = node.state || {};
|
||||
const state = st.state || 'UNKNOWN';
|
||||
const cov = node.covers;
|
||||
const active = _activeTier(state === 'FALLBACK' ? st : null);
|
||||
|
||||
const covTarget = cov
|
||||
? `<span class="vv-fb-arrow">→</span><span class="vv-fb-covers">covers ${cov.id} (${cov.hostname})</span>`
|
||||
: '';
|
||||
|
||||
const tierSection = cov
|
||||
? _tierSection(cov, active, cov.delays)
|
||||
: '<div style="color:#3a3a3a;font-size:11px;">No coverage configured</div>';
|
||||
|
||||
return `<div class="vv-card vv-fb-node">
|
||||
<div class="vv-fb-node-h">
|
||||
${_stateDot(state)}
|
||||
<span class="vv-fb-node-id">${node.id}</span>
|
||||
<span class="vv-fb-node-nm">${node.hostname}</span>
|
||||
${covTarget}
|
||||
<span style="flex:1"></span>
|
||||
${_stateBadge(state)}
|
||||
</div>
|
||||
<hr class="vv-fb-sep">
|
||||
${tierSection}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _render(data) {
|
||||
if (!data.fb_enabled) {
|
||||
document.getElementById('vv-fb-grid').innerHTML =
|
||||
'<div class="vv-fb-disabled">FALLBACK_ENABLED=false — fallback monitoring is disabled</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = data.nodes || [];
|
||||
let html = '';
|
||||
|
||||
// Top: active fallback card (if any)
|
||||
html += _activeCard(nodes, data.handback_req || 3);
|
||||
|
||||
// Per-node cards
|
||||
for (const node of nodes) {
|
||||
html += _nodeCard(node);
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<div class="vv-fb-disabled">No nodes configured.</div>';
|
||||
}
|
||||
|
||||
document.getElementById('vv-fb-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-fb-ts').textContent = ts ? 'Updated: ' + ts : '';
|
||||
}
|
||||
|
||||
function vvFbLoad() {
|
||||
fetch('/plugins/varaverk/api/fallback.php')
|
||||
.then(r => r.json())
|
||||
.then(_render)
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
vvFbLoad();
|
||||
setInterval(vvFbLoad, 30000);
|
||||
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user