diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index 9821853..cdbce0e 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -26,6 +26,10 @@ // // OPERATIONAL SAFEGUARDS // Running a script from the UI is confirmed first. Several of these delete files. +// vvConfirmRun() guards all three entry points — the six Run buttons, the direct +// by-id run, and Git Pull. This line described intent rather than behaviour until +// 2026-08-07: Run was the only mutating control on the page that did not ask, while +// being the one that executes shell as root. // // Dry-run is offered alongside run for anything destructive, and its output is shown in // full rather than summarised — reading the list is the point. @@ -1508,12 +1512,27 @@ function vvSelectLog(btn) { vvOpenRight(job.dataset.id); } -// Run a script by ID directly — no DOM card needed. -function vvRunById(id) { +// Every other mutating control on this page already confirms — save, delete, move, conf save, +// lock clear. Run did not, and it is the one that executes shell as root: the library holds +// cleaners that delete files and the git pull itself, and the six Run buttons sit one row apart +// in a dense tree where the row under the cursor is easy to misjudge. The page header has +// promised this confirmation since it was written; the code never did it. +// +// Dry Run is named in the prompt on purpose. For anything destructive it is the actual answer to +// "are you sure", and it is the button immediately beside the one that raised the question. +function vvConfirmRun(id) { + return confirm('Run ' + id + ' now?\n\n' + + 'It starts immediately and as root, with the same effect as a scheduled run.\n' + + 'Use Dry Run first if you want to see what it would change.'); +} + +// The confirm lives in the callers rather than here, so a caller that has already asked — or has +// button state to manage on cancel, as the pull does — is not forced to ask twice. +function vvRunStart(id, data) { vvOpenRight(id); vvSetDot(id); vvRunningSet.add(id); - vvPost('/plugins/varaverk/api/run.php', {id, manual: '1'}) + return vvPost('/plugins/varaverk/api/run.php', data) .then(d => { if (!d.ok) { document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start'); @@ -1523,11 +1542,20 @@ function vvRunById(id) { }); } +// Run a script by ID directly — no DOM card needed. +function vvRunById(id) { + if (!vvConfirmRun(id)) return; + vvRunStart(id, {id, manual: '1'}); +} + function vvGitPull(btn) { const id = 'git_pull_execute.sh'; + // Asked before the button is disabled, so cancelling does not leave it stuck reading "Pulling…" + // for the next four seconds while nothing is pulling. + if (!vvConfirmRun(id)) return; btn.disabled = true; btn.textContent = '⟳ Pulling…'; - vvRunById(id); + vvRunStart(id, {id, manual: '1'}); // Re-enable once the log poll confirms it's running or done setTimeout(() => { btn.disabled = false; btn.textContent = '↻ Git Pull'; }, 4000); } @@ -1535,22 +1563,13 @@ function vvGitPull(btn) { function vvRunJob(btn) { const job = btn.closest('[data-id]'); const id = job.dataset.id; + if (!vvConfirmRun(id)) return; const location = job.querySelector('.vv-rsync-location')?.value.trim() || ''; const extra_args = job.querySelector('.vv-script-args')?.value.trim() || ''; - vvOpenRight(id); - vvSetDot(id); - vvRunningSet.add(id); const data = {id}; if (location) data.location = location; if (extra_args) data.extra_args = extra_args; - vvPost('/plugins/varaverk/api/run.php', data) - .then(d => { - if (!d.ok) { - document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start'); - vvClearDot(id); - vvRunningSet.delete(id); - } - }); + vvRunStart(id, data); } function vvDryRun(btn) {