feat: v0.5.6 - Node-Detail-Modal mit Minikarte im Dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5b2a5867d5
commit
7bf58a32fb
|
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## [0.5.6] - 2026-02-17
|
||||
### Added
|
||||
- Node-Detail-Modal im Dashboard: Klick auf Node-Zeile oeffnet Modal mit allen Node-Daten
|
||||
- Leaflet-Minikarte im Modal zeigt Node-Position (oder "Keine Position" Hinweis)
|
||||
- Zwei-Spalten-Layout: Datentabelle links, Karte rechts (responsive)
|
||||
|
||||
## [0.5.5] - 2026-02-17
|
||||
### Changed
|
||||
- SMTP-Versand auf EmailMessage + aiosmtplib.SMTP (async context manager) umgestellt
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.5.5"
|
||||
version: "0.5.6"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<title>MeshDD-Bot Dashboard</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -162,7 +163,38 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Node Detail Modal -->
|
||||
<div class="modal fade" id="nodeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title d-flex align-items-center gap-2">
|
||||
<span class="status-dot" id="modalStatusDot"></span>
|
||||
<span id="modalNodeName">Node</span>
|
||||
</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body py-2">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tbody id="modalNodeDetails"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div id="modalMapContainer" style="height:250px;border-radius:.375rem;overflow:hidden"></div>
|
||||
<div id="modalNoPosition" class="text-body-secondary text-center py-5 d-none">
|
||||
<i class="bi bi-geo-alt-fill fs-3 d-block mb-1"></i>Keine Position verfuegbar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ function renderNodes() {
|
|||
const hops = node.hop_count != null ? node.hop_count : '-';
|
||||
const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-';
|
||||
const onlineClass = isOnline(node.last_seen) ? 'text-success' : '';
|
||||
return `<tr>
|
||||
return `<tr data-node-id="${escapeHtml(node.node_id)}" style="cursor:pointer">
|
||||
<td class="${onlineClass}">${escapeHtml(name)}</td>
|
||||
<td class="text-body-secondary">${escapeHtml(hw)}</td>
|
||||
<td class="text-end px-1">${snr}</td>
|
||||
|
|
@ -263,4 +263,85 @@ if (sidebarToggle) {
|
|||
sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open'));
|
||||
}
|
||||
|
||||
// Node Detail Modal
|
||||
let nodeModalMap = null;
|
||||
let nodeModalMarker = null;
|
||||
const nodeModalEl = document.getElementById('nodeModal');
|
||||
const nodeModal = new bootstrap.Modal(nodeModalEl);
|
||||
|
||||
nodesTable.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr[data-node-id]');
|
||||
if (!row) return;
|
||||
showNodeModal(row.dataset.nodeId);
|
||||
});
|
||||
|
||||
function showNodeModal(nodeId) {
|
||||
const node = nodes[nodeId];
|
||||
if (!node) return;
|
||||
|
||||
// Header
|
||||
const name = node.long_name || node.short_name || node.node_id;
|
||||
document.getElementById('modalNodeName').textContent = name;
|
||||
const dot = document.getElementById('modalStatusDot');
|
||||
if (isOnline(node.last_seen)) {
|
||||
dot.classList.add('connected');
|
||||
} else {
|
||||
dot.classList.remove('connected');
|
||||
}
|
||||
|
||||
// Details table
|
||||
const fmt = (v) => v != null && v !== '' ? escapeHtml(String(v)) : '-';
|
||||
const fmtTime = (ts) => ts ? new Date(ts * 1000).toLocaleString('de-DE') : '-';
|
||||
const rows = [
|
||||
['Node-ID', fmt(node.node_id)],
|
||||
['Long Name', fmt(node.long_name)],
|
||||
['Short Name', fmt(node.short_name)],
|
||||
['Hardware', fmt(node.hw_model)],
|
||||
['SNR', node.snr != null ? `${node.snr.toFixed(1)} dB` : '-'],
|
||||
['RSSI', node.rssi != null ? `${node.rssi} dBm` : '-'],
|
||||
['Batterie', node.battery != null ? `${node.battery}%` : '-'],
|
||||
['Spannung', node.voltage != null ? `${node.voltage.toFixed(2)} V` : '-'],
|
||||
['Hops', fmt(node.hop_count)],
|
||||
['Via MQTT', node.via_mqtt ? 'Ja' : 'Nein'],
|
||||
['Hoehe', node.alt != null ? `${node.alt} m` : '-'],
|
||||
['Erste Verbindung', fmtTime(node.first_seen)],
|
||||
['Letzte Verbindung', fmtTime(node.last_seen)],
|
||||
];
|
||||
document.getElementById('modalNodeDetails').innerHTML = rows.map(([label, val]) =>
|
||||
`<tr><td class="text-body-secondary py-0 pe-2" style="white-space:nowrap">${label}</td><td class="py-0">${val}</td></tr>`
|
||||
).join('');
|
||||
|
||||
// Map
|
||||
const mapContainer = document.getElementById('modalMapContainer');
|
||||
const noPos = document.getElementById('modalNoPosition');
|
||||
const hasPosition = node.lat != null && node.lon != null && (node.lat !== 0 || node.lon !== 0);
|
||||
|
||||
if (hasPosition) {
|
||||
mapContainer.classList.remove('d-none');
|
||||
noPos.classList.add('d-none');
|
||||
} else {
|
||||
mapContainer.classList.add('d-none');
|
||||
noPos.classList.remove('d-none');
|
||||
}
|
||||
|
||||
nodeModal.show();
|
||||
|
||||
if (hasPosition) {
|
||||
nodeModalEl.addEventListener('shown.bs.modal', function onShown() {
|
||||
nodeModalEl.removeEventListener('shown.bs.modal', onShown);
|
||||
if (!nodeModalMap) {
|
||||
nodeModalMap = L.map(mapContainer).setView([node.lat, node.lon], 14);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(nodeModalMap);
|
||||
nodeModalMarker = L.marker([node.lat, node.lon]).addTo(nodeModalMap);
|
||||
} else {
|
||||
nodeModalMap.setView([node.lat, node.lon], 14);
|
||||
nodeModalMarker.setLatLng([node.lat, node.lon]);
|
||||
}
|
||||
nodeModalMap.invalidateSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connectWebSocket();
|
||||
|
|
|
|||
Loading…
Reference in a new issue