MeshDD-Bot/static/js/dashboard.js
ppfeiffer d5ea4eee4a feat: Redesign dashboard with Bootstrap 5.3 dark theme
Replace custom CSS layout with Bootstrap 5.3, add Bootstrap Icons,
responsive card grid for stats, improved nodes table with hardware
column, and styled message list. Online nodes highlighted in green.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:02:34 +01:00

127 lines
4.7 KiB
JavaScript

const nodesTable = document.getElementById('nodesTable');
const messagesList = document.getElementById('messagesList');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const nodeCountBadge = document.getElementById('nodeCountBadge');
let nodes = {};
let ws;
function connectWebSocket() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}/ws`);
ws.onopen = () => {
statusDot.classList.add('connected');
statusText.textContent = 'Verbunden';
};
ws.onclose = () => {
statusDot.classList.remove('connected');
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(node => { nodes[node.node_id] = node; });
renderNodes();
break;
case 'node_update':
nodes[msg.data.node_id] = msg.data;
renderNodes();
break;
case 'new_message':
addMessage(msg.data);
break;
case 'stats_update':
updateStats(msg.data);
break;
}
};
}
function renderNodes() {
const sorted = Object.values(nodes).sort((a, b) => (b.last_seen || 0) - (a.last_seen || 0));
nodeCountBadge.textContent = sorted.length;
nodesTable.innerHTML = sorted.map(node => {
const name = node.long_name || node.short_name || node.node_id;
const shortId = node.node_id ? node.node_id.slice(-4) : '?';
const hw = node.hw_model || '-';
const snr = node.snr != null ? `${node.snr.toFixed(1)} dB` : '-';
const battery = renderBattery(node.battery);
const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-';
const onlineClass = isOnline(node.last_seen) ? 'text-success' : '';
return `<tr>
<td class="${onlineClass}">${escapeHtml(name)}</td>
<td><code>${escapeHtml(shortId)}</code></td>
<td class="text-body-secondary">${escapeHtml(hw)}</td>
<td class="text-center">${snr}</td>
<td class="text-center">${battery}</td>
<td class="text-end text-body-secondary">${lastSeen}</td>
</tr>`;
}).join('');
}
function renderBattery(level) {
if (level == null) return '<span class="text-body-secondary">-</span>';
let colorClass = 'bg-success';
if (level < 20) colorClass = 'bg-danger';
else if (level < 50) colorClass = 'bg-warning';
return `<span class="battery-bar">
<span class="battery-indicator"><span class="battery-fill ${colorClass}" style="width:${Math.min(level, 100)}%"></span></span>
<small>${level}%</small>
</span>`;
}
function addMessage(msg) {
const item = document.createElement('div');
item.className = 'list-group-item list-group-item-action py-2 px-3';
const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString('de-DE') : '';
const from = msg.from_node ? msg.from_node.slice(-4) : '?';
item.innerHTML = `
<div class="d-flex justify-content-between">
<small class="text-body-secondary"><i class="bi bi-person-fill me-1"></i>${escapeHtml(from)}</small>
<small class="text-body-secondary">${time}</small>
</div>
<div class="mt-1">${escapeHtml(msg.payload || '')}</div>`;
messagesList.prepend(item);
while (messagesList.children.length > 100) {
messagesList.removeChild(messagesList.lastChild);
}
}
function updateStats(stats) {
document.getElementById('statNodes').textContent = stats.total_nodes || 0;
document.getElementById('statPositions').textContent = stats.nodes_with_position || 0;
document.getElementById('statMessages').textContent = stats.total_messages || 0;
document.getElementById('statTextMessages').textContent = stats.text_messages || 0;
}
function isOnline(lastSeen) {
if (!lastSeen) return false;
return (Date.now() / 1000 - lastSeen) < 900; // < 15 min
}
function timeAgo(timestamp) {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
return `${Math.floor(seconds / 86400)}d`;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
connectWebSocket();