@@ -125,6 +125,15 @@ export const PLAYGROUND_HTML = `<!doctype html>
125125 font-size: 13px;
126126 }
127127
128+ .toggle {
129+ display: inline-flex;
130+ align-items: center;
131+ gap: 6px;
132+ font-size: 13px;
133+ color: var(--subtle);
134+ user-select: none;
135+ }
136+
128137 .render-output {
129138 min-height: 130px;
130139 border: 1px dashed rgba(15, 118, 110, 0.35);
@@ -193,6 +202,19 @@ export const PLAYGROUND_HTML = `<!doctype html>
193202 <h2>Streaming Feed</h2>
194203 <pre id="stream-output">[]</pre>
195204 </section>
205+
206+ <section class="card span-12">
207+ <h2>Debug Stats</h2>
208+ <div class="actions">
209+ <button id="refresh-debug" class="secondary">Refresh Debug Stats</button>
210+ <label class="toggle">
211+ <input id="auto-refresh-debug" type="checkbox" checked />
212+ Auto refresh (2s)
213+ </label>
214+ </div>
215+ <div class="status" id="debug-status">Waiting for debug stats...</div>
216+ <pre id="debug-output">{}</pre>
217+ </section>
196218 </div>
197219 </div>
198220
@@ -205,6 +227,10 @@ export const PLAYGROUND_HTML = `<!doctype html>
205227 const diagnosticsEl = byId("diagnostics");
206228 const streamOutputEl = byId("stream-output");
207229 const copyPlanLinkEl = byId("copy-plan-link");
230+ const refreshDebugEl = byId("refresh-debug");
231+ const autoRefreshDebugEl = byId("auto-refresh-debug");
232+ const debugStatusEl = byId("debug-status");
233+ const debugOutputEl = byId("debug-output");
208234
209235 const controls = [
210236 byId("run-prompt"),
@@ -225,6 +251,10 @@ export const PLAYGROUND_HTML = `<!doctype html>
225251 statusEl.textContent = text;
226252 };
227253
254+ const setDebugStatus = (text) => {
255+ debugStatusEl.textContent = text;
256+ };
257+
228258 const safeJson = (value) => {
229259 try {
230260 return JSON.stringify(value ?? {}, null, 2);
@@ -244,6 +274,8 @@ export const PLAYGROUND_HTML = `<!doctype html>
244274 let interactiveBlobModuleUrl = null;
245275 let babelStandalonePromise;
246276 let diagnosticsSnapshot = {};
277+ let debugRefreshTimer = null;
278+ let debugRefreshInFlight = false;
247279
248280 const resetInteractiveMount = () => {
249281 interactiveMountVersion += 1;
@@ -274,6 +306,146 @@ export const PLAYGROUND_HTML = `<!doctype html>
274306 });
275307 };
276308
309+ const toDebugCount = (value) =>
310+ typeof value === "number" && Number.isFinite(value) && value >= 0
311+ ? value
312+ : 0;
313+
314+ const compactDebugSnapshot = (payload) => {
315+ const inbound = isRecord(payload && payload.inbound) ? payload.inbound : {};
316+ const outbound = isRecord(payload && payload.outbound) ? payload.outbound : {};
317+ const inboundRoutes = Array.isArray(inbound.routes) ? inbound.routes : [];
318+ const outboundTargets = Array.isArray(outbound.targets) ? outbound.targets : [];
319+ const recent = Array.isArray(payload && payload.recent) ? payload.recent : [];
320+
321+ return {
322+ enabled: payload && payload.enabled === true,
323+ startedAt:
324+ payload && typeof payload.startedAt === "string"
325+ ? payload.startedAt
326+ : undefined,
327+ uptimeMs: toDebugCount(payload && payload.uptimeMs),
328+ inbound: {
329+ totalRequests: toDebugCount(inbound.totalRequests),
330+ routes: inboundRoutes.slice(0, 10),
331+ },
332+ outbound: {
333+ totalRequests: toDebugCount(outbound.totalRequests),
334+ targets: outboundTargets.slice(0, 10),
335+ },
336+ recent: recent.slice(-25),
337+ ...(payload && payload.error ? { error: String(payload.error) } : {}),
338+ };
339+ };
340+
341+ const setDebugOutput = (payload) => {
342+ debugOutputEl.textContent = safeJson(compactDebugSnapshot(payload));
343+ };
344+
345+ const formatDebugSummary = (snapshot) => {
346+ if (!snapshot.enabled) {
347+ return snapshot.error
348+ ? "Debug stats unavailable: " + snapshot.error
349+ : "Debug stats unavailable.";
350+ }
351+
352+ const inboundTotal = toDebugCount(
353+ snapshot.inbound && snapshot.inbound.totalRequests,
354+ );
355+ const outboundTotal = toDebugCount(
356+ snapshot.outbound && snapshot.outbound.totalRequests,
357+ );
358+ return (
359+ "Debug mode enabled. inbound=" +
360+ inboundTotal +
361+ ", outbound=" +
362+ outboundTotal +
363+ ". Updated " +
364+ new Date().toLocaleTimeString() +
365+ "."
366+ );
367+ };
368+
369+ async function requestDebugStats() {
370+ const response = await fetch("/api/debug/stats", {
371+ method: "GET",
372+ cache: "no-store",
373+ });
374+
375+ let payload = {};
376+ try {
377+ payload = await response.json();
378+ } catch {
379+ payload = {};
380+ }
381+
382+ if (!response.ok) {
383+ const message =
384+ isRecord(payload) && typeof payload.error === "string"
385+ ? payload.error
386+ : "request failed with status " + response.status;
387+ return {
388+ enabled: false,
389+ error: message,
390+ statusCode: response.status,
391+ };
392+ }
393+
394+ return isRecord(payload) ? payload : {};
395+ }
396+
397+ async function refreshDebugStats(options = {}) {
398+ const silent = options && options.silent === true;
399+ if (debugRefreshInFlight) {
400+ return;
401+ }
402+
403+ debugRefreshInFlight = true;
404+ if (!silent) {
405+ setDebugStatus("Refreshing debug stats...");
406+ }
407+
408+ try {
409+ const payload = await requestDebugStats();
410+ setDebugOutput(payload);
411+ setDebugStatus(formatDebugSummary(payload));
412+ if (
413+ payload &&
414+ payload.enabled !== true &&
415+ typeof payload.error === "string" &&
416+ payload.error.toLowerCase().includes("debug mode is disabled")
417+ ) {
418+ autoRefreshDebugEl.checked = false;
419+ restartDebugAutoRefresh();
420+ }
421+ } catch (error) {
422+ const message = error instanceof Error ? error.message : String(error);
423+ const snapshot = {
424+ enabled: false,
425+ error: message,
426+ };
427+ setDebugOutput(snapshot);
428+ setDebugStatus("Debug stats unavailable: " + message);
429+ } finally {
430+ debugRefreshInFlight = false;
431+ }
432+ }
433+
434+ const restartDebugAutoRefresh = () => {
435+ if (debugRefreshTimer) {
436+ clearInterval(debugRefreshTimer);
437+ debugRefreshTimer = null;
438+ }
439+
440+ if (!(autoRefreshDebugEl && autoRefreshDebugEl.checked)) {
441+ return;
442+ }
443+
444+ debugRefreshTimer = setInterval(() => {
445+ void refreshDebugStats({ silent: true });
446+ }, 2000);
447+ };
448+
277449 const ensureBabelStandalone = async () => {
278450 if (window.Babel && typeof window.Babel.transform === "function") {
279451 return;
@@ -732,6 +904,10 @@ export const PLAYGROUND_HTML = `<!doctype html>
732904 if (!response.ok) {
733905 throw new Error(payload && payload.error ? String(payload.error) : "request failed");
734906 }
907+
908+ if (path !== "/api/debug/stats") {
909+ void refreshDebugStats({ silent: true });
910+ }
735911 return payload;
736912 }
737913
@@ -787,6 +963,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
787963 throw error;
788964 } finally {
789965 setBusy(false);
966+ void refreshDebugStats({ silent: true });
790967 }
791968 }
792969
@@ -810,6 +987,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
810987 diagnosticsEl.textContent = String(error);
811988 } finally {
812989 setBusy(false);
990+ void refreshDebugStats({ silent: true });
813991 }
814992 }
815993
@@ -895,6 +1073,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
8951073 diagnosticsEl.textContent = String(error);
8961074 } finally {
8971075 setBusy(false);
1076+ void refreshDebugStats({ silent: true });
8981077 }
8991078 }
9001079
@@ -933,6 +1112,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
9331112 diagnosticsEl.textContent = String(error);
9341113 } finally {
9351114 setBusy(false);
1115+ void refreshDebugStats({ silent: true });
9361116 }
9371117 }
9381118
@@ -941,6 +1121,7 @@ export const PLAYGROUND_HTML = `<!doctype html>
9411121 htmlOutputEl.innerHTML = "";
9421122 writeDiagnostics({});
9431123 streamOutputEl.textContent = "[]";
1124+ void refreshDebugStats({ silent: true });
9441125 setStatus("Cleared.");
9451126 }
9461127
@@ -1006,15 +1187,29 @@ export const PLAYGROUND_HTML = `<!doctype html>
10061187 byId("stream-prompt").addEventListener("click", streamPrompt);
10071188 byId("run-plan").addEventListener("click", runPlan);
10081189 byId("probe-plan").addEventListener("click", probePlan);
1190+ refreshDebugEl.addEventListener("click", () => {
1191+ void refreshDebugStats();
1192+ });
1193+ autoRefreshDebugEl.addEventListener("change", () => {
1194+ restartDebugAutoRefresh();
1195+ });
10091196 copyPlanLinkEl.addEventListener("click", () => {
10101197 void copyPlanLink();
10111198 });
10121199 byId("clear").addEventListener("click", clearAll);
10131200
1201+ restartDebugAutoRefresh();
1202+ void refreshDebugStats({ silent: true });
10141203 void renderFromHashPayload();
10151204 window.addEventListener("hashchange", () => {
10161205 void renderFromHashPayload();
10171206 });
1207+ window.addEventListener("beforeunload", () => {
1208+ if (debugRefreshTimer) {
1209+ clearInterval(debugRefreshTimer);
1210+ debugRefreshTimer = null;
1211+ }
1212+ });
10181213 </script>
10191214 </body>
10201215</html>` ;
0 commit comments