Varaverk: FallBack + Watchdog tabs; plugin path restructure to Plugin/unraid/
- FallBack tab: per-node tier inventory + active fallback card with duration, tier, handback strikes, running container status - Watchdog tab: live system health (RAM bar + thresholds, load, uptime, daemon), docker watchdog strikes + skip list + restart history, stability strikes + reboot log, resource pressure alert card, config inventory (mem limits, required, pause/stop lists) - Swapped partnership/arrs tab order; FallBack between partnership and watchdog - Plugin source tree moved from Plugin/usr/local/emhttp/plugins/varaverk/ to Plugin/unraid/ - Deployment/ conf templates added
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/arrs.php';
|
||||
echo json_encode(vv_arrs_all());
|
||||
@@ -0,0 +1,121 @@
|
||||
<?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 ───────────────────────────────────────────────────
|
||||
$cacheFile = '/tmp/vv_partner_cache.json';
|
||||
$cacheTtl = 30;
|
||||
$partnerData = null;
|
||||
|
||||
if (file_exists($cacheFile) && (time() - (int)filemtime($cacheFile)) < $cacheTtl) {
|
||||
$partnerData = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
// 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,
|
||||
];
|
||||
@file_put_contents($cacheFile, json_encode($partnerData));
|
||||
}
|
||||
}
|
||||
$out['partner'] = $partnerData;
|
||||
|
||||
echo json_encode($out);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
$file = basename($_POST['file'] ?? '');
|
||||
if (!$file || !preg_match('/^[a-zA-Z0-9_\-]+\.lock$/', $file)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid filename']);
|
||||
exit;
|
||||
}
|
||||
$path = '/tmp/unraid_locks/' . $file;
|
||||
if (file_exists($path)) @unlink($path);
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
$enabled = ($_POST['enabled'] ?? '0') === '1';
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = vv_conf_toggle_script($id, $enabled);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf']);
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
require_once dirname(__DIR__) . '/include/confform.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$id = trim($_GET['id'] ?? '');
|
||||
if (!$id || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
$groups = vv_conf_fields_for_script($id);
|
||||
echo json_encode(['ok' => true, 'groups' => $groups]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
$rawJson = $_POST['changes'] ?? '[]';
|
||||
|
||||
if (!$id) { echo json_encode(['ok' => false, 'error' => 'Missing id']); exit; }
|
||||
|
||||
$changes = json_decode($rawJson, true);
|
||||
if (!is_array($changes)) { echo json_encode(['ok' => false, 'error' => 'Invalid changes']); exit; }
|
||||
|
||||
$allowed = vv_get_conf_files();
|
||||
foreach ($changes as $c) {
|
||||
if (empty($c['file']) || !in_array($c['file'], $allowed, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Unauthorized file: ' . ($c['file'] ?? '')]);
|
||||
exit;
|
||||
}
|
||||
if (empty($c['key']) || !preg_match('/^[A-Z_][A-Z0-9_]*$/', $c['key'])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid key: ' . ($c['key'] ?? '')]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$results = vv_conf_write_changes($changes);
|
||||
echo json_encode(['ok' => !in_array(false, $results, true), 'files' => $results]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$file = trim($_POST['file'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
// Must be an allowed file for this host
|
||||
$allowed = vv_get_conf_files();
|
||||
if (!$file || !in_array($file, $allowed)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'File not permitted']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = vv_write_conf_raw($file, $content);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write file']);
|
||||
@@ -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)]);
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$script = SCRIPTS_DIR . '/' . $id;
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Script not found: ' . $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$logFile = vv_job_log_path($id);
|
||||
$logDir = dirname($logFile);
|
||||
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
||||
|
||||
$location = trim($_POST['location'] ?? '');
|
||||
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$runner = dirname(__DIR__) . '/run_job.sh';
|
||||
$flags = vv_job_flags($id);
|
||||
$locArg = $location ? ' ' . escapeshellarg('--location=' . $location) : '';
|
||||
exec('nohup bash ' . escapeshellarg($runner) . ' ' . escapeshellarg($id) . ' ' . escapeshellarg($script) . ' --dry-run' . ($flags ? " $flags" : '') . ' --manual' . $locArg . ' >> ' . escapeshellarg($logFile) . ' 2>&1 </dev/null &');
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/fallback.php';
|
||||
echo json_encode(vv_fb_all());
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$enabled = ($_POST['enabled'] ?? '0') === '1';
|
||||
|
||||
if (!$name || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $name)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid flag name']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = vv_conf_flag_set($name, $enabled);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf']);
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_SERVER['REQUEST_METHOD'] === 'POST' ? ($_POST['id'] ?? '') : ($_GET['id'] ?? ''));
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid job id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$logFile = vv_job_log_path($id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['clear'])) {
|
||||
if (file_exists($logFile)) file_put_contents($logFile, '');
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!file_exists($logFile)) {
|
||||
echo json_encode(['ok' => true, 'content' => '', 'ts' => 0]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lines = array_slice(file($logFile) ?: [], -200);
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'content' => implode('', $lines),
|
||||
'ts' => filemtime($logFile),
|
||||
]);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/media.php';
|
||||
|
||||
echo json_encode(vv_media_sessions());
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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(),
|
||||
'fallback' => vv_fallback_state(),
|
||||
'fallback_active' => vv_fallback_active(),
|
||||
'partner' => vv_partner_state(),
|
||||
'resources' => vv_system_resources(),
|
||||
'cpu' => vv_cpu_per_core(),
|
||||
'mem' => vv_memory_breakdown(),
|
||||
'net' => vv_network_stats(),
|
||||
'gpu' => vv_gpu_stats(),
|
||||
'gpu_procs' => vv_gpu_processes(),
|
||||
'containers' => vv_docker_containers(),
|
||||
'stopped' => vv_docker_stopped(),
|
||||
'transcode' => vv_transcode_sessions(),
|
||||
'ups' => vv_ups_stats(),
|
||||
'parity' => vv_parity_status(),
|
||||
'storage' => vv_storage_pools(),
|
||||
'array_disks' => vv_array_disks(),
|
||||
'scripts' => vv_scripts_status(),
|
||||
'thresholds' => vv_disk_thresholds(),
|
||||
'vms' => vv_get_vms(),
|
||||
'docker_folders' => vv_get_docker_folders(),
|
||||
'ts' => time(),
|
||||
]);
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
// Move a script between *_SCRIPTS arrays in master.conf.
|
||||
// POST: script (rel path), to_array (var name, or '' to remove from all arrays).
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$script = trim($_POST['script'] ?? '');
|
||||
$toArray = trim($_POST['to_array'] ?? '');
|
||||
|
||||
if (!$script || str_contains($script, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid script']);
|
||||
exit;
|
||||
}
|
||||
if ($toArray && !preg_match('/^[A-Z_]+_SCRIPTS$/', $toArray)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid array name']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
if (!file_exists($confPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'master.conf not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||
if (!$lines) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$scriptEsc = preg_quote($script, '/');
|
||||
$removedLine = null;
|
||||
$inArray = false;
|
||||
|
||||
// Step 1: find and remove the script line from whatever array it is currently in.
|
||||
$newLines = [];
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||
if ($inArray && preg_match('/^\s*(?:#\s*)?"' . $scriptEsc . '(?:\s[^"]*)?"/', $line)) {
|
||||
$removedLine = ' "' . $script . '"' . "\n"; // normalise indentation when re-inserting
|
||||
continue; // drop from current location
|
||||
}
|
||||
$newLines[] = $line;
|
||||
}
|
||||
|
||||
// Step 2: insert into target array (if specified).
|
||||
if ($toArray) {
|
||||
$resultLines = [];
|
||||
$inTarget = false;
|
||||
$inserted = false;
|
||||
foreach ($newLines as $line) {
|
||||
if (preg_match('/^\s*' . preg_quote($toArray, '/') . '\s*=\s*\(/', $line)) $inTarget = true;
|
||||
if ($inTarget && !$inserted && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) {
|
||||
$resultLines[] = $removedLine ?? (' "' . $script . '"' . "\n");
|
||||
$inTarget = false;
|
||||
$inserted = true;
|
||||
}
|
||||
$resultLines[] = $line;
|
||||
}
|
||||
if (!$inserted) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Target array "' . $toArray . '" not found in master.conf']);
|
||||
exit;
|
||||
}
|
||||
$newLines = $resultLines;
|
||||
}
|
||||
|
||||
if (file_put_contents($confPath, implode('', $newLines)) === false) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/partnership.php';
|
||||
echo json_encode(vv_partnership_all());
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// Raw conf read/write — respects per-host file visibility from vv_get_conf_files().
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$allowed = vv_get_conf_files();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$file = trim($_GET['file'] ?? 'master.conf');
|
||||
if (!in_array($file, $allowed, true) || str_contains($file, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not allowed']);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => true, 'content' => vv_read_conf_raw($file), 'file' => $file, 'allowed' => $allowed]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$file = trim($_POST['file'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
if (!in_array($file, $allowed, true) || str_contains($file, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not allowed']);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => vv_write_conf_raw($file, $content)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// Read-only endpoint: return full content of any script in SCRIPTS_DIR.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$id = trim($_GET['id'] ?? '');
|
||||
|
||||
// Must be relative path within SCRIPTS_DIR, no traversal, must end in .sh or .md
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.(sh|md)$/', $id)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = SCRIPTS_DIR . '/' . $id;
|
||||
if (!file_exists($path)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'content' => file_get_contents($path)]);
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$logDir = '/var/log/varaverk';
|
||||
$runs = [];
|
||||
|
||||
try {
|
||||
$ri = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($logDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($ri as $file) {
|
||||
if ($file->getExtension() !== 'json') continue;
|
||||
$d = @json_decode(@file_get_contents($file->getPathname()), true);
|
||||
if (!is_array($d) || empty($d['start']) || empty($d['status'])) continue;
|
||||
if ($d['status'] === 'running') continue;
|
||||
$id = (string)($d['id'] ?? '');
|
||||
$runs[] = [
|
||||
'id' => $id,
|
||||
'label' => basename(str_replace('.sh', '', $id)),
|
||||
'status' => $d['status'],
|
||||
'start' => (int)$d['start'],
|
||||
'dur' => isset($d['end']) ? max(0, (int)$d['end'] - (int)$d['start']) : 0,
|
||||
];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
usort($runs, fn($a, $b) => $b['start'] - $a['start']);
|
||||
echo json_encode(['ok' => true, 'runs' => array_slice($runs, 0, 24)]);
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
// Rewrite a *_SCRIPTS array in master.conf with a new script order.
|
||||
// POST: array_name (e.g. "DAILY_SCRIPTS"), scripts (JSON: [{"id":"rel/path.sh","enabled":true}, ...])
|
||||
// Preserves original entry lines (including inline flags/args) where possible.
|
||||
// Scripts absent from the new list are dropped; new scripts are added as fresh entries.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$arrayName = trim($_POST['array_name'] ?? '');
|
||||
$raw = $_POST['scripts'] ?? '';
|
||||
$decoded = json_decode($raw, true);
|
||||
|
||||
if (!$arrayName || !preg_match('/^[A-Z_]+_SCRIPTS$/', $arrayName)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid array_name']);
|
||||
exit;
|
||||
}
|
||||
if (!is_array($decoded)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid scripts JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate each entry
|
||||
$order = [];
|
||||
foreach ($decoded as $item) {
|
||||
$id = trim((string)($item['id'] ?? ''));
|
||||
$enabled = (bool)($item['enabled'] ?? true);
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) continue;
|
||||
$order[] = ['id' => $id, 'enabled' => $enabled];
|
||||
}
|
||||
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
if (!file_exists($confPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'master.conf not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||
if (!$lines) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Find the array block and extract original entry lines keyed by script path.
|
||||
$arrayEsc = preg_quote($arrayName, '/');
|
||||
$blockStart = null;
|
||||
$blockEnd = null;
|
||||
$depth = 0;
|
||||
$origEntries = []; // path → original trimmed content line (e.g. '"Daily/script.sh --flag"')
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
if ($blockStart === null) {
|
||||
if (preg_match('/^\s*' . $arrayEsc . '\s*=\s*\(/', $line)) {
|
||||
$blockStart = $i;
|
||||
$depth = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$depth += substr_count($line, '(');
|
||||
$depth -= substr_count($line, ')');
|
||||
if ($depth <= 0) {
|
||||
$blockEnd = $i;
|
||||
break;
|
||||
}
|
||||
// Collect entries (enabled and commented)
|
||||
if (preg_match('/^\s*(?:#\s*)?"([^"]+)"/', $line, $m)) {
|
||||
$parts = preg_split('/\s+/', trim($m[1]));
|
||||
$path = $parts[0] ?? '';
|
||||
if (substr($path, -3) === '.sh' && !isset($origEntries[$path])) {
|
||||
// Store the full quoted expression (may include flags after the path)
|
||||
$origEntries[$path] = '"' . $m[1] . '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($blockStart === null || $blockEnd === null) {
|
||||
echo json_encode(['ok' => false, 'error' => "Array $arrayName not found in master.conf"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build replacement block lines
|
||||
$newBlockLines = [];
|
||||
// Preserve the opening line exactly (e.g. "DAILY_SCRIPTS=(")
|
||||
$newBlockLines[] = $lines[$blockStart];
|
||||
|
||||
foreach ($order as $item) {
|
||||
$id = $item['id'];
|
||||
$enabled = $item['enabled'];
|
||||
$entry = $origEntries[$id] ?? '"' . $id . '"';
|
||||
$prefix = $enabled ? ' ' : ' # ';
|
||||
$newBlockLines[] = $prefix . $entry . "\n";
|
||||
}
|
||||
|
||||
// Preserve the closing line exactly
|
||||
$newBlockLines[] = $lines[$blockEnd];
|
||||
|
||||
// Replace the original block in $lines
|
||||
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines);
|
||||
|
||||
if (file_put_contents($confPath, implode('', $lines)) === false) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// Save rsync standalone config (location + cron) for a specific rsync tier.
|
||||
// POST: flag_name (e.g. "DAILY_RSYNC_ENABLED"), orch_id, location, cron
|
||||
// Stored in schedule.json under "__rsync_{FLAG_NAME}".
|
||||
// Triggers a cron rebuild so the standalone entry takes effect immediately.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$flagName = trim($_POST['flag_name'] ?? '');
|
||||
$orchId = trim($_POST['orch_id'] ?? '');
|
||||
$location = trim($_POST['location'] ?? '');
|
||||
$cron = trim($_POST['cron'] ?? '');
|
||||
|
||||
if (!$flagName || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $flagName)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid flag_name']);
|
||||
exit;
|
||||
}
|
||||
if ($orchId && (str_contains($orchId, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $orchId))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid orch_id']);
|
||||
exit;
|
||||
}
|
||||
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$key = '__rsync_' . $flagName;
|
||||
$schedule = vv_schedule_load();
|
||||
$schedule[$key] = [
|
||||
'flag_name' => $flagName,
|
||||
'orch_id' => $orchId,
|
||||
'location' => $location,
|
||||
'cron' => $cron,
|
||||
];
|
||||
if (!vv_schedule_save($schedule)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
vv_cron_rebuild($schedule);
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$script = SCRIPTS_DIR . '/' . $id;
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Script not found: ' . $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$logFile = vv_job_log_path($id);
|
||||
$logDir = dirname($logFile);
|
||||
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
||||
|
||||
$location = trim($_POST['location'] ?? '');
|
||||
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$runner = dirname(__DIR__) . '/run_job.sh';
|
||||
$flags = vv_job_flags($id);
|
||||
$locArg = $location ? ' ' . escapeshellarg('--location=' . $location) : '';
|
||||
exec('nohup bash ' . escapeshellarg($runner) . ' ' . escapeshellarg($id) . ' ' . escapeshellarg($script) . ($flags ? " $flags" : '') . ' --manual' . $locArg . ' >> ' . escapeshellarg($logFile) . ' 2>&1 </dev/null &');
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// Save custom-script folder assignments to schedule.json (__folders key).
|
||||
// POST: folders (JSON-encoded object: {"FolderName": ["Custom/script.sh", ...]})
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw = $_POST['folders'] ?? '';
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
foreach ($decoded as $name => $scripts) {
|
||||
$name = trim((string)$name);
|
||||
if (!$name || strlen($name) > 80) continue;
|
||||
if (!is_array($scripts)) continue;
|
||||
$cleanScripts = [];
|
||||
foreach ($scripts as $s) {
|
||||
$s = trim((string)$s);
|
||||
if (!$s || str_contains($s, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $s)) continue;
|
||||
$cleanScripts[] = $s;
|
||||
}
|
||||
$clean[$name] = $cleanScripts;
|
||||
}
|
||||
|
||||
$schedule = vv_schedule_load();
|
||||
$schedule['__folders'] = $clean;
|
||||
if (!vv_schedule_save($schedule)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
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'] ?? '');
|
||||
$log_enabled = ($_POST['log_enabled'] ?? '0') === '1';
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Missing id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Basic cron validation — 5 fields, or known @event trigger, or empty
|
||||
if ($cron && !in_array($cron, ['array_start', 'array_stop'], true)
|
||||
&& !preg_match('/^(\S+\s+){4}\S+$/', $cron)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = vv_schedule_update($id, $enabled, $cron, $log_enabled);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write schedule']);
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$id = trim($_GET['id'] ?? '');
|
||||
if (!$id || !preg_match('/^Custom\/[a-zA-Z0-9_\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
$path = SCRIPTS_DIR . '/' . $id;
|
||||
echo json_encode(['ok' => true, 'content' => file_exists($path) ? file_get_contents($path) : '']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = trim($_POST['action'] ?? 'save');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
|
||||
if (!$name || !preg_match('/^[a-zA-Z0-9_\-]+$/', $name)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Name must be letters, numbers, _ or - only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = 'Custom/' . $name . '.sh';
|
||||
$path = SCRIPTS_DIR . '/Custom/' . $name . '.sh';
|
||||
|
||||
if ($action === 'delete') {
|
||||
if (!file_exists($path)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Script not found']);
|
||||
exit;
|
||||
}
|
||||
unlink($path);
|
||||
$schedule = vv_schedule_load();
|
||||
unset($schedule[$id]);
|
||||
vv_schedule_save($schedule);
|
||||
vv_cron_rebuild($schedule);
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$dir = SCRIPTS_DIR . '/Custom';
|
||||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||
if (file_put_contents($path, $content) === false) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Failed to write script']);
|
||||
exit;
|
||||
}
|
||||
chmod($path, 0755);
|
||||
|
||||
// Ensure schedule.json has an entry so the script appears in the job list
|
||||
$schedule = vv_schedule_load();
|
||||
if (!isset($schedule[$id])) {
|
||||
$schedule[$id] = ['id' => $id, 'enabled' => false, 'cron' => '', 'log_enabled' => false, 'updated' => date('c')];
|
||||
vv_schedule_save($schedule);
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'id' => $id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_GET['id'] ?? '');
|
||||
if (!$id || str_contains($id, '..') || !preg_match('/^[a-zA-Z0-9_.\/\-]+$/', $id)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$isScript = str_ends_with($id, '.sh');
|
||||
$name = basename($id, $isScript ? '.sh' : '');
|
||||
$dirName = basename(dirname($id)); // e.g. "Media", "Rsync", "Orchestrators"
|
||||
$slug = strtolower(str_replace(['_', '-'], ' ', $name));
|
||||
$parts = explode(' ', $slug);
|
||||
$first = $parts[0] ?? ''; // e.g. "radarr" from "radarr cleanup"
|
||||
|
||||
// Script header (bash scripts only)
|
||||
$path = SCRIPTS_DIR . '/' . $id;
|
||||
$header = ($isScript && file_exists($path)) ? vv_script_header_clean($path) : '';
|
||||
|
||||
// Section matcher: heading contains the full slug OR first meaningful word (>3 chars)
|
||||
$matcher = function(string $heading, bool $isIntro) use ($slug, $first): bool {
|
||||
if ($isIntro) return false;
|
||||
$h = strtolower(str_replace(['_', '-'], ' ', $heading));
|
||||
return str_contains($h, $slug)
|
||||
|| (strlen($first) > 3 && str_contains($h, $first));
|
||||
};
|
||||
|
||||
// Search main README/Manual + module-level files for this script's directory
|
||||
$scriptsDir = SCRIPTS_DIR;
|
||||
$searchFiles = [];
|
||||
foreach (['README', 'Manual'] as $docType) {
|
||||
$main = "$scriptsDir/$docType.md";
|
||||
if (file_exists($main)) $searchFiles[] = [$docType, $main];
|
||||
|
||||
$mod = "$scriptsDir/$docType-$dirName.md";
|
||||
if ($dirName && $dirName !== '.' && file_exists($mod) && $mod !== $main) {
|
||||
$searchFiles[] = ["$docType — $dirName", $mod];
|
||||
}
|
||||
}
|
||||
|
||||
$sections = [];
|
||||
foreach ($searchFiles as [$label, $file]) {
|
||||
$body = vv_readme_section($file, $matcher);
|
||||
if ($body) $sections[] = ['source' => $label, 'body' => $body];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'name' => $name,
|
||||
'header' => $header,
|
||||
'sections' => $sections,
|
||||
]);
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$scriptsDir = trim($_POST['scripts_dir'] ?? '');
|
||||
|
||||
if (!$scriptsDir) {
|
||||
echo json_encode(['ok' => false, 'error' => 'scripts_dir is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_dir($scriptsDir)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Directory does not exist']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cfgFile = '/boot/config/plugins/varaverk/varaverk.cfg';
|
||||
$cfgDir = dirname($cfgFile);
|
||||
if (!is_dir($cfgDir)) mkdir($cfgDir, 0755, true);
|
||||
|
||||
$content = 'SCRIPTS_DIR="' . addslashes($scriptsDir) . '"' . "\n";
|
||||
$ok = file_put_contents($cfgFile, $content) !== false;
|
||||
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write cfg file']);
|
||||
@@ -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,
|
||||
]);
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
// Returns current run status for all scheduled jobs.
|
||||
// Used by the scheduler page to light up running indicators without user interaction.
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$schedule = vv_schedule_load();
|
||||
$result = [];
|
||||
foreach ($schedule as $id => $entry) {
|
||||
$statFile = vv_job_stat_path($id);
|
||||
if (!file_exists($statFile)) continue;
|
||||
$stat = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: [];
|
||||
$status = $stat['status'] ?? 'unknown';
|
||||
if ($status === 'running' && !empty($stat['pid']) && !file_exists("/proc/{$stat['pid']}")) {
|
||||
$status = 'error';
|
||||
}
|
||||
$result[$id] = $status;
|
||||
}
|
||||
echo json_encode($result);
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$statFile = vv_job_stat_path($id);
|
||||
if (!file_exists($statFile)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No stat file — script may not be running']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stat = json_decode(file_get_contents($statFile) ?: '{}', true) ?: [];
|
||||
|
||||
if (($stat['status'] ?? '') !== 'running') {
|
||||
echo json_encode(['ok' => true, 'msg' => 'Not running']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pid = (int)($stat['pid'] ?? 0);
|
||||
if ($pid < 2) {
|
||||
echo json_encode(['ok' => false, 'error' => 'No valid PID in stat file']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Kill the whole process group so the script and all its children die together.
|
||||
// pgid is usually the same as the session leader PID from run_job.sh.
|
||||
$pgid = (int)trim(shell_exec("ps -o pgid= -p $pid 2>/dev/null") ?: '0');
|
||||
|
||||
if ($pgid > 1) {
|
||||
shell_exec("kill -TERM -$pgid 2>/dev/null");
|
||||
} else {
|
||||
// Fallback: kill the direct PID and its children
|
||||
shell_exec("pkill -TERM -P $pid 2>/dev/null");
|
||||
shell_exec("kill -TERM $pid 2>/dev/null");
|
||||
}
|
||||
|
||||
// Give it up to 3s to exit gracefully
|
||||
$dead = false;
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
usleep(500000);
|
||||
if (!file_exists("/proc/$pid")) { $dead = true; break; }
|
||||
}
|
||||
|
||||
// Force-kill if still alive
|
||||
if (!$dead) {
|
||||
if ($pgid > 1) shell_exec("kill -KILL -$pgid 2>/dev/null");
|
||||
shell_exec("pkill -KILL -P $pid 2>/dev/null");
|
||||
shell_exec("kill -KILL $pid 2>/dev/null");
|
||||
usleep(300000);
|
||||
$dead = !file_exists("/proc/$pid");
|
||||
}
|
||||
|
||||
// Clear any lock files in /tmp/unraid_locks whose content matches this PID
|
||||
$lockDir = '/tmp/unraid_locks';
|
||||
$cleared = [];
|
||||
foreach (glob("$lockDir/*.lock") ?: [] as $lf) {
|
||||
$content = trim(file_get_contents($lf) ?: '');
|
||||
$lockPid = (int)explode(':', $content)[0];
|
||||
if ($lockPid === $pid || !file_exists("/proc/$lockPid")) {
|
||||
@unlink($lf);
|
||||
$cleared[] = basename($lf);
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear by script name in case PID rotated
|
||||
$scriptBase = basename($id, '.sh');
|
||||
$namedLock = "$lockDir/{$scriptBase}.lock";
|
||||
if (file_exists($namedLock)) {
|
||||
@unlink($namedLock);
|
||||
if (!in_array(basename($namedLock), $cleared)) $cleared[] = basename($namedLock);
|
||||
}
|
||||
|
||||
// Update stat file
|
||||
$now = time();
|
||||
$stat['status'] = 'stopped';
|
||||
$stat['end'] = $now;
|
||||
$stat['exit'] = -1;
|
||||
unset($stat['pid']);
|
||||
file_put_contents($statFile, json_encode($stat));
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'killed' => $dead,
|
||||
'locks' => $cleared,
|
||||
]);
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
$allowed = ['stop', 'shutdown', 'restart'];
|
||||
if (!in_array($action, $allowed, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'invalid action']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cmd = match($action) {
|
||||
'stop' => '/usr/local/sbin/mdcmd stop',
|
||||
'shutdown' => '/sbin/shutdown -h now',
|
||||
'restart' => '/sbin/shutdown -r now',
|
||||
};
|
||||
|
||||
exec($cmd . ' > /dev/null 2>&1 &');
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/watchdog.php';
|
||||
echo json_encode(vv_wd_all());
|
||||
Reference in New Issue
Block a user