Add Varaverk plugin scaffold (PHP, unRAID native)
Plugin lives in Plugin/ — invisible to User Script Plugin. dev_install.sh symlinks into /usr/local/emhttp/plugins/varaverk/ for development. Pages: Monitor (5s poll), Scheduler (orchs + Advanced children, toggle + cron), Config (raw editor, host-aware file access), Docs (markdown + live $VAR substitution). API endpoints: monitor.php (JSON), scheduler.php (writes schedule.json + cron.d), config.php (writes conf files with host permission check). Includes: config.php (parser, host detection, var map), scheduler.php (job tree, cron.d rebuild), monitor.php (docker, GPU, resources, fallback, transcode), docs.php (file tree, var substitution, Parsedown renderer).
This commit is contained in:
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# dev_install.sh — symlinks plugin files into unRAID WebUI for local development.
|
||||
# Run once after cloning. Re-run if the plugin directory is moved.
|
||||
# Safe to re-run: removes stale symlink before recreating.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_NAME="varaverk"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE="$SCRIPT_DIR/usr/local/emhttp/plugins/$PLUGIN_NAME"
|
||||
TARGET="/usr/local/emhttp/plugins/$PLUGIN_NAME"
|
||||
|
||||
if [ ! -d "$SOURCE" ]; then
|
||||
echo "ERROR: Source not found: $SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -L "$TARGET" ]; then
|
||||
echo "Removing existing symlink: $TARGET"
|
||||
rm "$TARGET"
|
||||
elif [ -d "$TARGET" ]; then
|
||||
echo "ERROR: $TARGET exists as a real directory — remove it manually first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ln -s "$SOURCE" "$TARGET"
|
||||
echo "Linked: $TARGET -> $SOURCE"
|
||||
echo ""
|
||||
echo "Plugin available in unRAID WebUI under Utilities → Varaverk."
|
||||
echo "Changes in $SOURCE take effect immediately (no restart needed)."
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$file = trim($body['file'] ?? '');
|
||||
$content = $body['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,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/monitor.php';
|
||||
|
||||
echo json_encode([
|
||||
'fallback' => vv_fallback_state(),
|
||||
'resources' => vv_system_resources(),
|
||||
'gpu' => vv_gpu_stats(),
|
||||
'gpu_procs' => vv_gpu_processes(),
|
||||
'containers' => vv_docker_containers(),
|
||||
'stopped' => vv_docker_stopped(),
|
||||
'transcode' => vv_transcode_sessions(),
|
||||
'ts' => time(),
|
||||
]);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$id = trim($body['id'] ?? '');
|
||||
$enabled = (bool)($body['enabled'] ?? false);
|
||||
$cron = trim($body['cron'] ?? '');
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Missing id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Basic cron validation — 5 fields or empty
|
||||
if ($cron && !preg_match('/^(\S+\s+){4}\S+$/', $cron)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = vv_schedule_update($id, $enabled, $cron);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write schedule']);
|
||||
@@ -0,0 +1,96 @@
|
||||
/* Varaverk plugin styles — inherits unRAID theme, adds plugin-specific layout */
|
||||
|
||||
#varaverk-wrap { padding: 10px; font-family: inherit; }
|
||||
|
||||
/* Tab bar */
|
||||
#vv-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid #444; }
|
||||
.vv-tab { padding: 6px 16px; text-decoration: none; color: #aaa; border-radius: 4px 4px 0 0; }
|
||||
.vv-tab:hover { color: #fff; background: #333; }
|
||||
.vv-tab.active { color: #fff; background: #555; border-bottom: 2px solid #fff; }
|
||||
|
||||
/* Cards / layout */
|
||||
.vv-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.vv-card { flex: 1; min-width: 200px; background: #1e1e1e; border: 1px solid #444;
|
||||
border-radius: 6px; padding: 12px; }
|
||||
.vv-wide { flex: 100%; }
|
||||
.vv-card h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase;
|
||||
color: #888; letter-spacing: 0.05em; }
|
||||
|
||||
/* Fallback state badge */
|
||||
.vv-state-badge { font-size: 20px; font-weight: bold; padding: 4px 0; }
|
||||
.vv-state-normal { color: #4caf50; }
|
||||
.vv-state-failover { color: #f44336; }
|
||||
.vv-state-no_internet { color: #ff9800; }
|
||||
.vv-state-dark { color: #9e9e9e; }
|
||||
.vv-state-unknown { color: #666; }
|
||||
|
||||
/* Docker table */
|
||||
#vv-docker-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
#vv-docker-table th { text-align: left; padding: 4px 8px; color: #888;
|
||||
border-bottom: 1px solid #444; }
|
||||
#vv-docker-table td { padding: 4px 8px; border-bottom: 1px solid #2a2a2a; }
|
||||
.vv-status-up { color: #4caf50; }
|
||||
.vv-status-down { color: #f44336; }
|
||||
|
||||
/* Scheduler */
|
||||
#vv-scheduler { max-width: 900px; }
|
||||
.vv-hint { color: #888; font-size: 13px; margin-bottom: 16px; }
|
||||
.vv-job { margin-bottom: 4px; }
|
||||
.vv-orch { background: #1e1e1e; border: 1px solid #444; border-radius: 6px;
|
||||
padding: 10px 12px; margin-bottom: 8px; }
|
||||
.vv-script { background: #161616; border: 1px solid #333; border-radius: 4px;
|
||||
padding: 6px 10px; margin: 4px 0; margin-left: 20px; }
|
||||
.vv-job-row { display: flex; align-items: center; gap: 10px; }
|
||||
.vv-job-label { flex: 1; font-size: 14px; }
|
||||
.vv-cron { width: 160px; background: #111; border: 1px solid #444; color: #ddd;
|
||||
padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 13px; }
|
||||
.vv-children { padding-top: 6px; }
|
||||
.vv-advanced-toggle { background: none; border: 1px solid #555; color: #aaa;
|
||||
padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||
.vv-advanced-toggle:hover { border-color: #888; color: #fff; }
|
||||
|
||||
/* Toggle switch */
|
||||
.vv-toggle { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }
|
||||
.vv-toggle input { opacity: 0; width: 0; height: 0; }
|
||||
.vv-slider { position: absolute; inset: 0; background: #444; border-radius: 20px; cursor: pointer;
|
||||
transition: 0.2s; }
|
||||
.vv-slider:before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; bottom: 3px;
|
||||
background: #fff; border-radius: 50%; transition: 0.2s; }
|
||||
.vv-toggle input:checked + .vv-slider { background: #4caf50; }
|
||||
.vv-toggle input:checked + .vv-slider:before { transform: translateX(16px); }
|
||||
|
||||
/* Config editor */
|
||||
#vv-conf-tabs { display: flex; gap: 4px; margin-bottom: 8px; }
|
||||
.vv-conf-tab { padding: 4px 12px; text-decoration: none; color: #aaa;
|
||||
border: 1px solid #444; border-radius: 4px; font-size: 13px; }
|
||||
.vv-conf-tab.active { color: #fff; background: #333; border-color: #666; }
|
||||
#vv-conf-editor { width: 100%; min-height: 500px; background: #111; color: #ddd;
|
||||
border: 1px solid #444; padding: 12px; font-family: monospace;
|
||||
font-size: 13px; line-height: 1.5; border-radius: 4px; box-sizing: border-box; resize: vertical; }
|
||||
#vv-conf-actions { margin-top: 8px; display: flex; align-items: center; gap: 10px; }
|
||||
#vv-conf-actions button { padding: 6px 18px; background: #4caf50; border: none;
|
||||
color: #fff; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
||||
#vv-conf-actions button:hover { background: #388e3c; }
|
||||
#vv-conf-status { font-size: 13px; color: #aaa; }
|
||||
|
||||
/* Docs */
|
||||
#vv-docs { display: flex; gap: 16px; }
|
||||
#vv-docs-sidebar { width: 220px; flex-shrink: 0; }
|
||||
#vv-docs-sidebar h3 { font-size: 12px; text-transform: uppercase; color: #888; margin: 0 0 8px; }
|
||||
#vv-docs-sidebar ul { list-style: none; padding: 0; margin: 0; }
|
||||
#vv-docs-sidebar li { margin: 2px 0; }
|
||||
#vv-docs-sidebar a { display: block; padding: 3px 8px; font-size: 12px; color: #aaa;
|
||||
text-decoration: none; border-radius: 3px; }
|
||||
#vv-docs-sidebar a:hover { background: #222; color: #fff; }
|
||||
#vv-docs-sidebar a.active { background: #333; color: #fff; }
|
||||
#vv-docs-content { flex: 1; min-width: 0; }
|
||||
.vv-doc-body { background: #1a1a1a; border: 1px solid #444; border-radius: 6px;
|
||||
padding: 20px; line-height: 1.7; }
|
||||
.vv-doc-body h1, .vv-doc-body h2, .vv-doc-body h3 { color: #ddd; }
|
||||
.vv-doc-body code { background: #111; padding: 1px 5px; border-radius: 3px; font-size: 12px; }
|
||||
.vv-doc-body pre { background: #111; padding: 12px; border-radius: 4px; overflow-x: auto; }
|
||||
.vv-doc-hint { font-size: 12px; color: #666; margin-top: 8px; }
|
||||
|
||||
/* Live var substitution colours */
|
||||
code.vv-live-var { color: #4caf50; background: #0d1f0d; }
|
||||
code.vv-unknown-var { color: #ff9800; background: #1f130d; }
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
// Config file parser and writer.
|
||||
// Reads master.conf and the appropriate host*.conf based on running host.
|
||||
|
||||
define('SCRIPTS_DIR', '/mnt/user/appdata/unraid_scripts');
|
||||
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
|
||||
|
||||
function vv_get_hostname(): string {
|
||||
return trim(shell_exec('hostname -s') ?: '');
|
||||
}
|
||||
|
||||
function vv_detect_host(): string {
|
||||
// Reads master.conf to find HOST1/HOST2 hostnames, returns 'host1', 'host2', or 'unknown'
|
||||
$master = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1_NAME\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m1);
|
||||
preg_match('/^\s*HOST2_NAME\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m2);
|
||||
$hostname = vv_get_hostname();
|
||||
if (!empty($m1[1]) && strcasecmp($hostname, $m1[1]) === 0) return 'host1';
|
||||
if (!empty($m2[1]) && strcasecmp($hostname, $m2[1]) === 0) return 'host2';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function vv_is_owner(): bool {
|
||||
return vv_detect_host() === 'host1';
|
||||
}
|
||||
|
||||
function vv_read_conf_raw(string $filename): string {
|
||||
$path = CONF_DIR . '/' . $filename;
|
||||
return file_exists($path) ? file_get_contents($path) : '';
|
||||
}
|
||||
|
||||
function vv_write_conf_raw(string $filename, string $content): bool {
|
||||
$path = CONF_DIR . '/' . $filename;
|
||||
return file_put_contents($path, $content) !== false;
|
||||
}
|
||||
|
||||
function vv_get_conf_files(): array {
|
||||
// Returns conf files this host is allowed to view/edit
|
||||
$host = vv_detect_host();
|
||||
$files = [];
|
||||
if ($host === 'host1') {
|
||||
$files[] = 'master.conf';
|
||||
$files[] = 'host1.conf';
|
||||
} elseif ($host === 'host2') {
|
||||
$files[] = 'host2.conf';
|
||||
} else {
|
||||
// Unknown host — show all for dev/debug
|
||||
foreach (glob(CONF_DIR . '/*.conf') as $f) {
|
||||
$files[] = basename($f);
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
// Parse conf into key=>value map for $VAR substitution in docs
|
||||
function vv_conf_vars(): array {
|
||||
$vars = [];
|
||||
$files = ['master.conf'];
|
||||
$host = vv_detect_host();
|
||||
if ($host === 'host1') $files[] = 'host1.conf';
|
||||
if ($host === 'host2') $files[] = 'host2.conf';
|
||||
|
||||
foreach ($files as $f) {
|
||||
$raw = vv_read_conf_raw($f);
|
||||
// Match: VAR_NAME="value" or VAR_NAME=value (no quotes)
|
||||
preg_match_all('/^\s*([A-Z0-9_]+)\s*=\s*["\']?([^"\'#\n]*?)["\']?\s*(?:#.*)?$/m', $raw, $m);
|
||||
foreach ($m[1] as $i => $key) {
|
||||
$vars[$key] = trim($m[2][$i]);
|
||||
}
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// Docs — markdown file discovery, $VAR substitution, and rendering.
|
||||
// Requires parsedown or similar. Falls back to <pre> if not available.
|
||||
|
||||
define('PARSEDOWN_PATH', '/usr/local/emhttp/plugins/varaverk/lib/Parsedown.php');
|
||||
|
||||
function vv_docs_tree(): array {
|
||||
$base = SCRIPTS_DIR;
|
||||
$tree = [];
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($files as $f) {
|
||||
if ($f->isFile() && strtolower($f->getExtension()) === 'md') {
|
||||
$rel = ltrim(str_replace($base, '', $f->getPathname()), '/');
|
||||
$tree[] = $rel;
|
||||
}
|
||||
}
|
||||
sort($tree);
|
||||
return $tree;
|
||||
}
|
||||
|
||||
function vv_docs_render(string $rel, array $vars): string {
|
||||
$path = SCRIPTS_DIR . '/' . $rel;
|
||||
if (!file_exists($path)) return '<p>File not found.</p>';
|
||||
|
||||
$md = file_get_contents($path);
|
||||
|
||||
// Substitute `$VAR_NAME` markers with live conf values
|
||||
$md = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function($m) use ($vars) {
|
||||
$key = $m[1];
|
||||
return isset($vars[$key])
|
||||
? '<code class="vv-live-var">' . htmlspecialchars($vars[$key]) . '</code>'
|
||||
: '<code class="vv-unknown-var">$' . htmlspecialchars($key) . '</code>';
|
||||
}, $md);
|
||||
|
||||
// Render markdown
|
||||
if (file_exists(PARSEDOWN_PATH)) {
|
||||
require_once PARSEDOWN_PATH;
|
||||
$pd = new Parsedown();
|
||||
$pd->setSafeMode(true);
|
||||
return $pd->text($md);
|
||||
}
|
||||
|
||||
// Fallback: plain preformatted text
|
||||
return '<pre>' . htmlspecialchars($md) . '</pre>';
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
// Monitor helpers — docker, GPU, resources, transcode sessions, fallback state.
|
||||
|
||||
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 --format=csv,noheader,nounits 2>/dev/null');
|
||||
if (!$out) return ['available' => false];
|
||||
|
||||
$parts = array_map('trim', explode(',', $out));
|
||||
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),
|
||||
];
|
||||
}
|
||||
|
||||
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 {
|
||||
// RAM
|
||||
$mem = [];
|
||||
foreach (file('/proc/meminfo') ?: [] as $line) {
|
||||
if (preg_match('/^(MemTotal|MemAvailable):\s+(\d+)/', $line, $m))
|
||||
$mem[$m[1]] = (int)$m[2];
|
||||
}
|
||||
|
||||
// CPU (1-second sample)
|
||||
$cpu = (int)trim(shell_exec("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d. -f1") ?: '0');
|
||||
|
||||
// Disk — ramdisk + cache
|
||||
$ramdisk = vv_df('/mnt/ramdisk_transcodes');
|
||||
$cache = vv_df('/mnt/cache');
|
||||
|
||||
return [
|
||||
'ram_total_mb' => (int)(($mem['MemTotal'] ?? 0) / 1024),
|
||||
'ram_free_mb' => (int)(($mem['MemAvailable'] ?? 0) / 1024),
|
||||
'cpu_percent' => $cpu,
|
||||
'ramdisk' => $ramdisk,
|
||||
'cache' => $cache,
|
||||
];
|
||||
}
|
||||
|
||||
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_fallback_state(): array {
|
||||
// State file written by fallback.sh
|
||||
$stateFile = '/tmp/fallback_state.db';
|
||||
if (!file_exists($stateFile)) return ['state' => 'UNKNOWN'];
|
||||
$raw = [];
|
||||
foreach (file($stateFile) ?: [] as $line) {
|
||||
[$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
|
||||
$raw[trim($k)] = trim($v);
|
||||
}
|
||||
return [
|
||||
'state' => $raw['state'] ?? 'UNKNOWN',
|
||||
'failover_start' => $raw['failover_start'] ?? '0',
|
||||
'tier2_started' => $raw['tier2_started'] ?? 'false',
|
||||
'tier3_started' => $raw['tier3_started'] ?? 'false',
|
||||
'tier4_started' => $raw['tier4_started'] ?? 'false',
|
||||
];
|
||||
}
|
||||
|
||||
function vv_transcode_sessions(): array {
|
||||
// Read from transcode state file written by transcode_manager.sh
|
||||
$stateFile = '/tmp/transcode_state.db';
|
||||
if (!file_exists($stateFile)) return [];
|
||||
$raw = [];
|
||||
foreach (file($stateFile) ?: [] as $line) {
|
||||
[$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
|
||||
$raw[trim($k)] = trim($v);
|
||||
}
|
||||
return $raw;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
// Scheduler — manages schedule.json and /etc/cron.d/varaverk.
|
||||
// schedule.json is per-host, never synced.
|
||||
|
||||
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 {
|
||||
$schedule = vv_schedule_load();
|
||||
$schedule[$id] = [
|
||||
'id' => $id,
|
||||
'enabled' => $enabled,
|
||||
'cron' => $cron,
|
||||
'updated' => date('c'),
|
||||
];
|
||||
if (!vv_schedule_save($schedule)) return false;
|
||||
return vv_cron_rebuild($schedule);
|
||||
}
|
||||
|
||||
function vv_cron_rebuild(array $schedule): bool {
|
||||
$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']}";
|
||||
$cron = $entry['cron'];
|
||||
$lines[] = "$cron " . CRON_USER . " bash \"$script\" >> /var/log/varaverk/jobs.log 2>&1";
|
||||
}
|
||||
$lines[] = "";
|
||||
|
||||
return file_put_contents(CRON_FILE, implode("\n", $lines)) !== false;
|
||||
}
|
||||
|
||||
// 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' => ''];
|
||||
$orchs[] = [
|
||||
'id' => $id,
|
||||
'label' => basename($path, '.sh'),
|
||||
'type' => 'orchestrator',
|
||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||
'cron' => $entry['cron'] ?? '',
|
||||
'children' => vv_script_children($path, $schedule),
|
||||
];
|
||||
}
|
||||
return $orchs;
|
||||
}
|
||||
|
||||
// Parse an orchestrator script to find which child scripts it calls
|
||||
function vv_script_children(string $orchPath, array $schedule): array {
|
||||
$scriptsDir = SCRIPTS_DIR;
|
||||
$content = file_get_contents($orchPath) ?: '';
|
||||
$children = [];
|
||||
|
||||
// Match: bash "path/to/script.sh" or source path/to/script.sh
|
||||
preg_match_all('/(?:bash|source|\.)\s+"?([^"\s]+\.sh)"?/m', $content, $m);
|
||||
foreach ($m[1] as $rel) {
|
||||
// Normalise relative paths
|
||||
$rel = ltrim(str_replace($scriptsDir . '/', '', $rel), './');
|
||||
$id = $rel;
|
||||
$entry = $schedule[$id] ?? ['enabled' => false, 'cron' => ''];
|
||||
$children[] = [
|
||||
'id' => $id,
|
||||
'label' => basename($rel, '.sh'),
|
||||
'type' => 'script',
|
||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||
'cron' => $entry['cron'] ?? '',
|
||||
];
|
||||
}
|
||||
return $children;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Varaverk — shared JS utilities
|
||||
// Page-specific JS lives inline in each page partial.
|
||||
|
||||
// Flash a status element briefly then fade
|
||||
function vvFlashStatus(el, msg, ok) {
|
||||
el.textContent = msg;
|
||||
el.style.color = ok ? '#4caf50' : '#f44336';
|
||||
setTimeout(() => { el.textContent = ''; }, 3000);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
$files = vv_get_conf_files();
|
||||
$active = $_GET['conf'] ?? ($files[0] ?? '');
|
||||
if (!in_array($active, $files)) $active = $files[0] ?? '';
|
||||
?>
|
||||
|
||||
<div id="vv-config">
|
||||
|
||||
<!-- File selector -->
|
||||
<div id="vv-conf-tabs">
|
||||
<?php foreach ($files as $f): ?>
|
||||
<a href="?tab=config&conf=<?= urlencode($f) ?>"
|
||||
class="vv-conf-tab<?= $f === $active ? ' active' : '' ?>">
|
||||
<?= htmlspecialchars($f) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($active): ?>
|
||||
<form id="vv-conf-form">
|
||||
<input type="hidden" name="file" value="<?= htmlspecialchars($active) ?>">
|
||||
<textarea id="vv-conf-editor" name="content" spellcheck="false"><?=
|
||||
htmlspecialchars(vv_read_conf_raw($active))
|
||||
?></textarea>
|
||||
<div id="vv-conf-actions">
|
||||
<button type="button" onclick="vvSaveConf()">Save</button>
|
||||
<span id="vv-conf-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p>No configuration files found.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function vvSaveConf() {
|
||||
const form = document.getElementById('vv-conf-form');
|
||||
const file = form.querySelector('[name=file]').value;
|
||||
const content = form.querySelector('[name=content]').value;
|
||||
const status = document.getElementById('vv-conf-status');
|
||||
|
||||
status.textContent = 'Saving...';
|
||||
fetch('/plugins/varaverk/api/config.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({file, content})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => { status.textContent = d.ok ? '✓ Saved' : '✗ ' + (d.error ?? 'Error'); })
|
||||
.catch(() => { status.textContent = '✗ Request failed'; });
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/include/docs.php';
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$tree = vv_docs_tree();
|
||||
$vars = vv_conf_vars();
|
||||
$active = $_GET['doc'] ?? '';
|
||||
|
||||
// Validate: must be a .md file within SCRIPTS_DIR
|
||||
$active = preg_match('/^[a-zA-Z0-9_\-\/]+\.md$/', $active) ? $active : '';
|
||||
if ($active && !file_exists(SCRIPTS_DIR . '/' . $active)) $active = '';
|
||||
?>
|
||||
|
||||
<div id="vv-docs">
|
||||
|
||||
<div id="vv-docs-sidebar">
|
||||
<h3>Documents</h3>
|
||||
<ul>
|
||||
<?php foreach ($tree as $rel): ?>
|
||||
<li>
|
||||
<a href="?tab=docs&doc=<?= urlencode($rel) ?>"
|
||||
class="<?= $rel === $active ? 'active' : '' ?>">
|
||||
<?= htmlspecialchars($rel) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="vv-docs-content">
|
||||
<?php if ($active): ?>
|
||||
<div class="vv-doc-body">
|
||||
<?= vv_docs_render($active, $vars) ?>
|
||||
</div>
|
||||
<p class="vv-doc-hint">
|
||||
Values shown in <code class="vv-live-var">green</code> are live from your conf files.
|
||||
<code class="vv-unknown-var">Orange</code> means the variable was not found.
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p>Select a document from the sidebar.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php require_once dirname(__DIR__) . '/include/monitor.php'; ?>
|
||||
|
||||
<div id="vv-monitor">
|
||||
|
||||
<div class="vv-row">
|
||||
<div class="vv-card" id="vv-fallback">
|
||||
<h3>Fallback State</h3>
|
||||
<div class="vv-state-badge" id="vv-fallback-state">—</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-resources">
|
||||
<h3>Resources</h3>
|
||||
<div id="vv-resource-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-gpu-card">
|
||||
<h3>GPU</h3>
|
||||
<div id="vv-gpu-body">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-row">
|
||||
<div class="vv-card vv-wide" id="vv-transcode">
|
||||
<h3>Transcode</h3>
|
||||
<div id="vv-transcode-body">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-row">
|
||||
<div class="vv-card vv-wide" id="vv-docker">
|
||||
<h3>Containers</h3>
|
||||
<table id="vv-docker-table">
|
||||
<thead><tr><th>Name</th><th>Status</th><th>Image</th></tr></thead>
|
||||
<tbody id="vv-docker-body"><tr><td colspan="3">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Poll monitor API every 5 seconds
|
||||
function vvPollMonitor() {
|
||||
fetch('/plugins/varaverk/api/monitor.php')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
// Fallback state
|
||||
const fb = d.fallback ?? {};
|
||||
const badge = document.getElementById('vv-fallback-state');
|
||||
badge.textContent = fb.state ?? '—';
|
||||
badge.className = 'vv-state-badge vv-state-' + (fb.state ?? 'unknown').toLowerCase();
|
||||
|
||||
// Resources
|
||||
const res = d.resources ?? {};
|
||||
document.getElementById('vv-resource-body').innerHTML =
|
||||
`<p>RAM: ${res.ram_free_mb ?? '—'} MB free / ${res.ram_total_mb ?? '—'} MB</p>
|
||||
<p>CPU: ${res.cpu_percent ?? '—'}%</p>
|
||||
<p>Ramdisk: ${res.ramdisk?.used_mb ?? '—'} MB / ${res.ramdisk?.size_mb ?? '—'} MB</p>`;
|
||||
|
||||
// GPU
|
||||
const gpu = d.gpu ?? {};
|
||||
if (gpu.available) {
|
||||
document.getElementById('vv-gpu-body').innerHTML =
|
||||
`<p>${gpu.name}</p>
|
||||
<p>VRAM: ${gpu.memory_used} / ${gpu.memory_total} MB</p>
|
||||
<p>Util: ${gpu.utilization}% Temp: ${gpu.temperature}°C</p>`;
|
||||
} else {
|
||||
document.getElementById('vv-gpu-body').innerHTML = '<p>No GPU detected</p>';
|
||||
}
|
||||
|
||||
// Docker
|
||||
const containers = d.containers ?? [];
|
||||
const tbody = document.getElementById('vv-docker-body');
|
||||
tbody.innerHTML = containers.length
|
||||
? containers.map(c =>
|
||||
`<tr><td>${c.name}</td><td class="vv-status-${c.status.startsWith('Up') ? 'up' : 'down'}">${c.status}</td><td>${c.image}</td></tr>`
|
||||
).join('')
|
||||
: '<tr><td colspan="3">No running containers</td></tr>';
|
||||
})
|
||||
.catch(() => {}); // silent on poll failure
|
||||
}
|
||||
|
||||
vvPollMonitor();
|
||||
setInterval(vvPollMonitor, 5000);
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
$tree = vv_job_tree();
|
||||
?>
|
||||
|
||||
<div id="vv-scheduler">
|
||||
|
||||
<p class="vv-hint">Orchestrators run their child scripts in the correct order.
|
||||
Disable an orchestrator and enable individual scripts below it to run them
|
||||
in isolation for debugging or testing.</p>
|
||||
|
||||
<?php foreach ($tree as $orch): ?>
|
||||
<div class="vv-job vv-orch" data-id="<?= htmlspecialchars($orch['id']) ?>">
|
||||
<div class="vv-job-row">
|
||||
<label class="vv-toggle">
|
||||
<input type="checkbox" class="vv-enabled"
|
||||
<?= $orch['enabled'] ? 'checked' : '' ?>
|
||||
onchange="vvSaveJob(this)">
|
||||
<span class="vv-slider"></span>
|
||||
</label>
|
||||
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
|
||||
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
|
||||
placeholder="cron expression" onblur="vvSaveJob(this)">
|
||||
<span class="vv-job-status" id="vv-status-<?= md5($orch['id']) ?>"></span>
|
||||
<?php if (!empty($orch['children'])): ?>
|
||||
<button class="vv-advanced-toggle" onclick="vvToggleAdvanced(this)">Advanced ▸</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($orch['children'])): ?>
|
||||
<div class="vv-children" style="display:none;">
|
||||
<?php foreach ($orch['children'] as $child): ?>
|
||||
<div class="vv-job vv-script" data-id="<?= htmlspecialchars($child['id']) ?>">
|
||||
<div class="vv-job-row">
|
||||
<label class="vv-toggle">
|
||||
<input type="checkbox" class="vv-enabled"
|
||||
<?= $child['enabled'] ? 'checked' : '' ?>
|
||||
onchange="vvSaveJob(this)">
|
||||
<span class="vv-slider"></span>
|
||||
</label>
|
||||
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
|
||||
<input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
|
||||
placeholder="cron expression" onblur="vvSaveJob(this)">
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function vvSaveJob(el) {
|
||||
const row = el.closest('.vv-job');
|
||||
const id = row.dataset.id;
|
||||
const enabled = row.querySelector('.vv-enabled').checked;
|
||||
const cron = row.querySelector('.vv-cron').value.trim();
|
||||
|
||||
fetch('/plugins/varaverk/api/scheduler.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({id, enabled, cron})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const statusEl = document.getElementById('vv-status-' + btoa(id).replace(/=/g,''));
|
||||
if (statusEl) statusEl.textContent = d.ok ? '✓' : '✗ ' + (d.error ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
function vvToggleAdvanced(btn) {
|
||||
const children = btn.closest('.vv-job').querySelector('.vv-children');
|
||||
const visible = children.style.display !== 'none';
|
||||
children.style.display = visible ? 'none' : 'block';
|
||||
btn.textContent = visible ? 'Advanced ▸' : 'Advanced ▾';
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
Menu="Utilities:85"
|
||||
Title="Varaverk"
|
||||
Icon="varaverk.png"
|
||||
---
|
||||
<?php
|
||||
$plugin = 'varaverk';
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
$pluginDir = "$docroot/plugins/$plugin";
|
||||
|
||||
// Determine active tab
|
||||
$tab = $_GET['tab'] ?? 'monitor';
|
||||
$validTabs = ['monitor', 'scheduler', 'config', 'docs'];
|
||||
if (!in_array($tab, $validTabs)) $tab = 'monitor';
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="/plugins/<?=$plugin?>/css/varaverk.css">
|
||||
|
||||
<div id="varaverk-wrap">
|
||||
|
||||
<!-- Tab bar -->
|
||||
<div id="vv-tabs">
|
||||
<?php foreach ($validTabs as $t): ?>
|
||||
<a href="?tab=<?=$t?>" class="vv-tab<?= $t === $tab ? ' active' : '' ?>">
|
||||
<?= ucfirst($t) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
<div id="vv-content">
|
||||
<?php
|
||||
$page = "$pluginDir/pages/$tab.php";
|
||||
if (file_exists($page)) include $page;
|
||||
else echo "<p>Page not found: $tab</p>";
|
||||
?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/plugins/<?=$plugin?>/js/varaverk.js"></script>
|
||||
Reference in New Issue
Block a user