// ── Channel color palette ────────────────────────────────────── const CH_COLORS = [ { bg: 'rgba(13,202,240,.12)', border: '#0dcaf0', text: '#0dcaf0' }, // 0 cyan { bg: 'rgba(25,135,84,.12)', border: '#198754', text: '#198754' }, // 1 green { bg: 'rgba(255,193,7,.12)', border: '#ffc107', text: '#d9a406' }, // 2 yellow { bg: 'rgba(220,53,69,.12)', border: '#dc3545', text: '#dc3545' }, // 3 red { bg: 'rgba(102,16,242,.12)', border: '#6610f2', text: '#6610f2' }, // 4 indigo { bg: 'rgba(13,110,253,.12)', border: '#0d6efd', text: '#0d6efd' }, // 5 blue { bg: 'rgba(32,201,151,.12)', border: '#20c997', text: '#20c997' }, // 6 teal { bg: 'rgba(253,126,20,.12)', border: '#fd7e14', text: '#fd7e14' }, // 7 orange ]; function chColor(chIdx) { return CH_COLORS[chIdx % CH_COLORS.length] || CH_COLORS[0]; } // ── State ────────────────────────────────────────────────────── let nodes = {}; let channels = {}; let myNodeId = null; let ws; let msgChannelFilter = 'all'; let messages = []; initPage({}); connectWebSocket(); // ── WebSocket ───────────────────────────────────────────────── function connectWebSocket() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${proto}//${location.host}/ws`); ws.onopen = () => { document.getElementById('statusDot').classList.add('connected'); document.getElementById('statusText').textContent = 'Verbunden'; }; ws.onclose = () => { document.getElementById('statusDot').classList.remove('connected'); document.getElementById('statusText').textContent = 'Getrennt - Reconnect...'; setTimeout(connectWebSocket, 3000); }; ws.onerror = () => { ws.close(); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case 'initial': msg.data.forEach(n => { nodes[n.node_id] = n; }); break; case 'node_update': nodes[msg.data.node_id] = msg.data; break; case 'channels': channels = msg.data; renderFilterBar(); break; case 'my_node_id': myNodeId = msg.data; break; case 'initial_messages': messages = []; document.getElementById('messagesList').innerHTML = ''; msg.data.reverse().forEach(m => addMessage(m)); break; case 'new_message': addMessage(msg.data); break; } }; } // ── Message rendering ───────────────────────────────────────── function addMessage(msg) { messages.push(msg); if (messages.length > 300) messages.shift(); const isSent = myNodeId && msg.from_node === myNodeId; const chIdx = msg.channel != null ? msg.channel : 0; const chName = channels[chIdx] != null ? channels[chIdx] : `Ch ${chIdx}`; const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''; const fromNode = nodes[msg.from_node]; const fromName = isSent ? 'Bot' : ((fromNode && fromNode.long_name) ? fromNode.long_name : (msg.from_node || '?')); const fromId = isSent ? '' : msg.from_node || ''; const col = chColor(chIdx); const item = document.createElement('div'); item.className = 'msg-full-item' + (isSent ? ' msg-full-sent' : ''); item.dataset.channel = String(chIdx); if (msgChannelFilter !== 'all' && String(chIdx) !== msgChannelFilter) { item.classList.add('d-none'); } if (isSent) { // Sent: right-aligned, green item.innerHTML = `