nav resilience after pool restart, thinner marker rings, drop dead marker popup

This commit is contained in:
2026-07-31 10:17:28 -04:00
parent d84f7e3911
commit ff61a4c5e9
6 changed files with 163 additions and 100 deletions

View File

@@ -156,6 +156,42 @@ router.afterEach(async (to) => {
document.title = page && page !== 'Home' ? `${siteTitle} - ${page}` : siteTitle
})
// A deploy replaces the hashed chunk files, so a tab that was open across it
// still asks for chunks that no longer exist. The dynamic import rejects, the
// navigation aborts, and to the user the site simply stops navigating. Reload
// once to pick up the new index.html; the sessionStorage flag stops a reload
// loop if the chunk is missing for any other reason.
const CHUNK_RELOAD_KEY = 'shopdb-chunk-reloaded'
function isChunkLoadError(error) {
const text = `${error?.message || ''} ${error?.name || ''}`
return /dynamically imported module|Importing a module script failed|ChunkLoadError|Loading chunk/i.test(text)
}
function reloadOnceForStaleChunk(error) {
if (!isChunkLoadError(error)) return false
if (sessionStorage.getItem(CHUNK_RELOAD_KEY)) return false
sessionStorage.setItem(CHUNK_RELOAD_KEY, '1')
window.location.reload()
return true
}
router.onError(error => {
reloadOnceForStaleChunk(error)
})
// Vite raises this for a failed module preload, which does not always surface
// as a router error.
window.addEventListener('vite:preloadError', event => {
if (reloadOnceForStaleChunk(event?.payload || event)) event.preventDefault?.()
})
// A navigation that completes proves the current chunks are good, so clear the
// flag - otherwise the one allowed reload is spent for the rest of the session.
router.afterEach(() => {
sessionStorage.removeItem(CHUNK_RELOAD_KEY)
})
// Navigation guard
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
@@ -197,7 +233,12 @@ router.beforeEach(async (to, from, next) => {
// First-run: steer a fresh admin into the setup wizard (once, until finished).
if (authStore.isAuthenticated && authStore.isAdmin && to.path !== '/setup') {
if (!isSetupLoaded()) {
await refreshSetupState()
// Bounded like the plugin gate: a hung request during an app-pool restart
// must not hold the navigation open. Its own catch already fails open.
await Promise.race([
refreshSetupState(),
new Promise(resolve => setTimeout(resolve, 4000)),
])
}
if (!setupComplete.value && !setupSkipped.value) {
return next('/setup')