diff --git a/web/src/api/http.ts b/web/src/api/http.ts index 2da07f3..acefec3 100644 --- a/web/src/api/http.ts +++ b/web/src/api/http.ts @@ -8,6 +8,13 @@ import { refreshIfNeeded, } from '../auth/session' +declare module 'axios' { + export interface AxiosRequestConfig { + // Public kiosk pages must remain usable when this browser has an expired login session. + skipAuthSession?: boolean + } +} + const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api', timeout: 15000, @@ -20,12 +27,12 @@ http.interceptors.request.use(async (config) => { config.url?.endsWith('/auth/logout') || config.url?.endsWith('/auth/sso/exchange') || config.url?.endsWith('/auth/sso/bind') - if (!isAuthenticationRequest) { + if (!isAuthenticationRequest && !config.skipAuthSession) { const activeToken = await refreshIfNeeded(true) if (activeToken) markActivity() } const token = localStorage.getItem(authStorageKeys.token) - if (token) config.headers.Authorization = `Bearer ${token}` + if (token && !config.skipAuthSession) config.headers.Authorization = `Bearer ${token}` return config }) @@ -40,7 +47,7 @@ http.interceptors.response.use( error.config?.url?.endsWith('/auth/sso/bind') const retryableConfig = error.config as (typeof error.config & { _jiaowuRetried?: boolean }) | undefined - if (error.response?.status === 401 && !isAuthenticationRequest && + if (error.response?.status === 401 && !isAuthenticationRequest && !error.config?.skipAuthSession && !retryableConfig?._jiaowuRetried) { const token = await refreshAuthSession() if (token && retryableConfig) { @@ -49,7 +56,7 @@ http.interceptors.response.use( return http.request(retryableConfig) } } - if (error.response?.status === 401 && !isAuthenticationRequest) { + if (error.response?.status === 401 && !isAuthenticationRequest && !error.config?.skipAuthSession) { clearAuthSession() goLogin(location.pathname + location.search + location.hash) } diff --git a/web/src/services/appUpdates.ts b/web/src/services/appUpdates.ts index eb1740d..a739366 100644 --- a/web/src/services/appUpdates.ts +++ b/web/src/services/appUpdates.ts @@ -18,10 +18,8 @@ interface AppUpdateCheckResponse { const updateChannel = import.meta.env.VITE_APP_UPDATE_CHANNEL?.trim() || 'production' -const foregroundCheckInterval = 5 * 60 * 1000 let checkInProgress: Promise | undefined -let lastCheckedAt = 0 function absoluteApiUrl(relativePath: string) { const configuredBase = String(http.defaults.baseURL ?? '/api') @@ -36,9 +34,7 @@ function absoluteApiUrl(relativePath: string) { async function checkForAppUpdate() { if (checkInProgress) return checkInProgress - if (!navigator.onLine || Date.now() - lastCheckedAt < foregroundCheckInterval) { - return - } + if (!navigator.onLine) return checkInProgress = (async () => { try { @@ -64,6 +60,9 @@ async function checkForAppUpdate() { nativeVersion: current.native, currentVersion, }, + // Update discovery is deliberately public. Do not delay startup for a + // token refresh or send a stale Authorization header to this endpoint. + skipAuthSession: true, timeout: 15000, }, ) @@ -96,7 +95,6 @@ async function checkForAppUpdate() { // OTA failure must never stop users from entering the bundled application. console.warn('[app-update] update check failed', error) } finally { - lastCheckedAt = Date.now() checkInProgress = undefined } })() @@ -116,10 +114,8 @@ export async function initializeAppUpdates() { console.warn('[app-update] bundle readiness confirmation failed', error) } + // Every cold start checks the latest public release immediately. A bundle + // downloaded here is activated when the app is next launched. void checkForAppUpdate() - window.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - void checkForAppUpdate() - } - }) + window.addEventListener('online', () => void checkForAppUpdate()) } diff --git a/web/src/views/VenueDisplayView.vue b/web/src/views/VenueDisplayView.vue index 073b12f..f1bfe21 100644 --- a/web/src/views/VenueDisplayView.vue +++ b/web/src/views/VenueDisplayView.vue @@ -23,7 +23,7 @@ async function load() { if (loading) return loading = true try { - data.value = (await http.get(`/venue-displays/${type.value}s/${id.value}`)).data + data.value = (await http.get(`/venue-displays/${type.value}s/${id.value}`, { skipAuthSession: true })).data await nextTick() fitPage() } finally {