MeshDD-Bot/static/js/dashboard.js
ppfeiffer 15955cf8d7 feat: Add Meshtastic bot with web dashboard and live map
Implements full MeshDD-Bot with TCP connection to Meshtastic devices,
SQLite storage for nodes/messages, aiohttp web dashboard with WebSocket
live updates, and Leaflet.js map view with color-coded node markers.
Includes bot commands (!ping, !nodes, !info, !help, !weather, !stats,
!uptime) and automatic version bumping via pre-commit hook.

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

110 lines
3.8 KiB
JavaScript

const nodesTable = document.getElementById('nodesTable');
const messagesList = document.getElementById('messagesList');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
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));
nodesTable.innerHTML = sorted.map(node => {
const name = node.long_name || node.short_name || node.node_id;
const snr = node.snr != null ? node.snr.toFixed(1) : '-';
const battery = renderBattery(node.battery);
const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-';
const shortId = node.node_id ? node.node_id.slice(-4) : '?';
return `<tr>
<td>${escapeHtml(name)}</td>
<td>${escapeHtml(shortId)}</td>
<td>${snr}</td>
<td>${battery}</td>
<td>${lastSeen}</td>
</tr>`;
}).join('');
}
function renderBattery(level) {
if (level == null) return '-';
let color = '#00e676';
if (level < 20) color = '#ff5252';
else if (level < 50) color = '#ff9100';
return `<div class="battery-indicator"><div class="battery-fill" style="width:${level}%;background:${color}"></div></div> ${level}%`;
}
function addMessage(msg) {
const li = document.createElement('li');
li.className = 'message-item';
const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString('de-DE') : '';
const from = msg.from_node ? msg.from_node.slice(-4) : '?';
li.innerHTML = `<div class="meta">${time} von ${escapeHtml(from)}</div><div class="text">${escapeHtml(msg.payload || '')}</div>`;
messagesList.prepend(li);
// Keep max 100 messages
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 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();