import type { AppConfig } from '@/config/types' import { resetVersionCheckCache } from '@/config/load-version' import { clearCachedPwaManifest } from '@/modules/pwa/manifest-cache' import { tokenName } from '@/themeConfig' const CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE' const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL' const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL' const CONFIG_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7 const MAX_COOKIE_VALUE_LENGTH = 3500 const VERSION_STORAGE_KEY = 'version' const VERSION_UPDATE_LOCK_KEY = 'version_update_lock' const VERSION_LAST_RELOAD_KEY = 'version_last_reload' type CachedConfigPayload = { cached_at: number config: AppConfig } type VersionUpdateLock = { version: string started_at: number } function getCookie(name: string) { const cookie = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`)) if (!cookie) return null return cookie.substring(name.length + 1) } function setCookie(name: string, value: string, maxAgeSeconds: number) { const secure = window.location.protocol === 'https:' ? '; Secure' : '' document.cookie = [ `${name}=${value}`, 'Path=/', `Max-Age=${maxAgeSeconds}`, 'SameSite=Lax', secure ].join('; ') } export function readCachedAppConfig(): AppConfig | null { try { const rawCookie = getCookie(CONFIG_COOKIE_NAME) if (!rawCookie) return null const decoded = decodeURIComponent(rawCookie) const payload = JSON.parse(decoded) as CachedConfigPayload if (!payload?.config) return null return payload.config } catch (error) { console.warn('Failed to read cached app config:', error) clearCachedAppConfig() return null } } function persistBootLogo(config: AppConfig) { try { const nested = (config as {user?: Record})?.user const root = (config || {}) as Record const avatar = String(nested?.avatar || root.avatar || '').trim() const version = String(localStorage.getItem(VERSION_STORAGE_KEY) || '').trim() const cachedUrl = localStorage.getItem('BOOT_LOGO_URL') || '' const cachedVer = localStorage.getItem('BOOT_LOGO_VERSION') || '' const cacheFresh = !!(cachedUrl && version && cachedVer && version === cachedVer) if (!cacheFresh && avatar) localStorage.setItem('BOOT_LOGO_URL', avatar) if (version) localStorage.setItem('BOOT_LOGO_VERSION', version) const apply = (window as Window & {__APPLY_BOOT_BRANDING__?: (c: unknown) => void}) .__APPLY_BOOT_BRANDING__ if (typeof apply === 'function') apply(config) } catch { /* ignore */ } } export function writeCachedAppConfig(config: AppConfig) { try { persistBootLogo(config) const payload: CachedConfigPayload = { cached_at: Date.now(), config } const encoded = encodeURIComponent(JSON.stringify(payload)) if (encoded.length > MAX_COOKIE_VALUE_LENGTH) { console.warn(`App config is too large for cookie cache. Size: ${encoded.length}`) return } setCookie(CONFIG_COOKIE_NAME, encoded, CONFIG_COOKIE_MAX_AGE_SECONDS) } catch (error) { console.warn('Failed to write app config cache:', error) } } export function clearCachedAppConfig() { document.cookie = `${CONFIG_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax` clearCachedPwaManifest() } export function getStoredAppVersion() { return localStorage.getItem(VERSION_STORAGE_KEY) } export function getDisplayAppVersion(fallbackVersion: string) { return getStoredAppVersion() || fallbackVersion } export function writeStoredAppVersion(version: string) { localStorage.setItem(VERSION_STORAGE_KEY, String(version).trim()) } export function getNormalizedVersion(version: unknown) { if (version === null || version === undefined) return '' return String(version).trim() } export function shouldUpdateForVersion(serverVersion: unknown) { const normalizedServerVersion = getNormalizedVersion(serverVersion) if (!normalizedServerVersion) return false const storedVersion = getStoredAppVersion() if (!storedVersion) { writeStoredAppVersion(normalizedServerVersion) return false } return storedVersion !== normalizedServerVersion } export function canStartVersionUpdate(serverVersion: string) { const version = getNormalizedVersion(serverVersion) if (!version) return false const lastReloadVersion = sessionStorage.getItem(VERSION_LAST_RELOAD_KEY) if (lastReloadVersion === version) { return false } const rawLock = sessionStorage.getItem(VERSION_UPDATE_LOCK_KEY) if (rawLock) { try { const lock = JSON.parse(rawLock) as VersionUpdateLock const isSameVersion = lock.version === version const isFreshLock = Date.now() - lock.started_at < 60_000 if (isSameVersion && isFreshLock) { return false } } catch { sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY) } } return true } export function startVersionUpdateLock(serverVersion: string) { const version = getNormalizedVersion(serverVersion) const lock: VersionUpdateLock = { version, started_at: Date.now() } sessionStorage.setItem(VERSION_UPDATE_LOCK_KEY, JSON.stringify(lock)) } export function finishVersionUpdateBeforeReload(serverVersion: string) { const version = getNormalizedVersion(serverVersion) writeStoredAppVersion(version) sessionStorage.setItem(VERSION_LAST_RELOAD_KEY, version) sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY) } export function cancelVersionUpdateLock() { sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY) } function clearReadableCookiesExceptConfigAlreadyHandled() { document.cookie.split(';').forEach((cookie) => { const name = cookie.split('=')[0]?.trim() if (!name) return document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax` }) } export async function clearBrowserRuntimeCaches() { try { if ('serviceWorker' in navigator) { const registrations = await navigator.serviceWorker.getRegistrations() await Promise.all(registrations.map((registration) => registration.unregister())) } } catch (error) { console.warn('Could not unregister service workers:', error) } try { if (!('caches' in window)) return const cacheKeys = await caches.keys() await Promise.all(cacheKeys.map((key) => caches.delete(key))) } catch (error) { console.warn('Could not clear browser caches:', error) } } function clearLocalStorageExceptToken() { const token = localStorage.getItem(tokenName) let bootLogo = '' let bootLogoVer = '' try { bootLogo = localStorage.getItem('BOOT_LOGO_URL') || '' bootLogoVer = localStorage.getItem('BOOT_LOGO_VERSION') || '' } catch { /* ignore */ } localStorage.clear() if (token) { localStorage.setItem(tokenName, token) } if (bootLogo) localStorage.setItem('BOOT_LOGO_URL', bootLogo) if (bootLogoVer) localStorage.setItem('BOOT_LOGO_VERSION', bootLogoVer) } export function clearAppStorageExceptToken() { clearReadableCookiesExceptConfigAlreadyHandled() clearLocalStorageExceptToken() sessionStorage.clear() } export function clearAllBootstrapFetchCaches() { localStorage.removeItem(CONFIG_SUCCESS_URL_KEY) localStorage.removeItem(VERSION_SUCCESS_URL_KEY) clearCachedPwaManifest() resetVersionCheckCache() window.__APP_CONFIG__ = undefined } export function hardReloadApp() { const refreshUrl = `${window.location.origin}${window.location.pathname}?__app_refresh=${Date.now()}` window.location.href = refreshUrl } export async function refreshConfigCacheForVersion( newVersion: string, fetchConfig: () => Promise ) { const normalizedVersion = getNormalizedVersion(newVersion) if (!normalizedVersion) { throw new Error('Version is empty') } startVersionUpdateLock(normalizedVersion) clearCachedAppConfig() clearAppStorageExceptToken() clearAllBootstrapFetchCaches() await clearBrowserRuntimeCaches() const freshConfig = await fetchConfig() writeCachedAppConfig(freshConfig) finishVersionUpdateBeforeReload(normalizedVersion) try { localStorage.setItem('BOOT_LOGO_VERSION', normalizedVersion) } catch { /* ignore */ } return freshConfig } export async function resetAppAndRefreshConfig( fetchConfig: () => Promise, version?: string | null ) { const normalizedVersion = getNormalizedVersion(version) clearCachedAppConfig() clearAppStorageExceptToken() clearAllBootstrapFetchCaches() await clearBrowserRuntimeCaches() const freshConfig = await fetchConfig() writeCachedAppConfig(freshConfig) if (normalizedVersion) { writeStoredAppVersion(normalizedVersion) try { localStorage.setItem('BOOT_LOGO_VERSION', normalizedVersion) } catch { /* ignore */ } } return freshConfig }