// ── 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({ onAuth: (user) => { if (!user) { document.getElementById('loginNotice').classList.remove('d-none'); } else { document.getElementById('messagesView').classList.remove('d-none'); 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 = `
${time} ${escapeHtml(chName)} ${escapeHtml(fromName)}
${escapeHtml(msg.payload || '')}
`; } else { // Received: left-aligned, channel colored item.innerHTML = `
${escapeHtml(fromName)} ${fromId ? `${escapeHtml(fromId)}` : ''} ${escapeHtml(chName)} ${time}
${escapeHtml(msg.payload || '')}
`; } const list = document.getElementById('messagesList'); list.prepend(item); updateCount(); } function updateCount() { const visible = document.getElementById('messagesList').querySelectorAll('.msg-full-item:not(.d-none)').length; document.getElementById('msgCount').textContent = visible; } // ── Channel filter bar ──────────────────────────────────────── function renderFilterBar() { const bar = document.getElementById('msgFilterBar'); const sorted = Object.entries(channels).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); const mkBtn = (ch, label, col) => { const active = ch === msgChannelFilter; const colStyle = col ? `background:${active ? col.border : 'transparent'};border-color:${col.border};color:${active ? '#fff' : col.text}` : ''; const cls = col ? '' : (active ? 'btn-secondary' : 'btn-outline-secondary'); return ``; }; bar.innerHTML = mkBtn('all', 'Alle', null) + sorted.map(([idx, name]) => mkBtn(String(idx), name, chColor(parseInt(idx)))).join(''); bar.onclick = (e) => { const btn = e.target.closest('button[data-ch]'); if (!btn) return; msgChannelFilter = btn.dataset.ch; renderFilterBar(); applyFilter(); }; } function applyFilter() { document.getElementById('messagesList').querySelectorAll('.msg-full-item').forEach(item => { const vis = msgChannelFilter === 'all' || item.dataset.channel === msgChannelFilter; item.classList.toggle('d-none', !vis); }); updateCount(); } // Clear button document.getElementById('msgClearBtn').addEventListener('click', () => { messages = []; document.getElementById('messagesList').innerHTML = ''; updateCount(); });