Scripts list on left, persistent log panel on right. On screens ≤880px falls back to stacked single-column. Right panel stays open until page refresh or another selection. Running dot pulses green on the job row and in the log header while script executes, clears when log goes stale.
250 lines
9.3 KiB
PHP
250 lines
9.3 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. Edit cron then <strong>Save</strong>.
|
|
<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">
|
|
|
|
<?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">
|
|
<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" onclick="vvSelectLog(this)">Log</button>
|
|
<span class="vv-job-dot"></span>
|
|
<span class="vv-job-status"></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): $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">
|
|
<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" onclick="vvSelectLog(this)">Log</button>
|
|
<span class="vv-job-dot"></span>
|
|
<span class="vv-job-status"></span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="vv-card-footer">
|
|
<button class="vv-save-btn" onclick="vvSaveCard(this)">Save Schedule</button>
|
|
<span class="vv-save-status"></span>
|
|
</div>
|
|
|
|
</div>
|
|
<?php endforeach; ?>
|
|
|
|
</div><!-- /#vv-sched-left -->
|
|
|
|
<!-- ── Right: persistent log panel ── -->
|
|
<div id="vv-sched-right">
|
|
<div class="vv-card" style="min-height:200px;">
|
|
<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>
|
|
</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());
|
|
}
|
|
|
|
// 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');
|
|
|
|
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 status = job.querySelector('.vv-job-status');
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron})
|
|
.then(d => { if (status) vvFlashStatus(status, d.ok ? '✓' : '✗ ' + (d.error ?? ''), d.ok); });
|
|
}
|
|
|
|
function vvSaveCard(btn) {
|
|
const card = btn.closest('.vv-sched-card');
|
|
const status = card.querySelector('.vv-save-status');
|
|
const jobs = card.querySelectorAll('[data-id]');
|
|
let pending = jobs.length, allOk = true;
|
|
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();
|
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron})
|
|
.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>
|