import { useEffect, useState } from 'react' import { writeLastUpdateCheckAt } from './local-time.ts' import { createBusyGate } from './busy.ts' /** * Update check as a fixed dismissible toast — never inserts into document flow * (avoids content layout shift). */ export function UpdateBanner() { const [available, setAvailable] = useState(null) const [dismissed, setDismissed] = useState(false) const [waitingWorker, setWaitingWorker] = useState(null) const [refreshing, setRefreshing] = useState(false) const [showBusy, setShowBusy] = useState(false) const appId = '__PACKAGE_ID__' function dismissKey(token: string) { return `${appId}-update-dismissed-${token}` } function isDismissed(token: string) { try { return localStorage.getItem(dismissKey(token)) !== null } catch { return false } } function markDismissed(token: string) { try { localStorage.setItem(dismissKey(token), String(Date.now())) } catch { /* ignore */ } } async function check() { if (typeof window === 'undefined') return try { const root = document.documentElement const appBase = root.getAttribute('data-app-base') || '' const runningSha = root.getAttribute('data-running-sha') || '' const versionPath = `${appBase}/api/version` const res = await fetch(versionPath, { cache: 'no-store', credentials: 'same-origin' }) const version = res.ok ? ((await res.json()) as { sha?: string; shortSha?: string }) : {} let waiting: ServiceWorker | null = null if ('serviceWorker' in navigator) { const regs = await navigator.serviceWorker.getRegistrations() for (const reg of regs) { if (reg.waiting) waiting = reg.waiting if (reg.update) await reg.update().catch(() => null) } } setWaitingWorker(waiting) let next: string | null = null if (waiting) next = 'worker' else if (runningSha && version.sha && runningSha !== version.sha) { next = version.shortSha || String(version.sha).slice(0, 7) } setAvailable(next) setDismissed(next ? isDismissed(next) : false) writeLastUpdateCheckAt(new Date().toISOString(), appId) } catch { /* ignore */ } } useEffect(() => { void check() const onVis = () => { if (document.visibilityState === 'visible') void check() } document.addEventListener('visibilitychange', onVis) return () => document.removeEventListener('visibilitychange', onVis) }, []) const show = Boolean(available) && !dismissed if (!show) { return