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>
110 lines
3.8 KiB
JavaScript
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();
|