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 fireBtn.title = isOneShot
? 'fire once — one-shot, consumed after the manual fire' ? '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) { if (!activeTargets.length) {
fireBtn.disabled = true; fireBtn.disabled = true;
fireBtn.title = 'every target is cancelled — nothing to fire'; 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) { async function fireScheduleNow(id, isOneShot, targets, btn) {
const targetList = targets.length ? targets.join(', ') : '(no active targets)'; 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 const prompt = isOneShot
? `fire schedule #${id} now to ${targetList}?\n\n` ? `fire schedule #${id} now to ${targetList}?\n\n`
+ 'this is a ONE-SHOT — firing now consumes the schedule. ' + 'this is a ONE-SHOT — firing now consumes the schedule. '
+ 'the scheduled fire time will no longer trigger.' + 'the scheduled fire time will no longer trigger.'
: `fire schedule #${id} now to ${targetList}?\n\n` : `fire schedule #${id} now to ${targetList}?\n\n`
+ 'this is RECURRING — sends an extra pulse out-of-band. ' + 'this is RECURRING — sends an extra pulse out-of-band. '
+ 'the regular cadence keeps firing on schedule.'; + 'leave "reset timer" on to re-arm the next regular fire to '
if (!(await themedConfirm({ message: prompt, danger: true }))) return; + '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 // Capture child nodes so we can restore on error, then replace
// with DOM-built content (textContent + element children rather // with DOM-built content (textContent + element children rather
// than innerHTML — the format string only carries server-side // 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', { const resp = await fetch('/api/schedules/' + encodeURIComponent(id) + '/fire-now', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reset_timer: resetTimer }),
}); });
if (!resp.ok) { if (!resp.ok) {
const text = await resp.text().catch(() => ''); const text = await resp.text().catch(() => '');
@ -1022,9 +1034,9 @@ async function fireScheduleNow(id, isOneShot, targets, btn) {
restoreBtn(); restoreBtn();
return; return;
} }
// Backend returns FireNowReport { ok, failed, missing, one_shot_consumed }. // Backend returns FireNowReport { ok, failed, missing, one_shot_consumed,
// Flash the per-target outcome on the button itself so the operator // timer_reset }. Flash the per-target outcome on the button itself so the
// sees the result immediately, then refresh to pick up the // operator sees the result immediately, then refresh to pick up the
// authoritative per-target `last_result` annotations. // authoritative per-target `last_result` annotations.
let report = null; let report = null;
try { report = await resp.json(); } catch { /* shape drift / empty body — ignore */ } 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.ok) bits.push(report.ok + ' ok');
if (report.failed) bits.push(report.failed + ' failed'); if (report.failed) bits.push(report.failed + ' failed');
if (report.missing) bits.push(report.missing + ' missing'); 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); while (btn.firstChild) btn.removeChild(btn.firstChild);
btn.textContent = '↯ fired: ' + (bits.join(', ') || 'no targets') + suffix; btn.textContent = '↯ fired: ' + (bits.join(', ') || 'no targets') + suffix;
btn.classList.add('btn-fire-now-flashed'); btn.classList.add('btn-fire-now-flashed');