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:
@@ -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);
|
||||||
@@ -303,10 +303,9 @@ foreach ($tree as $orch) {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let vvActiveId = null;
|
let vvActiveId = null;
|
||||||
let vvRunningId = null;
|
let vvRunningSet = new Set();
|
||||||
let vvLastTs = 0;
|
|
||||||
let vvStaleCount = 0;
|
|
||||||
let vvPollTimer = null;
|
let vvPollTimer = null;
|
||||||
|
let vvStatusTimer = null;
|
||||||
let vvEditorId = null;
|
let vvEditorId = null;
|
||||||
let vvConfId = null;
|
let vvConfId = null;
|
||||||
|
|
||||||
@@ -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
|
|
||||||
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) + '"]');
|
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
||||||
if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
|
if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
|
||||||
} else {
|
if (id === vvActiveId) document.getElementById('vv-log-dot').style.display = 'inline-block';
|
||||||
headerDot.style.display = 'none';
|
}
|
||||||
|
|
||||||
|
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) {
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user