feat(debug): replace HTTP polling with WebSocket for real-time debug updates
This commit is contained in:
+56
-34
@@ -528,15 +528,15 @@
|
|||||||
<h2>🔍 Live Debug</h2>
|
<h2>🔍 Live Debug</h2>
|
||||||
<div style="display: flex; align-items: center; gap: 8px">
|
<div style="display: flex; align-items: center; gap: 8px">
|
||||||
<div class="live-dot"></div>
|
<div class="live-dot"></div>
|
||||||
<span style="font-size: 12px; color: var(--muted)">polling every</span>
|
<span style="font-size:12px;color:var(--muted);">every</span>
|
||||||
<select id="debug-interval" onchange="onDebugIntervalChange()"
|
<select id="debug-interval" onchange="setDebugInterval(parseInt(this.value))"
|
||||||
style="font-size: 12px; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px;">
|
style="font-size:12px;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:2px 6px;margin:0 4px;">
|
||||||
<option value="500">0.5s</option>
|
<option value="100">100ms</option>
|
||||||
|
<option value="250">250ms</option>
|
||||||
|
<option value="500">500ms</option>
|
||||||
<option value="1000" selected>1s</option>
|
<option value="1000" selected>1s</option>
|
||||||
<option value="2000">2s</option>
|
<option value="2000">2s</option>
|
||||||
<option value="5000">5s</option>
|
<option value="5000">5s</option>
|
||||||
<option value="10000">10s</option>
|
|
||||||
<option value="30000">30s</option>
|
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-ghost btn-xs" onclick="toggleDebug()">✕</button>
|
<button class="btn btn-ghost btn-xs" onclick="toggleDebug()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -636,7 +636,8 @@
|
|||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
let views = [];
|
let views = [];
|
||||||
let debugPollTimer = null;
|
let debugSocket = null;
|
||||||
|
let debugReconnectTimer = null;
|
||||||
let debugVisible = false;
|
let debugVisible = false;
|
||||||
let debugIntervalMs = 1000;
|
let debugIntervalMs = 1000;
|
||||||
const { origin } = location;
|
const { origin } = location;
|
||||||
@@ -970,9 +971,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
// DEBUG PANEL
|
// DEBUG PANEL (WebSocket)
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
|
function connectDebugSocket() {
|
||||||
|
if (debugSocket) return;
|
||||||
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${protocol}//${location.host}/ws?interval=${debugIntervalMs}`;
|
||||||
|
debugSocket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
debugSocket.onopen = () => {
|
||||||
|
if (debugReconnectTimer) { clearTimeout(debugReconnectTimer); debugReconnectTimer = null; }
|
||||||
|
};
|
||||||
|
|
||||||
|
debugSocket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
lastDebugData = JSON.parse(event.data);
|
||||||
|
renderDebug(lastDebugData);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
debugSocket.onclose = () => {
|
||||||
|
debugSocket = null;
|
||||||
|
if (debugVisible) {
|
||||||
|
debugReconnectTimer = setTimeout(connectDebugSocket, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
debugSocket.onerror = () => {
|
||||||
|
debugSocket?.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectDebugSocket() {
|
||||||
|
if (debugReconnectTimer) { clearTimeout(debugReconnectTimer); debugReconnectTimer = null; }
|
||||||
|
if (debugSocket) {
|
||||||
|
debugSocket.onclose = null; // prevent auto-reconnect
|
||||||
|
debugSocket.close();
|
||||||
|
debugSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDebugInterval(ms) {
|
||||||
|
debugIntervalMs = ms;
|
||||||
|
if (debugSocket && debugSocket.readyState === WebSocket.OPEN) {
|
||||||
|
debugSocket.send(JSON.stringify({ type: "debug-interval", interval: ms }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleDebug() {
|
function toggleDebug() {
|
||||||
debugVisible = !debugVisible;
|
debugVisible = !debugVisible;
|
||||||
const panel = document.getElementById("debug-panel");
|
const panel = document.getElementById("debug-panel");
|
||||||
@@ -980,39 +1026,15 @@
|
|||||||
if (debugVisible) {
|
if (debugVisible) {
|
||||||
panel.classList.add("visible");
|
panel.classList.add("visible");
|
||||||
btn.textContent = "🔍 Hide Debug";
|
btn.textContent = "🔍 Hide Debug";
|
||||||
pollDebug();
|
connectDebugSocket();
|
||||||
debugPollTimer = setInterval(pollDebug, debugIntervalMs);
|
|
||||||
} else {
|
} else {
|
||||||
panel.classList.remove("visible");
|
panel.classList.remove("visible");
|
||||||
btn.textContent = "🔍 Debug";
|
btn.textContent = "🔍 Debug";
|
||||||
if (debugPollTimer) {
|
disconnectDebugSocket();
|
||||||
clearInterval(debugPollTimer);
|
|
||||||
debugPollTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDebugIntervalChange() {
|
|
||||||
debugIntervalMs = parseInt(document.getElementById("debug-interval").value);
|
|
||||||
if (debugPollTimer) {
|
|
||||||
clearInterval(debugPollTimer);
|
|
||||||
debugPollTimer = setInterval(pollDebug, debugIntervalMs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastDebugData = null;
|
let lastDebugData = null;
|
||||||
|
|
||||||
async function pollDebug() {
|
|
||||||
if (!debugVisible) return;
|
|
||||||
try {
|
|
||||||
const res = await fetch(origin + "/api/debug");
|
|
||||||
lastDebugData = await res.json();
|
|
||||||
renderDebug(lastDebugData);
|
|
||||||
} catch {
|
|
||||||
/* silent */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let debugTickInterval = null;
|
let debugTickInterval = null;
|
||||||
|
|
||||||
function renderDebug(data) {
|
function renderDebug(data) {
|
||||||
|
|||||||
+114
-34
@@ -236,6 +236,45 @@ function devicesSnapshot(): DeviceEntry[] {
|
|||||||
return [...devices.values()].sort((a, b) => b.lastSeen - a.lastSeen);
|
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
|
// BROWSER + PAGE POOL
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -390,6 +429,17 @@ class PagePool {
|
|||||||
|
|
||||||
let pagePool: 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
|
// RENDER CACHE + QUEUE
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -907,38 +957,7 @@ async function handleAPI(req: Request, path: string): Promise<Response> {
|
|||||||
|
|
||||||
// GET /api/debug
|
// GET /api/debug
|
||||||
if (path === "/api/debug" && method === "GET") {
|
if (path === "/api/debug" && method === "GET") {
|
||||||
const viewsDebug = viewCacheAll().map((v) => {
|
return json(getDebugData());
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/views
|
// POST /api/views
|
||||||
@@ -1064,13 +1083,69 @@ async function serveStatic(path: string): Promise<Response | null> {
|
|||||||
// SERVER
|
// SERVER
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve<{ interval: number }>({
|
||||||
port: CONFIG.PORT,
|
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) {
|
async fetch(req) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const path = url.pathname;
|
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
|
// View serving — /v/:id
|
||||||
const viewMatch = path.match(/^\/v\/([a-f0-9-]+)$/);
|
const viewMatch = path.match(/^\/v\/([a-f0-9-]+)$/);
|
||||||
if (viewMatch) {
|
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);
|
return serveView(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user