Files
Varaverk/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php
T
Gmer4Lfe a1745a575b Scheduler UI polish + per-script verbose logging + coffee report as orchestrator
- Cron and Advanced button vertically aligned in their respective rows
- Verbose checkbox next to Log button — saves immediately, persists to schedule.json
- --log flag injected into cron lines, manual Run, and Dry Run when verbose enabled
- Coffee report rewritten as lean orchestrator over COFFEE_REPORT_SCRIPTS array
- COFFEE_REPORT_SCRIPTS added to master.conf (7 Sunday monitor scripts)
- Scheduler Advanced tab auto-discovers children via existing array detection
2026-05-23 20:05:26 -04:00

347 lines
14 KiB
PHP

<?php
require_once dirname(__DIR__) . '/include/scheduler.php';
$tree = vv_job_tree();
?>
<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">
<label class="vv-toggle" title="Enable/disable">
<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">
</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)">&#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>
<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 ?>">
<div class="vv-job-row">
<label class="vv-toggle" title="Enable/disable">
<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">
</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)">&#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>
<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; ?>
</div><!-- /#vv-sched-cards -->
<!-- Single global save -->
<div class="vv-sched-footer">
<button class="vv-save-btn" onclick="vvSaveAll(this)">Save Schedule</button>
<span id="vv-save-all-status" class="vv-save-status"></span>
</div>
</div><!-- /#vv-sched-left -->
<!-- ── Right: persistent log panel ── -->
<div id="vv-sched-right">
<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;">
<span id="vv-log-title" style="font-size:13px;font-weight:bold;color:#ccc;">No script selected</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:8px;">
<span id="vv-log-ts" class="vv-log-ts"></span>
<button class="vv-btn-sm" onclick="vvClearRightLog()">Clear</button>
</div>
</div>
<pre id="vv-log-pre" class="vv-log-pre vv-log-right-pre">Click Log, Run, or Dry Run on a script to view output here.</pre>
</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 vvRunningId = null;
let vvLastTs = 0;
let vvStaleCount = 0;
let vvPollTimer = 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');
// Read viewport rects BEFORE any mutations
const leftRect = left.getBoundingClientRect();
const rightRect = right.getBoundingClientRect();
// Match footer heights
rf.style.height = lf.offsetHeight + 'px';
// Pin right panel bottom to left panel bottom using viewport coords
right.style.height = Math.round(leftRect.bottom - rightRect.top) + 'px';
// Pre fills from toolbar bottom to footer border, leaving 12px card padding below
// -12 footer margin, -12 card padding-bottom, -8 toolbar margin-bottom
const preH = Math.max(80, lf.getBoundingClientRect().top - 32 - toolbar.getBoundingClientRect().bottom);
pre.style.maxHeight = 'none';
pre.style.height = preH + 'px';
console.log('[vvFitRight]', {
leftBottom: Math.round(leftRect.bottom), rightTop: Math.round(rightRect.top),
rightBottomAfter: Math.round(right.getBoundingClientRect().bottom),
rfH: lf.offsetHeight, preH, preActual: pre.offsetHeight,
});
}
window.addEventListener('resize', vvFitRight);
// Open the right panel for a given job id
function vvOpenRight(id) {
// Deselect old
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;
// Highlight new
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
if (job) job.querySelector('.vv-job-row').classList.add('vv-row-selected');
const name = id.replace(/\.sh$/, '').split('/').pop();
document.getElementById('vv-log-title').textContent = name;
document.getElementById('vv-sched-right').classList.add('vv-panel-visible');
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');
if (!d.ok) { pre.textContent = '✗ ' + (d.error ?? 'Error'); return; }
pre.textContent = d.content || '(no log yet)';
pre.scrollTop = pre.scrollHeight;
ts.textContent = d.ts ? 'Last run: ' + new Date(d.ts * 1000).toLocaleString() : '';
// Running indicator: clear when log stops changing
if (vvRunningId === vvActiveId) {
if (d.ts && d.ts === vvLastTs) {
if (++vvStaleCount >= 2) vvSetRunning(null);
} else {
vvLastTs = d.ts || 0;
vvStaleCount = 0;
}
}
})
.catch(() => {});
}
function vvSetRunning(id) {
// Clear old dot
if (vvRunningId) {
const old = document.querySelector('[data-id="' + CSS.escape(vvRunningId) + '"]');
if (old) { const dot = old.querySelector('.vv-job-dot'); dot.className = 'vv-job-dot'; }
}
vvRunningId = id;
vvLastTs = 0;
vvStaleCount = 0;
const headerDot = document.getElementById('vv-log-dot');
if (id) {
headerDot.style.display = 'inline-block';
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
} else {
headerDot.style.display = 'none';
}
}
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);
vvSetRunning(id);
vvPost('/plugins/varaverk/api/run.php', {id})
.then(d => {
if (!d.ok) {
document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
vvSetRunning(null);
}
});
}
function vvDryRun(btn) {
const job = btn.closest('[data-id]');
const id = job.dataset.id;
vvOpenRight(id);
vvSetRunning(id);
vvPost('/plugins/varaverk/api/dryrun.php', {id})
.then(d => {
if (!d.ok) {
document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
vvSetRunning(null);
}
});
}
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});
}
function vvSaveAll() {
const status = document.getElementById('vv-save-all-status');
const jobs = document.querySelectorAll('#vv-sched-left [data-id]');
let pending = jobs.length, allOk = true;
if (!pending) { vvFlashStatus(status, '✗ No jobs found', false); return; }
status.textContent = 'Saving…';
jobs.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) allOk = false;
if (--pending === 0) vvFlashStatus(status, allOk ? '✓ 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 vvClearRightLog() {
if (!vvActiveId) return;
vvPost('/plugins/varaverk/api/log.php', {id: vvActiveId, clear: '1'})
.then(() => vvFetchRight())
.catch(() => {});
}
</script>
<?php
// Auto-open the most recently run script on page load
$lastId = null; $lastMtime = 0;
foreach ($tree as $orch) {
$log = vv_job_log_path($orch['id']);
if (file_exists($log) && ($m = filemtime($log)) > $lastMtime) { $lastMtime = $m; $lastId = $orch['id']; }
foreach (($orch['children'] ?? []) as $child) {
$log = vv_job_log_path($child['id']);
if (file_exists($log) && ($m = filemtime($log)) > $lastMtime) { $lastMtime = $m; $lastId = $child['id']; }
}
}
if ($lastId): ?>
<script>vvOpenRight(<?= json_encode($lastId) ?>);</script>
<?php endif; ?>