dashboard: reset-timer checkbox on recurring fire-now

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.
This commit is contained in:
iris 2026-06-22 00:11:07 +02:00 committed by mara
commit 4b318a809a

View file

@ -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');