/* net.js — WebSocket sync client + dev-mode error pipeline. */ export class ExhibitNet { constructor() { this.handlers = new Map(); this.ws = null; this.retry = 1000; } on(type, fn) { this.handlers.set(type, fn); return this; } connect() { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; this.ws = new WebSocket(`${proto}://${location.host}/ws`); this.ws.onopen = () => { this.retry = 1000; }; this.ws.onmessage = (ev) => { let m; try { m = JSON.parse(ev.data); } catch { return; } const h = this.handlers.get(m.type); if (h) h(m); }; this.ws.onclose = () => { setTimeout(() => this.connect(), this.retry); this.retry = Math.min(this.retry * 2, 15000); }; } } /* Dev-mode error checking: capture JS errors + unhandled rejections, show an * on-screen overlay, and report to the server for the admin error log. */ export function installErrorReporter(devMode) { const report = (payload) => { fetch('/api/client-error', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ua: navigator.userAgent, page: location.pathname, ...payload }), }).catch(() => {}); if (devMode) showOverlay(payload); }; window.addEventListener('error', (e) => report({ kind: 'error', msg: String(e.message), src: e.filename, line: e.lineno })); window.addEventListener('unhandledrejection', (e) => report({ kind: 'rejection', msg: String(e.reason && e.reason.message || e.reason) })); return report; } let overlayEl = null; function showOverlay(p) { if (!overlayEl) { overlayEl = document.createElement('div'); overlayEl.style.cssText = 'position:fixed;bottom:0;left:0;right:0;max-height:35vh;overflow:auto;' + 'background:rgba(120,0,0,.88);color:#fff;font:12px monospace;padding:8px;z-index:99999;white-space:pre-wrap'; document.body.appendChild(overlayEl); } overlayEl.textContent += `[${p.kind}] ${p.msg}${p.src ? ` (${p.src}:${p.line})` : ''}\n`; }