Consolidations (config.php gains 5 shared utilities): - vv_format_uptime() replaces 4 inline uptime-formatting blocks - vv_parse_conf_scalar() replaces vv_arr_scalar/vv_wd_scalar/vv_fb_scalar/vv_media_conf_scalar - vv_known_hosts() replaces vv_arr_known_hosts/vv_fb_known_hosts + inline parser in watchdog - vv_parse_kv_db() replaces inline key=value parsing in snapshot and monitor - vv_local_ip() replaces duplicate in docker_folders.php and inline in docker.php All module-level function names kept as thin aliases so call sites unchanged. Critical bug fixes: - api/system.php: added require_once config.php and POST-only guard (no auth on shutdown) - api/movescript.php + reorderarray.php: use vv_write_conf_raw (atomic) + vv_push_master_conf - api/snapshot.php: share /tmp/vv_cpu_stat.json with vv_cpu_per_core() instead of own state file Correctness: - vv_cpu_per_core() and vv_network_stats(): atomic tmp+rename for state files (concurrent poll safety) - ext_ip curl cache moved from /tmp/vv_ext_ip.cache to vv_cache_read/write (canonical cache dir) - monitor_remote.php + board.php + snapshot.php: all use vv_cache_read/write instead of ad-hoc /tmp files HTTP method guards added to write-only APIs that were missing them: - api/scheduler.php, conf_toggle.php, flag_toggle.php
118 lines
5.4 KiB
PHP
118 lines
5.4 KiB
PHP
<?php
|
|
// Live board data: locks, recent errors, partner reachability.
|
|
header('Content-Type: application/json');
|
|
require_once dirname(__DIR__) . '/include/config.php';
|
|
|
|
$out = ['ok' => true];
|
|
|
|
// ── Active locks ──────────────────────────────────────────────────────────
|
|
$lockDir = '/tmp/unraid_locks';
|
|
$locks = [];
|
|
if (is_dir($lockDir)) {
|
|
foreach (glob($lockDir . '/*.lock') ?: [] as $lf) {
|
|
$name = basename($lf, '.lock');
|
|
$age = time() - (int)filemtime($lf);
|
|
$content = trim(file_get_contents($lf) ?: '');
|
|
// content is "PID:scriptname" — extract PID
|
|
$pid = preg_match('/^(\d+)/', $content, $pm) ? $pm[1] : '';
|
|
// Skip if PID is still alive (it's legitimately running)
|
|
if ($pid && file_exists("/proc/$pid")) continue;
|
|
$locks[] = ['name' => $name, 'file' => basename($lf), 'age' => $age];
|
|
}
|
|
}
|
|
$out['locks'] = $locks;
|
|
|
|
// ── Recent errors ──────────────────────────────────────────────────────────
|
|
// Scan recursively so subdirectory logs (Orchestrators/, Media/, etc.) are included.
|
|
// e.script = relative path without extension = job ID base, e.g. "Orchestrators/transcode_management"
|
|
$errors = [];
|
|
if (is_dir(LOG_DIR)) {
|
|
$cutoff = time() - 7 * 86400;
|
|
$logBase = rtrim(LOG_DIR, '/') . '/';
|
|
try {
|
|
$ri = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator(LOG_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($ri as $lf) {
|
|
if ($lf->getExtension() !== 'log') continue;
|
|
if ($lf->getMTime() < $cutoff) continue;
|
|
$rel = ltrim(str_replace($logBase, '', $lf->getPathname()), '/');
|
|
$script = preg_replace('/\.log$/', '', $rel);
|
|
|
|
// Only report scripts whose last recorded run exited as warn or error.
|
|
// This eliminates dry-run false positives (exit 0 = ok) and success-run
|
|
// summaries that happen to contain words like "Failed: 0".
|
|
$statFile = $logBase . $script . '.json';
|
|
if (file_exists($statFile)) {
|
|
$stat = json_decode(@file_get_contents($statFile), true);
|
|
$status = $stat['status'] ?? '';
|
|
if ($status !== 'warn' && $status !== 'error') continue;
|
|
} else {
|
|
continue; // no stat file — never ran through run_job.sh, skip
|
|
}
|
|
|
|
$lines = array_slice(@file($lf->getPathname()) ?: [], -200);
|
|
$lastErr = null;
|
|
foreach (array_reverse($lines) as $raw) {
|
|
$clean = preg_replace('/\033\[[0-9;]*[mK]/', '', rtrim($raw));
|
|
if (!$clean) continue;
|
|
if (preg_match('/\[(?:ERROR|WARN|CRITICAL|FAILED)\]/i', $clean) ||
|
|
preg_match('/\b(?:ERROR|CRITICAL):\s/i', $clean) ||
|
|
str_contains($clean, '✗') ||
|
|
(str_contains($clean, '⚠') && !str_contains($clean, '♥'))) {
|
|
$lastErr = mb_substr($clean, 0, 220);
|
|
break;
|
|
}
|
|
}
|
|
// Fall back to last non-blank line if no error pattern found in log
|
|
if ($lastErr === null) {
|
|
foreach (array_reverse($lines) as $raw) {
|
|
$clean = preg_replace('/\033\[[0-9;]*[mK]/', '', rtrim($raw));
|
|
if ($clean && !str_starts_with($clean, '──')) { $lastErr = mb_substr($clean, 0, 220); break; }
|
|
}
|
|
}
|
|
if ($lastErr !== null)
|
|
$errors[] = ['script' => $script, 'line' => $lastErr, 'ts' => (int)$lf->getMTime()];
|
|
}
|
|
} catch (Exception $e) {}
|
|
usort($errors, fn($a, $b) => $b['ts'] - $a['ts']);
|
|
}
|
|
$out['errors'] = array_slice($errors, 0, 20);
|
|
|
|
// ── Partner reachability ───────────────────────────────────────────────────
|
|
$partnerData = vv_cache_read('board_partner', 30);
|
|
|
|
if (!$partnerData) {
|
|
// Discover partner hostname dynamically from master.conf (works for any number of hosts)
|
|
$vars = vv_conf_vars();
|
|
$mine = vv_get_hostname();
|
|
$partnerHost = null;
|
|
foreach ($vars as $k => $v) {
|
|
if (preg_match('/^HOST\d+$/', $k) && $v !== '' && strcasecmp($v, $mine) !== 0) {
|
|
$partnerHost = $v; // first non-self host is the partner
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($partnerHost) {
|
|
// Resolve via Tailscale — mirrors common.sh resolve_tailscale_ip().
|
|
// Uses `tailscale ip -4` so it survives IP changes; never relies on local DNS.
|
|
$partnerIp = vv_resolve_tailscale_ip($partnerHost);
|
|
$target = $partnerIp ?: $partnerHost;
|
|
$start = microtime(true);
|
|
$result = shell_exec('ping -c1 -W2 ' . escapeshellarg($target) . ' 2>&1');
|
|
$elapsed = (int)round((microtime(true) - $start) * 1000);
|
|
$reached = str_contains((string)$result, '1 received')
|
|
|| str_contains((string)$result, '1 packets received');
|
|
$partnerData = [
|
|
'host' => $partnerHost,
|
|
'reachable' => $reached,
|
|
'latency' => $reached ? $elapsed : null,
|
|
];
|
|
vv_cache_write('board_partner', $partnerData);
|
|
}
|
|
}
|
|
$out['partner'] = $partnerData;
|
|
|
|
echo json_encode($out);
|