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
|
# 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
|
## [0.5.5] - 2026-02-17
|
||||||
### Changed
|
### Changed
|
||||||
- SMTP-Versand auf EmailMessage + aiosmtplib.SMTP (async context manager) umgestellt
|
- SMTP-Versand auf EmailMessage + aiosmtplib.SMTP (async context manager) umgestellt
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "0.5.5"
|
version: "0.5.6"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
name: "MeshDD-Bot"
|
name: "MeshDD-Bot"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<title>MeshDD-Bot Dashboard</title>
|
<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@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 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">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -162,7 +163,38 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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://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>
|
<script src="/static/js/dashboard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ function renderNodes() {
|
||||||
const hops = node.hop_count != null ? node.hop_count : '-';
|
const hops = node.hop_count != null ? node.hop_count : '-';
|
||||||
const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-';
|
const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-';
|
||||||
const onlineClass = isOnline(node.last_seen) ? 'text-success' : '';
|
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="${onlineClass}">${escapeHtml(name)}</td>
|
||||||
<td class="text-body-secondary">${escapeHtml(hw)}</td>
|
<td class="text-body-secondary">${escapeHtml(hw)}</td>
|
||||||
<td class="text-end px-1">${snr}</td>
|
<td class="text-end px-1">${snr}</td>
|
||||||
|
|
@ -263,4 +263,85 @@ if (sidebarToggle) {
|
||||||
sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open'));
|
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();
|
connectWebSocket();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue