When orch is ON (god mode): - Children's toggle state is read from master.conf comment status - Toggling a child comments/uncomments its line in the *_SCRIPTS array - Child crons are suppressed in vv_cron_rebuild — orch is the sole trigger - Children not found in any *_SCRIPTS array are shown disabled (read-only) When orch is OFF: - All children flip to off; schedule.json updated immediately - A child with a cron value + enabled toggle gets its own independent cron entry - A child with no cron does nothing when enabled New: vv_conf_script_map() — cached per-request scan of master.conf arrays New: vv_parse_conf_array_full() — includes commented entries (disabled scripts) New: vv_conf_toggle_script() — comments/uncomments a script line in master.conf New: api/conf_toggle.php — endpoint for child toggle → master.conf write
955 lines
43 KiB
PHP
955 lines
43 KiB
PHP
<?php
|
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
|
$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 = [];
|
|
foreach ($tree as $orch) {
|
|
$treeMap[$orch['id']] = ['enabled' => $orch['enabled'], 'cron' => $orch['cron']];
|
|
foreach ($orch['children'] ?? [] as $child) {
|
|
$treeMap[$child['id']] = ['enabled' => $child['enabled'], 'cron' => $child['cron']];
|
|
}
|
|
}
|
|
?>
|
|
|
|
<div id="vv-scheduler">
|
|
<p class="vv-hint">Toggle saves immediately. Fill in cron expressions and hit
|
|
<strong>Save Schedule</strong> at the bottom. <strong>Run</strong> fires now;
|
|
<strong>Dry Run</strong> sets DRY_RUN=1. Log opens on the right.</p>
|
|
|
|
<div id="vv-sched-layout">
|
|
|
|
<!-- ── Left: script cards ── -->
|
|
<div id="vv-sched-left">
|
|
<div id="vv-sched-cards">
|
|
|
|
<?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
|
|
<div class="vv-card vv-wide vv-sched-card" data-id="<?= $oid ?>">
|
|
|
|
<div class="vv-job-row vv-orch-row">
|
|
<label class="vv-toggle" title="Enable/disable">
|
|
<input type="checkbox" class="vv-enabled"
|
|
<?= $orch['enabled'] ? 'checked' : '' ?>
|
|
onchange="vvSaveOrch(this)">
|
|
<span class="vv-slider"></span>
|
|
</label>
|
|
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
|
|
<?php if ($orch['type'] === 'event'): ?>
|
|
<span class="vv-event-badge"><?= $orch['cron'] === '@array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?></span>
|
|
<input type="hidden" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>">
|
|
<?php else: ?>
|
|
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
|
|
placeholder="cron expression">
|
|
<?php endif; ?>
|
|
<span class="vv-save-check"></span>
|
|
</div>
|
|
<?php if (!empty($orch['desc'])): ?>
|
|
<div class="vv-job-desc" title="<?= htmlspecialchars($orch['desc']) ?>"><?= htmlspecialchars($orch['desc']) ?></div>
|
|
<?php endif; ?>
|
|
<div class="vv-job-actions">
|
|
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
|
|
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</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' : '' ?>
|
|
onchange="vvSaveJob(this)">
|
|
<span>Verbose</span>
|
|
</label>
|
|
<span class="vv-job-dot"></span>
|
|
<?php if (!empty($orch['children'])): ?>
|
|
<button class="vv-advanced-toggle" style="margin-left:auto;width:110px" 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): $cid = htmlspecialchars($child['id']); ?>
|
|
<div class="vv-script" data-id="<?= $cid ?>"
|
|
data-conf-managed="<?= $child['conf_managed'] ? '1' : '0' ?>"
|
|
data-conf-enabled="<?= $child['conf_enabled'] === true ? '1' : ($child['conf_enabled'] === false ? '0' : '') ?>">
|
|
<div class="vv-job-row">
|
|
<label class="vv-toggle" title="Enable/disable">
|
|
<input type="checkbox" class="vv-enabled"
|
|
<?= $child['enabled'] ? 'checked' : '' ?>
|
|
onchange="vvSaveChild(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 (orch off only)">
|
|
<span class="vv-save-check"></span>
|
|
</div>
|
|
<?php if (!empty($child['desc'])): ?>
|
|
<div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['desc']) ?></div>
|
|
<?php endif; ?>
|
|
<div class="vv-job-actions">
|
|
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
|
|
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</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' : '' ?>
|
|
onchange="vvSaveJob(this)">
|
|
<span>Verbose</span>
|
|
</label>
|
|
<span class="vv-job-dot"></span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
</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)">▶ Run</button>
|
|
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ 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 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">🗑 Delete</button>
|
|
<span id="vv-save-all-status" class="vv-save-status"></span>
|
|
</div>
|
|
|
|
</div><!-- /#vv-sched-left -->
|
|
|
|
<!-- ── Right: suggestions + log panel ── -->
|
|
<div id="vv-sched-right" class="vv-panel-visible">
|
|
<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">☰ Scheduler Info</button>
|
|
<button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">← <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-auto-scroll-label" class="vv-log-label" title="Auto-scroll to newest line (pauses when you scroll up, resumes at bottom)" style="display:none">
|
|
<input type="checkbox" id="vv-auto-scroll" checked> <span>Auto Scroll</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="vvToggleInvert(this)"> <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-stop-btn" class="vv-btn-sm" onclick="vvStopJob()" style="display:none" title="Stop running script and clear any stuck locks">■ Stop</button>
|
|
<button id="vv-clear-btn" class="vv-btn-sm" onclick="vvClearRightLog()" style="display:none">Clear</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Suggestions view (default) -->
|
|
<div id="vv-suggestions">
|
|
<?php foreach ($blocks as $bi => $block):
|
|
$scripts = $block['scripts'];
|
|
$schedule = $block['schedule'];
|
|
// Extract just the 5-field cron from schedule string
|
|
$schedParts = preg_split('/\s+/', $schedule, 6);
|
|
$cronOnly = (count($schedParts) >= 5) ? implode(' ', array_slice($schedParts, 0, 5)) : '';
|
|
$schedLabel = (count($schedParts) >= 6) ? trim($schedParts[5], '() ') : $schedule;
|
|
// Status: any matching script configured?
|
|
$configured = false; $enabled = false;
|
|
foreach ($scripts as $s) {
|
|
if (isset($treeMap[$s['rel']])) {
|
|
$configured = true;
|
|
if ($treeMap[$s['rel']]['enabled']) $enabled = true;
|
|
}
|
|
}
|
|
// Trim trailing blank desc lines
|
|
$desc = $block['desc'];
|
|
while (!empty($desc) && trim(end($desc)) === '') array_pop($desc);
|
|
?>
|
|
<div class="vv-sug-block" data-bi="<?= $bi ?>">
|
|
<div class="vv-sug-header" onclick="vvToggleSugBlock(<?= $bi ?>)">
|
|
<span class="vv-sug-chevron">▸</span>
|
|
<span class="vv-sug-title"><?= htmlspecialchars($block['title']) ?></span>
|
|
<?php if ($cronOnly): ?>
|
|
<code class="vv-sug-cron"><?= htmlspecialchars($cronOnly) ?></code>
|
|
<?php endif; ?>
|
|
<?php if ($schedLabel && $schedLabel !== $cronOnly): ?>
|
|
<span class="vv-sug-label"><?= htmlspecialchars($schedLabel) ?></span>
|
|
<?php endif; ?>
|
|
<span class="vv-sug-status <?= $configured ? ($enabled ? 'vv-sug-on' : 'vv-sug-off') : 'vv-sug-none' ?>">
|
|
<?= $configured ? ($enabled ? '● on' : '○ off') : '—' ?>
|
|
</span>
|
|
</div>
|
|
<div class="vv-sug-body" id="vv-sug-body-<?= $bi ?>" style="display:none">
|
|
<?php if (!empty($desc)): ?>
|
|
<pre class="vv-sug-desc"><?= htmlspecialchars(implode("\n", $desc)) ?></pre>
|
|
<?php endif; ?>
|
|
<?php if (!empty($scripts)): ?>
|
|
<div class="vv-sug-scripts">
|
|
<?php foreach ($scripts as $s):
|
|
$inTree = isset($treeMap[$s['rel']]);
|
|
$scron = $s['cron'] ?: ($inTree ? $treeMap[$s['rel']]['cron'] : '');
|
|
?>
|
|
<div class="vv-sug-script-row">
|
|
<?php if ($s['cron']): ?>
|
|
<code class="vv-sug-inline-cron"><?= htmlspecialchars($s['cron']) ?></code>
|
|
<?php endif; ?>
|
|
<code class="vv-sug-path"><?= htmlspecialchars($s['rel']) ?></code>
|
|
<?php if ($inTree): ?>
|
|
<span class="vv-sug-configured">✓ in scheduler</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($blocks)): ?>
|
|
<p style="color:#666;padding:12px;">user_script_plug-in.sh not found at <?= htmlspecialchars(SCRIPTS_DIR) ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- 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;
|
|
foreach ($tree as $orch) {
|
|
$totalJobs++;
|
|
if (!empty($orch['cron']) && $orch['enabled']) $scheduledJobs++;
|
|
foreach (($orch['children'] ?? []) as $child) {
|
|
$totalJobs++;
|
|
if (!empty($child['cron']) && $child['enabled']) $scheduledJobs++;
|
|
}
|
|
}
|
|
$runningScripts = [];
|
|
exec('pgrep -af bash 2>/dev/null', $psLines);
|
|
foreach ($psLines as $line) {
|
|
if (preg_match('#' . preg_quote(SCRIPTS_DIR, '#') . '/([^\s]+\.sh)#', $line, $rm)) {
|
|
$runningScripts[] = basename($rm[1], '.sh');
|
|
}
|
|
}
|
|
$runningScripts = array_unique($runningScripts);
|
|
?>
|
|
<div class="vv-sched-footer vv-sched-info">
|
|
<span><?= $scheduledJobs ?> of <?= $totalJobs ?> job<?= $totalJobs !== 1 ? 's' : '' ?> scheduled</span>
|
|
<span><?= !empty($runningScripts) ? 'Currently Running: ' . htmlspecialchars(implode(', ', $runningScripts)) : 'Currently Running scripts: none' ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /#vv-sched-layout -->
|
|
</div>
|
|
|
|
<script>
|
|
let vvActiveId = null;
|
|
let vvRunningSet = new Set();
|
|
let vvPollTimer = null;
|
|
let vvStatusTimer = null;
|
|
let vvEditorId = null;
|
|
let vvConfId = null;
|
|
|
|
function vvPost(url, data) {
|
|
const params = new URLSearchParams({csrf_token, ...data});
|
|
return fetch(url, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
body: params
|
|
}).then(r => r.json());
|
|
}
|
|
|
|
function vvFitRight() {
|
|
const right = document.getElementById('vv-sched-right');
|
|
if (!right.classList.contains('vv-panel-visible')) return;
|
|
const left = document.getElementById('vv-sched-left');
|
|
|
|
const lf = left.querySelector('.vv-sched-footer');
|
|
const rf = right.querySelector('.vv-sched-info');
|
|
const toolbar = right.querySelector('.vv-log-toolbar');
|
|
const pre = document.getElementById('vv-log-pre');
|
|
const sug = document.getElementById('vv-suggestions');
|
|
|
|
const leftRect = left.getBoundingClientRect();
|
|
const rightRect = right.getBoundingClientRect();
|
|
|
|
rf.style.height = lf.offsetHeight + 'px';
|
|
right.style.height = Math.round(leftRect.bottom - rightRect.top) + 'px';
|
|
|
|
const contentH = Math.max(80, lf.getBoundingClientRect().top - 32 - toolbar.getBoundingClientRect().bottom);
|
|
if (pre.style.display !== 'none') {
|
|
pre.style.maxHeight = 'none';
|
|
pre.style.height = contentH + 'px';
|
|
}
|
|
if (sug.style.display !== 'none') {
|
|
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-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-invert-log-label').style.display = '';
|
|
document.getElementById('vv-stop-btn').style.display = '';
|
|
document.getElementById('vv-auto-scroll').checked = true;
|
|
const _pre = document.getElementById('vv-log-pre');
|
|
_pre.removeEventListener('scroll', vvOnLogScroll);
|
|
_pre.addEventListener('scroll', vvOnLogScroll);
|
|
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 vvOnLogScroll() {
|
|
const pre = document.getElementById('vv-log-pre');
|
|
const atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 20;
|
|
const cb = document.getElementById('vv-auto-scroll');
|
|
if (cb) cb.checked = atBottom;
|
|
}
|
|
|
|
function vvToggleInvert(cb) {
|
|
localStorage.setItem('vv-invert-log', cb.checked ? '1' : '0');
|
|
vvFetchRight();
|
|
}
|
|
|
|
function vvRestoreInvert() {
|
|
const val = localStorage.getItem('vv-invert-log');
|
|
if (val !== null) document.getElementById('vv-invert-log').checked = val === '1';
|
|
}
|
|
|
|
function vvBackToSuggestions() {
|
|
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; }
|
|
|
|
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-invert-log-label').style.display = 'none';
|
|
document.getElementById('vv-stop-btn').style.display = 'none';
|
|
document.getElementById('vv-auto-scroll').checked = true;
|
|
const _preBack = document.getElementById('vv-log-pre');
|
|
_preBack.removeEventListener('scroll', vvOnLogScroll);
|
|
_preBack.style.overflowY = '';
|
|
document.getElementById('vv-log-title').textContent = 'Suggested Schedules';
|
|
document.getElementById('vv-log-ts').textContent = '';
|
|
document.getElementById('vv-log-dot').style.display = 'none';
|
|
requestAnimationFrame(vvFitRight);
|
|
}
|
|
|
|
function vvOpenRight(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 = 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');
|
|
|
|
vvShowLogMode(id);
|
|
vvSetStopBtn(vvRunningSet.has(id));
|
|
requestAnimationFrame(vvFitRight);
|
|
|
|
vvFetchRight();
|
|
if (!vvPollTimer) vvPollTimer = setInterval(vvFetchRight, 2000);
|
|
}
|
|
|
|
function vvFetchRight() {
|
|
if (!vvActiveId) return;
|
|
fetch('/plugins/varaverk/api/log.php?id=' + encodeURIComponent(vvActiveId))
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
const pre = document.getElementById('vv-log-pre');
|
|
const ts = document.getElementById('vv-log-ts');
|
|
const autoScroll = document.getElementById('vv-auto-scroll').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 || '';
|
|
vvSetLogBtnState(vvActiveId, content.trim().length > 0);
|
|
pre.textContent = content.trim() ? (invert ? content.split('\n').reverse().join('\n') : content) : '(no log yet)';
|
|
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() : '';
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function vvSetStopBtn(running) {
|
|
const btn = document.getElementById('vv-stop-btn');
|
|
if (!btn || btn.style.display === 'none') return;
|
|
btn.disabled = !running;
|
|
btn.style.background = running ? '#c62828' : '#444';
|
|
btn.style.color = running ? '#fff' : '#888';
|
|
btn.style.cursor = running ? 'pointer' : 'default';
|
|
}
|
|
|
|
function vvSetDot(id) {
|
|
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
|
if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
|
|
if (id === vvActiveId) {
|
|
document.getElementById('vv-log-dot').style.display = 'inline-block';
|
|
vvSetStopBtn(true);
|
|
}
|
|
}
|
|
|
|
function vvClearDot(id) {
|
|
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
|
if (job) job.querySelector('.vv-job-dot').classList.remove('vv-dot-running');
|
|
if (id === vvActiveId) {
|
|
document.getElementById('vv-log-dot').style.display = 'none';
|
|
vvSetStopBtn(false);
|
|
}
|
|
}
|
|
|
|
function vvStopJob() {
|
|
if (!vvActiveId || !vvRunningSet.has(vvActiveId)) return;
|
|
const btn = document.getElementById('vv-stop-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = '…';
|
|
vvPost('/plugins/varaverk/api/stop.php', {id: vvActiveId})
|
|
.then(d => {
|
|
btn.textContent = '■ Stop';
|
|
if (d.ok) {
|
|
vvRunningSet.delete(vvActiveId);
|
|
vvClearDot(vvActiveId);
|
|
vvFetchRight();
|
|
} else {
|
|
btn.disabled = false;
|
|
vvSetStopBtn(true);
|
|
}
|
|
})
|
|
.catch(() => { btn.textContent = '■ Stop'; btn.disabled = false; vvSetStopBtn(true); });
|
|
}
|
|
|
|
function vvPollStatus() {
|
|
fetch('/plugins/varaverk/api/status.php')
|
|
.then(r => r.json())
|
|
.then(statuses => {
|
|
const nowRunning = new Set(
|
|
Object.entries(statuses).filter(([, s]) => s === 'running').map(([id]) => id)
|
|
);
|
|
for (const id of vvRunningSet) {
|
|
if (!nowRunning.has(id)) { vvClearDot(id); if (id === vvActiveId) vvFetchRight(); }
|
|
}
|
|
for (const id of nowRunning) {
|
|
if (!vvRunningSet.has(id)) { vvSetDot(id); if (id === vvActiveId) vvFetchRight(); }
|
|
}
|
|
vvRunningSet = nowRunning;
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function vvStartStatusPoll() {
|
|
if (vvStatusTimer) return;
|
|
vvPollStatus();
|
|
vvStatusTimer = setInterval(vvPollStatus, 3000);
|
|
}
|
|
|
|
function vvSelectLog(btn) {
|
|
const job = btn.closest('[data-id]');
|
|
vvOpenRight(job.dataset.id);
|
|
}
|
|
|
|
function vvRunJob(btn) {
|
|
const job = btn.closest('[data-id]');
|
|
const id = job.dataset.id;
|
|
vvOpenRight(id);
|
|
vvSetDot(id);
|
|
vvRunningSet.add(id);
|
|
vvPost('/plugins/varaverk/api/run.php', {id})
|
|
.then(d => {
|
|
if (!d.ok) {
|
|
document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
|
|
vvClearDot(id);
|
|
vvRunningSet.delete(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
function vvDryRun(btn) {
|
|
const job = btn.closest('[data-id]');
|
|
const id = job.dataset.id;
|
|
vvOpenRight(id);
|
|
vvSetDot(id);
|
|
vvRunningSet.add(id);
|
|
vvPost('/plugins/varaverk/api/dryrun.php', {id})
|
|
.then(d => {
|
|
if (!d.ok) {
|
|
document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
|
|
vvClearDot(id);
|
|
vvRunningSet.delete(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
function vvFlashSaved(job) {
|
|
const check = job.querySelector('.vv-save-check');
|
|
if (!check) return;
|
|
check.textContent = '✓';
|
|
check.classList.remove('vv-check-show');
|
|
void check.offsetWidth; // force reflow to restart animation
|
|
check.classList.add('vv-check-show');
|
|
}
|
|
|
|
function vvFlashStatus(el, msg, ok) {
|
|
el.textContent = msg;
|
|
el.style.color = ok ? '#4caf50' : '#f44336';
|
|
clearTimeout(el._t);
|
|
el._t = setTimeout(() => { el.textContent = ''; }, 3000);
|
|
}
|
|
|
|
// Generic save — used for log_enabled toggles and custom scripts.
|
|
function vvSaveJob(el) {
|
|
const job = el.closest('[data-id]');
|
|
const id = job.dataset.id;
|
|
const enabled = job.querySelector('.vv-enabled').checked ? '1' : '0';
|
|
const cron = job.querySelector('.vv-cron')?.value.trim() ?? '';
|
|
const log_enabled = job.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
|
.then(d => { if (d.ok) vvFlashSaved(job); });
|
|
}
|
|
|
|
// Orch toggle: saves orch state and propagates to children.
|
|
function vvSaveOrch(el) {
|
|
const card = el.closest('[data-id]');
|
|
const id = card.dataset.id;
|
|
const enabled = el.checked;
|
|
const cron = card.querySelector('.vv-orch-row .vv-cron')?.value.trim() ?? '';
|
|
const log_e = card.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled: enabled ? '1' : '0', cron, log_enabled: log_e})
|
|
.then(d => { if (d.ok) vvFlashSaved(card); });
|
|
vvApplyOrchState(card, enabled);
|
|
}
|
|
|
|
// Apply visual + interactive state to children based on whether orch is on or off.
|
|
function vvApplyOrchState(orchCard, orchEnabled) {
|
|
orchCard.querySelectorAll('.vv-script').forEach(child => {
|
|
const confManaged = child.dataset.confManaged === '1';
|
|
const confEnabled = child.dataset.confEnabled === '1';
|
|
const toggle = child.querySelector('.vv-enabled');
|
|
const cronInput = child.querySelector('input.vv-cron');
|
|
|
|
if (orchEnabled) {
|
|
// Orch is god: set toggle from master.conf state; hide independent cron
|
|
toggle.checked = confManaged ? confEnabled : false;
|
|
toggle.disabled = !confManaged;
|
|
if (cronInput) { cronInput.disabled = true; cronInput.style.opacity = '0.35'; }
|
|
} else {
|
|
// Orch off: all children switch off; cron field becomes active
|
|
toggle.checked = false;
|
|
toggle.disabled = false;
|
|
if (cronInput) { cronInput.disabled = false; cronInput.style.opacity = ''; }
|
|
// Persist the off state to schedule.json so cron rebuild reflects it
|
|
const cid = child.dataset.id;
|
|
const ccron = cronInput?.value.trim() ?? '';
|
|
const clog = child.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id: cid, enabled: '0', cron: ccron, log_enabled: clog});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Child toggle: conf_toggle when orch is on; scheduler save when orch is off.
|
|
function vvSaveChild(el) {
|
|
const child = el.closest('[data-id]');
|
|
const orchCard = child.closest('.vv-sched-card');
|
|
const orchOn = orchCard?.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
|
|
const id = child.dataset.id;
|
|
const enabled = el.checked;
|
|
|
|
if (orchOn) {
|
|
vvPost('/plugins/varaverk/api/conf_toggle.php', {id, enabled: enabled ? '1' : '0'})
|
|
.then(d => { if (d.ok) vvFlashSaved(child); });
|
|
} else {
|
|
const cron = child.querySelector('input.vv-cron')?.value.trim() ?? '';
|
|
const log_e = child.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled: enabled ? '1' : '0', cron, log_enabled: log_e})
|
|
.then(d => { if (d.ok) vvFlashSaved(child); });
|
|
}
|
|
}
|
|
|
|
function vvSaveAll() {
|
|
const status = document.getElementById('vv-save-all-status');
|
|
// Collect jobs to save: orchs always; children only when their orch is OFF (orch manages them when on).
|
|
const toSave = [];
|
|
document.querySelectorAll('#vv-sched-left [data-id]').forEach(job => {
|
|
if (job.classList.contains('vv-script')) {
|
|
const orchCard = job.closest('.vv-sched-card');
|
|
const orchOn = orchCard?.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
|
|
if (orchOn) return; // child under active orch — cron suppressed, skip
|
|
}
|
|
toSave.push(job);
|
|
});
|
|
let pending = toSave.length, allOk = true;
|
|
if (!pending) { vvFlashStatus(status, '✓ Saved', true); return; }
|
|
status.textContent = 'Saving…';
|
|
toSave.forEach(job => {
|
|
const id = job.dataset.id;
|
|
const enabled = job.querySelector('.vv-enabled').checked ? '1' : '0';
|
|
const cron = job.querySelector('.vv-cron')?.value.trim() ?? '';
|
|
const log_enabled = job.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
|
.then(d => {
|
|
if (d.ok) vvFlashSaved(job); else allOk = false;
|
|
if (--pending === 0) vvFlashStatus(status, allOk ? '✓ All saved' : '✗ Some failed', allOk);
|
|
});
|
|
});
|
|
}
|
|
|
|
function vvToggleAdvanced(btn) {
|
|
const card = btn.closest('.vv-sched-card');
|
|
const children = card.querySelector('.vv-children');
|
|
const visible = children.style.display !== 'none';
|
|
children.style.display = visible ? 'none' : 'block';
|
|
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(() => { 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-stop-btn').style.display = 'none';
|
|
document.getElementById('vv-auto-scroll-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-stop-btn').style.display = 'none';
|
|
document.getElementById('vv-auto-scroll-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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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');
|
|
const open = body.style.display !== 'none';
|
|
body.style.display = open ? 'none' : 'block';
|
|
chevron.textContent = open ? '▸' : '▾';
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
// On page load: apply orch state for any orch that is already enabled.
|
|
document.querySelectorAll('.vv-sched-card').forEach(card => {
|
|
const orchOn = card.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
|
|
if (orchOn) vvApplyOrchState(card, true);
|
|
});
|
|
|
|
requestAnimationFrame(function() {
|
|
vvRestoreInvert();
|
|
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();
|
|
}
|
|
vvStartStatusPoll();
|
|
});
|
|
</script>
|