// panes/websocket.js — generic WebSocket console with URL switcher, presets,
// JSON pretty-print, auto-reconnect, message history.
import { registerPane } from '../app.js';
import { escapeHtml } from '../lib/api.js';
import { ok, err, copyToClipboard, historyStore } from '../lib/ui.js';
const ENDPOINTS = [
{ label: 'echo', url: '/ws/echo' },
];
const sendHistory = historyStore('cx_ws_send_history', 30);
const TPL = `
WebSocket
Connect to any WebSocket endpoint and inspect frames.
idle
Send presets
History
0
`;
let ws = null;
let stats = { in: 0, out: 0, bytes: 0 };
let reconnectTimer = null;
function setState(host, state, klass) {
host.querySelector('#ws-state').textContent = state;
const p = host.querySelector('#ws-pill');
p.classList.remove('ok', 'err', 'warn', 'info', 'muted');
p.classList.add(klass || 'muted');
}
function updateStats(host) {
host.querySelector('#ws-stats').textContent = `${stats.in} in · ${stats.out} out · ${stats.bytes} b`;
host.querySelector('#ws-frame-count').textContent = `${stats.in + stats.out}`;
}
function maybePretty(host, raw) {
if (!host.querySelector('#ws-pretty').checked) return raw;
try { return JSON.stringify(JSON.parse(raw), null, 2); } catch { return raw; }
}
function append(host, dir, raw) {
const pre = host.querySelector('#ws-log');
const showTs = host.querySelector('#ws-ts').checked;
const text = maybePretty(host, raw);
const ts = showTs ? `[${new Date().toISOString().slice(11, 23)}] ` : '';
const arrow = dir === 'in' ? '←' : dir === 'out' ? '→' : '·';
pre.textContent += `${ts}${arrow} ${text}\n`;
pre.scrollTop = pre.scrollHeight;
}
function defaultUrl() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${proto}//${location.host}/ws/echo`;
}
function connect(host) {
if (ws && ws.readyState <= 1) return;
let url = host.querySelector('#ws-url').value.trim();
if (url.startsWith('/')) {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
url = `${proto}//${location.host}${url}`;
}
if (!url) url = defaultUrl();
setState(host, 'connecting…', 'warn');
append(host, '·', `connecting ${url}`);
try { ws = new WebSocket(url); } catch (e) { setState(host, 'error', 'err'); append(host, '·', e.message); return; }
ws.onopen = () => { setState(host, 'connected', 'ok'); append(host, '·', 'connected'); };
ws.onclose = (e) => {
setState(host, `closed (${e.code})`, 'err');
append(host, '·', `closed code=${e.code} reason=${e.reason}`);
if (host.querySelector('#ws-autoreconnect').checked) {
reconnectTimer = setTimeout(() => connect(host), 2000);
}
};
ws.onerror = () => append(host, '·', 'error');
ws.onmessage = (e) => { stats.in++; stats.bytes += (e.data?.length || 0); updateStats(host); append(host, 'in', e.data); };
}
function disconnect() {
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
try { ws?.close(); } catch {}
}
function renderHistory(host) {
const items = sendHistory.list();
host.querySelector('#ws-hist-count').textContent = String(items.length);
host.querySelector('#ws-history').innerHTML = items.length
? items.map((h, i) => ``).join('')
: 'empty
';
host.querySelectorAll('#ws-history [data-hi]').forEach(b => {
b.addEventListener('click', () => {
host.querySelector('#ws-input').value = sendHistory.list()[+b.dataset.hi]?.text || '';
});
});
}
function send(host, text) {
if (!ws || ws.readyState !== 1) return err('not connected');
ws.send(text);
stats.out++; stats.bytes += text.length; updateStats(host);
append(host, 'out', text);
sendHistory.push({ text });
renderHistory(host);
}
registerPane('websocket', {
label: 'WebSocket',
init(host) {
host.innerHTML = TPL;
const sel = host.querySelector('#ws-preset');
sel.innerHTML = ENDPOINTS.map((e, i) => ``).join('');
host.querySelector('#ws-url').value = defaultUrl();
sel.addEventListener('change', () => {
const e = ENDPOINTS[+sel.value]; if (!e) return;
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
host.querySelector('#ws-url').value = `${proto}//${location.host}${e.url}`;
});
host.querySelector('#ws-connect').addEventListener('click', () => connect(host));
host.querySelector('#ws-disconnect').addEventListener('click', disconnect);
host.querySelector('#ws-clear').addEventListener('click', () => {
host.querySelector('#ws-log').textContent = '';
stats = { in: 0, out: 0, bytes: 0 }; updateStats(host);
});
host.querySelector('#ws-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#ws-log').textContent));
host.querySelector('#ws-form').addEventListener('submit', (e) => {
e.preventDefault();
const v = host.querySelector('#ws-input').value;
if (!v) return;
send(host, v);
host.querySelector('#ws-input').value = '';
});
host.querySelector('#ws-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); host.querySelector('#ws-form').requestSubmit(); }
});
host.querySelectorAll('[data-q]').forEach(b => b.addEventListener('click', () => send(host, b.dataset.q)));
renderHistory(host);
updateStats(host);
},
});