From 4b318a809aa5a33663a6a00fb7d1724bd6fdda25 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 22 Jun 2026 00:11:07 +0200 Subject: [PATCH] dashboard: reset-timer checkbox on recurring fire-now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manually firing a recurring schedule now offers a "reset timer" checkbox (default on) in the confirm dialog: when checked, the fire-now POST sends {reset_timer:true} and the backend re-arms next_fire_at to now + interval. Unchecking keeps today's behaviour (extra out-of-band pulse, cadence intact). One-shot schedules omit the checkbox — they're consumed regardless. The result flash shows "— timer reset" when the backend reports timer_reset. Pairs with the backend reset_timer/timer_reset work. Closes #1848. --- frontend/packages/dashboard/src/schedules.js | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/frontend/packages/dashboard/src/schedules.js b/frontend/packages/dashboard/src/schedules.js index 07011a56..6bb5cf7b 100644 --- a/frontend/packages/dashboard/src/schedules.js +++ b/frontend/packages/dashboard/src/schedules.js @@ -737,7 +737,7 @@ function renderScheduleRow(s, agents) { }, '↯'); fireBtn.title = isOneShot ? 'fire once — one-shot, consumed after the manual fire' - : 'fire once now — recurring, next regular fire unaffected'; + : 'fire once now — recurring; choose whether to reset the next-fire timer'; if (!activeTargets.length) { fireBtn.disabled = true; fireBtn.title = 'every target is cancelled — nothing to fire'; @@ -986,14 +986,25 @@ async function submitEditSchedule(originalSchedule, form_) { } async function fireScheduleNow(id, isOneShot, targets, btn) { const targetList = targets.length ? targets.join(', ') : '(no active targets)'; + // Recurring schedules get a "reset timer" checkbox (default on): firing + // now also re-arms the next regular fire to now + interval. One-shot is + // consumed regardless, so the flag is moot there (no checkbox). + const checkboxes = isOneShot ? [] : [{ + name: 'reset_timer', + label: 'reset timer — re-arm next fire to now + interval', + checked: true, + }]; const prompt = isOneShot ? `fire schedule #${id} now to ${targetList}?\n\n` + 'this is a ONE-SHOT — firing now consumes the schedule. ' + 'the scheduled fire time will no longer trigger.' : `fire schedule #${id} now to ${targetList}?\n\n` + 'this is RECURRING — sends an extra pulse out-of-band. ' - + 'the regular cadence keeps firing on schedule.'; - if (!(await themedConfirm({ message: prompt, danger: true }))) return; + + 'leave "reset timer" on to re-arm the next regular fire to ' + + 'now + interval; uncheck it to keep the existing cadence.'; + const confirmRes = await themedConfirm({ message: prompt, danger: true, checkboxes }); + if (!confirmRes) return; + const resetTimer = !!confirmRes.reset_timer; // Capture child nodes so we can restore on error, then replace // with DOM-built content (textContent + element children rather // than innerHTML — the format string only carries server-side @@ -1015,6 +1026,7 @@ async function fireScheduleNow(id, isOneShot, targets, btn) { const resp = await fetch('/api/schedules/' + encodeURIComponent(id) + '/fire-now', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ reset_timer: resetTimer }), }); if (!resp.ok) { const text = await resp.text().catch(() => ''); @@ -1022,9 +1034,9 @@ async function fireScheduleNow(id, isOneShot, targets, btn) { restoreBtn(); return; } - // Backend returns FireNowReport { ok, failed, missing, one_shot_consumed }. - // Flash the per-target outcome on the button itself so the operator - // sees the result immediately, then refresh to pick up the + // Backend returns FireNowReport { ok, failed, missing, one_shot_consumed, + // timer_reset }. Flash the per-target outcome on the button itself so the + // operator sees the result immediately, then refresh to pick up the // authoritative per-target `last_result` annotations. let report = null; try { report = await resp.json(); } catch { /* shape drift / empty body — ignore */ } @@ -1033,7 +1045,8 @@ async function fireScheduleNow(id, isOneShot, targets, btn) { if (report.ok) bits.push(report.ok + ' ok'); if (report.failed) bits.push(report.failed + ' failed'); if (report.missing) bits.push(report.missing + ' missing'); - const suffix = report.one_shot_consumed ? ' — consumed' : ''; + const suffix = report.one_shot_consumed ? ' — consumed' + : (report.timer_reset ? ' — timer reset' : ''); while (btn.firstChild) btn.removeChild(btn.firstChild); btn.textContent = '↯ fired: ' + (bits.join(', ') || 'no targets') + suffix; btn.classList.add('btn-fire-now-flashed');