diff --git a/frontend/packages/dashboard/src/call.js b/frontend/packages/dashboard/src/call.js index 88ea4588..d71f812e 100644 --- a/frontend/packages/dashboard/src/call.js +++ b/frontend/packages/dashboard/src/call.js @@ -15,7 +15,6 @@ // registers once via `initCall`. import { $, el, form, Panel, appendLinkified } from './common.js'; -import { themedToast } from './modal.js'; import { fmtAgo } from './util.js'; import { questionsState, QUESTION_HISTORY_LIMIT } from './state.js'; @@ -576,7 +575,7 @@ function buildQuestionLi(q) { const existing = f.querySelector('input[name="answer"]'); if (existing) existing.remove(); f.append(el('input', { type: 'hidden', name: 'answer', value: merged })); - if (!merged) { ev.preventDefault(); themedToast('pick an option or type an answer', { type: 'error' }); } + if (!merged) { ev.preventDefault(); alert('pick an option or type an answer'); } }, true); if (hasOptions) f.append(optionGroup); const buttons = el('div', { class: 'q-buttons' }); diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index 370517e5..ba351a83 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -4,10 +4,6 @@ // infrastructure for the side panel. import { linkify as termLinkify } from '@hive/shared/terminal.js'; -// Themed dialog/toast helpers (modal.js imports `el` back from here — a safe -// deferred cycle: neither side uses the other at module-init time, only inside -// runtime handlers). -import { themedConfirm, themedPrompt, themedToast } from './modal.js'; // ─── helpers ──────────────────────────────────────────────────────────── export const $ = (id) => document.getElementById(id); @@ -62,9 +58,9 @@ export function bindAsyncForms(onSuccess) { const f = e.target; if (!(f instanceof HTMLFormElement) || !f.hasAttribute('data-async')) return; e.preventDefault(); - if (f.dataset.confirm && !(await themedConfirm({ message: f.dataset.confirm }))) return; + if (f.dataset.confirm && !confirm(f.dataset.confirm)) return; if (f.dataset.prompt) { - const ans = await themedPrompt({ message: f.dataset.prompt }); + const ans = prompt(f.dataset.prompt, ''); if (ans === null) return; // operator hit Cancel // Drop into a hidden input named after `data-prompt-field` (or // 'note' by default) so the value rides along on the POST. @@ -92,7 +88,7 @@ export function bindAsyncForms(onSuccess) { || (resp.status >= 200 && resp.status < 400); if (!ok) { const text = await resp.text().catch(() => ''); - themedToast('action failed: ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' }); + alert('action failed: ' + resp.status + (text ? '\n\n' + text : '')); if (btn) { btn.disabled = false; btn.innerHTML = original; } return; } @@ -105,7 +101,7 @@ export function bindAsyncForms(onSuccess) { onSuccess(); } } catch (err) { - themedToast('action failed: ' + err, { type: 'error' }); + alert('action failed: ' + err); if (btn) { btn.disabled = false; btn.innerHTML = original; } } }); diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index bf8fcd7f..08f637f7 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -467,52 +467,6 @@ body.dashboard-shell { .tc-confirm { color: var(--green); } .tc-confirm.tc-danger { color: var(--red); } -/* themedPrompt input field */ -.tc-promptfield { display: flex; flex-direction: column; gap: 0.4em; } -.tc-promptlabel { color: var(--subtext0); font-size: 0.9em; } -.tc-input { - font-family: inherit; - font-size: 1em; - width: 100%; - box-sizing: border-box; - background: var(--crust); - color: var(--fg); - border: 1px solid var(--purple-dim); - padding: 0.4em 0.6em; -} -.tc-input:focus { outline: 1px solid var(--green); } - -/* themedToast — non-blocking transient notifications (errors/info/ok) */ -.tc-toasts { - position: fixed; - top: 1em; - right: 1em; - z-index: 1100; - display: flex; - flex-direction: column; - gap: 0.5em; - max-width: min(28em, 92vw); - pointer-events: none; -} -.tc-toast { - pointer-events: auto; - cursor: pointer; - background: var(--bg-elev); - border: 1px solid var(--purple-dim); - border-left-width: 3px; - box-shadow: 0 4px 20px -6px var(--crust); - padding: 0.6em 0.9em; - font-size: 0.9em; - color: var(--fg); - white-space: pre-wrap; - opacity: 1; - transition: opacity 0.2s ease, transform 0.2s ease; -} -.tc-toast-out { opacity: 0; transform: translateX(0.5em); } -.tc-toast-error { border-left-color: var(--red); } -.tc-toast-info { border-left-color: var(--purple-dim); } -.tc-toast-ok { border-left-color: var(--green); } - .agent-status { font-size: 0.82em; color: var(--subtext0); diff --git a/frontend/packages/dashboard/src/modal.js b/frontend/packages/dashboard/src/modal.js index fe3d05f4..01ff8f24 100644 --- a/frontend/packages/dashboard/src/modal.js +++ b/frontend/packages/dashboard/src/modal.js @@ -123,60 +123,3 @@ export function themedConfirm(opts = {}) { return out; }); } - -// themedPrompt({ title, message, label, placeholder, value, confirmLabel, cancelLabel }) -// → Promise. Themed replacement for window.prompt(): an input -// dialog that resolves to the entered string on confirm, or null on cancel. -export function themedPrompt(opts = {}) { - const { - title = '', message = '', label = '', placeholder = '', value = '', - confirmLabel = 'ok', cancelLabel = 'cancel', - } = opts; - const input = el('input', { type: 'text', class: 'tc-input', placeholder }); - if (value) input.value = value; - const content = el('div', { class: 'tc-promptfield' }, - label ? el('label', { class: 'tc-promptlabel' }, label) : null, - input); - const result = openDialog({ - title, - message, - content, - buttons: [ - { label: cancelLabel, value: '__cancel__', class: 'tc-cancel' }, - { label: confirmLabel, value: '__ok__', class: 'tc-confirm', autofocus: true }, - ], - }).then((v) => (v === '__ok__' ? input.value : null)); - // Prefer focusing the field over the OK button once the dialog has mounted. - setTimeout(() => input.focus(), 0); - return result; -} - -// themedToast(message, { type, duration }) — non-blocking transient -// notification; a lighter alternative to a modal for feedback that needs no -// decision (errors, validation, status). Stacks in a fixed top-right -// container, auto-dismisses after `duration` ms (errors linger longer), and -// can be clicked to dismiss early. `type` ∈ {'info','error','ok'}. -export function themedToast(message, opts = {}) { - const { type = 'info', duration } = opts; - const ms = duration != null ? duration : (type === 'error' ? 8000 : 4000); - let container = document.getElementById('tc-toasts'); - if (!container) { - container = el('div', { id: 'tc-toasts', class: 'tc-toasts' }); - document.body.append(container); - } - const toast = el('div', { - class: 'tc-toast tc-toast-' + type, - role: type === 'error' ? 'alert' : 'status', - }, message); - let removed = false; - const remove = () => { - if (removed) return; - removed = true; - toast.classList.add('tc-toast-out'); - setTimeout(() => toast.remove(), 200); - }; - toast.addEventListener('click', remove); - container.append(toast); - if (ms > 0) setTimeout(remove, ms); - return toast; -} diff --git a/frontend/packages/dashboard/src/schedules.js b/frontend/packages/dashboard/src/schedules.js index 3ae45889..b396f4d8 100644 --- a/frontend/packages/dashboard/src/schedules.js +++ b/frontend/packages/dashboard/src/schedules.js @@ -12,7 +12,6 @@ // target-chip pickers. Render + format helpers come from util.js. import { $, el } from './common.js'; -import { themedConfirm, themedToast } from './modal.js'; import { paintAtomic, fmtAgo, fmtDuration } from './util.js'; import { containersState } from './state.js'; @@ -531,14 +530,14 @@ async function submitNewScheduleInline(tr, submitBtn) { const firstFireStr = String(tr.querySelector('input[name="new_first_fire"]')?.value || ''); if (!targets.length) { - themedToast('schedule must have at least one target — tick at least one agent column', { type: 'error' }); + alert('schedule must have at least one target — tick at least one agent column'); return; } - if (!body) { themedToast('prompt body must be non-empty', { type: 'error' }); return; } - if (!firstFireStr) { themedToast('first fire timestamp is required', { type: 'error' }); return; } + if (!body) { alert('prompt body must be non-empty'); return; } + if (!firstFireStr) { alert('first fire timestamp is required'); return; } const firstFireDate = new Date(firstFireStr); if (Number.isNaN(firstFireDate.getTime())) { - themedToast('first fire is not a valid datetime', { type: 'error' }); return; + alert('first fire is not a valid datetime'); return; } const first_fire_at_unix = Math.floor(firstFireDate.getTime() / 1000); @@ -555,7 +554,7 @@ async function submitNewScheduleInline(tr, submitBtn) { }; const intervalTotal = part('d', 86400) + part('h', 3600) + part('m', 60) + part('s', 1); if (Number.isNaN(intervalTotal)) { - themedToast('interval fields must be non-negative integers (or blank for one-shot)', { type: 'error' }); + alert('interval fields must be non-negative integers (or blank for one-shot)'); return; } const interval_seconds = intervalTotal > 0 ? intervalTotal : null; @@ -575,14 +574,14 @@ async function submitNewScheduleInline(tr, submitBtn) { }); if (!resp.ok) { const text = await resp.text().catch(() => ''); - themedToast('schedule submit failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' }); + alert('schedule submit failed: http ' + resp.status + (text ? '\n\n' + text : '')); return; } // Reset carry so the next render shows an empty row. resetNewScheduleCarry(); await refreshSchedules(); } catch (err) { - themedToast('schedule submit failed: ' + err, { type: 'error' }); + alert('schedule submit failed: ' + err); } finally { submitBtn.disabled = false; submitBtn.innerHTML = originalLabel; @@ -900,11 +899,11 @@ async function submitEditSchedule(originalSchedule, form_) { const newDescription = String(fd.get('description') || '').trim(); const newNextFireStr = String(fd.get('next_fire') || ''); - if (!newBody) { themedToast('body must be non-empty', { type: 'error' }); return; } - if (!newNextFireStr) { themedToast('next-fire timestamp is required', { type: 'error' }); return; } + if (!newBody) { alert('body must be non-empty'); return; } + if (!newNextFireStr) { alert('next-fire timestamp is required'); return; } const newNextFireDate = new Date(newNextFireStr); if (Number.isNaN(newNextFireDate.getTime())) { - themedToast('next-fire is not a valid datetime', { type: 'error' }); return; + alert('next-fire is not a valid datetime'); return; } const newNextFireUnix = Math.floor(newNextFireDate.getTime() / 1000); @@ -912,7 +911,7 @@ async function submitEditSchedule(originalSchedule, form_) { // `edit_interval_*` names in this form). const intervalTotal = intervalSecondsFromFormData(fd, 'edit_interval_'); if (Number.isNaN(intervalTotal)) { - themedToast('interval fields must be non-negative integers', { type: 'error' }); + alert('interval fields must be non-negative integers'); return; } const newIntervalSeconds = intervalTotal > 0 ? intervalTotal : null; @@ -930,7 +929,7 @@ async function submitEditSchedule(originalSchedule, form_) { .map((t) => t.target), ); if (!newTargets.size) { - themedToast('schedule must have at least one target — uncheck submit, or use ✕ cancel all instead', { type: 'error' }); + alert('schedule must have at least one target — uncheck submit, or use ✕ cancel all instead'); return; } const targetsAdd = [...newTargets].filter((t) => !originalActive.has(t)); @@ -972,14 +971,14 @@ async function submitEditSchedule(originalSchedule, form_) { }); if (!resp.ok) { const text = await resp.text().catch(() => ''); - themedToast('edit failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' }); + alert('edit failed: http ' + resp.status + (text ? '\n\n' + text : '')); return; } editingSchedules.delete(s.id); scheduleEditCarry.delete(s.id); await refreshSchedules(); } catch (err) { - themedToast('edit failed: ' + err, { type: 'error' }); + alert('edit failed: ' + err); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalLabel; } } @@ -993,7 +992,7 @@ async function fireScheduleNow(id, isOneShot, targets, btn) { : `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; + 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 — the format string only carries server-side @@ -1018,7 +1017,7 @@ async function fireScheduleNow(id, isOneShot, targets, btn) { }); if (!resp.ok) { const text = await resp.text().catch(() => ''); - themedToast('fire-now failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' }); + alert('fire-now failed: http ' + resp.status + (text ? '\n\n' + text : '')); restoreBtn(); return; } @@ -1042,16 +1041,16 @@ async function fireScheduleNow(id, isOneShot, targets, btn) { // refresh wipes the row in place. setTimeout(refreshSchedules, 1500); } catch (err) { - themedToast('fire-now failed: ' + err, { type: 'error' }); + alert('fire-now failed: ' + err); restoreBtn(); } } async function cancelScheduleAll(id) { - if (!(await themedConfirm({ message: `cancel schedule #${id}? this stops all future fires for every target.`, danger: true }))) return; + if (!confirm(`cancel schedule #${id}? this stops all future fires for every target.`)) return; await postScheduleCancel(id, null); } async function cancelScheduleTargets(id, targets) { - if (!(await themedConfirm({ message: `cancel schedule #${id} for ${targets.join(', ')}? other targets keep firing.`, danger: true }))) return; + if (!confirm(`cancel schedule #${id} for ${targets.join(', ')}? other targets keep firing.`)) return; await postScheduleCancel(id, targets); } async function postScheduleCancel(id, targets) { @@ -1064,12 +1063,12 @@ async function postScheduleCancel(id, targets) { const resp = await fetch('/api/schedules/' + encodeURIComponent(id) + '/cancel', opts); if (!resp.ok) { const text = await resp.text().catch(() => ''); - themedToast('cancel failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' }); + alert('cancel failed: http ' + resp.status + (text ? '\n\n' + text : '')); return; } await refreshSchedules(); } catch (err) { - themedToast('cancel failed: ' + err, { type: 'error' }); + alert('cancel failed: ' + err); } } diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 6bfc31e7..4765815e 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -18,7 +18,7 @@ import { makePathLink, appendText, appendLinkified, openStream, renderServerWarnings, bindAsyncForms, } from './common.js'; -import { themedConfirm, themedToast } from './modal.js'; +import { themedConfirm } from './modal.js'; import { createTabStrip } from '@hive/shared/tabs.js'; import { containersState, syncContainersFromSnapshot, @@ -295,10 +295,10 @@ window.marked = marked; || (resp.status >= 200 && resp.status < 400); if (!ok) { const text = await resp.text().catch(() => ''); - themedToast('action failed: ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' }); + alert('action failed: ' + resp.status + (text ? '\n\n' + text : '')); } } catch (err) { - themedToast('action failed: ' + err, { type: 'error' }); + alert('action failed: ' + err); } } @@ -1056,7 +1056,7 @@ window.marked = marked; const promptMsg = names.length === 1 ? `move ${names[0]} → ${newParentLabel}?` : `move ${names.length} agents (${names.join(', ')}) → ${newParentLabel}?`; - if (!(await themedConfirm({ message: promptMsg, danger: true }))) { + if (!confirm(promptMsg)) { sel.selectedIndex = 0; return; } @@ -1105,7 +1105,7 @@ window.marked = marked; sel.disabled = false; sel.selectedIndex = 0; if (failures.length) { - themedToast(`M0V3 completed with ${failures.length} failure${failures.length === 1 ? '' : 's'}:\n\n` + failures.join('\n'), { type: 'error', duration: 0 }); + alert(`M0V3 completed with ${failures.length} failure${failures.length === 1 ? '' : 's'}:\n\n` + failures.join('\n')); } }); wrap.append(sel); @@ -1216,7 +1216,7 @@ window.marked = marked; btn.disabled = false; btn.innerHTML = original; if (failures.length) { - themedToast(`${label} completed with ${failures.length} failure${failures.length === 1 ? '' : 's'}:\n\n` + failures.join('\n'), { type: 'error', duration: 0 }); + alert(`${label} completed with ${failures.length} failure${failures.length === 1 ? '' : 's'}:\n\n` + failures.join('\n')); } // Container-lifecycle events (ContainerStateChanged / // ContainerRemoved / RebuildQueueChanged) flow over the existing