varaverk: monitor tab overhaul — grid layout, dynamic thresholds, new cards
Layout: - Switch from three flex rows to single CSS grid (repeat(8,1fr)) — eliminates gap-count discrepancy between rows; span 1/2/4/8 cards are now pixel-identical across all rows regardless of how many cards share the row New cards: - Scripts card: last-run status (ok/warn/running/unknown), clickable filter pills, 7-row scroll, flex:1 matching Power/Parity - Streams card: live per-second progress counters, color-coded play buttons (blue=live, orange=transcoding, green=direct, yellow=paused), server badges with counts, left-fill column layout (3 per col) - Transcode card: NVMe indicator, active session count + scrollable list, session rows with server icon + title + type + method Dynamic thresholds: - vv_disk_thresholds() reads /boot/config/plugins/dynamix/dynamix.cfg - Disk utilization bars: green/orange/red based on warning/critical settings - Temperature colors: hot/max for HDD, hotssd/maxssd for SSD/NVMe - Thresholds update every poll cycle; changing Unraid settings takes effect automatically Other: - SSD path detection uses findmnt FSTYPE to exclude tmpfs/ramfs (fixes RAM being reported as SSD transcode target) - varaverk.css h3: add white-space:normal + overflow:hidden to neutralise any Unraid global h3 styles that could force card width
This commit is contained in:
@@ -1,57 +1,100 @@
|
||||
<?php require_once dirname(__DIR__) . '/include/monitor.php'; ?>
|
||||
|
||||
<div id="vv-monitor">
|
||||
<div id="vv-monitor" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;">
|
||||
|
||||
<div class="vv-row">
|
||||
<div class="vv-card" id="vv-system" style="flex:1;min-width:250px;">
|
||||
<div class="vv-card" id="vv-system" style="grid-column:span 1;position:relative;overflow:hidden;">
|
||||
<div id="vv-system-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-partner" style="flex:1;min-width:250px;">
|
||||
<h3>Partner & Fallback</h3>
|
||||
<div class="vv-card" id="vv-partner" style="grid-column:span 1;">
|
||||
<h3 style="display:flex;align-items:center;justify-content:space-between;">
|
||||
Partner & Fallback
|
||||
<svg width="32" height="18" viewBox="0 0 32 18" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity:0.45;flex-shrink:0;">
|
||||
<!-- Server left -->
|
||||
<rect x="0.5" y="2" width="9" height="14" rx="1.5" stroke="#888" stroke-width="1" fill="#1a1a1a"/>
|
||||
<line x1="0.5" y1="5.5" x2="9.5" y2="5.5" stroke="#555" stroke-width="0.6"/>
|
||||
<circle cx="2.5" cy="3.8" r="0.8" fill="#4caf50"/>
|
||||
<line x1="2" y1="8.5" x2="8" y2="8.5" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="2" y1="10.5" x2="8" y2="10.5" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="2" y1="12.5" x2="6" y2="12.5" stroke="#333" stroke-width="0.6"/>
|
||||
<!-- Arrows between -->
|
||||
<line x1="10.5" y1="8" x2="21.5" y2="8" stroke="#555" stroke-width="0.8"/>
|
||||
<polyline points="13,5.5 10.5,8 13,10.5" stroke="#555" stroke-width="0.8" fill="none"/>
|
||||
<polyline points="19,5.5 21.5,8 19,10.5" stroke="#555" stroke-width="0.8" fill="none"/>
|
||||
<!-- Server right -->
|
||||
<rect x="22.5" y="2" width="9" height="14" rx="1.5" stroke="#888" stroke-width="1" fill="#1a1a1a"/>
|
||||
<line x1="22.5" y1="5.5" x2="31.5" y2="5.5" stroke="#555" stroke-width="0.6"/>
|
||||
<circle cx="24.5" cy="3.8" r="0.8" fill="#4caf50"/>
|
||||
<line x1="24" y1="8.5" x2="30" y2="8.5" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="24" y1="10.5" x2="30" y2="10.5" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="24" y1="12.5" x2="28" y2="12.5" stroke="#333" stroke-width="0.6"/>
|
||||
</svg>
|
||||
</h3>
|
||||
<div id="vv-partner-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-cpu" style="flex:2;min-width:240px;">
|
||||
<div class="vv-card" id="vv-cpu" style="grid-column:span 2;">
|
||||
<h3>CPU</h3>
|
||||
<div id="vv-cpu-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-memory" style="flex:2;min-width:240px;">
|
||||
<div class="vv-card" id="vv-memory" style="grid-column:span 2;">
|
||||
<h3>Memory</h3>
|
||||
<div id="vv-memory-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-network" style="flex:2;min-width:240px;">
|
||||
<div class="vv-card" id="vv-network" style="grid-column:span 2;">
|
||||
<h3>Network</h3>
|
||||
<div id="vv-network-body">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-row">
|
||||
<div class="vv-card" id="vv-gpu-card" style="flex:2;min-width:240px;">
|
||||
<div class="vv-card" id="vv-ups-card" style="grid-column:span 1;">
|
||||
<h3>Power</h3>
|
||||
<div id="vv-ups-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-parity-card" style="grid-column:span 1;">
|
||||
<h3>Parity</h3>
|
||||
<div id="vv-parity-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-storage-card" style="grid-column:span 2;">
|
||||
<h3>Pools</h3>
|
||||
<div id="vv-storage-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-array-card" style="grid-column:span 4;">
|
||||
<h3>Array</h3>
|
||||
<div id="vv-array-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-scripts-card" style="grid-column:span 1;">
|
||||
<h3>Scripts</h3>
|
||||
<div id="vv-scripts-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-gpu-card" style="grid-column:span 1;">
|
||||
<h3>GPU</h3>
|
||||
<div id="vv-gpu-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-transcode">
|
||||
<h3>Transcode</h3>
|
||||
<div class="vv-card" id="vv-transcode" style="grid-column:span 2;">
|
||||
<h3>Transcode System</h3>
|
||||
<div id="vv-transcode-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-streams" style="flex:2;min-width:280px;">
|
||||
<div class="vv-card" id="vv-streams" style="grid-column:span 4;">
|
||||
<h3>Streams</h3>
|
||||
<div id="vv-streams-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card vv-wide" id="vv-docker">
|
||||
<div class="vv-card" id="vv-docker" style="grid-column:span 8;">
|
||||
<h3>Containers</h3>
|
||||
<table id="vv-docker-table">
|
||||
<thead><tr><th>Name</th><th>Status</th><th>Image</th></tr></thead>
|
||||
<tbody id="vv-docker-body"><tr><td colspan="3">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -59,7 +102,7 @@
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function vvMeter(label, pct, text) {
|
||||
const color = pct >= 85 ? '#f44336' : pct >= 65 ? '#ff9800' : '#4caf50';
|
||||
const color = `hsl(${Math.round(120 * (1 - pct / 100))},70%,45%)`;
|
||||
return `<div style="margin-bottom:7px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;color:#666;margin-bottom:3px;">
|
||||
<span>${label}</span><span style="color:#999;">${text}</span>
|
||||
@@ -73,7 +116,14 @@ function vvMeter(label, pct, text) {
|
||||
// ── Rolling history ───────────────────────────────────────────────────────────
|
||||
|
||||
const VV_HIST_MAX = 24; // 24 × 5 s = 120 s
|
||||
let vvCpuHistory = [];
|
||||
let vvCpuHistory = [];
|
||||
let vvLastSessions = [];
|
||||
let vvLastStreamPollAt = 0;
|
||||
let vvLastStreamNames = [];
|
||||
let vvStreamServerCount = 0;
|
||||
let vvScriptsFilter = null;
|
||||
let vvLastScripts = {};
|
||||
let vvThresholds = {util_warn:70,util_crit:90,hdd_warn:45,hdd_crit:55,ssd_warn:60,ssd_crit:70};
|
||||
|
||||
let vvNetRxHistory = [];
|
||||
let vvNetTxHistory = [];
|
||||
@@ -203,8 +253,9 @@ function vvRenderCpu(cpu) {
|
||||
if (cores.length) {
|
||||
html += `<div style="display:flex;align-items:flex-end;gap:3px;height:54px;margin:10px 0 4px;">`;
|
||||
cores.forEach(c => {
|
||||
const color = vvCoreColor(c.freq_mhz, c.max_mhz, c.min_mhz);
|
||||
const usePct = c.usage_pct ?? 0;
|
||||
const hue = Math.round(120 * (1 - usePct / 100));
|
||||
const color = `hsl(${hue},70%,45%)`;
|
||||
const barH = Math.max(2, Math.round(usePct * 0.46)); // max ~46px at 100%
|
||||
const label = c.freq_mhz ? (c.freq_mhz >= 1000 ? (c.freq_mhz/1000).toFixed(1)+'G' : c.freq_mhz+'M') : '';
|
||||
html += `<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:1px;min-width:10px;">
|
||||
@@ -219,9 +270,9 @@ function vvRenderCpu(cpu) {
|
||||
|
||||
// Freq legend
|
||||
html += `<div style="display:flex;justify-content:space-between;font-size:9px;color:#444;margin-bottom:8px;">
|
||||
<span style="color:hsl(220,70%,45%)">Low freq</span>
|
||||
<span style="color:hsl(120,70%,45%)">Mid</span>
|
||||
<span style="color:hsl(0,70%,45%)">High freq</span>
|
||||
<span style="color:hsl(120,70%,45%)">Low</span>
|
||||
<span style="color:hsl(60,70%,45%)">Med</span>
|
||||
<span style="color:hsl(0,70%,45%)">High</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -282,6 +333,56 @@ function vvRenderMemory(mem) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// ── Scripts render (filter-aware) ─────────────────────────────────────────────
|
||||
|
||||
function vvRenderScripts() {
|
||||
const sc = vvLastScripts;
|
||||
const scList = sc.scripts ?? [];
|
||||
const running = sc.running_count ?? 0;
|
||||
const ok = sc.ok_count ?? 0;
|
||||
const warn = sc.warn_count ?? 0;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
function vvScriptPill(type, count, color, bg, border, icon) {
|
||||
const active = vvScriptsFilter === type;
|
||||
return `<span onclick="vvScriptsFilterSet('${type}')"
|
||||
style="font-size:10px;color:${color};background:${bg};border:1px solid ${active ? color : border};
|
||||
padding:1px 7px;border-radius:10px;cursor:pointer;font-weight:${active ? '700' : '400'};">${icon} ${count} ${type}</span>`;
|
||||
}
|
||||
|
||||
let html = `<div style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px;">`;
|
||||
if (running > 0) html += vvScriptPill('running', running, '#4fc3f7', '#0a2233', '#1e4060', '●');
|
||||
if (ok > 0) html += vvScriptPill('ok', ok, '#4caf50', '#0a1f0a', '#1a3a1a', '✓');
|
||||
if (warn > 0) html += vvScriptPill('warn', warn, '#ff9800', '#1f1200', '#3a2200', '!');
|
||||
if (!running && !ok && !warn) html += `<span style="font-size:10px;color:#555;">No recent runs</span>`;
|
||||
html += `</div>`;
|
||||
|
||||
const filtered = vvScriptsFilter ? scList.filter(s => s.status === vvScriptsFilter) : scList;
|
||||
let listHtml = '';
|
||||
filtered.forEach(s => {
|
||||
const diff = now - (s.last_ts ?? now);
|
||||
const ago = diff < 60 ? diff + 's' : diff < 3600 ? Math.floor(diff / 60) + 'm' : diff < 86400 ? Math.floor(diff / 3600) + 'h' : Math.floor(diff / 86400) + 'd';
|
||||
const icon = s.status === 'running' ? '●' : s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : '?';
|
||||
const color = s.status === 'running' ? '#4fc3f7' : s.status === 'ok' ? '#4caf50' : s.status === 'warn' ? '#ff9800' : '#555';
|
||||
const name = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name;
|
||||
listHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;">
|
||||
<span style="color:${color};flex-shrink:0;width:10px;text-align:center;">${icon}</span>
|
||||
<span style="color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}</span>
|
||||
<span style="color:#444;font-size:10px;flex-shrink:0;">${ago}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
if (!filtered.length) listHtml = `<p style="color:#555;font-style:italic;font-size:12px;">${vvScriptsFilter ? 'None in this group' : 'No script logs found'}</p>`;
|
||||
html += `<div style="max-height:133px;overflow-y:auto;">${listHtml}</div>`;
|
||||
const el = document.getElementById('vv-scripts-body');
|
||||
if (el) el.innerHTML = html;
|
||||
}
|
||||
|
||||
function vvScriptsFilterSet(type) {
|
||||
vvScriptsFilter = vvScriptsFilter === type ? null : type;
|
||||
vvRenderScripts();
|
||||
}
|
||||
|
||||
// ── Poll ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function vvPollMonitor() {
|
||||
@@ -289,6 +390,9 @@ function vvPollMonitor() {
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
|
||||
// ── Thresholds (from dynamix.cfg via backend) ────────────────────────────
|
||||
if (d.thresholds) vvThresholds = d.thresholds;
|
||||
|
||||
// ── System ──────────────────────────────────────────────────────────────
|
||||
const sys = d.system ?? {};
|
||||
const now = new Date();
|
||||
@@ -304,10 +408,10 @@ function vvPollMonitor() {
|
||||
<div style="font-size:14px;font-weight:700;color:#ddd;">${sys.name}</div>
|
||||
<div style="font-size:11px;color:#666;margin-top:2px;">${sys.comment}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-shrink:0;margin-left:8px;">
|
||||
<button onclick="vvArrayAction('stop')" class="vv-sys-btn" title="Stop Array">■</button>
|
||||
<button onclick="vvArrayAction('shutdown')" class="vv-sys-btn" title="Shutdown">⏻</button>
|
||||
<button onclick="vvArrayAction('restart')" class="vv-sys-btn" title="Restart">↺</button>
|
||||
<div style="display:flex;flex-direction:column;gap:0;flex-shrink:0;margin-left:8px;align-items:flex-start;align-self:flex-start;">
|
||||
<button onclick="vvArrayAction('stop')" class="vv-sys-btn" title="Stop Array" style="width:50px;min-width:0;padding:3px 0;margin:0;font-size:6px;line-height:1;display:block;box-sizing:border-box;overflow:hidden;">■</button>
|
||||
<button onclick="vvArrayAction('shutdown')" class="vv-sys-btn" title="Shutdown" style="width:50px;min-width:0;padding:3px 0;margin:0;font-size:6px;line-height:1;display:block;box-sizing:border-box;overflow:hidden;">⏻</button>
|
||||
<button onclick="vvArrayAction('restart')" class="vv-sys-btn" title="Restart" style="width:50px;min-width:0;padding:3px 0;margin:0;font-size:6px;line-height:1;display:block;box-sizing:border-box;overflow:hidden;">↺</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:22px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
|
||||
@@ -318,7 +422,40 @@ function vvPollMonitor() {
|
||||
<span style="color:#555;">Uptime</span> <span style="color:#aaa;">${sys.uptime}</span>
|
||||
<span style="color:#555;">Array</span> <span style="color:${arrayColor};">${sys.array_state}</span>
|
||||
<span style="color:#555;">Version</span> <span style="color:#555;">${ver}</span>
|
||||
</div>`;
|
||||
</div>
|
||||
<svg width="38" height="64" viewBox="0 0 38 64" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
style="position:absolute;bottom:8px;right:8px;opacity:0.13;pointer-events:none;">
|
||||
<!-- Case body -->
|
||||
<rect x="1" y="1" width="36" height="62" rx="3" stroke="#aaa" stroke-width="1.2" fill="#111"/>
|
||||
<!-- Top strip -->
|
||||
<rect x="1" y="1" width="36" height="10" rx="3" fill="#1c1c1c" stroke="#aaa" stroke-width="1.2"/>
|
||||
<!-- Power button -->
|
||||
<circle cx="19" cy="6" r="2.5" stroke="#ff9800" stroke-width="1" fill="none"/>
|
||||
<line x1="19" y1="3.8" x2="19" y2="2.2" stroke="#ff9800" stroke-width="1"/>
|
||||
<!-- USB dots top -->
|
||||
<rect x="26" y="4" width="4" height="2" rx="0.5" fill="#555"/>
|
||||
<!-- Mesh front panel -->
|
||||
<rect x="3" y="14" width="16" height="44" rx="1" fill="#0d0d0d" stroke="#444" stroke-width="0.6"/>
|
||||
<!-- Mesh lines -->
|
||||
<line x1="3" y1="17" x2="19" y2="17" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="20" x2="19" y2="20" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="23" x2="19" y2="23" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="26" x2="19" y2="26" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="29" x2="19" y2="29" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="32" x2="19" y2="32" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="35" x2="19" y2="35" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="38" x2="19" y2="38" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="41" x2="19" y2="41" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="44" x2="19" y2="44" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="47" x2="19" y2="47" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="50" x2="19" y2="50" stroke="#333" stroke-width="0.6"/>
|
||||
<line x1="3" y1="53" x2="19" y2="53" stroke="#333" stroke-width="0.6"/>
|
||||
<!-- Glass side panel -->
|
||||
<rect x="21" y="14" width="14" height="44" rx="1" fill="#08080f" stroke="#444" stroke-width="0.6" opacity="0.7"/>
|
||||
<!-- Bottom feet -->
|
||||
<rect x="5" y="60" width="5" height="2" rx="1" fill="#333"/>
|
||||
<rect x="28" y="60" width="5" height="2" rx="1" fill="#333"/>
|
||||
</svg>`;
|
||||
|
||||
// ── Partner & Fallback ───────────────────────────────────────────────────
|
||||
const pt = d.partner ?? {};
|
||||
@@ -435,6 +572,183 @@ function vvPollMonitor() {
|
||||
document.getElementById('vv-network-body').innerHTML = '<p style="color:#555;font-style:italic">No network interface detected</p>';
|
||||
}
|
||||
|
||||
// ── UPS / Power ─────────────────────────────────────────────────────────
|
||||
const ups = d.ups ?? {};
|
||||
if (ups.available) {
|
||||
const onBatt = ups.status === 'ONBATT';
|
||||
const statColor = onBatt ? '#f44336' : ups.status === 'ONLINE' ? '#4caf50' : '#ff9800';
|
||||
const loadPct = ups.load_pct ?? 0;
|
||||
const loadHue = Math.round(120 * (1 - loadPct / 100));
|
||||
const bPct = ups.bcharge ?? 0;
|
||||
const bHue = Math.round(120 * (bPct / 100));
|
||||
const bColor = onBatt ? '#f44336' : `hsl(${bHue},70%,45%)`;
|
||||
const timeLeft = ups.timeleft != null ? ups.timeleft.toFixed(1) + ' min' : '—';
|
||||
const watts = ups.watts != null ? ups.watts + ' W' : '—';
|
||||
const lineV = ups.line_v != null ? ups.line_v + ' V' : '—';
|
||||
const outV = ups.output_v != null ? ups.output_v + ' V' : '—';
|
||||
const xfers = ups.num_xfers ?? 0;
|
||||
|
||||
document.getElementById('vv-ups-body').innerHTML =
|
||||
`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<span style="font-size:11px;color:#666;">${ups.model}</span>
|
||||
<span style="font-size:11px;font-weight:600;color:${statColor};">${ups.status}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:7px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
|
||||
<span style="color:#666;">Load</span>
|
||||
<span style="color:#999;">${loadPct.toFixed(1)}% · ${watts}</span>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${loadPct}%;height:100%;background:hsl(${loadHue},70%,45%);border-radius:3px;transition:width 0.4s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
|
||||
<span style="color:#666;">Battery</span>
|
||||
<span style="color:#999;">${bPct.toFixed(1)}% · ${timeLeft}</span>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${bPct}%;height:100%;background:${bColor};border-radius:3px;transition:width 0.4s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:11px;">
|
||||
<span style="color:#555;">Line in</span> <span style="color:#888;">${lineV}</span>
|
||||
<span style="color:#555;">Output</span> <span style="color:#888;">${outV}</span>
|
||||
<span style="color:#555;">Transfers</span> <span style="color:${xfers > 0 ? '#ff9800' : '#555'};">${xfers}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
document.getElementById('vv-ups-body').innerHTML = '<p style="color:#555;font-style:italic;font-size:12px;">No UPS detected</p>';
|
||||
}
|
||||
|
||||
// ── Parity ──────────────────────────────────────────────────────────────
|
||||
const par = d.parity ?? {};
|
||||
(function() {
|
||||
const valid = par.valid !== false;
|
||||
const inProg = par.in_progress;
|
||||
const validColor = valid ? '#4caf50' : '#f44336';
|
||||
const validLabel = valid ? 'Parity is valid' : 'Parity is INVALID';
|
||||
|
||||
function vvRelTime(ts) {
|
||||
if (!ts) return '';
|
||||
const diff = Math.floor(Date.now() / 1000) - ts;
|
||||
const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
|
||||
const parts = [];
|
||||
if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
|
||||
if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
|
||||
if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
|
||||
return parts.join(', ') + ' ago';
|
||||
}
|
||||
function vvDueIn(ts) {
|
||||
if (!ts) return '—';
|
||||
const diff = ts - Math.floor(Date.now() / 1000);
|
||||
if (diff <= 0) return 'overdue';
|
||||
const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
|
||||
const parts = [];
|
||||
if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
|
||||
if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
|
||||
if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
|
||||
return 'Due in: ' + parts.join(', ');
|
||||
}
|
||||
function vvFmtDate(ts) {
|
||||
if (!ts) return '—';
|
||||
return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||
}
|
||||
|
||||
const exitColor = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336';
|
||||
const errColor = (par.errors ?? 0) > 0 ? '#f44336' : '#555';
|
||||
const speedStr = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : '';
|
||||
const nextDate = vvFmtDate(par.next_ts);
|
||||
const dueIn = vvDueIn(par.next_ts);
|
||||
|
||||
let html = `<div style="font-size:13px;font-weight:600;color:${validColor};margin-bottom:10px;">${validLabel}</div>`;
|
||||
|
||||
if (inProg) {
|
||||
const pct = par.resync_pct ?? 0;
|
||||
html += `<div style="font-size:11px;color:#aaa;margin-bottom:4px;">Check in progress — ${pct}%</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;margin-bottom:10px;">
|
||||
<div style="width:${pct}%;height:100%;background:#4caf50;border-radius:3px;transition:width 2s;"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:11px;">
|
||||
<span style="color:#555;">Last check</span>
|
||||
<span style="color:#888;">${par.last_date ?? '—'}${speedStr}</span>
|
||||
<span style="color:#555;">Status</span>
|
||||
<span style="color:${exitColor};">${par.exit_label ?? '—'}</span>
|
||||
<span style="color:#555;">Errors</span>
|
||||
<span style="color:${errColor};">${par.errors ?? 0}</span>
|
||||
<span style="color:#555;">Next check</span>
|
||||
<span style="color:#888;">${nextDate}</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#555;margin-top:8px;">${dueIn}</div>`;
|
||||
|
||||
document.getElementById('vv-parity-body').innerHTML = html;
|
||||
})();
|
||||
|
||||
// ── Storage helpers ──────────────────────────────────────────────────────
|
||||
function vvTempColor(tempC, transport) {
|
||||
if (tempC === null) return '#444';
|
||||
const isSsd = transport === 'nvme' || transport === 'ssd';
|
||||
const warn = isSsd ? vvThresholds.ssd_warn : vvThresholds.hdd_warn;
|
||||
const crit = isSsd ? vvThresholds.ssd_crit : vvThresholds.hdd_crit;
|
||||
return tempC >= crit ? '#f44336' : tempC >= warn ? '#ff9800' : '#4caf50';
|
||||
}
|
||||
function vvFmt(v) { return v >= 1024 ? (v / 1024).toFixed(1) + ' TB' : v + ' GB'; }
|
||||
|
||||
function vvDiskRow(disk) {
|
||||
const tempC = disk.temp;
|
||||
const tempColor = vvTempColor(tempC, disk.transport);
|
||||
const tempStr = tempC !== null ? `${tempC}°` : '—';
|
||||
const isParity = disk.role === 'parity';
|
||||
const nameColor = isParity ? '#6a8faf' : '#aaa';
|
||||
const pct = disk.pct ?? 0;
|
||||
const barColor = isParity ? '#1e3a5a'
|
||||
: pct >= vvThresholds.util_crit ? '#f44336'
|
||||
: pct >= vvThresholds.util_warn ? '#ff9800'
|
||||
: '#4caf50';
|
||||
const barWidth = isParity ? '100' : pct;
|
||||
const spinLabel = (!isParity && !disk.mounted) ? `<span style="color:#555;font-size:9px;margin-left:4px;">↓</span>` : '';
|
||||
const right = isParity
|
||||
? `<span style="color:#444;font-size:10px;">${vvFmt(disk.size_gb)}</span>`
|
||||
: `<span style="color:#555;font-size:10px;">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;
|
||||
return `<div style="margin-bottom:7px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;">
|
||||
<span style="color:${nameColor};">${disk.name}${spinLabel}</span>
|
||||
${right}
|
||||
<span style="color:${tempColor};font-size:10px;margin-left:6px;flex-shrink:0;">${tempStr}</span>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${barWidth}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.4s;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function vvDiskCol(disks) {
|
||||
return `<div style="flex:1;min-width:0;">${disks.map(vvDiskRow).join('')}</div>`;
|
||||
}
|
||||
|
||||
// ── Pools — single column ─────────────────────────────────────────────────
|
||||
const storageDisks = d.storage ?? [];
|
||||
if (storageDisks.length) {
|
||||
document.getElementById('vv-storage-body').innerHTML = storageDisks.map(vvDiskRow).join('');
|
||||
} else {
|
||||
document.getElementById('vv-storage-body').innerHTML = '<p style="color:#555;font-style:italic;font-size:12px;">No pools found</p>';
|
||||
}
|
||||
|
||||
// ── Array disks — min 3 columns, balanced left-to-right, grows as needed ───
|
||||
const arrayDisks = d.array_disks ?? [];
|
||||
if (arrayDisks.length) {
|
||||
const numCols = Math.max(3, Math.ceil(arrayDisks.length / 6));
|
||||
const perCol = Math.ceil(arrayDisks.length / numCols);
|
||||
const cols = [];
|
||||
for (let i = 0; i < numCols; i++) cols.push(arrayDisks.slice(i * perCol, (i + 1) * perCol));
|
||||
document.getElementById('vv-array-body').innerHTML =
|
||||
`<div style="display:flex;gap:10px;">${cols.map(vvDiskCol).join('')}</div>`;
|
||||
} else {
|
||||
document.getElementById('vv-array-body').innerHTML = '<p style="color:#555;font-style:italic;font-size:12px;">No array disks</p>';
|
||||
}
|
||||
|
||||
// ── GPU ─────────────────────────────────────────────────────────────────
|
||||
const gpu = d.gpu ?? {};
|
||||
const gpuProcs = d.gpu_procs ?? [];
|
||||
@@ -468,6 +782,10 @@ function vvPollMonitor() {
|
||||
document.getElementById('vv-gpu-body').innerHTML = '<p style="color:#555;font-style:italic">No GPU detected</p>';
|
||||
}
|
||||
|
||||
// ── Scripts ─────────────────────────────────────────────────────────────
|
||||
vvLastScripts = d.scripts ?? {};
|
||||
vvRenderScripts();
|
||||
|
||||
// ── Transcode ───────────────────────────────────────────────────────────
|
||||
const tc = d.transcode ?? {};
|
||||
if (!tc.available) {
|
||||
@@ -476,41 +794,90 @@ function vvPollMonitor() {
|
||||
} else {
|
||||
const loc = tc.is_ramdisk ? 'Ramdisk' : 'SSD';
|
||||
const locColor = tc.is_ramdisk ? '#4caf50' : '#ff9800';
|
||||
const rd = tc.ramdisk ?? {};
|
||||
const usedMb = rd.used_mb ?? 0;
|
||||
const sizeMb = rd.size_mb ?? 0;
|
||||
const pct = sizeMb > 0 ? Math.round(usedMb / sizeMb * 100) : 0;
|
||||
const barColor = pct > 85 ? '#f44336' : pct > 65 ? '#ff9800' : '#4caf50';
|
||||
|
||||
// RAM disk bar
|
||||
const rd = tc.ramdisk ?? {};
|
||||
const rdUsed = rd.used_mb ?? 0;
|
||||
const rdSize = rd.size_mb ?? 0;
|
||||
const rdPct = rdSize > 0 ? Math.round(rdUsed / rdSize * 100) : 0;
|
||||
const rdHue = Math.round(120 * (1 - rdPct / 100));
|
||||
|
||||
// SSD bar
|
||||
const ssd = tc.ssd ?? {};
|
||||
const ssdUsed = ssd.used_mb ?? 0;
|
||||
const ssdSize = ssd.size_mb ?? 0;
|
||||
const ssdPct = ssdSize > 0 ? Math.round(ssdUsed / ssdSize * 100) : 0;
|
||||
const ssdHue = Math.round(120 * (1 - ssdPct / 100));
|
||||
|
||||
// Flip info
|
||||
const ago = tc.last_flip_ago;
|
||||
let flipStr = 'never';
|
||||
if (ago !== null && ago !== undefined) {
|
||||
if (ago < 60) flipStr = ago + 's ago';
|
||||
else if (ago < 3600) flipStr = Math.floor(ago / 60) + 'm ago';
|
||||
else flipStr = Math.floor(ago / 3600) + 'h ' + Math.floor((ago % 3600) / 60) + 'm ago';
|
||||
if (ago < 60) flipStr = ago + 's ago';
|
||||
else if (ago < 3600) flipStr = Math.floor(ago / 60) + 'm ago';
|
||||
else flipStr = Math.floor(ago / 3600) + 'h ' + Math.floor((ago % 3600) / 60) + 'm ago';
|
||||
}
|
||||
|
||||
const totalSess = (tc.ram_sessions ?? 0) + (tc.ssd_sessions ?? 0);
|
||||
let sessStr = totalSess === 0 ? 'none' : totalSess + ' active';
|
||||
if (tc.ram_sessions > 0 && tc.ssd_sessions > 0)
|
||||
sessStr += ` (split: ${tc.ram_sessions} RAM / ${tc.ssd_sessions} SSD)`;
|
||||
// Server icon helper
|
||||
function vvSrvIcon(type, name) {
|
||||
const cfg = { emby: ['#4caf50','#fff','E'], jellyfin: ['#00A4DC','#fff','JF'], plex: ['#E5A00D','#000','P'] };
|
||||
const [bg, fg, lbl] = cfg[type] ?? ['#555','#fff', (type[0] || '?').toUpperCase()];
|
||||
return `<span title="${name}" style="display:inline-flex;align-items:center;justify-content:center;
|
||||
width:18px;height:18px;background:${bg};border-radius:3px;font-size:8px;font-weight:bold;
|
||||
color:${fg};flex-shrink:0;">${lbl}</span>`;
|
||||
}
|
||||
|
||||
document.getElementById('vv-transcode-body').innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<span style="background:${locColor}22;border:1px solid ${locColor};color:${locColor};
|
||||
padding:2px 10px;border-radius:12px;font-size:12px;font-weight:bold;">● ${loc}</span>
|
||||
<span style="color:#888;font-size:12px;">Sessions: ${sessStr}</span>
|
||||
<span style="color:#666;font-size:12px;margin-left:auto;">Flips this hour: ${tc.flip_count_hour ?? 0}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:4px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;color:#666;margin-bottom:3px;">
|
||||
<span>Ramdisk</span><span>${usedMb} MB / ${sizeMb} MB (${pct}%)</span>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${pct}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.4s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#555;margin-top:6px;">Last flip: ${flipStr}</div>`;
|
||||
// Active sessions from shared streams data
|
||||
const activeSessions = vvLastSessions.filter(s => s.is_tc);
|
||||
const typeLabel = t => ({ LiveTvProgram:'Live TV', Movie:'Movie', Episode:'TV', Audio:'Music' }[t] ?? t);
|
||||
|
||||
let sessHtml = '';
|
||||
if (activeSessions.length) {
|
||||
let rowsHtml = '';
|
||||
activeSessions.forEach(s => {
|
||||
const meth = s.method.replace('Transcode', 'TC').replace('Direct ', '');
|
||||
rowsHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;min-width:0;overflow:hidden;">
|
||||
${vvSrvIcon(s.server_type, s.server)}
|
||||
<span style="font-size:10px;color:#888;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${s.title}</span>
|
||||
<span style="font-size:9px;color:#555;flex-shrink:0;">${typeLabel(s.type)}</span>
|
||||
<span style="font-size:9px;color:#ff9800;flex-shrink:0;">${meth}</span>
|
||||
</div>`;
|
||||
});
|
||||
sessHtml = `<div style="margin-top:8px;border-top:1px solid #2a2a2a;padding-top:6px;">
|
||||
<div style="font-size:10px;color:#555;margin-bottom:4px;">Active transcodes <span style="color:#ff9800;font-weight:600;">${activeSessions.length}</span></div>
|
||||
<div style="max-height:57px;overflow-y:auto;overflow-x:hidden;">${rowsHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const nvmeColor = ssd.available ? '#4caf50' : '#f44336';
|
||||
document.getElementById('vv-transcode-body').innerHTML =
|
||||
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
<span style="background:${locColor}22;border:1px solid ${locColor};color:${locColor};
|
||||
padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;">● ${loc}</span>
|
||||
<span style="background:${nvmeColor}22;border:1px solid ${nvmeColor};color:${nvmeColor};
|
||||
padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;">● NVMe</span>
|
||||
</div>
|
||||
<span style="font-size:10px;color:#555;">Flips: ${tc.flip_count_hour ?? 0}/hr · ${flipStr}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:7px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
|
||||
<span style="color:#666;">Ramdisk</span>
|
||||
<span style="color:#555;font-size:10px;">${rdUsed} / ${rdSize} MB</span>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${rdPct}%;height:100%;background:hsl(${rdHue},70%,45%);border-radius:3px;transition:width 0.4s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:4px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
|
||||
<span style="color:#666;">SSD Fallback</span>
|
||||
<span style="color:#555;font-size:10px;">${ssd.available ? ssdUsed + ' / ' + ssdSize + ' MB' : '—'}</span>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${ssdPct}%;height:100%;background:hsl(${ssdHue},70%,45%);border-radius:3px;transition:width 0.4s;"></div>
|
||||
</div>
|
||||
</div>${sessHtml}`;
|
||||
}
|
||||
|
||||
// ── Docker ──────────────────────────────────────────────────────────────
|
||||
@@ -528,6 +895,20 @@ function vvPollMonitor() {
|
||||
vvPollMonitor();
|
||||
setInterval(vvPollMonitor, 5000);
|
||||
|
||||
// Pin pools card width to CPU card width across rows
|
||||
function vvSyncCardWidths() {
|
||||
const cpu = document.getElementById('vv-cpu');
|
||||
const pools = document.getElementById('vv-storage-card');
|
||||
if (!cpu || !pools) return;
|
||||
const w = cpu.offsetWidth;
|
||||
if (w === 0) return;
|
||||
pools.style.flex = 'none';
|
||||
pools.style.boxSizing = 'border-box';
|
||||
pools.style.width = w + 'px';
|
||||
}
|
||||
new ResizeObserver(vvSyncCardWidths).observe(document.getElementById('vv-cpu'));
|
||||
requestAnimationFrame(vvSyncCardWidths);
|
||||
|
||||
// ── Media streams (slower poll — media server API calls) ──────────────────────
|
||||
|
||||
function vvFmtSec(sec) {
|
||||
@@ -539,63 +920,109 @@ function vvFmtSec(sec) {
|
||||
: `${m}:${String(s).padStart(2,'0')}`;
|
||||
}
|
||||
|
||||
function vvRenderStreams() {
|
||||
if (vvStreamServerCount === 0) return;
|
||||
const sessions = vvLastSessions;
|
||||
const names = vvLastStreamNames;
|
||||
const el = document.getElementById('vv-streams-body');
|
||||
if (!el) return;
|
||||
|
||||
// Per-server counts from full session list
|
||||
const serverCounts = {};
|
||||
sessions.forEach(s => serverCounts[s.server] = (serverCounts[s.server] || 0) + 1);
|
||||
const badges = names.map(n => {
|
||||
const cnt = serverCounts[n] ?? 0;
|
||||
return cnt > 0
|
||||
? `<span class="vv-server-badge">${n} <b style="color:#ccc;">${cnt}</b></span>`
|
||||
: `<span class="vv-server-badge" style="color:#444;">${n}</span>`;
|
||||
}).join('');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
el.innerHTML = `<div class="vv-stream-servers">${badges}</div>`
|
||||
+ '<p class="vv-stream-empty">Nothing playing</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = Math.max(0, Math.floor(Date.now() / 1000) - vvLastStreamPollAt);
|
||||
|
||||
function vvStreamRow(s) {
|
||||
const isLive = s.type === 'LiveTvProgram' || s.dur_sec === 0;
|
||||
const inc = s.paused ? 0 : elapsed;
|
||||
const curSec = Math.max(0, (s.pos_sec ?? 0) + inc);
|
||||
const barColor = isLive ? '#ff9800' : (s.is_tc ? '#ff9800' : '#4caf50');
|
||||
const tcColor = s.paused ? '#fdd835' : '#555';
|
||||
|
||||
const icon = s.paused ? '⏸' : '▶';
|
||||
const iconColor = s.paused ? '#fdd835' : isLive ? '#2196f3' : s.is_tc ? '#ff9800' : '#aaa';
|
||||
|
||||
let timeStr = '';
|
||||
let pct = s.pct ?? 0;
|
||||
if (isLive) {
|
||||
pct = 75;
|
||||
timeStr = `<span style="color:${tcColor};">${vvFmtSec(curSec)}</span>`;
|
||||
} else if (s.dur_sec > 0) {
|
||||
pct = Math.min(100, Math.round(curSec / s.dur_sec * 100));
|
||||
timeStr = `<span style="color:${tcColor};">${vvFmtSec(curSec)} / ${vvFmtSec(s.dur_sec)}</span>`;
|
||||
}
|
||||
|
||||
return `<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:2px;">
|
||||
<span style="color:${s.paused ? '#fdd835' : '#aaa'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">
|
||||
<span style="color:${iconColor};">${icon}</span> ${s.title}</span>
|
||||
<span style="color:#444;font-size:10px;margin-left:6px;flex-shrink:0;">${s.server}</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:10px;color:#555;margin-bottom:3px;">
|
||||
<span>${s.user}</span>
|
||||
${timeStr}
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
||||
<div style="width:${pct}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.9s linear;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Left-fill columns of 3 — col 1 fills first, col 2 opens when col 1 hits 3
|
||||
const shown = sessions.slice(0, 12);
|
||||
const perCol = 3;
|
||||
const sCols = [];
|
||||
for (let i = 0; i < shown.length; i += perCol) sCols.push(shown.slice(i, i + perCol));
|
||||
|
||||
const vvStreamCol = col =>
|
||||
`<div style="flex:1;min-width:0;">${col.map(s =>
|
||||
`<div style="margin-bottom:8px;">${vvStreamRow(s)}</div>`
|
||||
).join('')}</div>`;
|
||||
|
||||
const overflow = sessions.length > 12
|
||||
? `<div style="font-size:10px;color:#555;margin-top:4px;">+${sessions.length - 12} more not shown</div>`
|
||||
: '';
|
||||
|
||||
el.innerHTML = `<div class="vv-stream-servers">${badges}</div>
|
||||
<div style="display:flex;gap:10px;">${sCols.map(vvStreamCol).join('')}</div>${overflow}`;
|
||||
}
|
||||
|
||||
function vvPollStreams() {
|
||||
fetch('/plugins/varaverk/api/media.php')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const sessions = d.sessions ?? [];
|
||||
const names = d.server_names ?? [];
|
||||
const el = document.getElementById('vv-streams-body');
|
||||
vvLastSessions = d.sessions ?? [];
|
||||
vvLastStreamNames = d.server_names ?? [];
|
||||
vvStreamServerCount = d.server_count ?? 0;
|
||||
vvLastStreamPollAt = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (d.server_count === 0) {
|
||||
const el = document.getElementById('vv-streams-body');
|
||||
if (vvStreamServerCount === 0) {
|
||||
el.innerHTML = '<p class="vv-stream-empty">No media servers detected.<br>'
|
||||
+ '<span>Add EMBY_API_KEY / JELLYFIN_API_KEY / PLEX_TOKEN to master.conf to configure.</span></p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Server badges in header
|
||||
const badges = names.map(n =>
|
||||
`<span class="vv-server-badge">${n}</span>`
|
||||
).join('');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
el.innerHTML = `<div class="vv-stream-servers">${badges}</div>`
|
||||
+ '<p class="vv-stream-empty">Nothing playing</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = sessions.map(s => {
|
||||
const icon = s.paused ? '⏸' : '▶';
|
||||
const barColor = s.is_tc ? '#ff9800' : '#4caf50';
|
||||
const methColor = s.is_tc ? '#ff9800' : '#4caf50';
|
||||
const timeStr = s.dur_sec > 0
|
||||
? `${vvFmtSec(s.pos_sec)} / ${vvFmtSec(s.dur_sec)}`
|
||||
: '';
|
||||
return `<div class="vv-stream-row">
|
||||
<div class="vv-stream-top">
|
||||
<span class="vv-stream-icon">${icon}</span>
|
||||
<span class="vv-stream-title" title="${s.title}">${s.title}</span>
|
||||
<span class="vv-server-badge vv-server-badge-sm">${s.server}</span>
|
||||
</div>
|
||||
<div class="vv-stream-meta">
|
||||
<span class="vv-stream-user">${s.user}</span>
|
||||
<span class="vv-stream-client">${s.client}</span>
|
||||
<span class="vv-stream-method" style="color:${methColor};">${s.method}</span>
|
||||
${timeStr ? `<span class="vv-stream-time">${timeStr}</span>` : ''}
|
||||
</div>
|
||||
<div class="vv-stream-bar">
|
||||
<div style="width:${s.pct}%;background:${barColor};"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.innerHTML = `<div class="vv-stream-servers">${badges}</div>${rows}`;
|
||||
vvRenderStreams();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
vvPollStreams();
|
||||
setInterval(vvPollStreams, 12000);
|
||||
setInterval(vvRenderStreams, 1000);
|
||||
|
||||
// ── System clock tick (updates time every 30s without a full poll) ────────────
|
||||
function vvTickClock() {
|
||||
|
||||
Reference in New Issue
Block a user