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:
@@ -1,4 +1,4 @@
|
||||
Menu="Utilities:85"
|
||||
Menu="Tasks:95"
|
||||
Title="Varaverk"
|
||||
Icon="varaverk.png"
|
||||
---
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = trim($_POST['action'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
|
||||
if (!$name || !in_array($action, ['start', 'stop'], true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Confirm container exists
|
||||
$check = trim(shell_exec('docker ps -a --filter ' . escapeshellarg('name=^' . $name . '$') . " --format '{{.Names}}' 2>/dev/null") ?? '');
|
||||
if ($check !== $name) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Container not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
exec(($action === 'start' ? 'docker start' : 'docker stop') . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc);
|
||||
|
||||
echo json_encode(['ok' => $rc === 0, 'output' => implode("\n", $out)]);
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/monitor.php';
|
||||
require_once dirname(__DIR__) . '/include/vms.php';
|
||||
require_once dirname(__DIR__) . '/include/docker_folders.php';
|
||||
|
||||
echo json_encode([
|
||||
'system' => vv_system_info(),
|
||||
@@ -20,7 +22,9 @@ echo json_encode([
|
||||
'parity' => vv_parity_status(),
|
||||
'storage' => vv_storage_pools(),
|
||||
'array_disks' => vv_array_disks(),
|
||||
'scripts' => vv_scripts_status(),
|
||||
'thresholds' => vv_disk_thresholds(),
|
||||
'ts' => time(),
|
||||
'scripts' => vv_scripts_status(),
|
||||
'thresholds' => vv_disk_thresholds(),
|
||||
'vms' => vv_get_vms(),
|
||||
'docker_folders' => vv_get_docker_folders(),
|
||||
'ts' => time(),
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,28 @@
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
// Batch save — all entries in one load/write/rebuild cycle
|
||||
if (!empty($_POST['batch'])) {
|
||||
$entries = json_decode($_POST['batch'], true) ?: [];
|
||||
$clean = [];
|
||||
foreach ($entries as $e) {
|
||||
$id = trim($e['id'] ?? '');
|
||||
$cron = trim($e['cron'] ?? '');
|
||||
if (!$id) continue;
|
||||
if ($cron && !in_array($cron, ['@array_start', '@array_stop'], true)
|
||||
&& !preg_match('/^(\S+\s+){4}\S+$/', $cron)) $cron = '';
|
||||
$clean[] = [
|
||||
'id' => $id,
|
||||
'enabled' => ($e['enabled'] ?? '0') === '1',
|
||||
'cron' => $cron,
|
||||
'log_enabled' => ($e['log_enabled'] ?? '0') === '1',
|
||||
];
|
||||
}
|
||||
$ok = vv_schedule_update_batch($clean);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write schedule']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
$enabled = (bool)($_POST['enabled'] ?? false);
|
||||
$cron = trim($_POST['cron'] ?? '');
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/monitor.php';
|
||||
require_once dirname(__DIR__) . '/include/media.php';
|
||||
|
||||
// CPU% — delta from own state file so it doesn't conflict with monitor.php
|
||||
$cpuPct = 0;
|
||||
$cpuLine = '';
|
||||
foreach (file('/proc/stat') ?: [] as $line) {
|
||||
if (strncmp($line, 'cpu ', 4) === 0) { $cpuLine = $line; break; }
|
||||
}
|
||||
if (preg_match('/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/', $cpuLine, $m)) {
|
||||
$c = [(int)$m[1],(int)$m[2],(int)$m[3],(int)$m[4],(int)$m[5],(int)$m[6],(int)$m[7]];
|
||||
$sf = '/tmp/vv_snap_cpu.json';
|
||||
$p = file_exists($sf) ? (json_decode(file_get_contents($sf), true) ?: null) : null;
|
||||
file_put_contents($sf, json_encode($c));
|
||||
if ($p && is_array($p)) {
|
||||
$dt = array_sum($c) - array_sum($p);
|
||||
$di = ($c[3] + $c[4]) - ($p[3] + $p[4]);
|
||||
$cpuPct = $dt > 0 ? max(0, min(100, (int)round((1 - $di / $dt) * 100))) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// RAM%
|
||||
$mem = [];
|
||||
foreach (file('/proc/meminfo') ?: [] as $line) {
|
||||
if (preg_match('/^(MemTotal|MemAvailable):\s+(\d+)/', $line, $m)) $mem[$m[1]] = (int)$m[2];
|
||||
}
|
||||
$ramTotalMb = (int)(($mem['MemTotal'] ?? 0) / 1024);
|
||||
$ramUsedMb = (int)((($mem['MemTotal'] ?? 0) - ($mem['MemAvailable'] ?? 0)) / 1024);
|
||||
$ramPct = $ramTotalMb > 0 ? (int)round($ramUsedMb / $ramTotalMb * 100) : 0;
|
||||
|
||||
// Fallback state (fast file read, no exec)
|
||||
$fallbackState = 'UNKNOWN';
|
||||
foreach (@file('/tmp/fallback_state.db') ?: [] as $line) {
|
||||
if (preg_match('/^state=(.+)/', trim($line), $m)) { $fallbackState = trim($m[1]); break; }
|
||||
}
|
||||
|
||||
// Partner
|
||||
$partner = vv_partner_state();
|
||||
$peers = array_values(array_filter($partner['hosts'], fn($h) => !$h['is_me']));
|
||||
|
||||
// Media sessions — cached 30s so the HTTP calls don't hold up every snapshot poll
|
||||
$streamCount = 0;
|
||||
$transcodeCount = 0;
|
||||
$mediaCacheFile = '/tmp/vv_snap_media.json';
|
||||
$cacheMaxAge = 30;
|
||||
$cacheValid = file_exists($mediaCacheFile) && (time() - filemtime($mediaCacheFile)) < $cacheMaxAge;
|
||||
if ($cacheValid) {
|
||||
$cached = json_decode(file_get_contents($mediaCacheFile), true) ?: [];
|
||||
} else {
|
||||
$media = vv_media_sessions();
|
||||
$cached = [
|
||||
'stream_count' => count($media['sessions']),
|
||||
'transcode_count' => count(array_filter($media['sessions'], fn($s) => !empty($s['is_tc']))),
|
||||
];
|
||||
file_put_contents($mediaCacheFile, json_encode($cached));
|
||||
}
|
||||
$streamCount = (int)($cached['stream_count'] ?? 0);
|
||||
$transcodeCount = (int)($cached['transcode_count'] ?? 0);
|
||||
|
||||
echo json_encode([
|
||||
'cpu_pct' => $cpuPct,
|
||||
'ram_pct' => $ramPct,
|
||||
'ram_used_mb' => $ramUsedMb,
|
||||
'ram_total_mb' => $ramTotalMb,
|
||||
'fallback' => $fallbackState,
|
||||
'partner_enabled' => $partner['enabled'],
|
||||
'peers' => $peers,
|
||||
'stream_count' => $streamCount,
|
||||
'transcode_count' => $transcodeCount,
|
||||
]);
|
||||
@@ -121,6 +121,17 @@
|
||||
@media (max-width: 1024px) {
|
||||
#vv-monitor { grid-template-columns: repeat(4, 1fr) !important; }
|
||||
#vv-docker { grid-column: span 4 !important; }
|
||||
/* Reset explicit placements so cards reflow in the 4-col grid */
|
||||
#vv-docker-folders { grid-column: span 4 !important; }
|
||||
#vv-parity-card { grid-column: auto !important; }
|
||||
#vv-storage-card { grid-column: auto !important; }
|
||||
#vv-array-card { grid-column: auto !important; }
|
||||
}
|
||||
|
||||
/* Containers+VMs: single column when viewport is narrow */
|
||||
@media (max-width: 900px) {
|
||||
.vv-df-cols { flex-direction: column; }
|
||||
.vv-df-fname { flex: 0 1 auto; }
|
||||
}
|
||||
|
||||
/* Phone layout — scheduler row/actions fixes + monitor single-column */
|
||||
@@ -133,10 +144,30 @@
|
||||
.vv-cron { flex: 1 1 80px; width: auto; min-width: 80px; }
|
||||
/* Slightly tighter label on narrow screens */
|
||||
.vv-job-label { font-size: 14px; }
|
||||
/* Footer buttons wrap instead of overflowing */
|
||||
.vv-sched-footer { flex-wrap: wrap; }
|
||||
/* Log toolbar wraps so Stop/Clear stay inside the card */
|
||||
.vv-log-toolbar { flex-wrap: wrap; align-items: flex-start; }
|
||||
.vv-log-toolbar > div { flex-wrap: wrap; }
|
||||
/* Snapshot footer — smaller on mobile */
|
||||
.vv-snap-footer { gap: 10px !important; }
|
||||
.vv-snap-item { gap: 4px; }
|
||||
.vv-snap-label { font-size: 10px; }
|
||||
.vv-snap-bar { width: 44px; height: 5px; }
|
||||
.vv-snap-val { font-size: 11px; min-width: 26px; }
|
||||
.vv-snap-div { font-size: 11px; }
|
||||
.vv-snap-state { font-size: 11px; }
|
||||
#vv-snap-partner { font-size: 11px; }
|
||||
.vv-snap-media { font-size: 11px; }
|
||||
|
||||
/* Monitor single-column */
|
||||
/* CPU core bars — shrink gap and min-width so many cores don't overflow */
|
||||
.vv-cpu-cores { gap: 1px !important; }
|
||||
.vv-cpu-core { min-width: 4px !important; }
|
||||
|
||||
/* Monitor single-column — explicit placement cards need override too */
|
||||
#vv-monitor { grid-template-columns: 1fr !important; }
|
||||
#vv-monitor > .vv-card { grid-column: 1 / -1 !important; }
|
||||
#vv-docker-folders { grid-column: 1 / -1 !important; }
|
||||
}
|
||||
|
||||
/* Shared footer (Save Schedule left, info right) — same min-height so log card ends level with script cards */
|
||||
@@ -481,6 +512,8 @@ code.vv-unknown-var { color: #ff9800; background: #1f130d; }
|
||||
.vv-arrange-btn-active { background: #1a3a1e !important; color: #4caf50 !important; border-color: #2d5c33 !important; }
|
||||
.vv-arrange-save-btn { background: #1a3a1e; border-color: #2d5c33; color: #4caf50; }
|
||||
.vv-arrange-save-btn:hover { background: #22502a; }
|
||||
#vv-arrange-btn { background: #7b1fa2; border-color: #7b1fa2; }
|
||||
#vv-arrange-btn:hover { background: #4a148c; border-color: #4a148c; }
|
||||
|
||||
/* ── Arrange workspace panel ─────────────────────────────────────────────── */
|
||||
.vv-arrange-ws-hdr { font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase;
|
||||
@@ -518,8 +551,53 @@ code.vv-unknown-var { color: #ff9800; background: #1f130d; }
|
||||
.vv-folder-children.vv-drop-target { background: rgba(76,175,80,0.07); border-radius: 4px;
|
||||
outline: 1px dashed #3a6a3e; }
|
||||
.vv-folder-new-row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; }
|
||||
.vv-new-folder-btn { background: #1c1e1c; border-color: #2e3a2e; color: #6a9e6a; }
|
||||
.vv-new-folder-btn:hover { background: #222e22; }
|
||||
.vv-new-folder-btn { background: #0277bd !important; border: none !important; color: #fff !important; }
|
||||
.vv-new-folder-btn:hover { background: #01579b !important; }
|
||||
|
||||
/* ── Snapshot footer (right panel status bar) ────────────────────────────── */
|
||||
.vv-snap-footer { display: flex !important; flex-direction: row !important;
|
||||
align-items: center !important; justify-content: center; gap: 20px; flex-wrap: wrap; }
|
||||
.vv-snap-item { display: flex; align-items: center; gap: 7px; }
|
||||
.vv-snap-label { font-size: 12px; color: #555; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.vv-snap-bar { width: 90px; height: 8px; background: #222; border-radius: 4px; overflow: hidden; flex-shrink: 0; }
|
||||
.vv-snap-bar span { display: block; height: 100%; border-radius: 4px; width: 0;
|
||||
transition: width 0.5s, background-color 0.5s; }
|
||||
.vv-snap-val { font-size: 15px; color: #aaa; font-family: monospace; min-width: 36px; }
|
||||
.vv-snap-div { color: #333; font-size: 15px; }
|
||||
.vv-snap-state { font-size: 15px; font-weight: bold; }
|
||||
#vv-snap-partner { font-size: 15px; color: #666; }
|
||||
.vv-snap-media { font-size: 15px; color: #666; }
|
||||
|
||||
/* ── Containers and VMs card ──────────────────────────────────────────────── */
|
||||
.vv-df-section-hdr { font-size: 10px; font-weight: bold; color: #555; text-transform: uppercase;
|
||||
letter-spacing: 0.08em; padding: 4px 2px 5px; border-bottom: 1px solid #222;
|
||||
margin-bottom: 4px; }
|
||||
.vv-df-empty { font-size: 12px; color: #555; font-style: italic; padding: 4px 2px 8px; }
|
||||
.vv-df-vm-row { display: flex; align-items: center; gap: 8px; padding: 5px 2px;
|
||||
border-bottom: 1px solid #1a1a1a; }
|
||||
.vv-df-vm-row:last-of-type { border-bottom: none; }
|
||||
.vv-df-vm-icon { font-size: 15px; line-height: 1; flex-shrink: 0; }
|
||||
.vv-df-vm-meta { font-size: 10px; color: #555; }
|
||||
.vv-df-cols { display: flex; gap: 10px; align-items: flex-start; }
|
||||
.vv-df-col { flex: 1; min-width: 0; }
|
||||
.vv-df-folder { border-bottom: 1px solid #1a1a1a; }
|
||||
.vv-df-folder:last-child { border-bottom: none; }
|
||||
.vv-df-folder-hdr { display: flex; align-items: center; gap: 6px; padding: 5px 4px;
|
||||
cursor: pointer; user-select: none; border-radius: 3px; }
|
||||
.vv-df-folder-hdr:hover { background: rgba(255,255,255,0.03); }
|
||||
.vv-df-chevron { color: #555; font-size: 10px; width: 10px; flex-shrink: 0; }
|
||||
.vv-df-fname { flex: 1; font-size: 12px; color: #aaa; font-weight: 500;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.vv-df-folder-body { padding-left: 10px; padding-bottom: 3px; }
|
||||
.vv-df-container { display: flex; align-items: center; gap: 7px; padding: 3px 6px;
|
||||
cursor: pointer; border-radius: 3px; user-select: none; }
|
||||
.vv-df-container:hover { background: rgba(255,255,255,0.04); }
|
||||
.vv-df-active { background: rgba(255,255,255,0.05) !important; outline: 1px solid #444; }
|
||||
.vv-df-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||
.vv-df-cname { flex: 1; font-size: 12px; color: #ccc;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.vv-df-status { font-size: 10px; color: #555; flex-shrink: 0; white-space: nowrap; }
|
||||
.vv-df-actions { display: flex; gap: 6px; padding: 3px 6px 5px 24px; flex-wrap: wrap; }
|
||||
|
||||
/* ── Rsync standalone controls ────────────────────────────────────────────── */
|
||||
.vv-rsync-location { width: 120px; flex-shrink: 1; min-width: 60px; font-size: 11px; font-family: monospace;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 103 B |
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
function vv_local_ip(): string {
|
||||
static $ip = null;
|
||||
if ($ip !== null) return $ip;
|
||||
$ip = trim(shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? '');
|
||||
if (!$ip) $ip = gethostbyname(gethostname());
|
||||
return $ip;
|
||||
}
|
||||
|
||||
function vv_container_webui(string $name, array $portMap): string {
|
||||
$template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml';
|
||||
if (!file_exists($template)) return '';
|
||||
|
||||
$xml = @file_get_contents($template) ?: '';
|
||||
if (!preg_match('/<WebUI>(.*?)<\/WebUI>/s', $xml, $m)) return '';
|
||||
|
||||
$url = trim($m[1]);
|
||||
if (!$url) return '';
|
||||
|
||||
$url = str_replace('[IP]', vv_local_ip(), $url);
|
||||
|
||||
// [PORT:XXXX] → mapped host port
|
||||
$url = preg_replace_callback('/\[PORT:(\d+)\]/', function($pm) use ($name, $portMap) {
|
||||
return $portMap[$name][$pm[1]] ?? $pm[1];
|
||||
}, $url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
function vv_get_docker_folders(): array {
|
||||
$folderFile = '/boot/config/plugins/folder.view3/docker.json';
|
||||
$folderData = file_exists($folderFile)
|
||||
? (json_decode(@file_get_contents($folderFile), true) ?: [])
|
||||
: [];
|
||||
|
||||
// One docker ps call: names, status, port mappings
|
||||
$raw = shell_exec("docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null") ?? '';
|
||||
$statusMap = [];
|
||||
$portMap = [];
|
||||
foreach (explode("\n", trim($raw)) as $line) {
|
||||
$parts = explode("\t", $line, 3);
|
||||
if (count($parts) < 2) continue;
|
||||
[$cname, $status, $ports] = array_pad($parts, 3, '');
|
||||
$cname = trim($cname);
|
||||
if ($cname === '') continue;
|
||||
$statusMap[$cname] = trim($status);
|
||||
foreach (explode(',', $ports) as $entry) {
|
||||
if (preg_match('/(\d+)->(\d+)\/tcp/', trim($entry), $pm)) {
|
||||
$portMap[$cname][$pm[2]] = $pm[1]; // containerPort => hostPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$folderContainerNames = [];
|
||||
$folders = [];
|
||||
|
||||
foreach ($folderData as $id => $f) {
|
||||
$containers = [];
|
||||
foreach ($f['containers'] ?? [] as $cname) {
|
||||
$folderContainerNames[] = $cname;
|
||||
$status = $statusMap[$cname] ?? '';
|
||||
$running = str_starts_with($status, 'Up');
|
||||
$containers[] = [
|
||||
'name' => $cname,
|
||||
'running' => $running,
|
||||
'status' => $status,
|
||||
'webui' => vv_container_webui($cname, $portMap),
|
||||
];
|
||||
}
|
||||
usort($containers, fn($a, $b) => $b['running'] <=> $a['running'] ?: strcmp($a['name'], $b['name']));
|
||||
|
||||
$folders[] = [
|
||||
'id' => $id,
|
||||
'name' => $f['name'] ?? 'Unnamed',
|
||||
'icon' => $f['icon'] ?? '',
|
||||
'containers' => $containers,
|
||||
];
|
||||
}
|
||||
usort($folders, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
$ungrouped = [];
|
||||
foreach ($statusMap as $cname => $status) {
|
||||
if (in_array($cname, $folderContainerNames, true)) continue;
|
||||
$running = str_starts_with($status, 'Up');
|
||||
$ungrouped[] = [
|
||||
'name' => $cname,
|
||||
'running' => $running,
|
||||
'status' => $status,
|
||||
'webui' => vv_container_webui($cname, $portMap),
|
||||
];
|
||||
}
|
||||
usort($ungrouped, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return [
|
||||
'available' => true,
|
||||
'folders' => $folders,
|
||||
'ungrouped' => $ungrouped,
|
||||
];
|
||||
}
|
||||
@@ -33,6 +33,23 @@ function vv_schedule_update(string $id, bool $enabled, string $cron, bool $log_e
|
||||
return vv_cron_rebuild($schedule);
|
||||
}
|
||||
|
||||
function vv_schedule_update_batch(array $entries): bool {
|
||||
$schedule = vv_schedule_load();
|
||||
foreach ($entries as $e) {
|
||||
$id = trim($e['id'] ?? '');
|
||||
if (!$id) continue;
|
||||
$schedule[$id] = [
|
||||
'id' => $id,
|
||||
'enabled' => (bool)($e['enabled'] ?? false),
|
||||
'cron' => trim($e['cron'] ?? ''),
|
||||
'log_enabled' => (bool)($e['log_enabled'] ?? false),
|
||||
'updated' => date('c'),
|
||||
];
|
||||
}
|
||||
if (!vv_schedule_save($schedule)) return false;
|
||||
return vv_cron_rebuild($schedule);
|
||||
}
|
||||
|
||||
function vv_job_flags(string $id): string {
|
||||
$schedule = vv_schedule_load();
|
||||
return !empty($schedule[$id]['log_enabled']) ? '--log' : '';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
function vv_get_vms(): array {
|
||||
if (!file_exists('/usr/bin/virsh')) return ['available' => false, 'vms' => []];
|
||||
|
||||
exec('virsh list --all --name 2>/dev/null', $names, $rc);
|
||||
if ($rc !== 0) return ['available' => false, 'vms' => []];
|
||||
|
||||
$vms = [];
|
||||
foreach ($names as $raw) {
|
||||
$name = trim($raw);
|
||||
if ($name === '') continue;
|
||||
|
||||
$state = trim(shell_exec('virsh domstate ' . escapeshellarg($name) . ' 2>/dev/null') ?? 'unknown');
|
||||
|
||||
$vcpus = null;
|
||||
$memMb = null;
|
||||
if ($state === 'running') {
|
||||
$info = shell_exec('virsh dominfo ' . escapeshellarg($name) . ' 2>/dev/null') ?? '';
|
||||
if (preg_match('/CPU\(s\)\s*:\s*(\d+)/i', $info, $m)) $vcpus = (int)$m[1];
|
||||
if (preg_match('/Used memory\s*:\s*(\d+)/i', $info, $m)) $memMb = (int)round((int)$m[1] / 1024);
|
||||
}
|
||||
|
||||
// OS detection from libvirt XML
|
||||
$os = 'linux';
|
||||
$xmlPath = '/etc/libvirt/qemu/' . $name . '.xml';
|
||||
if (file_exists($xmlPath)) {
|
||||
$xml = @file_get_contents($xmlPath) ?: '';
|
||||
if (stripos($xml, 'windows') !== false || stripos($xml, 'win10') !== false || stripos($xml, 'win11') !== false) $os = 'windows';
|
||||
elseif (stripos($xml, 'darwin') !== false || stripos($xml, 'macos') !== false) $os = 'macos';
|
||||
}
|
||||
$nl = strtolower($name);
|
||||
if (str_contains($nl, 'win')) $os = 'windows';
|
||||
elseif (str_contains($nl, 'mac') || str_contains($nl, 'osx')) $os = 'macos';
|
||||
elseif (str_contains($nl, 'bsd') || str_contains($nl, 'freebsd')) $os = 'bsd';
|
||||
|
||||
$vms[] = [
|
||||
'name' => $name,
|
||||
'state' => $state,
|
||||
'os' => $os,
|
||||
'vcpus' => $vcpus,
|
||||
'mem_mb' => $memMb,
|
||||
];
|
||||
}
|
||||
|
||||
return ['available' => true, 'vms' => $vms];
|
||||
}
|
||||
@@ -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