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:
@@ -110,17 +110,18 @@ function vv_fetch_jf_sessions(array $srv): array {
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'server' => $srv['name'],
|
||||
'user' => $s['UserName'] ?? '?',
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
'client' => trim(($s['Client'] ?? '') . ' / ' . ($s['DeviceName'] ?? ''), ' /'),
|
||||
'method' => $method,
|
||||
'paused' => !empty($ps['IsPaused']),
|
||||
'pct' => $pct,
|
||||
'pos_sec' => (int)($pos / 10000000),
|
||||
'dur_sec' => (int)($dur / 10000000),
|
||||
'is_tc' => $tc !== null,
|
||||
'server' => $srv['name'],
|
||||
'server_type' => $srv['type'],
|
||||
'user' => $s['UserName'] ?? '?',
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
'client' => trim(($s['Client'] ?? '') . ' / ' . ($s['DeviceName'] ?? ''), ' /'),
|
||||
'method' => $method,
|
||||
'paused' => !empty($ps['IsPaused']),
|
||||
'pct' => $pct,
|
||||
'pos_sec' => (int)($pos / 10000000),
|
||||
'dur_sec' => (int)($dur / 10000000),
|
||||
'is_tc' => $tc !== null,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
@@ -160,17 +161,18 @@ function vv_fetch_plex_sessions(array $srv): array {
|
||||
|
||||
$player = $m['Player'] ?? [];
|
||||
$result[] = [
|
||||
'server' => $srv['name'],
|
||||
'user' => ($m['User']['title'] ?? '?'),
|
||||
'title' => $title,
|
||||
'type' => ucfirst($type),
|
||||
'client' => trim(($player['product'] ?? '') . ' / ' . ($player['title'] ?? ''), ' /'),
|
||||
'method' => $method,
|
||||
'paused' => ($player['state'] ?? '') === 'paused',
|
||||
'pct' => $pct,
|
||||
'pos_sec' => (int)($offset / 1000),
|
||||
'dur_sec' => (int)($dur / 1000),
|
||||
'is_tc' => $isTc,
|
||||
'server' => $srv['name'],
|
||||
'server_type' => $srv['type'],
|
||||
'user' => ($m['User']['title'] ?? '?'),
|
||||
'title' => $title,
|
||||
'type' => ucfirst($type),
|
||||
'client' => trim(($player['product'] ?? '') . ' / ' . ($player['title'] ?? ''), ' /'),
|
||||
'method' => $method,
|
||||
'paused' => ($player['state'] ?? '') === 'paused',
|
||||
'pct' => $pct,
|
||||
'pos_sec' => (int)($offset / 1000),
|
||||
'dur_sec' => (int)($dur / 1000),
|
||||
'is_tc' => $isTc,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
|
||||
@@ -423,13 +423,26 @@ function vv_transcode_sessions(): array {
|
||||
$ramdiskPath = '/mnt/ramdisk_transcodes/transcoding-temp';
|
||||
$ramSessions = count(glob("$ramdiskPath/*/", GLOB_ONLYDIR) ?: []);
|
||||
|
||||
// SSD path: look for any other transcoding-temp sibling
|
||||
// SSD path: first transcoding-temp mount that is not a RAM filesystem (tmpfs/ramfs)
|
||||
$ssdPath = '';
|
||||
$ssdSessions = 0;
|
||||
foreach (glob('/mnt/*/transcoding-temp/', GLOB_ONLYDIR) ?: [] as $p) {
|
||||
if (!str_contains($p, 'ramdisk')) { $ssdPath = $p; break; }
|
||||
$parts = explode('/', rtrim($p, '/'));
|
||||
array_pop($parts);
|
||||
$mount = implode('/', $parts) ?: '/';
|
||||
$fsType = trim(shell_exec('findmnt -n -o FSTYPE ' . escapeshellarg($mount) . ' 2>/dev/null') ?: '');
|
||||
if ($fsType === 'tmpfs' || $fsType === 'ramfs') continue;
|
||||
$ssdPath = $p;
|
||||
break;
|
||||
}
|
||||
$ssd = ['available' => false];
|
||||
if ($ssdPath) {
|
||||
$ssdSessions = count(glob($ssdPath . '/*/', GLOB_ONLYDIR) ?: []);
|
||||
$parts = explode('/', rtrim($ssdPath, '/'));
|
||||
array_pop($parts);
|
||||
$ssdMount = implode('/', $parts) ?: '/';
|
||||
$ssd = vv_df($ssdMount);
|
||||
}
|
||||
if ($ssdPath) $ssdSessions = count(glob($ssdPath . '/*/', GLOB_ONLYDIR) ?: []);
|
||||
|
||||
// Ramdisk disk usage
|
||||
$rd = vv_df('/mnt/ramdisk_transcodes');
|
||||
@@ -444,5 +457,237 @@ function vv_transcode_sessions(): array {
|
||||
'ram_sessions' => $ramSessions,
|
||||
'ssd_sessions' => $ssdSessions,
|
||||
'ramdisk' => $rd,
|
||||
'ssd' => $ssd,
|
||||
];
|
||||
}
|
||||
|
||||
function vv_disk_entry(array $d, string $key, string $role = 'data'): ?array {
|
||||
$name = $d['name'] ?? $key;
|
||||
$isParity = $role === 'parity';
|
||||
$mounted = ($d['fsStatus'] ?? '') === 'Mounted';
|
||||
// Parity has no filesystem — use raw size only
|
||||
$size_kb = (int)($isParity ? ($d['size'] ?? 0) : ($mounted ? ($d['fsSize'] ?? 0) : ($d['size'] ?? 0)));
|
||||
$used_kb = (int)($isParity ? 0 : ($mounted ? ($d['fsUsed'] ?? 0) : 0));
|
||||
if ($size_kb <= 0) return null;
|
||||
$tempRaw = trim($d['temp'] ?? '');
|
||||
return [
|
||||
'name' => $name,
|
||||
'role' => $role,
|
||||
'size_gb' => round($size_kb / 1048576, 1),
|
||||
'used_gb' => round($used_kb / 1048576, 1),
|
||||
'pct' => (!$isParity && $size_kb > 0) ? round($used_kb / $size_kb * 100, 1) : null,
|
||||
'temp' => is_numeric($tempRaw) ? (int)$tempRaw : null,
|
||||
'transport' => $d['transport'] ?? 'ata',
|
||||
'mounted' => $mounted,
|
||||
'status' => $d['status'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
function vv_ups_stats(): array {
|
||||
$raw = shell_exec('apcaccess 2>/dev/null') ?: '';
|
||||
if (!$raw) return ['available' => false];
|
||||
|
||||
$fields = [];
|
||||
foreach (explode("\n", $raw) as $line) {
|
||||
if (preg_match('/^(\w+)\s*:\s*(.+)$/', trim($line), $m)) {
|
||||
$fields[trim($m[1])] = trim($m[2]);
|
||||
}
|
||||
}
|
||||
if (empty($fields)) return ['available' => false];
|
||||
|
||||
$parse_num = fn(string $k) => isset($fields[$k]) ? (float)$fields[$k] : null;
|
||||
|
||||
$loadPct = $parse_num('LOADPCT');
|
||||
$nomPower = $parse_num('NOMPOWER');
|
||||
$watts = ($loadPct !== null && $nomPower !== null) ? round($loadPct / 100 * $nomPower) : null;
|
||||
|
||||
return [
|
||||
'available' => true,
|
||||
'model' => $fields['MODEL'] ?? '',
|
||||
'status' => trim(explode(' ', $fields['STATUS'] ?? 'UNKNOWN')[0]),
|
||||
'line_v' => $parse_num('LINEV'),
|
||||
'output_v' => $parse_num('OUTPUTV'),
|
||||
'load_pct' => $loadPct,
|
||||
'nom_power' => $nomPower,
|
||||
'watts' => $watts,
|
||||
'bcharge' => $parse_num('BCHARGE'),
|
||||
'timeleft' => $parse_num('TIMELEFT'),
|
||||
'num_xfers' => (int)($fields['NUMXFERS'] ?? 0),
|
||||
'on_batt_s' => $parse_num('CUMONBATT'),
|
||||
'selftest' => $fields['SELFTEST'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
function vv_parity_status(): array {
|
||||
$var = [];
|
||||
foreach (@file('/var/local/emhttp/var.ini') ?: [] as $line) {
|
||||
if (preg_match('/^(\w+)="([^"]*)"/', $line, $m)) $var[$m[1]] = $m[2];
|
||||
}
|
||||
|
||||
$isValid = ($var['mdNumInvalid'] ?? '0') === '0';
|
||||
$exitCode = (int)($var['sbSyncExit'] ?? 0);
|
||||
$errors = (int)($var['sbSyncErrs'] ?? 0);
|
||||
$inProgress = ($var['mdResync'] ?? '0') !== '0';
|
||||
$resyncPos = (int)($var['mdResyncPos'] ?? 0);
|
||||
$resyncSize = (int)($var['mdResyncSize'] ?? 1);
|
||||
$resyncPct = $resyncSize > 0 ? round($resyncPos / $resyncSize * 100, 1) : 0;
|
||||
|
||||
// Last check from log
|
||||
$lastDate = null; $lastDuration = 0; $lastSpeed = 0; $lastErrors = 0; $lastExit = 0;
|
||||
$logFile = '/boot/config/parity-checks.log';
|
||||
if (file_exists($logFile)) {
|
||||
$lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
if ($lines) {
|
||||
$p = explode('|', trim(end($lines)));
|
||||
$lastDate = trim($p[0] ?? '');
|
||||
$lastDuration = (int)($p[1] ?? 0);
|
||||
$lastSpeed = (int)($p[2] ?? 0);
|
||||
$lastExit = (int)($p[3] ?? 0);
|
||||
$lastErrors = (int)($p[4] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse last date string to timestamp
|
||||
$lastTs = $lastDate ? strtotime($lastDate) : null;
|
||||
|
||||
// Next scheduled check from cron
|
||||
$nextTs = null;
|
||||
$cronFile = '/boot/config/plugins/dynamix/parity-check.cron';
|
||||
foreach (@file($cronFile) ?: [] as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') continue;
|
||||
if (!str_contains($line, 'mdcmd')) continue;
|
||||
$p = preg_split('/\s+/', $line);
|
||||
// cron: min hour dom month dow command...
|
||||
if (count($p) >= 5 && is_numeric($p[0]) && is_numeric($p[1]) && is_numeric($p[2])) {
|
||||
$next = new DateTime('now');
|
||||
$next->setTime((int)$p[1], (int)$p[0], 0);
|
||||
$next->setDate((int)$next->format('Y'), (int)$next->format('n'), (int)$p[2]);
|
||||
if ($next->getTimestamp() <= time()) $next->modify('+1 month');
|
||||
$nextTs = $next->getTimestamp();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$exitMap = ['0' => 'Completed', '-4' => 'Aborted', '-5' => 'Cancelled'];
|
||||
return [
|
||||
'valid' => $isValid,
|
||||
'in_progress' => $inProgress,
|
||||
'resync_pct' => $resyncPct,
|
||||
'exit_code' => $exitCode,
|
||||
'exit_label' => $exitMap[(string)$lastExit] ?? 'Unknown',
|
||||
'errors' => $lastErrors,
|
||||
'last_date' => $lastDate,
|
||||
'last_ts' => $lastTs,
|
||||
'last_duration' => $lastDuration,
|
||||
'last_speed_mb' => $lastSpeed > 0 ? round($lastSpeed / 1048576, 1) : null,
|
||||
'next_ts' => $nextTs,
|
||||
];
|
||||
}
|
||||
|
||||
function vv_storage_pools(): array {
|
||||
$ini = @parse_ini_file('/var/local/emhttp/disks.ini', true) ?: [];
|
||||
$out = [];
|
||||
foreach ($ini as $key => $d) {
|
||||
if (($d['type'] ?? '') !== 'Cache') continue;
|
||||
if (($d['fsStatus'] ?? '') !== 'Mounted') continue;
|
||||
$entry = vv_disk_entry($d, $key);
|
||||
if ($entry) $out[] = $entry;
|
||||
}
|
||||
usort($out, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
return $out;
|
||||
}
|
||||
|
||||
function vv_array_disks(): array {
|
||||
$ini = @parse_ini_file('/var/local/emhttp/disks.ini', true) ?: [];
|
||||
$parity = [];
|
||||
$data = [];
|
||||
foreach ($ini as $key => $d) {
|
||||
$type = $d['type'] ?? '';
|
||||
if ($type === 'Parity') {
|
||||
$entry = vv_disk_entry($d, $key, 'parity');
|
||||
if ($entry) $parity[] = $entry;
|
||||
} elseif ($type === 'Data') {
|
||||
$entry = vv_disk_entry($d, $key, 'data');
|
||||
if ($entry) $data[] = $entry;
|
||||
}
|
||||
}
|
||||
usort($parity, fn($a, $b) => strnatcmp($a['name'], $b['name']));
|
||||
usort($data, fn($a, $b) => strnatcmp($a['name'], $b['name']));
|
||||
return array_merge($parity, $data);
|
||||
}
|
||||
|
||||
function vv_disk_thresholds(): array {
|
||||
$cfg = @file_get_contents('/boot/config/plugins/dynamix/dynamix.cfg') ?: '';
|
||||
$get = function(string $key) use ($cfg): ?int {
|
||||
return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?(\d+)"?/m', $cfg, $m)
|
||||
? (int)$m[1] : null;
|
||||
};
|
||||
return [
|
||||
'util_warn' => $get('warning') ?? 70,
|
||||
'util_crit' => $get('critical') ?? 90,
|
||||
'hdd_warn' => $get('hot') ?? 45,
|
||||
'hdd_crit' => $get('max') ?? 55,
|
||||
'ssd_warn' => $get('hotssd') ?? 60,
|
||||
'ssd_crit' => $get('maxssd') ?? 70,
|
||||
];
|
||||
}
|
||||
|
||||
function vv_log_tail(string $path, int $lines): string {
|
||||
$fp = @fopen($path, 'r');
|
||||
if (!$fp) return '';
|
||||
fseek($fp, 0, SEEK_END);
|
||||
$size = ftell($fp);
|
||||
if ($size <= 0) { fclose($fp); return ''; }
|
||||
$chunk = min($size, 4096);
|
||||
fseek($fp, -$chunk, SEEK_END);
|
||||
$data = fread($fp, $chunk);
|
||||
fclose($fp);
|
||||
$all = explode("\n", $data ?: '');
|
||||
return implode("\n", array_slice($all, -$lines));
|
||||
}
|
||||
|
||||
function vv_scripts_status(): array {
|
||||
$tmpBase = '/tmp/user.scripts/tmpScripts';
|
||||
$runDir = '/tmp/user.scripts/running';
|
||||
|
||||
$running = [];
|
||||
foreach (glob($runDir . '/*') ?: [] as $f) {
|
||||
$running[basename($f)] = true;
|
||||
}
|
||||
|
||||
$scripts = [];
|
||||
foreach (glob($tmpBase . '/*/') ?: [] as $dir) {
|
||||
$name = basename(rtrim($dir, '/'));
|
||||
$logPath = $dir . 'log.txt';
|
||||
$ts = @filemtime($logPath);
|
||||
if (!$ts) continue;
|
||||
|
||||
$isRunning = isset($running[$name]);
|
||||
|
||||
if ($isRunning) {
|
||||
$status = 'running';
|
||||
} else {
|
||||
$tail = vv_log_tail($logPath, 15);
|
||||
$finished = stripos($tail, 'Script Finished') !== false;
|
||||
if ($finished) {
|
||||
$hasWarn = (bool)preg_match('/permission denied|command not found|no such file|: error[\s:]/i', $tail);
|
||||
$status = $hasWarn ? 'warn' : 'ok';
|
||||
} else {
|
||||
$status = 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
$scripts[] = ['name' => $name, 'last_ts' => $ts, 'status' => $status, 'running' => $isRunning];
|
||||
}
|
||||
|
||||
usort($scripts, fn($a, $b) => ($b['last_ts'] ?? 0) <=> ($a['last_ts'] ?? 0));
|
||||
$scripts = array_slice($scripts, 0, 12);
|
||||
|
||||
return [
|
||||
'scripts' => $scripts,
|
||||
'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
|
||||
'ok_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'ok')),
|
||||
'warn_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'warn')),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user