formatting and comments

This commit is contained in:
2026-07-16 00:12:26 -07:00
parent edb6d2b5b4
commit 994854d104
45 changed files with 2514 additions and 628 deletions
+64 -27
View File
@@ -1,3 +1,12 @@
/**
* Install, offline and explicit-update lifecycle for the control-console PWA.
*
* Registration is restricted to `/control/`; `/obs/*` is deliberately outside
* the scope. The worker caches only the public application shell and static
* assets. API responses, WebSockets and secrets remain network-only. A waiting
* worker activates only after user confirmation and after every registered
* one-time-secret or dirty-form blocker has cleared.
*/
import { useEffect, useSyncExternalStore } from 'react'
interface InstallChoice {
@@ -58,9 +67,10 @@ function addManifest() {
function observeRegistration(registration: ServiceWorkerRegistration) {
emit({
registration,
waitingWorker: registration.waiting && navigator.serviceWorker.controller
? registration.waiting
: snapshot.waitingWorker,
waitingWorker:
registration.waiting && navigator.serviceWorker.controller
? registration.waiting
: snapshot.waitingWorker,
})
registration.addEventListener('updatefound', () => {
@@ -76,18 +86,23 @@ function observeRegistration(registration: ServiceWorkerRegistration) {
async function removeLegacyRootWorker() {
const registrations = await navigator.serviceWorker.getRegistrations()
await Promise.all(registrations.map(async registration => {
const scopePath = new URL(registration.scope).pathname
const workers = [registration.installing, registration.waiting, registration.active]
const isLegacy = scopePath === '/'
&& workers.some(worker => worker && new URL(worker.scriptURL).pathname === '/sw.js')
if (isLegacy) await registration.unregister()
}))
await Promise.all(
registrations.map(async registration => {
const scopePath = new URL(registration.scope).pathname
const workers = [registration.installing, registration.waiting, registration.active]
const isLegacy =
scopePath === '/' &&
workers.some(worker => worker && new URL(worker.scriptURL).pathname === '/sw.js')
if (isLegacy) await registration.unregister()
}),
)
const cacheNames = await caches.keys()
await Promise.all(cacheNames
.filter(name => name.startsWith('lxc-control-shell-'))
.map(name => caches.delete(name)))
await Promise.all(
cacheNames
.filter(name => name.startsWith('lxc-control-shell-'))
.map(name => caches.delete(name)),
)
}
export function cleanupLegacyPwa() {
@@ -121,18 +136,25 @@ export function initializePwa() {
const updateDisplayMode = () => emit({ standalone: isStandalone() })
window.addEventListener('online', updateConnection)
window.addEventListener('offline', updateConnection)
const modernListener = (displayMode as unknown as {
addEventListener?: (type: 'change', listener: () => void) => void
}).addEventListener
const modernListener = (
displayMode as unknown as {
addEventListener?: (type: 'change', listener: () => void) => void
}
).addEventListener
if (modernListener) modernListener.call(displayMode, 'change', updateDisplayMode)
else (displayMode as unknown as { addListener?: (listener: () => void) => void })
.addListener?.call(displayMode, updateDisplayMode)
else
(displayMode as unknown as { addListener?: (listener: () => void) => void }).addListener?.call(
displayMode,
updateDisplayMode,
)
window.addEventListener('beforeinstallprompt', event => {
event.preventDefault()
emit({ installPrompt: event as BeforeInstallPromptEvent })
})
window.addEventListener('appinstalled', () => emit({ installPrompt: undefined, standalone: true }))
window.addEventListener('appinstalled', () =>
emit({ installPrompt: undefined, standalone: true }),
)
if (!import.meta.env.PROD || !('serviceWorker' in navigator)) return
@@ -151,13 +173,17 @@ export function initializePwa() {
void snapshot.registration?.update()
}
})
window.setInterval(() => {
if (navigator.onLine) void snapshot.registration?.update()
}, 60 * 60 * 1000)
window.setInterval(
() => {
if (navigator.onLine) void snapshot.registration?.update()
},
60 * 60 * 1000,
)
}
export function authRoute(name: 'login' | 'register' | 'setup'): string {
const inControlScope = location.pathname === '/control' || location.pathname.startsWith('/control/')
const inControlScope =
location.pathname === '/control' || location.pathname.startsWith('/control/')
return inControlScope ? `/control/${name}` : `/${name}`
}
@@ -165,7 +191,9 @@ export function usePwaUpdateBlocker(key: string, reason: string, active: boolean
useEffect(() => {
if (active) updateBlockers.set(key, reason)
else updateBlockers.delete(key)
return () => { updateBlockers.delete(key) }
return () => {
updateBlockers.delete(key)
}
}, [active, key, reason])
}
@@ -182,10 +210,14 @@ function applyUpdate() {
if (!worker) return
const reasons = [...new Set(updateBlockers.values())]
if (reasons.length > 0) {
window.alert(`暂时不能更新,请先处理以下内容:\n\n${reasons.map(reason => `• ${reason}`).join('\n')}`)
window.alert(
`暂时不能更新,请先处理以下内容:\n\n${reasons.map(reason => `• ${reason}`).join('\n')}`,
)
return
}
const confirmed = window.confirm('更新会刷新控制台。请先保存设置、邀请码、恢复码或刚轮换的 OBS 令牌,确定现在更新吗?')
const confirmed = window.confirm(
'更新会刷新控制台。请先保存设置、邀请码、恢复码或刚轮换的 OBS 令牌,确定现在更新吗?',
)
if (!confirmed) return
reloadForUpdate = true
worker.postMessage({ type: 'SKIP_WAITING' })
@@ -198,7 +230,12 @@ export function PwaControls() {
return (
<div className="pwa-controls" aria-live="polite">
{!state.online && <span className="pwa-state offline"><i aria-hidden="true" />离线</span>}
{!state.online && (
<span className="pwa-state offline">
<i aria-hidden="true" />
离线
</span>
)}
{state.waitingWorker && (
<button type="button" className="pwa-action update" onClick={applyUpdate}>
更新可用