Monitor tab: - CPU card: per-core bars (freq-colored), overall usage bar, rolling history chart - Memory card: breakdown by system/VM/ZFS/Docker/free with progress bars - Network card: LAN + ext + Tailscale IP display, live RX/TX chart with auto-scale - Streams card: Emby/Jellyfin/Plex sessions with playback bar and server badges - GPU card: adds power draw, encode%, decode% meters - Transcode card: ramdisk vs SSD location pill, session count, flip history - Canvas chart helpers: vvDrawChart, vvDrawNetChart, vvMeter, vvFmtBps, vvFmtGib api/monitor.php: adds cpu, mem, net to response payload
286 lines
12 KiB
PHP
286 lines
12 KiB
PHP
<?php
|
|
// Scheduler — manages schedule.json and /etc/cron.d/varaverk.
|
|
// schedule.json is per-host, never synced.
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/confform.php';
|
|
|
|
define('SCHEDULE_FILE', '/boot/config/plugins/varaverk/schedule.json');
|
|
define('CRON_FILE', '/etc/cron.d/varaverk');
|
|
define('CRON_USER', 'root');
|
|
|
|
function vv_schedule_load(): array {
|
|
if (!file_exists(SCHEDULE_FILE)) return [];
|
|
$data = json_decode(file_get_contents(SCHEDULE_FILE), true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
function vv_schedule_save(array $schedule): bool {
|
|
$dir = dirname(SCHEDULE_FILE);
|
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
|
return file_put_contents(SCHEDULE_FILE, json_encode($schedule, JSON_PRETTY_PRINT)) !== false;
|
|
}
|
|
|
|
function vv_schedule_update(string $id, bool $enabled, string $cron, bool $log_enabled = false): bool {
|
|
$schedule = vv_schedule_load();
|
|
$schedule[$id] = [
|
|
'id' => $id,
|
|
'enabled' => $enabled,
|
|
'cron' => $cron,
|
|
'log_enabled' => $log_enabled,
|
|
'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' : '';
|
|
}
|
|
|
|
define('LOG_DIR', '/var/log/varaverk');
|
|
|
|
function vv_job_log_path(string $id): string {
|
|
return LOG_DIR . '/' . preg_replace('/\.sh$/', '.log', $id);
|
|
}
|
|
|
|
function vv_cron_rebuild(array $schedule): bool {
|
|
if (!is_dir(LOG_DIR)) mkdir(LOG_DIR, 0755, true);
|
|
|
|
$lines = ["# Varaverk — managed by plugin, do not edit manually"];
|
|
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
|
|
$lines[] = "";
|
|
|
|
$scriptsDir = SCRIPTS_DIR;
|
|
foreach ($schedule as $entry) {
|
|
if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue;
|
|
$script = "$scriptsDir/{$entry['id']}";
|
|
$logFile = vv_job_log_path($entry['id']);
|
|
$logDir = dirname($logFile);
|
|
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
|
$flags = !empty($entry['log_enabled']) ? ' --log' : '';
|
|
$lines[] = "{$entry['cron']} bash -c 'echo; echo \"── \$(date \"+%Y-%m-%d %H:%M:%S\") ──────────────────────\"; bash \"$script\"$flags' >> \"$logFile\" 2>&1";
|
|
}
|
|
$lines[] = "";
|
|
|
|
// dcron (dillon's cron, Unraid's system crond) reads /etc/cron.d/ but refuses
|
|
// world-writable files and does not use a username field in system crontab format.
|
|
if (file_put_contents(CRON_FILE, implode("\n", $lines)) === false) return false;
|
|
chmod(CRON_FILE, 0600);
|
|
return true;
|
|
}
|
|
|
|
// Extract the suggested cron and label from a bash script header.
|
|
// Looks for: # Schedule: */7 * * * * (every 7 minutes via User Scripts plugin)
|
|
function vv_script_suggested_cron(string $path): array {
|
|
if (!file_exists($path)) return ['cron' => '', 'label' => ''];
|
|
$lines = array_slice(file($path) ?: [], 0, 30);
|
|
foreach ($lines as $raw) {
|
|
$raw = rtrim($raw);
|
|
if (!preg_match('/^#\s*Schedule:\s*(.+)$/i', $raw, $m)) continue;
|
|
$tail = trim($m[1]);
|
|
$parts = preg_split('/\s+/', $tail, 6);
|
|
$cron = implode(' ', array_slice($parts, 0, 5));
|
|
$label = isset($parts[5]) ? trim($parts[5], '() ') : '';
|
|
return ['cron' => $cron, 'label' => $label];
|
|
}
|
|
return ['cron' => '', 'label' => ''];
|
|
}
|
|
|
|
// Parse user_script_plug-in.sh into an array of script blocks.
|
|
// Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron})
|
|
function vv_parse_user_script_template(): array {
|
|
$file = SCRIPTS_DIR . '/user_script_plug-in.sh';
|
|
if (!file_exists($file)) return [];
|
|
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
|
$prefix = rtrim(SCRIPTS_DIR, '/') . '/';
|
|
$blocks = [];
|
|
$cur = null;
|
|
|
|
foreach ($lines as $line) {
|
|
// Block header: # ── TITLE ──...
|
|
if (preg_match('/^# ── (.+?) ─/', $line, $m)) {
|
|
if ($cur) $blocks[] = $cur;
|
|
$cur = ['title' => trim($m[1]), 'schedule' => '', 'desc' => [], 'scripts' => []];
|
|
continue;
|
|
}
|
|
if (!$cur) continue;
|
|
|
|
// Schedule / Background — captured but not added to desc
|
|
if (preg_match('/^# (Schedule|Background):\s+(.+)$/i', $line, $m)) {
|
|
if (strtolower($m[1]) === 'schedule') $cur['schedule'] = trim($m[2]);
|
|
continue;
|
|
}
|
|
|
|
// Sunday block inline cron: # 0 6 * * 0 bash /path/script.sh [args]
|
|
if (preg_match('/^#\s+(\S+ +\S+ +\S+ +\S+ +\S+)\s+bash\s+(\S+\.sh)/', $line, $m)) {
|
|
$rel = str_replace($prefix, '', trim($m[2]));
|
|
$cur['scripts'][] = ['rel' => $rel, 'cron' => preg_replace('/\s+/', ' ', trim($m[1]))];
|
|
continue;
|
|
}
|
|
|
|
// Standard bash line: # bash /prefix/path/script.sh [args]
|
|
if (preg_match('/^# bash\s+(\S+\.sh)/', $line, $m)) {
|
|
$rel = str_replace($prefix, '', $m[1]);
|
|
// Only add primary command (dedupe by rel)
|
|
$rels = array_column($cur['scripts'], 'rel');
|
|
if (!in_array($rel, $rels)) {
|
|
$cur['scripts'][] = ['rel' => $rel, 'cron' => ''];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Description line
|
|
if (preg_match('/^# ?(.*)$/', $line, $m)) {
|
|
$inner = $m[1];
|
|
if (!preg_match('/^[─━=\-]{3,}\s*$/', $inner) && !preg_match('/^█/', $inner)) {
|
|
$cur['desc'][] = $inner;
|
|
}
|
|
}
|
|
}
|
|
if ($cur) $blocks[] = $cur;
|
|
return $blocks;
|
|
}
|
|
|
|
// Extract a one-line description from a bash script header.
|
|
// Supports two patterns:
|
|
// 1. # PURPOSE (or # DESCRIPTION) block — returns first non-separator line after it
|
|
// 2. First meaningful comment line after the banner
|
|
function vv_script_description(string $path): string {
|
|
if (!file_exists($path)) return '';
|
|
$lines = array_slice(file($path) ?: [], 0, 50);
|
|
$purposeNext = false;
|
|
$first = '';
|
|
foreach ($lines as $raw) {
|
|
$raw = rtrim($raw);
|
|
if (str_starts_with($raw, '#!')) continue; // shebang
|
|
if (!str_starts_with($raw, '#')) continue; // non-comment
|
|
$inner = ltrim(substr($raw, 1)); // strip leading #
|
|
if ($inner === '') continue; // blank
|
|
if (preg_match('/^[\s=\-─━\*]+$/', $inner)) continue; // pure separator
|
|
if (preg_match('/^={3,}/', $inner)) continue; // banner (=== Title ===)
|
|
if (preg_match('/^\s*(PURPOSE|DESCRIPTION|Description)\s*$/i', $inner)) {
|
|
$purposeNext = true;
|
|
continue;
|
|
}
|
|
if ($purposeNext) {
|
|
return mb_substr(trim($inner), 0, 200);
|
|
}
|
|
if (!$first) $first = mb_substr(trim($inner), 0, 200);
|
|
}
|
|
return $first;
|
|
}
|
|
|
|
function vv_custom_scripts(): array {
|
|
$dir = SCRIPTS_DIR . '/Custom';
|
|
$schedule = vv_schedule_load();
|
|
$scripts = [];
|
|
foreach (glob("$dir/*.sh") ?: [] as $path) {
|
|
$rel = 'Custom/' . basename($path);
|
|
$entry = $schedule[$rel] ?? [];
|
|
$scripts[] = [
|
|
'id' => $rel,
|
|
'label' => basename($path, '.sh'),
|
|
'desc' => vv_script_description($path),
|
|
'enabled' => (bool)($entry['enabled'] ?? false),
|
|
'cron' => $entry['cron'] ?? '',
|
|
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
|
];
|
|
}
|
|
return $scripts;
|
|
}
|
|
|
|
// Walk the scripts repo and return the job tree:
|
|
// orchestrators as top-level, individual scripts as children
|
|
function vv_job_tree(): array {
|
|
$scriptsDir = SCRIPTS_DIR;
|
|
$schedule = vv_schedule_load();
|
|
|
|
// Orchestrators are in Orchestrators/ and their children are all scripts they call
|
|
// For now: walk top-level folders, treat *_management.sh or *_maintenance.sh as orchs
|
|
$orchPattern = "$scriptsDir/Orchestrators/*.sh";
|
|
$orchs = [];
|
|
|
|
foreach (glob($orchPattern) ?: [] as $path) {
|
|
$id = 'Orchestrators/' . basename($path);
|
|
$entry = $schedule[$id] ?? ['enabled' => false, 'cron' => ''];
|
|
$suggested = vv_script_suggested_cron($path);
|
|
$orchs[] = [
|
|
'id' => $id,
|
|
'label' => basename($path, '.sh'),
|
|
'desc' => vv_script_description($path),
|
|
'type' => 'orchestrator',
|
|
'enabled' => (bool)($entry['enabled'] ?? false),
|
|
'cron' => $entry['cron'] ?? '',
|
|
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
|
'suggested_cron' => $suggested['cron'],
|
|
'suggested_label' => $suggested['label'],
|
|
'children' => vv_script_children($path, $schedule),
|
|
];
|
|
}
|
|
return $orchs;
|
|
}
|
|
|
|
// Parse a bash array from master.conf content and return its script paths.
|
|
// Handles entries with inline args ("script.sh --flag") and skips commented lines (#"...").
|
|
function vv_parse_conf_array(string $conf, string $varName): array {
|
|
if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\((.*?)^\s*\)/ms', $conf, $m)) {
|
|
return [];
|
|
}
|
|
$scripts = [];
|
|
preg_match_all('/^\s*(?!#)"([^"]+)"/m', $m[1], $entries);
|
|
foreach ($entries[1] as $entry) {
|
|
$parts = preg_split('/\s+/', trim($entry));
|
|
$path = $parts[0] ?? '';
|
|
if (substr($path, -3) === '.sh') $scripts[] = $path;
|
|
}
|
|
return $scripts;
|
|
}
|
|
|
|
// Parse an orchestrator script to find which child scripts it calls.
|
|
// Two strategies, merged and deduped:
|
|
// 1. Static paths: $SCRIPT_DIR/../Category/script.sh or $SCRIPTS_ROOT/Category/script.sh
|
|
// 2. master.conf arrays: detects ${VARNAME[@]} iteration and reads the array from master.conf
|
|
// Root-level files (load_config.sh etc.) excluded — must be in a subdirectory.
|
|
// Each candidate validated against the filesystem.
|
|
function vv_script_children(string $orchPath, array $schedule): array {
|
|
$scriptsDir = SCRIPTS_DIR;
|
|
$content = file_get_contents($orchPath) ?: '';
|
|
$children = [];
|
|
$seen = [];
|
|
|
|
$addChild = function(string $rel) use ($scriptsDir, $schedule, &$children, &$seen) {
|
|
if (isset($seen[$rel]) || !file_exists("$scriptsDir/$rel")) return;
|
|
$seen[$rel] = true;
|
|
$entry = $schedule[$rel] ?? ['enabled' => false, 'cron' => ''];
|
|
$children[] = [
|
|
'id' => $rel,
|
|
'label' => basename($rel, '.sh'),
|
|
'desc' => vv_script_description("$scriptsDir/$rel"),
|
|
'type' => 'script',
|
|
'enabled' => (bool)($entry['enabled'] ?? false),
|
|
'cron' => $entry['cron'] ?? '',
|
|
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
|
];
|
|
};
|
|
|
|
// Strategy 1: static variable paths ($VAR/../Category/script.sh or $VAR/Category/script.sh)
|
|
preg_match_all(
|
|
'/\$[A-Z_]+\/(?:\.\.\/)?([A-Za-z][A-Za-z0-9_.\-]*\/[A-Za-z0-9_.\-]+\.sh)/',
|
|
$content, $m
|
|
);
|
|
foreach ($m[1] as $rel) $addChild($rel);
|
|
|
|
// Strategy 2: master.conf arrays — find every ${VARNAME_SCRIPTS[@]} the orch iterates
|
|
preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
|
|
if (!empty($refs[1])) {
|
|
$confRaw = file_get_contents(CONF_DIR . '/master.conf') ?: '';
|
|
foreach (array_unique($refs[1]) as $varName) {
|
|
foreach (vv_parse_conf_array($confRaw, $varName) as $rel) $addChild($rel);
|
|
}
|
|
}
|
|
|
|
return $children;
|
|
}
|