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>
124 lines
3.9 KiB
JavaScript
124 lines
3.9 KiB
JavaScript
const map = L.map('map').setView([51.1657, 10.4515], 6); // Germany center
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
const markers = {};
|
|
const statusDot = document.getElementById('statusDot');
|
|
const statusText = document.getElementById('statusText');
|
|
const nodeCount = document.getElementById('nodeCount');
|
|
let ws;
|
|
|
|
function createIcon(color) {
|
|
return L.divIcon({
|
|
className: '',
|
|
html: `<svg width="24" height="24" viewBox="0 0 24 24">
|
|
<circle cx="12" cy="12" r="8" fill="${color}" stroke="#fff" stroke-width="2" opacity="0.9"/>
|
|
</svg>`,
|
|
iconSize: [24, 24],
|
|
iconAnchor: [12, 12],
|
|
popupAnchor: [0, -12]
|
|
});
|
|
}
|
|
|
|
function getNodeColor(node) {
|
|
const age = Date.now() / 1000 - (node.last_seen || 0);
|
|
if (age < 900) return '#00e676'; // < 15 min: green
|
|
if (age < 3600) return '#ff9100'; // < 1h: orange
|
|
return '#ff5252'; // > 1h: red
|
|
}
|
|
|
|
function nodePopup(node) {
|
|
const name = node.long_name || node.short_name || node.node_id;
|
|
const lastSeen = node.last_seen ? new Date(node.last_seen * 1000).toLocaleString('de-DE') : '-';
|
|
const bat = node.battery != null ? `${node.battery}%` : '-';
|
|
const snr = node.snr != null ? `${node.snr.toFixed(1)} dB` : '-';
|
|
const alt = node.alt != null ? `${Math.round(node.alt)} m` : '-';
|
|
const hw = node.hw_model || '-';
|
|
|
|
return `<div class="node-popup">
|
|
<strong>${escapeHtml(name)}</strong><br>
|
|
<span class="label">ID:</span> ${escapeHtml(node.node_id)}<br>
|
|
<span class="label">Hardware:</span> ${escapeHtml(hw)}<br>
|
|
<span class="label">Batterie:</span> ${bat}<br>
|
|
<span class="label">SNR:</span> ${snr}<br>
|
|
<span class="label">Höhe:</span> ${alt}<br>
|
|
<span class="label">Zuletzt:</span> ${lastSeen}
|
|
</div>`;
|
|
}
|
|
|
|
function updateMarker(node) {
|
|
if (!node.lat || !node.lon) return;
|
|
|
|
const id = node.node_id;
|
|
const icon = createIcon(getNodeColor(node));
|
|
|
|
if (markers[id]) {
|
|
markers[id].setLatLng([node.lat, node.lon]);
|
|
markers[id].setIcon(icon);
|
|
markers[id].setPopupContent(nodePopup(node));
|
|
} else {
|
|
markers[id] = L.marker([node.lat, node.lon], { icon })
|
|
.addTo(map)
|
|
.bindPopup(nodePopup(node));
|
|
}
|
|
}
|
|
|
|
function fitBounds() {
|
|
const coords = Object.values(markers).map(m => m.getLatLng());
|
|
if (coords.length > 0) {
|
|
map.fitBounds(L.latLngBounds(coords).pad(0.1));
|
|
}
|
|
}
|
|
|
|
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 = '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 => updateMarker(node));
|
|
nodeCount.textContent = `${Object.keys(markers).length} Nodes`;
|
|
fitBounds();
|
|
break;
|
|
case 'node_update':
|
|
updateMarker(msg.data);
|
|
nodeCount.textContent = `${Object.keys(markers).length} Nodes`;
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Refresh marker colors every 60s
|
|
setInterval(() => {
|
|
Object.values(markers).forEach(marker => {
|
|
// Markers will be refreshed on next update
|
|
});
|
|
}, 60000);
|
|
|
|
connectWebSocket();
|