Files
breadcrumb-the-shire/web/js/components/app-error-debug.js
fs ad9101df8e feat: add developer error page with request ID tracking and structured logging
Custom-built error handler for the web channel that shows a tabbed debug
dashboard (exception, request context, environment, SQL queries) in dev mode
and a clean branded error page with copyable request ID in prod mode.
All errors are logged as JSON lines to storage/logs/errors/ for lookup by
request ID. Uses the project's design tokens for visual consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:49:22 +01:00

59 lines
2.5 KiB
JavaScript

/* Error Debug Page — interactive behavior (inlined by ErrorRenderer) */
document.addEventListener('DOMContentLoaded', () => {
// ── Tab switching ──────────────────────────────────
const tabs = document.querySelectorAll('.app-error-debug-tab');
const panels = document.querySelectorAll('.app-error-debug-panel');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = tab.dataset.panel;
tabs.forEach(t => t.classList.remove('active'));
panels.forEach(p => p.classList.remove('active'));
tab.classList.add('active');
const panel = document.getElementById(target);
if (panel) panel.classList.add('active');
});
});
// ── Frame expand/collapse ──────────────────────────
document.querySelectorAll('.app-error-debug-frame__header').forEach(header => {
header.addEventListener('click', () => {
header.closest('.app-error-debug-frame')?.classList.toggle('open');
});
});
// Open the first frame by default.
const firstFrame = document.querySelector('.app-error-debug-frame');
if (firstFrame) firstFrame.classList.add('open');
// ── Query expand/collapse ──────────────────────────
document.querySelectorAll('.app-error-debug-query__header').forEach(header => {
header.addEventListener('click', () => {
header.closest('.app-error-debug-query')?.classList.toggle('open');
});
});
// ── Copy to clipboard ─────────────────────────────
document.querySelectorAll('.app-error-debug-copy-button').forEach(button => {
button.addEventListener('click', async (e) => {
e.stopPropagation();
const text = button.dataset.copy;
if (!text) return;
try {
await navigator.clipboard.writeText(text);
button.classList.add('copied');
const original = button.innerHTML;
button.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>';
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = original;
}, 1500);
} catch {
// Clipboard API may not be available in all contexts.
}
});
});
});