diff --git a/Plugin/dev_install.sh b/Plugin/dev_install.sh new file mode 100755 index 0000000..fe09c46 --- /dev/null +++ b/Plugin/dev_install.sh @@ -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)." diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/api/config.php b/Plugin/usr/local/emhttp/plugins/varaverk/api/config.php new file mode 100644 index 0000000..aca63b4 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/api/config.php @@ -0,0 +1,17 @@ + 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']); diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/api/monitor.php b/Plugin/usr/local/emhttp/plugins/varaverk/api/monitor.php new file mode 100644 index 0000000..8b05328 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/api/monitor.php @@ -0,0 +1,14 @@ + 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(), +]); diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php new file mode 100644 index 0000000..7d7f6e4 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php @@ -0,0 +1,22 @@ + 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']); diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css b/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css new file mode 100644 index 0000000..4dc005d --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css @@ -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; } diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/include/config.php b/Plugin/usr/local/emhttp/plugins/varaverk/include/config.php new file mode 100644 index 0000000..4cf2995 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/include/config.php @@ -0,0 +1,72 @@ +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; +} diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/include/docs.php b/Plugin/usr/local/emhttp/plugins/varaverk/include/docs.php new file mode 100644 index 0000000..e2596ef --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/include/docs.php @@ -0,0 +1,48 @@ + 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 '

File not found.

'; + + $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]) + ? '' . htmlspecialchars($vars[$key]) . '' + : '$' . htmlspecialchars($key) . ''; + }, $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 '
' . htmlspecialchars($md) . '
'; +} diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/include/monitor.php b/Plugin/usr/local/emhttp/plugins/varaverk/include/monitor.php new file mode 100644 index 0000000..8ed8fa3 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/include/monitor.php @@ -0,0 +1,121 @@ +/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; +} diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php new file mode 100644 index 0000000..d057df3 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php @@ -0,0 +1,98 @@ + $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; +} diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/js/varaverk.js b/Plugin/usr/local/emhttp/plugins/varaverk/js/varaverk.js new file mode 100644 index 0000000..ef48c86 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/js/varaverk.js @@ -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); +} diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/pages/config.php b/Plugin/usr/local/emhttp/plugins/varaverk/pages/config.php new file mode 100644 index 0000000..a1e31b5 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/pages/config.php @@ -0,0 +1,54 @@ + + +
+ + +
+ + + + + +
+ + +
+ + +
+ + +
+
+ +

No configuration files found.

+ + +
+ + diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/pages/docs.php b/Plugin/usr/local/emhttp/plugins/varaverk/pages/docs.php new file mode 100644 index 0000000..02d6cdb --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/pages/docs.php @@ -0,0 +1,44 @@ + + +
+ +
+

Documents

+ +
+ +
+ +
+ +
+

+ Values shown in green are live from your conf files. + Orange means the variable was not found. +

+ +

Select a document from the sidebar.

+ +
+ +
diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/pages/monitor.php b/Plugin/usr/local/emhttp/plugins/varaverk/pages/monitor.php new file mode 100644 index 0000000..b9efcab --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/pages/monitor.php @@ -0,0 +1,85 @@ + + +
+ +
+
+

Fallback State

+
+
+ +
+

Resources

+
Loading...
+
+ +
+

GPU

+
Loading...
+
+
+ +
+
+

Transcode

+
Loading...
+
+
+ +
+
+

Containers

+ + + +
NameStatusImage
Loading...
+
+
+ +
+ + diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php new file mode 100644 index 0000000..5fca296 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php @@ -0,0 +1,79 @@ + + +
+ +

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.

+ + +
+
+ + + + + + + +
+ + + + +
+ + +
+ + diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/varaverk.page b/Plugin/usr/local/emhttp/plugins/varaverk/varaverk.page new file mode 100644 index 0000000..22fc807 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/varaverk.page @@ -0,0 +1,40 @@ +Menu="Utilities:85" +Title="Varaverk" +Icon="varaverk.png" +--- + + + + +
+ + +
+ + + + + +
+ + +
+ Page not found: $tab

"; + ?> +
+ +
+ +