diff --git a/docs/web-ui.md b/docs/web-ui.md index dd681aca..76853b0c 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -227,10 +227,7 @@ they share enough conceptual ground to live together). **N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed scheduled prompts (#444 / #459). Lists every schedule with its description, targets, body, recurrence interval, next-fire -time, and per-target last-result. Per-row controls: a -`↯ fire now` button sends an out-of-band manual pulse to -every active target (#467 — recurring schedules keep their -cadence; one-shots are consumed after the manual fire), an +time, and per-target last-result. Per-row controls: an `✎ edit` button opens an inline edit form (#474 — body / description / interval / next-fire editable, targets stay immutable; submit PATCHes `/api/schedules/{id}`), and a @@ -503,14 +500,6 @@ not ours. - `POST /api/schedules/{id}/cancel` — cancel a schedule. Body `{ targets?: ["name", …] }` cancels just those recipients; absent or empty body cancels the whole schedule. -- `POST /api/schedules/{id}/fire-now` — out-of-band manual - pulse (#467). Fires the schedule body once immediately to - every active target. Recurring schedules: `next_fire_at_unix` - is untouched; the regular cadence continues. One-shots: the - schedule is consumed (cancelled) after the manual fan-out. - Per-target `last_result` is annotated as a manual fire so - the audit trail distinguishes scheduled fires from operator- - triggered ones. - `POST /meta-update` — `nix flake update` the selected `meta/flake.lock` inputs, then rebuild the affected agents. - `GET /dashboard/stream` — unified live event channel: diff --git a/frontend/packages/dashboard/src/app.js b/frontend/packages/dashboard/src/app.js index 7056579c..48c78aa5 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/frontend/packages/dashboard/src/app.js @@ -2345,23 +2345,6 @@ window.marked = marked; if (!cancelled) { const actions = el('div', { class: 'schedule-actions' }); - // Fire-now (#467): out-of-band manual pulse. Recurring schedules - // get an extra fan-out without disturbing `next_fire_at`; one-shots - // are consumed (cancelled after fan-out), per damocles's design. - // Disabled when every target is cancelled — there's nothing to - // fire to. Backend: POST /api/schedules/{id}/fire-now. - const activeTargets = (s.targets || []).filter((t) => !t.cancelled_at_unix); - const fireBtn = el('button', { type: 'button', class: 'btn btn-fire-now' }, '↯ fire now'); - const isOneShot = !s.interval_seconds; - fireBtn.title = isOneShot - ? 'fire this schedule once, immediately (one-shot — schedule is consumed after the manual fire)' - : 'fire this schedule once, immediately (recurring — next regularly-scheduled fire is unaffected)'; - if (!activeTargets.length) { - 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)); - actions.append(fireBtn); const editBtn = el('button', { type: 'button', class: 'btn btn-edit-schedule' }, editingSchedules.has(s.id) ? '✎ close edit' : '✎ edit'); editBtn.title = 'edit body / description / interval / next-fire (targets stay immutable)'; @@ -2568,68 +2551,6 @@ window.marked = marked; if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalLabel; } } } - 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` - + '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 (!confirm(prompt)) return; - // Capture child nodes so we can restore on error, then replace - // with DOM-built content (textContent + element children rather - // than innerHTML — per argus's review note on #471, the format - // string only carries server-side ints/bool today but textContent - // is the safer pattern if a stringy field ever lands). - const originalChildren = btn ? Array.from(btn.childNodes) : []; - const restoreBtn = () => { - if (!btn) return; - btn.disabled = false; - while (btn.firstChild) btn.removeChild(btn.firstChild); - for (const n of originalChildren) btn.appendChild(n); - }; - if (btn) { - btn.disabled = true; - while (btn.firstChild) btn.removeChild(btn.firstChild); - btn.append(el('span', { class: 'spinner' }, '◐'), ' firing…'); - } - try { - const resp = await fetch('/api/schedules/' + encodeURIComponent(id) + '/fire-now', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - if (!resp.ok) { - const text = await resp.text().catch(() => ''); - alert('fire-now failed: http ' + resp.status + (text ? '\n\n' + text : '')); - 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 - // 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' : ''; - while (btn.firstChild) btn.removeChild(btn.firstChild); - btn.textContent = '↯ 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); - restoreBtn(); - } - } async function cancelScheduleAll(id) { if (!confirm(`cancel schedule #${id}? this stops all future fires for every target.`)) return; await postScheduleCancel(id, null); diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 1c00ae6c..117ed785 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -850,16 +850,6 @@ ul form.inline { display: inline-block; } .btn-start { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .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). */