From f35382e57cd2ec24d622a41d0276947f657931bc Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 26 May 2026 13:47:40 +0200 Subject: [PATCH] schedules fire-now: render FireNowReport inline on the button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit damocles's #472 returns `{ ok, failed, missing, one_shot_consumed }` from the fire-now endpoint. Parse the response and flash the per-target outcome on the button itself for ~1.5s before refreshSchedules() repaints — operator sees the result immediately without a modal alert or an `/api/schedules` re-fetch round-trip. Button label transitions: ↯ fire now → ◐ firing… → ↯ fired: 3 ok, 1 missing — consumed (green flash, then row refresh) --- frontend/packages/dashboard/src/app.js | 27 ++++++++++++++++--- frontend/packages/dashboard/src/dashboard.css | 9 +++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/packages/dashboard/src/app.js b/frontend/packages/dashboard/src/app.js index ff206209..a5375ace 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/frontend/packages/dashboard/src/app.js @@ -2360,7 +2360,7 @@ window.marked = marked; fireBtn.disabled = true; fireBtn.title = 'every target is cancelled — nothing to fire'; } - fireBtn.addEventListener('click', () => fireScheduleNow(s.id, isOneShot, activeTargets.map((t) => t.target))); + fireBtn.addEventListener('click', () => fireScheduleNow(s.id, isOneShot, activeTargets.map((t) => t.target), fireBtn)); actions.append(fireBtn); const editBtn = el('button', { type: 'button', class: 'btn btn-edit-schedule' }, editingSchedules.has(s.id) ? '✎ close edit' : '✎ edit'); @@ -2568,7 +2568,7 @@ window.marked = marked; if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalLabel; } } } - async function fireScheduleNow(id, isOneShot, targets) { + async function fireScheduleNow(id, isOneShot, targets, btn) { const targetList = targets.length ? targets.join(', ') : '(no active targets)'; const prompt = isOneShot ? `fire schedule #${id} now to ${targetList}?\n\n` @@ -2578,6 +2578,8 @@ window.marked = marked; + 'this is RECURRING — sends an extra pulse out-of-band. ' + 'the regular cadence keeps firing on schedule.'; if (!confirm(prompt)) return; + const originalLabel = btn ? btn.innerHTML : ''; + if (btn) { btn.disabled = true; btn.innerHTML = ' firing…'; } try { const resp = await fetch('/api/schedules/' + encodeURIComponent(id) + '/fire-now', { method: 'POST', @@ -2586,11 +2588,30 @@ window.marked = marked; if (!resp.ok) { const text = await resp.text().catch(() => ''); alert('fire-now failed: http ' + resp.status + (text ? '\n\n' + text : '')); + if (btn) { btn.disabled = false; btn.innerHTML = originalLabel; } return; } - await refreshSchedules(); + // 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 + // authoritative per-target `last_result` annotations. + let report = null; + try { report = await resp.json(); } catch { /* shape drift / empty body — ignore */ } + if (btn && report) { + const bits = []; + 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' : ''; + btn.innerHTML = '↯ fired: ' + (bits.join(', ') || 'no targets') + suffix; + btn.classList.add('btn-fire-now-flashed'); + } + // Hold the flash briefly so the operator can read it before the + // refresh wipes the row in place. + setTimeout(() => { refreshSchedules(); }, 1500); } catch (err) { alert('fire-now failed: ' + err); + if (btn) { btn.disabled = false; btn.innerHTML = originalLabel; } } } async function cancelScheduleAll(id) { diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index d197e174..1c00ae6c 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -851,6 +851,15 @@ ul form.inline { display: inline-block; } .btn-talk { color: var(--cyan); border-color: var(--cyan); } .btn-spawn { color: var(--amber); border-color: var(--amber); } .btn-fire-now { color: var(--mauve, #cba6f7); border-color: var(--mauve, #cba6f7); } +/* Post-fire flash: report renders directly on the button for ~1.5s + so the operator sees ok/failed/missing/consumed counts inline + without a modal. Green when at least one ok; muted otherwise. */ +.btn-fire-now-flashed { + color: var(--green); + border-color: var(--green); + text-shadow: 0 0 6px currentColor; + box-shadow: 0 0 8px -2px currentColor; +} /* #474: inline edit button on each schedule row. Yellow reads as a parallel destructive-adjacent action (edit changes state, but isn't deletion). */