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:
Gmer4Lfe
2026-05-23 16:19:12 -04:00
parent 5e4f510a42
commit 4ab3ddbc47
15 changed files with 829 additions and 0 deletions
@@ -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}% &nbsp; 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>