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'};
|
||||
|
||||
Reference in New Issue
Block a user