nuts / shampoo / files / src / debug.ts
Branch shampoo · default branch · all branches
debug.ts
f1262fd9 2026-04-12 Pavel Safronov | 1 | /** |
| 2 | * Tiny on-screen debug log + remote terminal forwarder. | |
| 3 | * Call dbg('some message') from anywhere to show it in the overlay | |
| 4 | * and forward it to the Vite dev server terminal as [web] prefix. | |
| 5 | * Tap the overlay to dismiss. | |
| 6 | * | |
| 7 | * In production (import.meta.env.PROD) all calls are no-ops. | |
| 8 | */ | |
| 9 | ||
| 10 | const ENABLED = false // flip to true to re-enable on-screen log + remote forwarding | |
| 11 | ||
| 12 | let panel: HTMLDivElement | null = null | |
| 13 | const MAX_LINES = 20 | |
| 14 | const lines: string[] = [] | |
| 15 | ||
| 16 | function getPanel(): HTMLDivElement { | |
| 17 | if (panel) return panel | |
| 18 | panel = document.createElement('div') | |
| 19 | Object.assign(panel.style, { | |
| 20 | position: 'fixed', | |
| 21 | bottom: '0', | |
| 22 | left: '0', | |
| 23 | right: '0', | |
| 24 | maxHeight: '40vh', | |
| 25 | overflowY: 'auto', | |
| 26 | background: 'rgba(0,0,0,0.78)', | |
| 27 | color: '#0f0', | |
| 28 | fontFamily: 'monospace', | |
| 29 | fontSize: '11px', | |
| 30 | padding: '8px', | |
| 31 | zIndex: '9999', | |
| 32 | whiteSpace: 'pre-wrap', | |
| 33 | wordBreak: 'break-all', | |
| 34 | pointerEvents: 'auto', | |
| 35 | }) | |
| 36 | panel.addEventListener('click', () => { | |
| 37 | panel?.remove() | |
| 38 | panel = null | |
| 39 | lines.length = 0 | |
| 40 | }) | |
| 41 | document.body.appendChild(panel) | |
| 42 | return panel | |
| 43 | } | |
| 44 | ||
| 45 | function ts(): string { | |
| 46 | const d = new Date() | |
| 47 | return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}:${d.getSeconds().toString().padStart(2, '0')}.${d.getMilliseconds().toString().padStart(3, '0')}` | |
| 48 | } | |
| 49 | ||
| 50 | function sendToServer(level: 'log' | 'error', msg: string): void { | |
| 51 | // Best-effort: fire-and-forget, never throws | |
| 52 | fetch('/__log', { | |
| 53 | method: 'POST', | |
| 54 | headers: { 'content-type': 'application/json' }, | |
| 55 | body: JSON.stringify({ level, msg }), | |
| 56 | }).catch(() => { /* dev server may not be listening */ }) | |
| 57 | } | |
| 58 | ||
| 59 | function display(msg: string, isError = false): void { | |
| 60 | lines.push(msg) | |
| 61 | if (lines.length > MAX_LINES) lines.shift() | |
| 62 | const p = getPanel() | |
| 63 | p.style.color = isError ? '#f55' : '#0f0' | |
| 64 | p.textContent = lines.join('\n') | |
| 65 | p.scrollTop = p.scrollHeight | |
| 66 | } | |
| 67 | ||
| 68 | export function dbg(...args: unknown[]): void { | |
| 69 | if (import.meta.env.PROD || !ENABLED) return | |
| 70 | const msg = `[${ts()}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}` | |
| 71 | console.log(msg) | |
| 72 | display(msg) | |
| 73 | sendToServer('log', msg) | |
| 74 | } | |
| 75 | ||
| 76 | export function dbgError(...args: unknown[]): void { | |
| 77 | if (import.meta.env.PROD || !ENABLED) return | |
| 78 | const msg = `[${ts()}] ❌ ${args.map(a => a instanceof Error ? a.message : typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}` | |
| 79 | console.error(msg) | |
| 80 | display(msg, true) | |
| 81 | sendToServer('error', msg) | |
| 82 | } | |
98ba5504 2026-04-13 Pavel Safronov | 83 | |
| 84 | export function remoteLog(msg: string): void { | |
| 85 | if (import.meta.env.PROD) return | |
| 86 | sendToServer('log', msg) | |
| 87 | } |