$os['hostname'] ?? ($ident['NAME'] ?? gethostname()), 'comment' => $ident['COMMENT'] ?? '', 'timezone' => $ident['timeZone'] ?? 'UTC', 'cpu_model' => $cpu['brand'] ?? ($ident['SYS_MODEL'] ?? ''), 'cpu_threads' => (int)($cpu['threads'] ?? 0), 'cpu_cores' => (int)($cpu['cores'] ?? 0), 'reg_type' => 'Unraid OS ' . ($var['regTy'] ?? ''), 'reg_to' => $var['regTo'] ?? '', 'uptime' => $uptime, 'uptime_sec' => $uptimeSec, 'array_state' => strtoupper($api['array']['state'] ?? $var['mdState'] ?? 'UNKNOWN'), 'version' => trim($os['release'] ?? '') ?: $version, 'load_avg' => $load ? [round($load[0], 2), round($load[1], 2), round($load[2], 2)] : null, ]; } // ── Local fallback ──────────────────────────────────────────────────────── vv_api_record_fallback('system_info'); $cpuModel = ''; foreach (@file('/proc/cpuinfo') ?: [] as $line) { if (preg_match('/^model name\s*:\s*(.+)/', $line, $m)) { $cpuModel = trim($m[1]); break; } } $uptimeSec = (int)explode(' ', @file_get_contents('/proc/uptime') ?: '0')[0]; $uptime = vv_format_uptime($uptimeSec); $load = sys_getloadavg(); return [ 'name' => $ident['NAME'] ?? gethostname(), 'comment' => $ident['COMMENT'] ?? '', 'timezone' => $ident['timeZone'] ?? 'UTC', 'cpu_model' => $ident['SYS_MODEL'] ?? $cpuModel, 'cpu_threads' => 0, 'cpu_cores' => 0, 'reg_type' => 'Unraid OS ' . ($var['regTy'] ?? ''), 'reg_to' => $var['regTo'] ?? '', 'uptime' => $uptime, 'uptime_sec' => $uptimeSec, 'array_state' => $var['mdState'] ?? 'UNKNOWN', 'version' => $version, 'load_avg' => $load ? [round($load[0], 2), round($load[1], 2), round($load[2], 2)] : null, ]; } function vv_docker_containers(): array { $out = shell_exec('docker ps --format \'{"name":"{{.Names}}","status":"{{.Status}}","image":"{{.Image}}"}\' 2>/dev/null'); $containers = []; foreach (explode("\n", trim($out ?? '')) as $line) { if (!$line) continue; $c = json_decode($line, true); if ($c) $containers[] = $c; } return $containers; } function vv_docker_stopped(): array { $out = shell_exec('docker ps -a --filter "status=exited" --filter "status=created" --format \'{"name":"{{.Names}}","status":"{{.Status}}"}\' 2>/dev/null'); $containers = []; foreach (explode("\n", trim($out ?? '')) as $line) { if (!$line) continue; $c = json_decode($line, true); if ($c) $containers[] = $c; } return $containers; } function vv_gpu_stats(): array { $out = shell_exec('nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu,power.draw,utilization.encoder,utilization.decoder --format=csv,noheader,nounits 2>/dev/null'); if (!$out) return ['available' => false]; $parts = array_map('trim', explode(',', $out)); $power = is_numeric($parts[5] ?? '') ? round((float)$parts[5], 1) : null; return [ 'available' => true, 'name' => $parts[0] ?? '', 'memory_used' => (int)($parts[1] ?? 0), 'memory_total' => (int)($parts[2] ?? 0), 'utilization' => (int)($parts[3] ?? 0), 'temperature' => (int)($parts[4] ?? 0), 'power_w' => $power, 'enc_pct' => (int)($parts[6] ?? 0), 'dec_pct' => (int)($parts[7] ?? 0), ]; } function vv_gpu_processes(): array { $out = shell_exec('nvidia-smi --query-compute-apps=pid,used_gpu_memory,name --format=csv,noheader,nounits 2>/dev/null'); $procs = []; foreach (explode("\n", trim($out ?? '')) as $line) { if (!$line) continue; $parts = array_map('trim', explode(',', $line)); $procs[] = [ 'pid' => $parts[0] ?? '', 'memory_mb' => $parts[1] ?? '', 'name' => $parts[2] ?? '', ]; } return $procs; } function vv_system_resources(): array { $mem = []; foreach (file('/proc/meminfo') ?: [] as $line) { if (preg_match('/^(MemTotal|MemAvailable):\s+(\d+)/', $line, $m)) $mem[$m[1]] = (int)$m[2]; } return [ 'ram_total_mb' => (int)(($mem['MemTotal'] ?? 0) / 1024), 'ram_free_mb' => (int)(($mem['MemAvailable'] ?? 0) / 1024), 'cache' => vv_df('/mnt/cache'), ]; } function vv_cpu_per_core(): array { // Parse /proc/stat — [user, nice, system, idle, iowait, irq, softirq] $raw = []; foreach (file('/proc/stat') ?: [] as $line) { if (!preg_match('/^(cpu\d*)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/', $line, $m)) continue; $raw[$m[1]] = [(int)$m[2],(int)$m[3],(int)$m[4],(int)$m[5],(int)$m[6],(int)$m[7],(int)$m[8]]; } $stateFile = VV_CACHE_DIR . '/vv_cpu_stat.json'; $prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : []; // Atomic write — concurrent fast/slow polls read a consistent snapshot $tmp = $stateFile . '.tmp'; file_put_contents($tmp, json_encode($raw)); rename($tmp, $stateFile); $usage = function(array $c, ?array $p): int { if (!$p) return 0; $dt = array_sum($c) - array_sum($p); $di = ($c[3] + $c[4]) - ($p[3] + $p[4]); return $dt > 0 ? max(0, min(100, (int)round((1 - $di / $dt) * 100))) : 0; }; $overall = $usage($raw['cpu'] ?? [], $prev['cpu'] ?? null); $cores = []; foreach ($raw as $cpu => $c) { if ($cpu === 'cpu') continue; $num = (int)substr($cpu, 3); $freqKhz = (int)@file_get_contents("/sys/devices/system/cpu/$cpu/cpufreq/scaling_cur_freq"); $maxKhz = (int)@file_get_contents("/sys/devices/system/cpu/$cpu/cpufreq/cpuinfo_max_freq"); $minKhz = (int)@file_get_contents("/sys/devices/system/cpu/$cpu/cpufreq/cpuinfo_min_freq"); $cores[] = [ 'core' => $num, 'usage_pct' => $usage($c, $prev[$cpu] ?? null), 'freq_mhz' => $freqKhz > 0 ? (int)round($freqKhz / 1000) : 0, 'max_mhz' => $maxKhz > 0 ? (int)round($maxKhz / 1000) : 0, 'min_mhz' => $minKhz > 0 ? (int)round($minKhz / 1000) : 0, ]; } usort($cores, fn($a, $b) => $a['core'] - $b['core']); return ['overall' => $overall, 'cores' => $cores]; } function vv_memory_breakdown(): array { $mem = []; foreach (file('/proc/meminfo') ?: [] as $line) { if (preg_match('/^(\w+):\s+(\d+)/', $line, $m)) $mem[$m[1]] = (int)$m[2]; } $totalKb = $mem['MemTotal'] ?? 0; // ZFS ARC $arcKb = 0; foreach (@file('/proc/spl/kstat/zfs/arcstats') ?: [] as $line) { if (preg_match('/^size\s+\d+\s+(\d+)/', $line, $m)) { $arcKb = (int)($m[1] / 1024); break; } } // Docker — sum docker stats used memory per container (matches Unraid dashboard) $dockerKb = 0; $dsOut = shell_exec("docker stats --no-stream --format '{{.MemUsage}}' 2>/dev/null") ?: ''; foreach (explode("\n", trim($dsOut)) as $line) { if (!preg_match('/^([0-9.]+)(GiB|MiB|KiB|B)\s*\//', trim($line), $m)) continue; $val = (float)$m[1]; $dockerKb += match($m[2]) { 'GiB' => (int)($val * 1048576), 'MiB' => (int)($val * 1024), 'KiB' => (int)$val, default => (int)($val / 1024), }; } // VM (QEMU/KVM RSS) $vmKb = 0; foreach (preg_split('/\s+/', trim(shell_exec('ps -C qemu-system-x86_64 -o rss= 2>/dev/null') ?: '')) as $rss) { if (is_numeric($rss) && $rss > 0) $vmKb += (int)$rss; } $freeKb = max(0, $mem['MemAvailable'] ?? 0); $systemKb = max(0, $totalKb - $freeKb - $arcKb - $dockerKb - $vmKb); // Top processes by RSS — group same-named procs, take top 5 $grouped = []; $psOut = shell_exec("ps -eo comm,rss --sort=-rss 2>/dev/null | tail -n +2 | head -40") ?: ''; foreach (explode("\n", trim($psOut)) as $line) { $parts = preg_split('/\s+/', trim($line), 2); if (count($parts) === 2 && is_numeric($parts[1]) && (int)$parts[1] > 0) $grouped[$parts[0]] = ($grouped[$parts[0]] ?? 0) + (int)$parts[1]; } arsort($grouped); $topProcs = []; foreach (array_slice($grouped, 0, 3, true) as $name => $kb) $topProcs[] = ['name' => $name, 'kb' => $kb]; // Swap — from API metrics when available, else /proc/meminfo $swapTotalKb = 0; $swapUsedKb = 0; $apiMem = vv_api_data()['metrics']['memory'] ?? []; if (!empty($apiMem['swapTotal'])) { $swapTotalKb = (int)(((float)$apiMem['swapTotal']) / 1024); $swapUsedKb = (int)(((float)$apiMem['swapUsed']) / 1024); } else { $swapTotalKb = $mem['SwapTotal'] ?? 0; $swapUsedKb = ($mem['SwapTotal'] ?? 0) - ($mem['SwapFree'] ?? 0); } return [ 'total_kb' => $totalKb, 'system_kb' => $systemKb, 'vm_kb' => $vmKb, 'zfs_kb' => $arcKb, 'docker_kb' => $dockerKb, 'free_kb' => $freeKb, 'swap_total_kb' => $swapTotalKb, 'swap_used_kb' => $swapUsedKb, 'top_procs' => $topProcs, ]; } function vv_df(string $path): array { $out = shell_exec("df -BM --output=size,used,avail '$path' 2>/dev/null | tail -1"); if (!$out) return ['available' => false, 'path' => $path]; $parts = preg_split('/\s+/', trim($out)); return [ 'available' => true, 'path' => $path, 'size_mb' => (int)$parts[0], 'used_mb' => (int)$parts[1], 'free_mb' => (int)$parts[2], ]; } function vv_network_stats(): array { $iface = trim(shell_exec("ip route show default 2>/dev/null | awk 'NR==1{print \$5}'") ?: ''); if (!$iface) { $best = ''; $bestBytes = 0; foreach (file('/proc/net/dev') ?: [] as $line) { if (!preg_match('/^\s*(\w+):\s+(\d+)/', $line, $m) || $m[1] === 'lo') continue; if ((int)$m[2] > $bestBytes) { $bestBytes = (int)$m[2]; $best = $m[1]; } } $iface = $best; } if (!$iface) return ['available' => false]; $rxBytes = $txBytes = 0; foreach (file('/proc/net/dev') ?: [] as $line) { if (!preg_match('/^\s*' . preg_quote($iface, '/') . ':\s+(.+)$/', $line, $m)) continue; $parts = preg_split('/\s+/', trim($m[1])); $rxBytes = (int)($parts[0] ?? 0); $txBytes = (int)($parts[8] ?? 0); break; } $stateFile = VV_CACHE_DIR . '/vv_net_stat.json'; $now = ['rx' => $rxBytes, 'tx' => $txBytes, 'ts' => microtime(true)]; $prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : []; $tmp = $stateFile . '.tmp'; file_put_contents($tmp, json_encode($now)); rename($tmp, $stateFile); $rxRate = $txRate = 0; if (!empty($prev['ts']) && ($dt = $now['ts'] - $prev['ts']) > 0.1) { $rxRate = max(0, (int)(($rxBytes - ($prev['rx'] ?? $rxBytes)) / $dt)); $txRate = max(0, (int)(($txBytes - ($prev['tx'] ?? $txBytes)) / $dt)); } $speedMbps = (int)@file_get_contents("/sys/class/net/$iface/speed"); // Local IP — use primary iface $localIp = trim(shell_exec( "ip -4 addr show " . escapeshellarg($iface) . " 2>/dev/null | awk '/inet /{print \$2}' | cut -d/ -f1 | head -1" ) ?: ''); // External IP — curl ifconfig.me, cached 5 min so we don't hammer it $extIp = ''; $extData = vv_cache_read('ext_ip', 300); if ($extData) { $extIp = $extData['ip'] ?? ''; } else { $fetched = trim(shell_exec('curl -sf --max-time 4 https://ifconfig.me 2>/dev/null') ?: ''); if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $fetched)) { $extIp = $fetched; vv_cache_write('ext_ip', ['ip' => $extIp]); } } // Tailscale IP — use `tailscale ip` CLI (interface name varies: tailscale0, tailscale1, etc.) $tsIp = trim(shell_exec('tailscale ip -4 2>/dev/null | head -1') ?: ''); return [ 'available' => true, 'iface' => $iface, 'speed_mbps' => $speedMbps > 0 ? $speedMbps : null, 'rx_bps' => $rxRate, 'tx_bps' => $txRate, 'local_ip' => $localIp, 'ext_ip' => $extIp, 'ts_ip' => $tsIp, ]; } 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, 'device' => $d['device'] ?? $key, '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]; } $numDisabled = (int)($var['mdNumDisabled'] ?? 0); $numMissing = (int)($var['mdNumMissing'] ?? 0); $exitCode = (int)($var['sbSyncExit'] ?? 0); $errors = (int)($var['sbSyncErrs'] ?? 0); // Emulated (DISK_DSBL) disks are protected by parity and don't make parity invalid. // True invalidity: sync errors on the last check, or unprotectable missing slots. $isValid = $errors === 0 && $numMissing === 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, 'num_disabled' => $numDisabled, 'num_missing' => $numMissing, '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 { // ── API path ────────────────────────────────────────────────────────────── $api = vv_api_data(); if ($api && isset($api['array']['caches'])) { $out = []; foreach ($api['array']['caches'] as $d) { $entry = vv_api_disk_entry($d, 'data'); if ($entry) $out[] = $entry; } if (!empty($out)) { usort($out, fn($a, $b) => strcmp($a['name'], $b['name'])); return $out; } } // ── Local fallback ──────────────────────────────────────────────────────── vv_api_record_fallback('storage_pools'); $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 { // ── API path ────────────────────────────────────────────────────────────── $api = vv_api_data(); if ($api && (isset($api['array']['parities']) || isset($api['array']['disks']))) { $parity = []; $data = []; foreach ($api['array']['parities'] ?? [] as $d) { $entry = vv_api_disk_entry($d, 'parity'); if ($entry) $parity[] = $entry; } foreach ($api['array']['disks'] ?? [] as $d) { $entry = vv_api_disk_entry($d, 'data'); if ($entry) $data[] = $entry; } if (!empty($parity) || !empty($data)) { 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); } } // ── Local fallback ──────────────────────────────────────────────────────── vv_api_record_fallback('array_disks'); $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_io_rates(): array { $snapFile = VV_CACHE_DIR . '/vv_diskio_snap.json'; $now = microtime(true); // Read current whole-disk stats from /proc/diskstats $current = []; foreach (@file('/proc/diskstats', FILE_IGNORE_NEW_LINES) ?: [] as $line) { $p = preg_split('/\s+/', trim($line)); if (count($p) < 14) continue; $dev = $p[2]; // Keep only whole disks: sda/sdb, nvme0n1, md*, not sda1/nvme0n1p1 if (!preg_match('/^(sd[a-z]+|nvme\d+n\d+|md\d+)$/', $dev)) continue; $current[$dev] = [(int)$p[5], (int)$p[9]]; // [sectors_read, sectors_written] } // Load previous snapshot $snap = @json_decode(@file_get_contents($snapFile) ?: '', true) ?: []; $prevTime = (float)($snap['t'] ?? $now); $prev = $snap['d'] ?? []; // Save current snapshot @file_put_contents($snapFile, json_encode(['t' => $now, 'd' => $current], JSON_UNESCAPED_UNICODE)); $dt = max(0.5, $now - $prevTime); $out = []; foreach ($current as $dev => [$rs, $ws]) { $entry = [ 'tr' => round($rs * 512 / 1073741824, 2), // cumulative GB read 'tw' => round($ws * 512 / 1073741824, 2), // cumulative GB written ]; if (isset($prev[$dev])) { [$prs, $pws] = $prev[$dev]; $r = max(0.0, ($rs - $prs) * 512 / $dt / 1048576); $w = max(0.0, ($ws - $pws) * 512 / $dt / 1048576); if ($r > 0.01) $entry['r'] = round($r, 1); if ($w > 0.01) $entry['w'] = round($w, 1); } $out[$dev] = $entry; } return $out; } 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, ]; } // Fetch a lightweight snapshot from each remote host that has an API key configured. // Results are cached in /tmp for 30 seconds so rapid monitor polls don't hammer remote hosts. function vv_remote_hosts_stats(): array { // Read ALL conf files — remote host keys live in their own host*.conf, not the current host's. $vars = vv_conf_vars(); foreach (glob(CONF_DIR . '/host*.conf') ?: [] as $f) { $raw = file_get_contents($f) ?: ''; preg_match_all('/^\s*([A-Z0-9_]+)\s*=\s*["\']?([^"\'#\n]*?)["\']?\s*(?:#.*)?$/m', $raw, $m); foreach ($m[1] as $i => $key) { if (!isset($vars[$key])) $vars[$key] = trim($m[2][$i]); } } $myHost = vv_detect_host(); $hostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== ''); sort($hostIds); $results = []; foreach ($hostIds as $id) { if (strtolower($id) === strtolower($myHost)) continue; // Background cache written by remote_arr_cache_writer.sh every 2h — use it if present. $bgCache = VV_CACHE_DIR . '/monitor_remote_' . strtolower($id) . '.json'; if (file_exists($bgCache)) { $cached = json_decode(file_get_contents($bgCache), true); if ($cached) { $cached['cache_age'] = time() - (int)filemtime($bgCache); $results[$id] = $cached; continue; } } // No background cache yet — fall back to live call (uses 30s inline cache). $key = $vars[strtoupper($id) . '_UNRAID_API_KEY'] ?? ''; if (!$key) { $results[$id] = ['available' => false, 'no_api_key' => true, 'host_id' => $id, 'hostname' => $vars[$id]]; continue; } $cacheFile = VV_CACHE_DIR . "/vv_remote_{$id}.json"; if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) { $cached = json_decode(file_get_contents($cacheFile), true); if ($cached) { $results[$id] = $cached; continue; } } $gql = '{ info { os { hostname uptime release } cpu { brand threads cores } } metrics { cpu { percentTotal } memory { percentTotal total used available } } array { state disks { fsSize fsUsed temp } caches { fsSize fsUsed temp } parities { temp } } vms { domains { name } } }'; $data = vv_unraid_api_query(strtolower($id), $gql, 4, $key); if (!$data) { $entry = ['available' => false, 'host_id' => $id, 'hostname' => $vars[$id]]; file_put_contents($cacheFile, json_encode($entry)); $results[$id] = $entry; continue; } $os = $data['info']['os'] ?? []; $cpu = $data['info']['cpu'] ?? []; $mMem = $data['metrics']['memory'] ?? []; $memPct = round((float)($mMem['percentTotal'] ?? 0)); if ($memPct === 0) { $totalBytes = (float)($mMem['total'] ?? 0); $availBytes = (float)($mMem['available'] ?? 0); $memPct = $totalBytes > 0 ? (int)round(($totalBytes - $availBytes) / $totalBytes * 100) : 0; } $memTotalGb = isset($mMem['total']) ? _vv_api_bytes_to_gb((float)$mMem['total']) : 0; $uptimeRaw = $os['uptime'] ?? ''; if (is_numeric($uptimeRaw)) { $uptimeSec = (int)$uptimeRaw; $days = intdiv($uptimeSec, 86400); $hours = intdiv($uptimeSec % 86400, 3600); $mins = intdiv($uptimeSec % 3600, 60); $uptime = ($days ? "{$days}d " : '') . ($hours ? "{$hours}h " : '') . "{$mins}m"; } else { $uptimeSec = 0; $uptime = $uptimeRaw ?: '—'; } $nodeMetrics = vv_api_node_metrics($data); $entry = array_merge([ 'available' => true, 'host_id' => $id, 'hostname' => $os['hostname'] ?? $vars[$id], 'version' => $os['release'] ?? '', 'uptime' => $uptime, 'uptime_sec' => $uptimeSec, 'cpu_load' => $nodeMetrics['cpu_pct'] ?? 0, 'cpu_threads' => (int)($cpu['threads'] ?? 0), 'mem_total_gb' => $memTotalGb, 'mem_used_pct' => $memPct, 'array_state' => $data['array']['state'] ?? 'UNKNOWN', ], $nodeMetrics); file_put_contents($cacheFile, json_encode($entry)); $results[$id] = $entry; } return $results; } 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_parse_bash_array(string $raw, string $varName): array { if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\(([^)]*)\)/ms', $raw, $m)) return []; $items = []; foreach (explode("\n", $m[1]) as $line) { $line = trim(preg_replace('/#.*$/', '', $line), " \t\"'"); if ($line !== '') $items[] = $line; } return $items; } function vv_transcode_sessions(): array { $v = vv_conf_vars(); $stateDir = rtrim($v['STATE_DIR'] ?? STATE_DIR, '/'); $stateFile = "$stateDir/transcode_state.db"; if (!file_exists($stateFile)) return ['available' => false]; $raw = []; foreach (file($stateFile) ?: [] as $line) { [$k, $v] = array_pad(explode('=', trim($line), 2), 2, ''); $raw[trim($k)] = trim($v); } $target = $raw['current_target'] ?? ''; $lastFlip = (int)($raw['last_flip_time'] ?? 0); $flipCount = (int)($raw['flip_count_hour'] ?? 0); $isRamdisk = str_contains($target, 'ramdisk'); // Count active session dirs in both known locations $ramdiskPath = '/mnt/ramdisk_transcodes/transcoding-temp'; $ramSessions = count(glob("$ramdiskPath/*/", GLOB_ONLYDIR) ?: []); // 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) { $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); } // Ramdisk disk usage $rd = vv_df('/mnt/ramdisk_transcodes'); // Last cleanup values from transcode management log $lastRdFreed = null; $lastSsdFreed = null; $logFile = LOG_DIR . '/Orchestrators/transcode_management.log'; if (file_exists($logFile)) { $lines = file($logFile, FILE_IGNORE_NEW_LINES) ?: []; foreach (array_reverse($lines) as $line) { if ($lastRdFreed === null && preg_match('/Ramdisk freed:\s*(\S+)/u', $line, $m)) $lastRdFreed = $m[1]; if ($lastSsdFreed === null && preg_match('/SSD freed:\s*(\S+)/u', $line, $m)) $lastSsdFreed = $m[1]; if ($lastRdFreed !== null && $lastSsdFreed !== null) break; } } return [ 'available' => true, 'current_target' => $target, 'is_ramdisk' => $isRamdisk, 'flip_count_hour' => $flipCount, 'last_flip_time' => $lastFlip, 'last_flip_ago' => $lastFlip > 0 ? time() - $lastFlip : null, 'ram_sessions' => $ramSessions, 'ssd_sessions' => $ssdSessions, 'ramdisk' => $rd, 'ssd' => $ssd, 'last_rd_freed' => $lastRdFreed, 'last_ssd_freed' => $lastSsdFreed, ]; }