SSE events bridge, PWA service worker, offline banner, lazy admin tabs
- New /api/events SSE endpoint (authed): pushes status every 5s and chat
on log-file mtime change (~1.5s poll). Heartbeat every 15s, hard-caps
each stream at 10min so the browser gets a clean auth refresh on
reconnect. Auto-aborts on client disconnect.
- Factored shared helpers out of the existing routes:
- lib/server-status.ts (probeStatus, reused by /api/status + SSE)
- lib/chat-log.ts (parseLogLine, readChatMessages, logMtime, reused by
/api/chat + SSE)
- EventsBridge client (mounted in Providers) opens one EventSource per
authed session and writes live data into the TanStack Query cache for
["status"] and ["chat"] — no refactor needed in consuming components,
they keep reading their usual query keys.
- Now that SSE pushes updates, polling intervals bumped: StatusCard and
ServerControls 10s -> 60s, ChatBridge 5s -> 30s. SSE handles realtime,
polling is safety fallback.
- OfflineBanner: sticky amber bar when navigator.onLine flips false.
- PWA: minimal public/sw.js with shell + asset cache (network-first for
HTML, stale-while-revalidate for static assets, never touches /api/*
or text/event-stream). ServiceWorkerRegister client registers it in
production only.
- AdminTabs now uses next/dynamic with skeleton fallbacks for Players /
Chat / Mods / Backups / Logs, keeping initial /admin bundle smaller.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19d66c2de6
commit
359a12ef9d
14 changed files with 460 additions and 132 deletions
86
public/sw.js
Normal file
86
public/sw.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// HurkiCorgi MC — minimal service worker
|
||||
// Strategy: network-first for HTML/data, stale-while-revalidate for assets,
|
||||
// offline fallback to the cached shell.
|
||||
|
||||
const VERSION = "v1";
|
||||
const SHELL_CACHE = `shell-${VERSION}`;
|
||||
const ASSET_CACHE = `assets-${VERSION}`;
|
||||
const SHELL_URLS = ["/", "/icon.svg", "/manifest.json"];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(SHELL_CACHE).then((c) => c.addAll(SHELL_URLS)).catch(() => {})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(
|
||||
keys
|
||||
.filter((k) => k !== SHELL_CACHE && k !== ASSET_CACHE)
|
||||
.map((k) => caches.delete(k))
|
||||
);
|
||||
await self.clients.claim();
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
function isAsset(url) {
|
||||
return (
|
||||
url.pathname.startsWith("/_next/static/") ||
|
||||
url.pathname.endsWith(".svg") ||
|
||||
url.pathname.endsWith(".woff2") ||
|
||||
url.pathname.endsWith(".png") ||
|
||||
url.pathname.endsWith(".ico")
|
||||
);
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const req = event.request;
|
||||
if (req.method !== "GET") return;
|
||||
|
||||
const url = new URL(req.url);
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
// Never intercept API, auth, SSE, or anything under /api
|
||||
if (url.pathname.startsWith("/api/")) return;
|
||||
if (req.headers.get("accept")?.includes("text/event-stream")) return;
|
||||
|
||||
// Assets: stale-while-revalidate
|
||||
if (isAsset(url)) {
|
||||
event.respondWith(
|
||||
caches.open(ASSET_CACHE).then(async (cache) => {
|
||||
const cached = await cache.match(req);
|
||||
const fetchPromise = fetch(req)
|
||||
.then((res) => {
|
||||
if (res.ok) cache.put(req, res.clone());
|
||||
return res;
|
||||
})
|
||||
.catch(() => cached || Response.error());
|
||||
return cached || fetchPromise;
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML / navigations: network-first with cache fallback
|
||||
if (req.mode === "navigate" || req.headers.get("accept")?.includes("text/html")) {
|
||||
event.respondWith(
|
||||
fetch(req)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
const clone = res.clone();
|
||||
caches.open(SHELL_CACHE).then((c) => c.put(req, clone));
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch(async () => {
|
||||
const cached = await caches.match(req);
|
||||
return cached || caches.match("/");
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue