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