fix(ui): replace native confirm() dialogs with the global in-app modal

window.confirm blocks the JS event loop, which froze companion remote
input while open — the remote user could raise the mesh "Clear" prompt
(or reboot / backup-delete / uninstall confirms) and then never dismiss
it, because the synthetic events that would dismiss it queue behind the
dialog itself.

New promise-based appConfirm() (useAppConfirm.ts) + one AppConfirmModal
mounted globally in App.vue, built on BaseModal (Teleport-to-body,
full-viewport backdrop, glass card — the canonical modal contract). All
six native confirm() call sites migrated: mesh clear-all, mesh message
delete, dashboard reboot, backup delete, backup USB copy, app uninstall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-16 05:28:53 -04:00
co-authored by Claude Fable 5
parent 458444d700
commit 876ecc4bdf
7 changed files with 124 additions and 7 deletions
+44
View File
@@ -0,0 +1,44 @@
/**
* Promise-based in-app replacement for window.confirm(). Native browser
* modals block the JS event loop while open, which freezes the companion
* remote-input relay (and every other async path) — a remote user could
* raise a confirm they can never dismiss, since the synthetic events that
* would dismiss it are queued behind the very dialog they need to close.
*
* State is module-level so the single <AppConfirmModal /> mounted in
* App.vue serves every caller:
*
* const ok = await appConfirm('Clear everything? This cannot be undone.')
* const ok = await appConfirm({ message: '…', confirmLabel: 'Delete', danger: true })
*/
import { ref } from 'vue'
export interface AppConfirmOptions {
title?: string
message: string
confirmLabel?: string
cancelLabel?: string
/** Styles the confirm button as destructive (orange/warning). */
danger?: boolean
}
interface ActiveConfirm extends AppConfirmOptions {
_resolve: (value: boolean) => void
}
export const confirmState = ref<ActiveConfirm | null>(null)
export function appConfirm(options: AppConfirmOptions | string): Promise<boolean> {
const opts = typeof options === 'string' ? { message: options } : options
// A second confirm while one is open cancels the first — the browser
// primitive this replaces could only ever show one at a time.
confirmState.value?._resolve(false)
return new Promise<boolean>((resolve) => {
confirmState.value = { ...opts, _resolve: resolve }
})
}
export function resolveConfirm(result: boolean) {
confirmState.value?._resolve(result)
confirmState.value = null
}