- 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.