The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.
- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
(no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
bindAsyncForms(), and replaced all 8 window.confirm() sites with
themedConfirm (async, wrapped in a fire-and-forget IIFE where the
call site needs a synchronous boolean return, e.g. the slash-command
dispatcher).
Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
- asyncBtn now returns fn().finally(...) so callers can await/chain it
- Move re-fetch calls inside try/catch in core.js and permissions.js so
network errors from fetchAndRenderStalePerms / fetchAndRender* are
caught instead of escaping as unhandled rejections
- clearStaleAgent returns the asyncBtn promise so the function is
properly awaitable when a button is present
- Update asyncBtn doc comment to reflect the return-value contract
add `asyncBtn(btn, fn)` to `@hive/shared/forms.js` as the single
reusable component for async button actions:
1. double-click guard: returns immediately if btn is already disabled
2. saves btn.innerHTML, replaces with spinner while in-flight
3. restores btn on resolve or reject via finally
wire it into all ad-hoc disable/spinner/restore patterns:
- common.js: bindAsyncForms uses asyncBtn internally
- core.js: 'clear perms' button
- permissions.js: clearStaleAgent
- schedules.js: saveSchedule submit, editSchedule submit
- app.js: buildAnswerForm, buildInboxMarkAllRow
fireScheduleNow in schedules.js is left with its existing childNode
save/restore because it shows a custom result flash on the button
content after a successful fire-now (the auto-restore of asyncBtn
would overwrite it); the surrounding themedConfirm dialog already
acts as a natural double-click barrier before the fetch.
saveAll in permissions.js is also left as-is: it uses a custom
'queued ✓' success label + a 900ms delay before re-fetch; the
btn.dataset.busy flag is its own double-submit guard.