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>
<script>
let vvActiveId = null;
let vvRunningId = null;
let vvLastTs = 0;
let vvStaleCount = 0;
let vvPollTimer = null;
let vvEditorId = null;
let vvConfId = null;
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});
@@ -391,7 +390,6 @@ function vvBackToSuggestions() {
}
vvActiveId = null;
if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
vvSetRunning(null);
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-editor').style.display = 'none';
@@ -463,37 +461,44 @@ function vvFetchRight() {
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
}
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 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';
}
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';
}
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) {
@@ -505,12 +510,14 @@ function vvRunJob(btn) {
const job = btn.closest('[data-id]');
const id = job.dataset.id;
vvOpenRight(id);
vvSetRunning(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');
vvSetRunning(null);
vvClearDot(id);
vvRunningSet.delete(id);
}
});
}
@@ -519,12 +526,14 @@ function vvDryRun(btn) {
const job = btn.closest('[data-id]');
const id = job.dataset.id;
vvOpenRight(id);
vvSetRunning(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');
vvSetRunning(null);
vvClearDot(id);
vvRunningSet.delete(id);
}
});
}
@@ -809,5 +818,6 @@ requestAnimationFrame(function() {
}
vvFitRight();
}
vvStartStatusPoll();
});
</script>