- 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
232 lines
9.0 KiB
PHP
232 lines
9.0 KiB
PHP
<style>
|
|
.vv-arr-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; }
|
|
.vv-arr-head { display:flex;justify-content:space-between;align-items:center;margin-bottom:8px; }
|
|
.vv-arr-name { font-size:12px;font-weight:bold;text-transform:uppercase;color:#888;letter-spacing:.05em; }
|
|
.vv-arr-status{ display:flex;align-items:center;gap:5px; }
|
|
.vv-arr-dot { width:7px;height:7px;border-radius:50%;flex-shrink:0; }
|
|
.vv-arr-ver { font-size:10px;color:#555; }
|
|
.vv-arr-row { display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:2px 0; }
|
|
.vv-arr-lbl { font-size:11px;color:#555;white-space:nowrap; }
|
|
.vv-arr-val { font-size:12px;color:#bbb;text-align:right; }
|
|
.vv-arr-sep { border:none;border-top:1px solid #222;margin:6px 0; }
|
|
.vv-arr-sub { font-size:10px;color:#444;margin:1px 0 1px 2px; }
|
|
.vv-arr-sub.warn { color:#e57; }
|
|
.vv-arr-path { font-size:10px;color:#393939;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:4px; }
|
|
.vv-arr-node { grid-column:1/-1; }
|
|
.vv-arr-node-head { display:flex;align-items:baseline;gap:8px;margin-bottom:10px; }
|
|
.vv-arr-node-title{ font-size:12px;font-weight:bold;color:#666;letter-spacing:.06em;text-transform:uppercase; }
|
|
.vv-arr-node-host { font-size:11px;color:#3a3a3a; }
|
|
.vv-arr-cards { display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px; }
|
|
.vv-arr-stat-row { display:flex;gap:16px;flex-wrap:wrap; }
|
|
.vv-arr-stat { display:flex;flex-direction:column;gap:1px; }
|
|
.vv-arr-stat-val { font-size:15px;font-weight:bold;color:#ccc; }
|
|
.vv-arr-stat-lbl { font-size:10px;color:#555; }
|
|
</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;">Arrs</span>
|
|
<span style="font-size:11px;color:#3a3a3a;" id="vv-arrs-ts"></span>
|
|
</div>
|
|
|
|
<div id="vv-arrs-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 _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 ago';
|
|
if (d < 172800) return 'yesterday';
|
|
return Math.floor(d / 86400) + 'd ago';
|
|
}
|
|
|
|
function _n(v) { return v == null ? '—' : Number(v).toLocaleString(); }
|
|
|
|
function _bytes(b) {
|
|
if (!b) return '—';
|
|
const u = ['B','KB','MB','GB','TB']; let i = 0;
|
|
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
|
|
return b.toFixed(1) + ' ' + u[i];
|
|
}
|
|
|
|
function _row(lbl, val) {
|
|
return `<div class="vv-arr-row"><span class="vv-arr-lbl">${lbl}</span><span class="vv-arr-val">${val}</span></div>`;
|
|
}
|
|
|
|
function _arrCard(arr) {
|
|
const names = {sonarr:'Sonarr', radarr:'Radarr', lidarr:'Lidarr'};
|
|
const name = names[arr.type] || arr.type;
|
|
const online = arr.online;
|
|
const dotCol = online ? '#4caf50' : (arr.remote ? '#444' : '#c62828');
|
|
const verStr = online ? (arr.version || 'online') : (arr.remote ? 'not reachable' : 'offline');
|
|
|
|
let body = '';
|
|
|
|
if (online) {
|
|
// Library
|
|
if (arr.type === 'sonarr') {
|
|
body += _row('Series', _n(arr.total));
|
|
body += _row('Monitored', _n(arr.monitored));
|
|
body += _row('Episodes', _n(arr.episodes));
|
|
} else if (arr.type === 'radarr') {
|
|
body += _row('Movies', _n(arr.total));
|
|
body += _row('Monitored', _n(arr.monitored));
|
|
body += _row('Files', _n(arr.files));
|
|
} else if (arr.type === 'lidarr') {
|
|
body += _row('Artists', _n(arr.total));
|
|
body += _row('Monitored', _n(arr.monitored));
|
|
if (arr.albums) body += _row('Albums', _n(arr.albums));
|
|
}
|
|
|
|
// Disk — show largest entry
|
|
if (arr.disk && arr.disk.length) {
|
|
const d = arr.disk.reduce((a,b) => (b.totalSpace||0) > (a.totalSpace||0) ? b : a);
|
|
if (d.freeSpace && d.totalSpace) {
|
|
body += _row('Free', _bytes(d.freeSpace) + ' / ' + _bytes(d.totalSpace));
|
|
}
|
|
}
|
|
|
|
// Queue
|
|
const q = arr.queue || {};
|
|
const qDl = q.dl || 0;
|
|
const qWrn = q.warn || 0;
|
|
const qErr = q.err || 0;
|
|
const qTot = qDl + qWrn + qErr;
|
|
const qCol = qErr ? '#f44' : (qWrn ? '#ff9800' : '#bbb');
|
|
body += `<hr class="vv-arr-sep">`;
|
|
const qStr = qTot === 0
|
|
? `<span style="color:#3a3a3a">idle</span>`
|
|
: `<span style="color:${qCol}">${qDl}↓ ${qWrn}⚠ ${qErr}✗</span>`;
|
|
body += _row('Queue', qStr);
|
|
|
|
// Health
|
|
if (arr.health && arr.health.length) {
|
|
const col = arr.health.some(h => h.type === 'error') ? '#f44' : '#ff9800';
|
|
body += `<div class="vv-arr-sub warn" style="color:${col}">` +
|
|
arr.health.slice(0,2).map(h => h.message || h.type).join('<br>') + '</div>';
|
|
}
|
|
|
|
// Cleanup
|
|
const cl = arr.cleanup || {};
|
|
if (cl.last_run) {
|
|
body += `<hr class="vv-arr-sep">`;
|
|
const clOk = cl.status === 'ok'
|
|
? '<span style="color:#4caf50">✓</span>'
|
|
: '<span style="color:#f44">✗</span>';
|
|
body += _row('Cleanup', _relTime(cl.last_run) + ' ' + clOk);
|
|
if (cl.tracked != null) {
|
|
body += `<div class="vv-arr-sub">${_n(cl.tracked)} files · ${_n(cl.total)} items</div>`;
|
|
const orph = cl.orphans || 0, junk = cl.junk || 0;
|
|
if (orph || junk) {
|
|
body += `<div class="vv-arr-sub warn">${orph} orphans · ${junk} junk</div>`;
|
|
} else {
|
|
body += `<div class="vv-arr-sub">0 orphans · 0 junk</div>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Discovery
|
|
const disc = arr.discovery || {};
|
|
if (disc.last_run) {
|
|
if (!cl.last_run) body += `<hr class="vv-arr-sep">`;
|
|
const added = disc.added != null ? ` <span style="color:#6fcf97">+${disc.added}</span>` : '';
|
|
body += _row('Discovery', _relTime(disc.last_run) + added);
|
|
}
|
|
}
|
|
|
|
body += `<div class="vv-arr-path">${arr.root || '—'}</div>`;
|
|
|
|
return `<div class="vv-arr-card">
|
|
<div class="vv-arr-head">
|
|
<span class="vv-arr-name">${name}</span>
|
|
<span class="vv-arr-status">
|
|
<span class="vv-arr-dot" style="background:${dotCol}"></span>
|
|
<span class="vv-arr-ver">${verStr}</span>
|
|
</span>
|
|
</div>
|
|
${body}
|
|
</div>`;
|
|
}
|
|
|
|
function _nodeSection(node, ownerHost) {
|
|
const isOwner = node.host === ownerHost;
|
|
const badge = isOwner ? ' <span style="color:#3a4a3a;font-size:10px;">owner</span>' : '';
|
|
const cards = node.arrs.map(_arrCard).join('');
|
|
return `<div class="vv-card vv-arr-node">
|
|
<div class="vv-arr-node-head">
|
|
<span class="vv-arr-node-title">${node.host.toUpperCase()}</span>
|
|
<span class="vv-arr-node-host">${node.name}${badge}</span>
|
|
</div>
|
|
<div class="vv-arr-cards">${cards}</div>
|
|
</div>`;
|
|
}
|
|
|
|
function _syncCard(sync) {
|
|
let body = '';
|
|
if (!sync.last_run) {
|
|
body = '<div style="color:#3a3a3a;font-size:12px;">Never run</div>';
|
|
} else {
|
|
const ok = sync.status === 'ok'
|
|
? '<span style="color:#4caf50">✓</span>'
|
|
: '<span style="color:#f44">✗</span>';
|
|
body += _row('Last run', _relTime(sync.last_run) + ' ' + ok);
|
|
if (sync.added != null) body += _row('Added', `<span style="color:#6fcf97">+${_n(sync.added)}</span>`);
|
|
if (sync.nodes != null) body += _row('Nodes', _n(sync.nodes));
|
|
}
|
|
if (sync.blocklist_count != null) {
|
|
body += `<hr class="vv-arr-sep">`;
|
|
body += _row('Blocklist', _n(sync.blocklist_count) + ' entries');
|
|
}
|
|
return `<div class="vv-card" style="grid-column:span 4;">
|
|
<h3>Arrs Sync</h3>${body}</div>`;
|
|
}
|
|
|
|
function _recoveryCard(rec) {
|
|
let body = '';
|
|
if (!rec.last_run) {
|
|
body = '<div style="color:#3a3a3a;font-size:12px;">Never run</div>';
|
|
} else {
|
|
const ok = rec.status === 'ok'
|
|
? '<span style="color:#4caf50">✓</span>'
|
|
: '<span style="color:#f44">✗</span>';
|
|
body += _row('Last run', _relTime(rec.last_run) + ' ' + ok);
|
|
body += _row('Fixed', _n(rec.fixed));
|
|
body += _row('Re-searched',_n(rec.searched));
|
|
}
|
|
return `<div class="vv-card" style="grid-column:span 4;">
|
|
<h3>Failed / Stalled Recovery</h3>${body}</div>`;
|
|
}
|
|
|
|
function _render(data) {
|
|
let html = '';
|
|
for (const node of (data.nodes || [])) {
|
|
html += _nodeSection(node, data.host);
|
|
}
|
|
html += _syncCard(data.sync || {});
|
|
html += _recoveryCard(data.recovery || {});
|
|
|
|
document.getElementById('vv-arrs-grid').innerHTML = html || '<div style="grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;">No arrs configured.</div>';
|
|
|
|
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-arrs-ts').textContent = ts ? 'Updated: ' + ts : '';
|
|
}
|
|
|
|
function vvArrsLoad() {
|
|
fetch('/plugins/varaverk/api/arrs.php')
|
|
.then(r => r.json())
|
|
.then(_render)
|
|
.catch(() => {});
|
|
}
|
|
|
|
vvArrsLoad();
|
|
setInterval(vvArrsLoad, 60000);
|
|
|
|
})();
|
|
</script>
|