feat(debug): replace HTTP polling with WebSocket for real-time debug updates
This commit is contained in:
+114
-34
@@ -236,6 +236,45 @@ function devicesSnapshot(): DeviceEntry[] {
|
||||
return [...devices.values()].sort((a, b) => b.lastSeen - a.lastSeen);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DEBUG DATA HELPER (used by both REST and WebSocket)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function getDebugData() {
|
||||
const viewsDebug = viewCacheAll().map(v => {
|
||||
const idx = getCurrentURLIndex(v);
|
||||
return {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
urls: v.urls,
|
||||
method: v.method,
|
||||
metaRefreshEnabled: v.metaRefreshEnabled,
|
||||
cacheTtlSec: v.cacheTtlSec,
|
||||
viewportWidth: v.viewportWidth,
|
||||
viewportHeight: v.viewportHeight,
|
||||
currentUrlIndex: idx,
|
||||
currentUrl: v.urls[idx]?.url ?? "",
|
||||
remainingSec: getRemainingSec(v),
|
||||
totalCycleSec: v.urls.reduce((s, u) => s + u.durationSec, 0),
|
||||
cacheEntries: cacheStats().filter(c => c.viewId === v.id),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
server: {
|
||||
uptime: Math.round(process.uptime()),
|
||||
activeRenders: pagePool?.activeRenders ?? 0,
|
||||
queueLength: renderQueue.length,
|
||||
cacheEntries: renderCache.size,
|
||||
browsers: CONFIG.BROWSER_COUNT,
|
||||
pagesPerBrowser: CONFIG.PAGES_PER_BROWSER,
|
||||
maxConcurrentRenders: CONFIG.MAX_CONCURRENT_RENDERS,
|
||||
},
|
||||
views: viewsDebug,
|
||||
devices: devicesSnapshot(),
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// BROWSER + PAGE POOL
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -390,6 +429,17 @@ class PagePool {
|
||||
|
||||
let pagePool: PagePool;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// WEBSOCKET DEBUG CLIENTS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const debugClients = new Map<
|
||||
any,
|
||||
{ interval: number; timer: ReturnType<typeof setInterval> }
|
||||
>();
|
||||
const DEBUG_DEFAULT_INTERVAL_MS = 1000;
|
||||
const DEBUG_MIN_INTERVAL_MS = 100;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// RENDER CACHE + QUEUE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -907,38 +957,7 @@ async function handleAPI(req: Request, path: string): Promise<Response> {
|
||||
|
||||
// GET /api/debug
|
||||
if (path === "/api/debug" && method === "GET") {
|
||||
const viewsDebug = viewCacheAll().map((v) => {
|
||||
const idx = getCurrentURLIndex(v);
|
||||
return {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
urls: v.urls,
|
||||
method: v.method,
|
||||
metaRefreshEnabled: v.metaRefreshEnabled,
|
||||
cacheTtlSec: v.cacheTtlSec,
|
||||
viewportWidth: v.viewportWidth,
|
||||
viewportHeight: v.viewportHeight,
|
||||
currentUrlIndex: idx,
|
||||
currentUrl: v.urls[idx]?.url ?? "",
|
||||
remainingSec: getRemainingSec(v),
|
||||
totalCycleSec: v.urls.reduce((s, u) => s + u.durationSec, 0),
|
||||
cacheEntries: cacheStats().filter((c) => c.viewId === v.id),
|
||||
};
|
||||
});
|
||||
|
||||
return json({
|
||||
server: {
|
||||
uptime: Math.round(process.uptime()),
|
||||
activeRenders: pagePool?.activeRenders ?? 0,
|
||||
queueLength: renderQueue.length,
|
||||
cacheEntries: renderCache.size,
|
||||
browsers: CONFIG.BROWSER_COUNT,
|
||||
pagesPerBrowser: CONFIG.PAGES_PER_BROWSER,
|
||||
maxConcurrentRenders: CONFIG.MAX_CONCURRENT_RENDERS,
|
||||
},
|
||||
views: viewsDebug,
|
||||
devices: devicesSnapshot(),
|
||||
});
|
||||
return json(getDebugData());
|
||||
}
|
||||
|
||||
// POST /api/views
|
||||
@@ -1064,13 +1083,69 @@ async function serveStatic(path: string): Promise<Response | null> {
|
||||
// SERVER
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const server = Bun.serve({
|
||||
const server = Bun.serve<{ interval: number }>({
|
||||
port: CONFIG.PORT,
|
||||
websocket: {
|
||||
open(ws) {
|
||||
const interval = ws.data?.interval ?? DEBUG_DEFAULT_INTERVAL_MS;
|
||||
const timer = setInterval(() => {
|
||||
try {
|
||||
ws.send(JSON.stringify(getDebugData()));
|
||||
} catch {
|
||||
clearInterval(timer);
|
||||
debugClients.delete(ws);
|
||||
}
|
||||
}, interval);
|
||||
debugClients.set(ws, { interval, timer });
|
||||
// Send initial payload immediately
|
||||
try { ws.send(JSON.stringify(getDebugData())); } catch {}
|
||||
},
|
||||
message(ws, msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg as string);
|
||||
if (data.type === "debug-interval" && typeof data.interval === "number") {
|
||||
const client = debugClients.get(ws);
|
||||
if (!client) return;
|
||||
const interval = Math.max(DEBUG_MIN_INTERVAL_MS, data.interval);
|
||||
clearInterval(client.timer);
|
||||
client.interval = interval;
|
||||
client.timer = setInterval(() => {
|
||||
try { ws.send(JSON.stringify(getDebugData())); } catch {
|
||||
clearInterval(client.timer);
|
||||
debugClients.delete(ws);
|
||||
}
|
||||
}, interval);
|
||||
// Push immediately after rate change
|
||||
try { ws.send(JSON.stringify(getDebugData())); } catch {}
|
||||
}
|
||||
} catch { /* ignore malformed messages */ }
|
||||
},
|
||||
close(ws) {
|
||||
const client = debugClients.get(ws);
|
||||
if (client) {
|
||||
clearInterval(client.timer);
|
||||
debugClients.delete(ws);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname;
|
||||
|
||||
// ── WebSocket upgrade for debug panel ──
|
||||
if (path === "/ws") {
|
||||
const qInterval = parseInt(url.searchParams.get("interval") ?? "");
|
||||
const wsInterval = Math.max(
|
||||
DEBUG_MIN_INTERVAL_MS,
|
||||
Number.isFinite(qInterval) ? qInterval : DEBUG_DEFAULT_INTERVAL_MS,
|
||||
);
|
||||
if (server.upgrade(req, { data: { interval: wsInterval } })) {
|
||||
return; // upgraded — Bun will call websocket.open
|
||||
}
|
||||
return new Response("WebSocket upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
// View serving — /v/:id
|
||||
const viewMatch = path.match(/^\/v\/([a-f0-9-]+)$/);
|
||||
if (viewMatch) {
|
||||
@@ -1089,7 +1164,12 @@ const server = Bun.serve({
|
||||
});
|
||||
}
|
||||
|
||||
deviceTrack(ip, view.id, view.name, req.headers.get("User-Agent") ?? "unknown");
|
||||
deviceTrack(
|
||||
ip,
|
||||
view.id,
|
||||
view.name,
|
||||
req.headers.get("User-Agent") ?? "unknown",
|
||||
);
|
||||
return serveView(view);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user