varaverk: monitor overhaul — CPU/memory/network/streams cards; scheduler polish

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
This commit is contained in:
Gmer4Lfe
2026-05-24 10:52:42 -04:00
parent f7bef55e51
commit ab3e28531e
7 changed files with 938 additions and 61 deletions
@@ -1,7 +1,8 @@
<?php
require_once dirname(__DIR__) . '/include/scheduler.php';
$tree = vv_job_tree();
$blocks = vv_parse_user_script_template();
$tree = vv_job_tree();
$customs = vv_custom_scripts();
$blocks = vv_parse_user_script_template();
// Build a flat lookup: rel_path → [enabled, cron, in_tree]
$treeMap = [];
@@ -45,7 +46,11 @@ foreach ($tree as $orch) {
<div class="vv-job-actions">
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
<button class="vv-btn-sm" onclick="vvSelectLog(this)">Log</button>
<?php if (vv_conf_has_sections($orch['id'])): ?>
<button class="vv-btn-sm vv-conf-btn" onclick="vvEditConf('<?= $oid ?>')">Config</button>
<?php endif; ?>
<?php $orchLog = vv_job_log_path($orch['id']); ?>
<button class="vv-btn-sm vv-log-btn<?= (file_exists($orchLog) && filesize($orchLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
<label class="vv-log-label" title="Enable verbose --log output">
<input type="checkbox" class="vv-log-enabled"
<?= $orch['log_enabled'] ? 'checked' : '' ?>
@@ -80,7 +85,11 @@ foreach ($tree as $orch) {
<div class="vv-job-actions">
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
<button class="vv-btn-sm" onclick="vvSelectLog(this)">Log</button>
<?php if (vv_conf_has_sections($child['id'])): ?>
<button class="vv-btn-sm vv-conf-btn" onclick="vvEditConf('<?= $cid ?>')">Config</button>
<?php endif; ?>
<?php $childLog = vv_job_log_path($child['id']); ?>
<button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
<label class="vv-log-label" title="Enable verbose --log output">
<input type="checkbox" class="vv-log-enabled"
<?= $child['log_enabled'] ? 'checked' : '' ?>
@@ -97,11 +106,61 @@ foreach ($tree as $orch) {
</div>
<?php endforeach; ?>
<!-- Custom Scripts container -->
<div class="vv-card vv-wide vv-sched-card vv-custom-card">
<div class="vv-job-row">
<span class="vv-job-label" style="font-weight:bold;">Custom Scripts</span>
<span class="vv-custom-count"><?= count($customs) ?> script<?= count($customs) !== 1 ? 's' : '' ?></span>
<button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleCustom(this)">Scripts ▸</button>
</div>
<div class="vv-children" style="display:none;">
<?php if (empty($customs)): ?>
<p class="vv-custom-empty">No custom scripts yet — click <strong>+ Add Script</strong> to create one.</p>
<?php else: ?>
<?php foreach ($customs as $cs): $csid = htmlspecialchars($cs['id']); ?>
<div class="vv-script" data-id="<?= $csid ?>">
<div class="vv-job-row">
<label class="vv-toggle" title="Enable/disable">
<input type="checkbox" class="vv-enabled"
<?= $cs['enabled'] ? 'checked' : '' ?>
onchange="vvSaveJob(this)">
<span class="vv-slider"></span>
</label>
<span class="vv-job-label"><?= htmlspecialchars($cs['label']) ?></span>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>"
placeholder="cron expression">
<span class="vv-save-check"></span>
</div>
<?php if (!empty($cs['desc'])): ?>
<div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?></div>
<?php endif; ?>
<div class="vv-job-actions">
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
<button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
<?php $csLog = vv_job_log_path($cs['id']); ?>
<button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
<label class="vv-log-label" title="Enable verbose --log output">
<input type="checkbox" class="vv-log-enabled"
<?= $cs['log_enabled'] ? 'checked' : '' ?>
onchange="vvSaveJob(this)">
<span>Verbose</span>
</label>
<span class="vv-job-dot"></span>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div><!-- /#vv-sched-cards -->
<!-- Single global save -->
<div class="vv-sched-footer">
<button class="vv-save-btn" onclick="vvSaveAll(this)">Save Schedule</button>
<button id="vv-save-schedule-btn" class="vv-save-btn" onclick="vvSaveAll(this)">Save Schedule</button>
<button class="vv-save-btn vv-add-script-btn" onclick="vvAddScript()">+ Add Script</button>
<button id="vv-delete-script-btn" class="vv-save-btn vv-delete-btn" onclick="vvDeleteScript()" style="display:none">&#128465; Delete</button>
<span id="vv-save-all-status" class="vv-save-status"></span>
</div>
@@ -112,17 +171,23 @@ foreach ($tree as $orch) {
<div class="vv-card vv-log-card">
<div class="vv-log-toolbar" style="margin-bottom:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">&#8592; Back</button>
<button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">&#9776; Scheduler Info</button>
<button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">&#8592; <span id="vv-restore-label"></span></button>
<span id="vv-log-title" style="font-size:13px;font-weight:bold;color:#ccc;">Suggested Schedules</span>
<span id="vv-log-dot" style="display:none;width:8px;height:8px;border-radius:50%;background:#4caf50;flex-shrink:0;"></span>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<label id="vv-scroll-lock-label" class="vv-log-label" title="Lock auto-scroll" style="display:none">
<input type="checkbox" id="vv-scroll-lock"> <span>Scroll Lock</span>
<label id="vv-auto-scroll-label" class="vv-log-label" title="Auto-scroll to newest line" style="display:none">
<input type="checkbox" id="vv-auto-scroll" checked> <span>Auto Scroll</span>
</label>
<label id="vv-scroll-lock-label" class="vv-log-label" title="Lock viewport — cannot scroll while active" style="display:none">
<input type="checkbox" id="vv-scroll-lock" onchange="vvToggleScrollLock(this)"> <span>Scroll Lock</span>
</label>
<label id="vv-invert-log-label" class="vv-log-label" title="Show newest lines at top" style="display:none">
<input type="checkbox" id="vv-invert-log" onchange="vvFetchRight()"> <span>Invert</span>
</label>
<button id="vv-save-script-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveScript()" style="display:none">Save Script</button>
<button id="vv-save-conf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveConf()" style="display:none">Save Config</button>
<span id="vv-log-ts" class="vv-log-ts"></span>
<button id="vv-clear-btn" class="vv-btn-sm" onclick="vvClearRightLog()" style="display:none">Clear</button>
</div>
@@ -195,6 +260,18 @@ foreach ($tree as $orch) {
<!-- Log view (shown when a script is selected) -->
<pre id="vv-log-pre" class="vv-log-pre vv-log-right-pre" style="display:none"></pre>
<!-- Config form view (script conf sections) -->
<div id="vv-confform" style="display:none; overflow-y:auto;"></div>
<!-- Editor view (custom scripts) -->
<div id="vv-editor" style="display:none">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px;">
<span style="color:#888;font-size:12px;flex-shrink:0;">Name:</span>
<input type="text" id="vv-editor-name" class="vv-cron" style="flex:1" placeholder="script_name (no .sh)">
</div>
<textarea id="vv-editor-body" class="vv-editor-body" spellcheck="false"></textarea>
</div>
</div>
<?php
$totalJobs = 0; $scheduledJobs = 0;
@@ -230,6 +307,8 @@ let vvRunningId = null;
let vvLastTs = 0;
let vvStaleCount = 0;
let vvPollTimer = null;
let vvEditorId = null;
let vvConfId = null;
function vvPost(url, data) {
const params = new URLSearchParams({csrf_token, ...data});
@@ -258,26 +337,51 @@ function vvFitRight() {
right.style.height = Math.round(leftRect.bottom - rightRect.top) + 'px';
const contentH = Math.max(80, lf.getBoundingClientRect().top - 32 - toolbar.getBoundingClientRect().bottom);
// Apply to whichever content pane is visible
if (pre.style.display !== 'none') {
pre.style.maxHeight = 'none';
pre.style.height = contentH + 'px';
}
if (sug.style.display !== 'none') {
sug.style.height = contentH + 'px';
sug.style.height = contentH + 'px';
}
const ed = document.getElementById('vv-editor');
if (ed.style.display !== 'none') {
document.getElementById('vv-editor-body').style.height = Math.max(60, contentH - 38) + 'px';
}
const cf = document.getElementById('vv-confform');
if (cf.style.display !== 'none') {
cf.style.height = contentH + 'px';
}
}
window.addEventListener('resize', vvFitRight);
function vvShowLogMode(id) {
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = '';
document.getElementById('vv-back-btn').style.display = '';
document.getElementById('vv-clear-btn').style.display = '';
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-editor').style.display = 'none';
document.getElementById('vv-log-pre').style.display = '';
document.getElementById('vv-back-btn').style.display = '';
document.getElementById('vv-clear-btn').style.display = '';
document.getElementById('vv-save-script-btn').style.display = 'none';
document.getElementById('vv-save-conf-btn').style.display = 'none';
document.getElementById('vv-delete-script-btn').style.display = 'none';
document.getElementById('vv-confform').style.display = 'none';
document.getElementById('vv-auto-scroll-label').style.display = '';
document.getElementById('vv-scroll-lock-label').style.display = '';
document.getElementById('vv-invert-log-label').style.display = '';
document.getElementById('vv-save-schedule-btn').disabled = false;
document.getElementById('vv-save-schedule-btn').style.opacity = '';
const name = id.replace(/\.sh$/, '').split('/').pop();
document.getElementById('vv-log-title').textContent = name;
document.getElementById('vv-restore-btn').style.display = 'none';
}
function vvRestoreLastLog() {
const last = localStorage.getItem('vv-last-job');
if (last && document.querySelector('[data-id="' + CSS.escape(last) + '"]')) vvOpenRight(last);
}
function vvToggleScrollLock(cb) {
document.getElementById('vv-log-pre').style.overflowY = cb.checked ? 'hidden' : '';
}
function vvBackToSuggestions() {
@@ -289,12 +393,30 @@ function vvBackToSuggestions() {
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
vvSetRunning(null);
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-suggestions').style.display = '';
document.getElementById('vv-back-btn').style.display = 'none';
document.getElementById('vv-clear-btn').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-editor').style.display = 'none';
document.getElementById('vv-suggestions').style.display = '';
document.getElementById('vv-back-btn').style.display = 'none';
document.getElementById('vv-clear-btn').style.display = 'none';
document.getElementById('vv-save-script-btn').style.display = 'none';
document.getElementById('vv-save-conf-btn').style.display = 'none';
document.getElementById('vv-delete-script-btn').style.display = 'none';
document.getElementById('vv-confform').style.display = 'none';
vvConfId = null;
document.getElementById('vv-save-schedule-btn').disabled = false;
document.getElementById('vv-save-schedule-btn').style.opacity = '';
const lastJob = localStorage.getItem('vv-last-job');
if (lastJob) {
const lname = lastJob.replace(/\.sh$/, '').split('/').pop();
document.getElementById('vv-restore-label').textContent = lname;
document.getElementById('vv-restore-btn').style.display = '';
}
document.getElementById('vv-auto-scroll-label').style.display = 'none';
document.getElementById('vv-scroll-lock-label').style.display = 'none';
document.getElementById('vv-invert-log-label').style.display = 'none';
document.getElementById('vv-auto-scroll').checked = true;
document.getElementById('vv-scroll-lock').checked = false;
document.getElementById('vv-log-pre').style.overflowY = '';
document.getElementById('vv-log-title').textContent = 'Suggested Schedules';
document.getElementById('vv-log-ts').textContent = '';
document.getElementById('vv-log-dot').style.display = 'none';
@@ -307,6 +429,7 @@ function vvOpenRight(id) {
if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
}
vvActiveId = id;
localStorage.setItem('vv-last-job', id);
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
if (job) job.querySelector('.vv-job-row').classList.add('vv-row-selected');
@@ -324,12 +447,21 @@ function vvFetchRight() {
.then(d => {
const pre = document.getElementById('vv-log-pre');
const ts = document.getElementById('vv-log-ts');
const scrollLock = document.getElementById('vv-scroll-lock')?.checked;
const invert = document.getElementById('vv-invert-log')?.checked;
const autoScroll = document.getElementById('vv-auto-scroll').checked;
const scrollLock = document.getElementById('vv-scroll-lock').checked;
const invert = document.getElementById('vv-invert-log').checked;
const savedScroll = pre.scrollTop;
if (!d.ok) { pre.textContent = '✗ ' + (d.error ?? 'Error'); return; }
const content = d.content || '(no log yet)';
pre.textContent = invert ? content.split('\n').reverse().join('\n') : content;
if (!scrollLock) pre.scrollTop = invert ? 0 : pre.scrollHeight;
const content = d.content || '';
vvSetLogBtnState(vvActiveId, content.trim().length > 0);
pre.textContent = content.trim() ? (invert ? content.split('\n').reverse().join('\n') : content) : '(no log yet)';
if (scrollLock) {
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
} else if (autoScroll) {
pre.scrollTop = invert ? 0 : pre.scrollHeight;
} else {
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
}
ts.textContent = d.ts ? 'Last run: ' + new Date(d.ts * 1000).toLocaleString() : '';
// Running indicator: clear when log stops changing
@@ -450,13 +582,211 @@ function vvToggleAdvanced(btn) {
btn.textContent = visible ? 'Advanced ▸' : 'Advanced ▾';
}
function vvToggleCustom(btn) {
const card = btn.closest('.vv-custom-card');
const children = card.querySelector('.vv-children');
const visible = children.style.display !== 'none';
children.style.display = visible ? 'none' : 'block';
btn.textContent = visible ? 'Scripts ▸' : 'Scripts ▾';
}
function vvSetLogBtnState(id, hasLog) {
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
if (!job) return;
job.querySelector('.vv-log-btn').classList.toggle('vv-has-log', hasLog);
}
function vvClearRightLog() {
if (!vvActiveId) return;
vvPost('/plugins/varaverk/api/log.php', {id: vvActiveId, clear: '1'})
.then(() => vvFetchRight())
.then(() => { vvSetLogBtnState(vvActiveId, false); vvFetchRight(); })
.catch(() => {});
}
function vvAddScript() {
vvEditorId = null;
const nameEl = document.getElementById('vv-editor-name');
nameEl.value = '';
nameEl.readOnly = false;
document.getElementById('vv-editor-body').value = '#!/bin/bash\n\n';
vvShowEditorMode('New Script');
nameEl.focus();
}
function vvEditScript(id) {
if (vvActiveId) {
const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
}
vvActiveId = null;
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
vvEditorId = id;
const name = id.replace(/^Custom\//, '').replace(/\.sh$/, '');
const nameEl = document.getElementById('vv-editor-name');
nameEl.value = name;
nameEl.readOnly = true;
document.getElementById('vv-editor-body').value = 'Loading…';
fetch('/plugins/varaverk/api/script.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(d => { document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading script'; })
.catch(() => { document.getElementById('vv-editor-body').value = '# Error loading script'; });
vvShowEditorMode(name);
}
function vvShowEditorMode(title) {
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-editor').style.display = 'flex';
document.getElementById('vv-back-btn').style.display = '';
document.getElementById('vv-restore-btn').style.display = 'none';
document.getElementById('vv-save-script-btn').style.display = '';
document.getElementById('vv-clear-btn').style.display = 'none';
document.getElementById('vv-auto-scroll-label').style.display = 'none';
document.getElementById('vv-scroll-lock-label').style.display = 'none';
document.getElementById('vv-invert-log-label').style.display = 'none';
document.getElementById('vv-log-title').textContent = title;
document.getElementById('vv-log-ts').textContent = '';
document.getElementById('vv-confform').style.display = 'none';
document.getElementById('vv-save-conf-btn').style.display = 'none';
document.getElementById('vv-save-schedule-btn').disabled = true;
document.getElementById('vv-save-schedule-btn').style.opacity = '0.4';
document.getElementById('vv-delete-script-btn').style.display = vvEditorId ? '' : 'none';
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
requestAnimationFrame(vvFitRight);
}
function vvSaveScript() {
const name = document.getElementById('vv-editor-name').value.trim();
const content = document.getElementById('vv-editor-body').value;
if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) {
alert('Name must be letters, numbers, _ or - only (no spaces, no .sh)');
return;
}
const btn = document.getElementById('vv-save-script-btn');
btn.disabled = true;
btn.textContent = 'Saving…';
vvPost('/plugins/varaverk/api/script.php', {name, content})
.then(d => {
if (!d.ok) { alert('Save failed: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = 'Save Script'; return; }
localStorage.setItem('vv-last-job', d.id);
window.location.reload();
})
.catch(() => { btn.disabled = false; btn.textContent = 'Save Script'; });
}
function vvDeleteScript() {
if (!vvEditorId) return;
const name = vvEditorId.replace(/^Custom\//, '').replace(/\.sh$/, '');
if (!confirm('Delete "' + name + '.sh"? This cannot be undone.')) return;
const btn = document.getElementById('vv-delete-script-btn');
btn.disabled = true;
btn.textContent = 'Deleting…';
vvPost('/plugins/varaverk/api/script.php', {action: 'delete', name})
.then(d => {
if (!d.ok) { alert('Delete failed: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = '\u{1F5D1} Delete'; return; }
localStorage.removeItem('vv-last-job');
window.location.reload();
})
.catch(() => { btn.disabled = false; btn.textContent = '\u{1F5D1} Delete'; });
}
function vvShowConfMode(title) {
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-editor').style.display = 'none';
document.getElementById('vv-confform').style.display = '';
document.getElementById('vv-back-btn').style.display = '';
document.getElementById('vv-restore-btn').style.display = 'none';
document.getElementById('vv-save-script-btn').style.display = 'none';
document.getElementById('vv-save-conf-btn').style.display = '';
document.getElementById('vv-delete-script-btn').style.display = 'none';
document.getElementById('vv-clear-btn').style.display = 'none';
document.getElementById('vv-auto-scroll-label').style.display = 'none';
document.getElementById('vv-scroll-lock-label').style.display = 'none';
document.getElementById('vv-invert-log-label').style.display = 'none';
document.getElementById('vv-log-title').textContent = title;
document.getElementById('vv-log-ts').textContent = '';
document.getElementById('vv-save-schedule-btn').disabled = true;
document.getElementById('vv-save-schedule-btn').style.opacity = '0.4';
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
requestAnimationFrame(vvFitRight);
}
function vvEditConf(id) {
if (vvActiveId) {
const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
}
vvActiveId = null;
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
vvConfId = id;
const name = id.replace(/\.sh$/, '').split('/').pop();
const cf = document.getElementById('vv-confform');
cf.innerHTML = '<p class="vv-cf-empty">Loading…</p>';
vvShowConfMode(name + ' — Config');
fetch('/plugins/varaverk/api/confform.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(d => {
cf.innerHTML = (!d.ok || !d.groups || d.groups.length === 0)
? '<p class="vv-cf-empty">No configurable settings found for this host.</p>'
: vvRenderConfForm(d.groups);
requestAnimationFrame(vvFitRight);
})
.catch(() => { cf.innerHTML = '<p class="vv-cf-empty">Failed to load configuration.</p>'; });
}
function vvRenderConfForm(groups) {
const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
let html = '';
for (const g of groups) {
html += '<div class="vv-cf-group">';
html += '<div class="vv-cf-group-header">' + esc(g.subsection)
+ ' <span class="vv-cf-file">' + esc(g.file) + '</span></div>';
for (const f of g.fields) {
html += '<div class="vv-cf-field">';
html += '<div class="vv-cf-key">' + esc(f.key) + '</div>';
if (f.desc) html += '<div class="vv-cf-desc">' + esc(f.desc) + '</div>';
if (f.type === 'scalar') {
html += '<input class="vv-cf-input vv-cf-scalar" type="text"'
+ ' data-key="' + esc(f.key) + '" data-file="' + esc(f.file) + '" data-type="scalar"'
+ ' value="' + esc(f.value) + '">';
} else {
const rows = Math.min(20, (f.value.match(/\n/g) || []).length + 3);
html += '<textarea class="vv-cf-input vv-cf-array"'
+ ' data-key="' + esc(f.key) + '" data-file="' + esc(f.file) + '" data-type="' + esc(f.type) + '"'
+ ' rows="' + rows + '">' + esc(f.value) + '</textarea>';
}
html += '</div>';
}
html += '</div>';
}
return html;
}
function vvSaveConf() {
if (!vvConfId) return;
const inputs = document.querySelectorAll('#vv-confform .vv-cf-input');
const changes = [];
inputs.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
const btn = document.getElementById('vv-save-conf-btn');
btn.disabled = true; btn.textContent = 'Saving…';
vvPost('/plugins/varaverk/api/confform.php', {id: vvConfId, changes: JSON.stringify(changes)})
.then(d => {
btn.disabled = false;
if (d.ok) {
btn.textContent = '✓ Saved';
setTimeout(() => { btn.textContent = 'Save Config'; }, 2500);
} else {
btn.textContent = 'Save Config';
alert('Save failed: ' + (d.error ?? 'Unknown error'));
}
})
.catch(() => { btn.disabled = false; btn.textContent = 'Save Config'; });
}
function vvToggleSugBlock(bi) {
const body = document.getElementById('vv-sug-body-' + bi);
const chevron = body.closest('.vv-sug-block').querySelector('.vv-sug-chevron');
@@ -466,4 +796,18 @@ function vvToggleSugBlock(bi) {
}
</script>
<script>requestAnimationFrame(vvFitRight);</script>
<script>
requestAnimationFrame(function() {
const last = localStorage.getItem('vv-last-job');
if (last && document.querySelector('[data-id="' + CSS.escape(last) + '"]')) {
vvOpenRight(last);
} else {
if (last) {
const lname = last.replace(/\.sh$/, '').split('/').pop();
document.getElementById('vv-restore-label').textContent = lname;
document.getElementById('vv-restore-btn').style.display = '';
}
vvFitRight();
}
});
</script>