Varaverk: monitor grid overhaul + Containers & VMs card
Monitor layout (8-col grid): - Row 1: System | Power | CPU | Memory | Network - Row 2: Scripts | Partner & Fallback (span 3) | Containers & VMs (span 4) - Row 3: GPU (span 2) | Transcode (span 2) | Streams (span 4) - Row 4: Parity (cols 1-2) | Pools (cols 3-4) | Array (cols 5-8) New Containers & VMs card: - Reads FolderView3 folders from docker.json; expand/collapse per folder - VMs listed first (virsh); containers show start/stop/webui/edit actions - 2-column balanced layout; collapses to 1 column below 900px - Docker WebUI URLs resolved from dockerMan template XMLs New files: include/vms.php, include/docker_folders.php, api/docker_action.php, api/snapshot.php Responsive: explicit grid-column placements reset at 1024px breakpoint
This commit is contained in:
@@ -2,11 +2,38 @@
|
||||
|
||||
<div id="vv-monitor" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;">
|
||||
|
||||
<!-- Row 1: System | Power | CPU | Memory | Network -->
|
||||
<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="grid-column:span 1;">
|
||||
<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-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="grid-column:span 2;">
|
||||
<h3>Memory</h3>
|
||||
<div id="vv-memory-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-network" style="grid-column:span 2;">
|
||||
<h3>Network</h3>
|
||||
<div id="vv-network-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Scripts | Partner & Fallback (span 3) | Containers & VMs (span 4) -->
|
||||
<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-partner" style="grid-column:span 3;">
|
||||
<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;">
|
||||
@@ -33,47 +60,13 @@
|
||||
<div id="vv-partner-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-cpu" style="grid-column:span 2;">
|
||||
<h3>CPU</h3>
|
||||
<div id="vv-cpu-body">Loading...</div>
|
||||
<div class="vv-card" id="vv-docker-folders" style="grid-column:span 4;">
|
||||
<h3>Containers and VMs</h3>
|
||||
<div id="vv-docker-folders-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<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="grid-column:span 2;">
|
||||
<h3>Network</h3>
|
||||
<div id="vv-network-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<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;">
|
||||
<!-- Row 3: GPU | Transcode | Streams -->
|
||||
<div class="vv-card" id="vv-gpu-card" style="grid-column:span 2;">
|
||||
<h3>GPU</h3>
|
||||
<div id="vv-gpu-body">Loading...</div>
|
||||
</div>
|
||||
@@ -88,14 +81,23 @@
|
||||
<div id="vv-streams-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Row 4: Parity (cols 1-2) | Pools (col 3, span 2) | Array (col 5, span 4) -->
|
||||
<div class="vv-card" id="vv-parity-card" style="grid-column:1/span 2;">
|
||||
<h3>Parity</h3>
|
||||
<div id="vv-parity-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-storage-card" style="grid-column:3/span 2;">
|
||||
<h3>Pools</h3>
|
||||
<div id="vv-storage-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-array-card" style="grid-column:5/span 4;">
|
||||
<h3>Array</h3>
|
||||
<div id="vv-array-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -251,14 +253,14 @@ function vvRenderCpu(cpu) {
|
||||
|
||||
// Per-core vertical bars
|
||||
if (cores.length) {
|
||||
html += `<div style="display:flex;align-items:flex-end;gap:3px;height:54px;margin:10px 0 4px;">`;
|
||||
html += `<div class="vv-cpu-cores" style="display:flex;align-items:flex-end;gap:3px;height:54px;margin:10px 0 4px;">`;
|
||||
cores.forEach(c => {
|
||||
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;">
|
||||
html += `<div class="vv-cpu-core" style="flex:1;display:flex;flex-direction:column;align-items:center;gap:1px;min-width:10px;">
|
||||
<div style="font-size:8px;color:#555;line-height:1;">${label}</div>
|
||||
<div style="width:100%;height:46px;background:#1a1a1a;border-radius:2px;display:flex;align-items:flex-end;overflow:hidden;">
|
||||
<div style="width:100%;height:${barH}px;background:${color};border-radius:2px 2px 0 0;transition:height 0.4s;"></div>
|
||||
@@ -883,14 +885,11 @@ function vvPollMonitor() {
|
||||
</div>${sessHtml}`;
|
||||
}
|
||||
|
||||
// ── Docker ──────────────────────────────────────────────────────────────
|
||||
const containers = d.containers ?? [];
|
||||
const tbody = document.getElementById('vv-docker-body');
|
||||
tbody.innerHTML = containers.length
|
||||
? containers.map(c =>
|
||||
`<tr><td>${c.name}</td><td class="vv-status-${c.status.startsWith('Up') ? 'up' : 'down'}">${c.status}</td><td>${c.image}</td></tr>`
|
||||
).join('')
|
||||
: '<tr><td colspan="3">No running containers</td></tr>';
|
||||
// ── Containers and VMs ──────────────────────────────────────────────────
|
||||
const dfData = d.docker_folders ?? { available: false, folders: [], ungrouped: [] };
|
||||
dfData.vms = d.vms ?? { available: false, vms: [] };
|
||||
vvRenderDockerFolders(dfData);
|
||||
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
@@ -1038,6 +1037,167 @@ function vvTickClock() {
|
||||
}
|
||||
setInterval(vvTickClock, 30000);
|
||||
|
||||
// ── Containers and VMs card ───────────────────────────────────────────────────
|
||||
|
||||
let vvDfFolderOpen = {};
|
||||
let vvDfActive = null;
|
||||
let vvDfData = null;
|
||||
|
||||
function vvToggleFolder(id) {
|
||||
vvDfFolderOpen[id] = !vvDfFolderOpen[id];
|
||||
vvRenderDockerFolders(vvDfData);
|
||||
}
|
||||
|
||||
function vvToggleContainer(name) {
|
||||
vvDfActive = vvDfActive === name ? null : name;
|
||||
vvRenderDockerFolders(vvDfData);
|
||||
}
|
||||
|
||||
function vvDockerAction(action, name, webui) {
|
||||
if (action === 'webui') { window.open(webui, '_blank'); return; }
|
||||
if (action === 'edit') {
|
||||
window.location.href = '/Docker?action=template&xmlTemplate=' +
|
||||
encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.set('action', action);
|
||||
fd.set('name', name);
|
||||
fetch('/plugins/varaverk/api/docker_action.php', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(() => { vvDfActive = null; setTimeout(vvPollMonitor, 1500); })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function vvRenderDockerFolders(data) {
|
||||
vvDfData = data;
|
||||
const el = document.getElementById('vv-docker-folders-body');
|
||||
if (!el || !data) return;
|
||||
|
||||
const osIcon = os => ({ windows:'🪟', macos:'🍎', bsd:'🦬' })[os] ?? '🐧';
|
||||
const stateColor = s => ({ running:'#4caf50', paused:'#ff9800' })[s] ?? '#444';
|
||||
const stateLabel = s => ({ running:'Running', paused:'Paused', 'shut off':'Off', crashed:'Crashed' })[s] ?? s;
|
||||
|
||||
// ── VMs section ─────────────────────────────────────────────────────────────
|
||||
let html = '';
|
||||
const vms = data.vms?.vms ?? [];
|
||||
if (!vms.length) {
|
||||
html += '<div class="vv-df-empty">No VMs configured</div>';
|
||||
} else {
|
||||
vms.forEach(vm => {
|
||||
const sc = stateColor(vm.state);
|
||||
const pulse = vm.state === 'running' ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
|
||||
const parts = [];
|
||||
if (vm.vcpus) parts.push(vm.vcpus + ' vCPU');
|
||||
if (vm.mem_mb) parts.push(vm.mem_mb >= 1024 ? (vm.mem_mb/1024).toFixed(0)+' GB' : vm.mem_mb+' MB');
|
||||
const meta = parts.length ? `<span class="vv-df-vm-meta"> · ${parts.join(' · ')}</span>` : '';
|
||||
html += `<div class="vv-df-vm-row">
|
||||
<span class="vv-df-vm-icon">${osIcon(vm.os)}</span>
|
||||
<span style="width:7px;height:7px;border-radius:50%;background:${sc};flex-shrink:0;${pulse}"></span>
|
||||
<span class="vv-df-cname">${vm.name}</span>
|
||||
<span style="font-size:11px;font-weight:600;color:${sc};flex-shrink:0;">${stateLabel(vm.state)}</span>
|
||||
${meta}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.available) {
|
||||
html += '<div class="vv-df-empty">Docker not available</div>';
|
||||
el.innerHTML = html;
|
||||
return;
|
||||
}
|
||||
|
||||
function renderContainer(c) {
|
||||
const dot = c.running ? '#4caf50' : '#555';
|
||||
const pulse = c.running ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
|
||||
const active = vvDfActive === c.name;
|
||||
const sShort = c.status ? c.status.replace(/^Up\s+/, '').split(' ').slice(0,2).join(' ') : '—';
|
||||
const sn = c.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
const sw = (c.webui||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
|
||||
let actionBar = '';
|
||||
if (active) {
|
||||
const ta = c.running ? 'stop' : 'start';
|
||||
const tl = c.running ? '⏹ Stop' : '▶ Start';
|
||||
const tc = c.running ? '#f44336' : '#4caf50';
|
||||
const ws = c.webui ? '' : 'opacity:0.3;pointer-events:none;';
|
||||
actionBar = `<div class="vv-df-actions">
|
||||
<button onclick="event.stopPropagation();vvDockerAction('${ta}','${sn}','')"
|
||||
class="vv-btn-sm" style="border-color:${tc};color:${tc};">${tl}</button>
|
||||
<button onclick="event.stopPropagation();vvDockerAction('webui','${sn}','${sw}')"
|
||||
class="vv-btn-sm vv-run-btn" style="${ws}">🌐 WebUI</button>
|
||||
<button onclick="event.stopPropagation();vvDockerAction('edit','${sn}','')"
|
||||
class="vv-btn-sm vv-edit-btn">✎ Edit</button>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="vv-df-container${active ? ' vv-df-active' : ''}"
|
||||
onclick="event.stopPropagation();vvToggleContainer('${sn}')">
|
||||
<span class="vv-df-dot" style="background:${dot};${pulse}"></span>
|
||||
<span class="vv-df-cname">${c.name}</span>
|
||||
<span class="vv-df-status">${sShort}</span>
|
||||
</div>${actionBar}`;
|
||||
}
|
||||
|
||||
function renderFolder(f) {
|
||||
const open = !!vvDfFolderOpen[f.id];
|
||||
const total = f.containers.length;
|
||||
const running = f.containers.filter(c => c.running).length;
|
||||
const bColor = running === total ? '#4caf50' : running === 0 ? '#555' : '#ff9800';
|
||||
const badge = `<span style="font-size:10px;color:${bColor};flex-shrink:0;">${running}/${total}</span>`;
|
||||
const sid = f.id.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
|
||||
let iconHtml = '';
|
||||
if (f.isEmoji) {
|
||||
iconHtml = `<span style="font-size:11px;flex-shrink:0;">${f.icon}</span>`;
|
||||
} else if (f.icon) {
|
||||
iconHtml = `<img src="${f.icon}" style="width:13px;height:13px;object-fit:contain;border-radius:2px;flex-shrink:0;" onerror="this.style.display='none'">`;
|
||||
}
|
||||
|
||||
let out = `<div class="vv-df-folder">
|
||||
<div class="vv-df-folder-hdr" onclick="vvToggleFolder('${sid}')">
|
||||
<span class="vv-df-chevron">${open ? '▾' : '▸'}</span>
|
||||
${iconHtml}
|
||||
<span class="vv-df-fname">${f.name}</span>
|
||||
${badge}
|
||||
</div>`;
|
||||
if (open) {
|
||||
out += '<div class="vv-df-folder-body">';
|
||||
f.containers.forEach(c => { out += renderContainer(c); });
|
||||
out += '</div>';
|
||||
}
|
||||
out += '</div>';
|
||||
return out;
|
||||
}
|
||||
|
||||
// Build folder list — ungrouped gets a 📁 emoji icon
|
||||
const allFolders = (data.folders ?? []).map(f => ({ ...f, isEmoji: false }));
|
||||
const ug = data.ungrouped ?? [];
|
||||
if (ug.length) allFolders.push({ id:'__ungrouped__', name:'Ungrouped', icon:'📁', isEmoji:true, containers:ug });
|
||||
|
||||
if (!allFolders.length) {
|
||||
html += '<div class="vv-df-empty">No containers found</div>';
|
||||
el.innerHTML = html;
|
||||
return;
|
||||
}
|
||||
|
||||
// 2-column balanced split
|
||||
const half = Math.ceil(allFolders.length / 2);
|
||||
const left = allFolders.slice(0, half);
|
||||
const right = allFolders.slice(half);
|
||||
|
||||
html += `<div class="vv-df-cols">
|
||||
<div class="vv-df-col">${left.map(renderFolder).join('')}</div>
|
||||
<div class="vv-df-col">${right.map(renderFolder).join('')}</div>
|
||||
</div>`;
|
||||
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// Close action bar when clicking outside the card
|
||||
document.addEventListener('click', () => {
|
||||
if (vvDfActive !== null) { vvDfActive = null; vvRenderDockerFolders(vvDfData); }
|
||||
});
|
||||
|
||||
// ── Array actions ─────────────────────────────────────────────────────────────
|
||||
function vvArrayAction(action) {
|
||||
const labels = {stop: 'Stop Array', shutdown: 'Shutdown', restart: 'Restart'};
|
||||
|
||||
@@ -805,9 +805,25 @@ Still the same two servers, two households, the same media stack running itself.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vv-sched-footer vv-sched-info" id="vv-sched-info-footer">
|
||||
<span id="vv-footer-scheduled"><?= $scheduledJobs ?> of <?= $totalJobs ?> job<?= $totalJobs !== 1 ? 's' : '' ?> scheduled</span>
|
||||
<span id="vv-footer-running"><?= !empty($runningScripts) ? 'Running: ' . htmlspecialchars(implode(', ', $runningScripts)) : 'Idle' ?></span>
|
||||
<div class="vv-sched-footer vv-sched-info vv-snap-footer" id="vv-sched-info-footer">
|
||||
<span class="vv-snap-item">
|
||||
<span class="vv-snap-label">CPU</span>
|
||||
<span class="vv-snap-bar"><span id="vv-snap-cpu-bar"></span></span>
|
||||
<span id="vv-snap-cpu-val" class="vv-snap-val">—</span>
|
||||
</span>
|
||||
<span class="vv-snap-item">
|
||||
<span class="vv-snap-label">RAM</span>
|
||||
<span class="vv-snap-bar"><span id="vv-snap-ram-bar"></span></span>
|
||||
<span id="vv-snap-ram-val" class="vv-snap-val">—</span>
|
||||
</span>
|
||||
<span class="vv-snap-div">·</span>
|
||||
<span id="vv-snap-fallback" class="vv-snap-state">—</span>
|
||||
<span class="vv-snap-div">·</span>
|
||||
<span id="vv-snap-partner" class="vv-snap-partner">—</span>
|
||||
<span class="vv-snap-div">·</span>
|
||||
<span id="vv-snap-streams" class="vv-snap-media">—</span>
|
||||
<span class="vv-snap-div">·</span>
|
||||
<span id="vv-snap-transcodes" class="vv-snap-media">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1284,32 +1300,30 @@ function vvSaveChild(el) {
|
||||
|
||||
function vvSaveAll() {
|
||||
const status = document.getElementById('vv-save-all-status');
|
||||
// Collect jobs to save: orchs always; children only when their orch is OFF (orch manages them when on).
|
||||
// conf_flag children are always immediate-save (flag_toggle.php), never batch-saved.
|
||||
const toSave = [];
|
||||
document.querySelectorAll('#vv-sched-left [data-id]').forEach(job => {
|
||||
if (job.classList.contains('vv-script')) {
|
||||
if (job.dataset.type === 'conf_flag') return; // immediate-save only
|
||||
if (job.dataset.type === 'conf_flag') return;
|
||||
const orchCard = job.closest('.vv-sched-card');
|
||||
const orchOn = orchCard?.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
|
||||
if (orchOn) return; // child under active orch — cron suppressed, skip
|
||||
if (orchOn) return;
|
||||
}
|
||||
toSave.push(job);
|
||||
});
|
||||
let pending = toSave.length, allOk = true;
|
||||
if (!pending) { vvFlashStatus(status, '✓ Saved', true); return; }
|
||||
if (!toSave.length) { vvFlashStatus(status, '✓ Saved', true); return; }
|
||||
status.textContent = 'Saving…';
|
||||
toSave.forEach(job => {
|
||||
const id = job.dataset.id;
|
||||
const enabled = job.querySelector('.vv-enabled').checked ? '1' : '0';
|
||||
const cron = job.querySelector('.vv-cron')?.value.trim() ?? '';
|
||||
const log_enabled = job.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
||||
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
||||
.then(d => {
|
||||
if (d.ok) vvFlashSaved(job); else allOk = false;
|
||||
if (--pending === 0) vvFlashStatus(status, allOk ? '✓ All saved' : '✗ Some failed', allOk);
|
||||
});
|
||||
});
|
||||
const batch = toSave.map(job => ({
|
||||
id: job.dataset.id,
|
||||
enabled: job.querySelector('.vv-enabled').checked ? '1' : '0',
|
||||
cron: job.querySelector('.vv-cron')?.value.trim() ?? '',
|
||||
log_enabled: job.querySelector('.vv-log-enabled')?.checked ? '1' : '0',
|
||||
}));
|
||||
vvPost('/plugins/varaverk/api/scheduler.php', {batch: JSON.stringify(batch)})
|
||||
.then(d => {
|
||||
if (d.ok) toSave.forEach(job => vvFlashSaved(job));
|
||||
vvFlashStatus(status, d.ok ? '✓ All saved' : '✗ ' + (d.error ?? 'Failed'), d.ok);
|
||||
})
|
||||
.catch(() => vvFlashStatus(status, '✗ Request failed', false));
|
||||
}
|
||||
|
||||
function vvToggleAdvanced(btn) {
|
||||
@@ -2603,6 +2617,78 @@ function vvToggleFolder(header) {
|
||||
if (chevron) chevron.textContent = open ? '▾' : '▸';
|
||||
}
|
||||
|
||||
// ── Snapshot footer poll ──────────────────────────────────────────────────────
|
||||
let vvSnapTimer = null;
|
||||
function vvPollSnapshot() {
|
||||
fetch('/plugins/varaverk/api/snapshot.php')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const cpuBar = document.getElementById('vv-snap-cpu-bar');
|
||||
const cpuVal = document.getElementById('vv-snap-cpu-val');
|
||||
if (cpuBar && cpuVal) {
|
||||
const p = d.cpu_pct ?? 0;
|
||||
cpuBar.style.width = p + '%';
|
||||
cpuBar.style.backgroundColor = p > 85 ? '#f44336' : p > 60 ? '#ff9800' : '#4caf50';
|
||||
cpuVal.textContent = p + '%';
|
||||
}
|
||||
const ramBar = document.getElementById('vv-snap-ram-bar');
|
||||
const ramVal = document.getElementById('vv-snap-ram-val');
|
||||
if (ramBar && ramVal) {
|
||||
const p = d.ram_pct ?? 0;
|
||||
ramBar.style.width = p + '%';
|
||||
ramBar.style.backgroundColor = p > 85 ? '#f44336' : p > 70 ? '#ff9800' : '#4caf50';
|
||||
ramVal.textContent = p + '%';
|
||||
}
|
||||
const fb = document.getElementById('vv-snap-fallback');
|
||||
if (fb) {
|
||||
const s = (d.fallback ?? 'UNKNOWN').toUpperCase();
|
||||
fb.textContent = s;
|
||||
fb.style.color = s === 'NORMAL' ? '#4caf50'
|
||||
: s === 'FAILOVER' ? '#f44336'
|
||||
: s === 'NO_INTERNET' ? '#ff9800' : '#666';
|
||||
}
|
||||
const pt = document.getElementById('vv-snap-partner');
|
||||
if (pt) {
|
||||
if (!d.partner_enabled) {
|
||||
pt.textContent = 'No partnership'; pt.style.color = '#444';
|
||||
} else {
|
||||
const peers = d.peers ?? [];
|
||||
const on = peers.filter(p => p.online === true);
|
||||
const off = peers.filter(p => p.online !== true);
|
||||
if (off.length > 0 && peers.length === 1) {
|
||||
pt.textContent = off[0].hostname + ' ✕'; pt.style.color = '#f44336';
|
||||
} else if (off.length > 0) {
|
||||
pt.textContent = off.length + ' peer' + (off.length > 1 ? 's' : '') + ' down'; pt.style.color = '#f44336';
|
||||
} else if (on.length === 1) {
|
||||
pt.textContent = on[0].hostname + ' ●'; pt.style.color = '#4caf50';
|
||||
} else if (on.length > 1) {
|
||||
pt.textContent = on.length + ' peers ●'; pt.style.color = '#4caf50';
|
||||
} else {
|
||||
pt.textContent = '—'; pt.style.color = '#555';
|
||||
}
|
||||
}
|
||||
}
|
||||
const sm = document.getElementById('vv-snap-streams');
|
||||
if (sm) {
|
||||
const sc = d.stream_count ?? 0;
|
||||
sm.textContent = sc + (sc === 1 ? ' stream' : ' streams');
|
||||
sm.style.color = sc > 0 ? '#aaa' : '#444';
|
||||
}
|
||||
const tc = document.getElementById('vv-snap-transcodes');
|
||||
if (tc) {
|
||||
const n = d.transcode_count ?? 0;
|
||||
tc.textContent = n + (n === 1 ? ' transcode' : ' transcodes');
|
||||
tc.style.color = n > 0 ? '#ff9800' : '#444';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
function vvStartSnapPoll() {
|
||||
if (vvSnapTimer) return;
|
||||
vvPollSnapshot();
|
||||
vvSnapTimer = setInterval(vvPollSnapshot, 10000);
|
||||
}
|
||||
|
||||
function vvGetCurrentFolders() {
|
||||
const folders = {};
|
||||
document.querySelectorAll('.vv-folder-group').forEach(fg => {
|
||||
@@ -2701,5 +2787,6 @@ requestAnimationFrame(function() {
|
||||
}
|
||||
vvFitRight();
|
||||
vvStartStatusPoll();
|
||||
vvStartSnapPoll();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user