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:
Gmer4Lfe
2026-05-26 18:59:19 -04:00
parent 6078af0dbb
commit 072c8b720d
12 changed files with 689 additions and 83 deletions
@@ -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>