feat(scheduler): live running indicators for cron-triggered jobs

Add api/status.php — reads all scheduled jobs' stat files and returns a
{id: status} map. The scheduler page polls it every 3s and lights up the
dot on any job whose stat shows status=running, clearing it when done.

Replace single vvRunningId with vvRunningSet (Set) so multiple jobs can
show as running simultaneously. vvSetDot/vvClearDot handle per-job dots
and the header dot for the active job. Remove stale-count detection from
vvFetchRight — status poll is now the single source of truth.
This commit is contained in:
Gmer4Lfe
2026-05-24 18:46:45 -04:00
parent 6600565dd5
commit 7c14627bc3
2 changed files with 68 additions and 39 deletions
@@ -0,0 +1,19 @@
<?php
// Returns current run status for all scheduled jobs.
// Used by the scheduler page to light up running indicators without user interaction.
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php';
$schedule = vv_schedule_load();
$result = [];
foreach ($schedule as $id => $entry) {
$statFile = vv_job_stat_path($id);
if (!file_exists($statFile)) continue;
$stat = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: [];
$status = $stat['status'] ?? 'unknown';
if ($status === 'running' && !empty($stat['pid']) && !file_exists("/proc/{$stat['pid']}")) {
$status = 'error';
}
$result[$id] = $status;
}
echo json_encode($result);
@@ -302,13 +302,12 @@ foreach ($tree as $orch) {
</div> </div>
<script> <script>
let vvActiveId = null; let vvActiveId = null;
let vvRunningId = null; let vvRunningSet = new Set();
let vvLastTs = 0; let vvPollTimer = null;
let vvStaleCount = 0; let vvStatusTimer = null;
let vvPollTimer = null; let vvEditorId = null;
let vvEditorId = null; let vvConfId = null;
let vvConfId = null;
function vvPost(url, data) { function vvPost(url, data) {
const params = new URLSearchParams({csrf_token, ...data}); const params = new URLSearchParams({csrf_token, ...data});
@@ -391,7 +390,6 @@ function vvBackToSuggestions() {
} }
vvActiveId = null; vvActiveId = null;
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; } if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
vvSetRunning(null);
document.getElementById('vv-log-pre').style.display = 'none'; document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-editor').style.display = 'none'; document.getElementById('vv-editor').style.display = 'none';
@@ -463,37 +461,44 @@ function vvFetchRight() {
requestAnimationFrame(() => { pre.scrollTop = savedScroll; }); requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
} }
ts.textContent = d.ts ? 'Last run: ' + new Date(d.ts * 1000).toLocaleString() : ''; 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(() => {}); .catch(() => {});
} }
function vvSetRunning(id) { function vvSetDot(id) {
// Clear old dot const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
if (vvRunningId) { if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
const old = document.querySelector('[data-id="' + CSS.escape(vvRunningId) + '"]'); if (id === vvActiveId) document.getElementById('vv-log-dot').style.display = 'inline-block';
if (old) { const dot = old.querySelector('.vv-job-dot'); dot.className = 'vv-job-dot'; } }
}
vvRunningId = id; function vvClearDot(id) {
vvLastTs = 0; const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
vvStaleCount = 0; if (job) job.querySelector('.vv-job-dot').classList.remove('vv-dot-running');
const headerDot = document.getElementById('vv-log-dot'); if (id === vvActiveId) document.getElementById('vv-log-dot').style.display = 'none';
if (id) { }
headerDot.style.display = 'inline-block';
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]'); function vvPollStatus() {
if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running'); fetch('/plugins/varaverk/api/status.php')
} else { .then(r => r.json())
headerDot.style.display = 'none'; .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) { function vvSelectLog(btn) {
@@ -505,12 +510,14 @@ function vvRunJob(btn) {
const job = btn.closest('[data-id]'); const job = btn.closest('[data-id]');
const id = job.dataset.id; const id = job.dataset.id;
vvOpenRight(id); vvOpenRight(id);
vvSetRunning(id); vvSetDot(id);
vvRunningSet.add(id);
vvPost('/plugins/varaverk/api/run.php', {id}) vvPost('/plugins/varaverk/api/run.php', {id})
.then(d => { .then(d => {
if (!d.ok) { if (!d.ok) {
document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start'); document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
vvSetRunning(null); vvClearDot(id);
vvRunningSet.delete(id);
} }
}); });
} }
@@ -519,12 +526,14 @@ function vvDryRun(btn) {
const job = btn.closest('[data-id]'); const job = btn.closest('[data-id]');
const id = job.dataset.id; const id = job.dataset.id;
vvOpenRight(id); vvOpenRight(id);
vvSetRunning(id); vvSetDot(id);
vvRunningSet.add(id);
vvPost('/plugins/varaverk/api/dryrun.php', {id}) vvPost('/plugins/varaverk/api/dryrun.php', {id})
.then(d => { .then(d => {
if (!d.ok) { if (!d.ok) {
document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start'); document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
vvSetRunning(null); vvClearDot(id);
vvRunningSet.delete(id);
} }
}); });
} }
@@ -809,5 +818,6 @@ requestAnimationFrame(function() {
} }
vvFitRight(); vvFitRight();
} }
vvStartStatusPoll();
}); });
</script> </script>