From 48abc8bb3ffacdae75926dc2c48d6926d8f2b40e Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Thu, 1 Jan 2026 17:35:10 -0600 Subject: [PATCH] feat(map): add telemetry marker overlay and websocket integration for real-time updates on map page --- .../src/frontend/components/map/MapPage.vue | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue index f432045..20f574e 100644 --- a/meshchatx/src/frontend/components/map/MapPage.vue +++ b/meshchatx/src/frontend/components/map/MapPage.vue @@ -140,6 +140,102 @@
+ +
+
+
+
+ +
+
+

+ {{ + selectedMarker.peer?.display_name || + selectedMarker.telemetry.destination_hash.substring(0, 8) + }} +

+
+ {{ selectedMarker.telemetry.destination_hash }} +
+
+
+ +
+
+
+
+
+ Latitude +
+
+ {{ selectedMarker.telemetry.telemetry.location.latitude.toFixed(6) }} +
+
+
+
+ Longitude +
+
+ {{ selectedMarker.telemetry.telemetry.location.longitude.toFixed(6) }} +
+
+
+
+ Altitude +
+
{{ selectedMarker.telemetry.telemetry.location.altitude.toFixed(1) }}m
+
+
+
Speed
+
{{ selectedMarker.telemetry.telemetry.location.speed.toFixed(1) }}km/h
+
+
+ +
+
Signal
+
+ RSSI: {{ selectedMarker.telemetry.physical_link.rssi }} + SNR: {{ selectedMarker.telemetry.physical_link.snr }} + Q: {{ selectedMarker.telemetry.physical_link.q }}% +
+
+ +
+ + Updated: {{ formatTimestamp(selectedMarker.telemetry.timestamp) }} +
+ + +
+
+
{ @@ -643,12 +770,19 @@ export default { // Check screen size for mobile this.checkScreenSize(); window.addEventListener("resize", this.checkScreenSize); + + // Update info every few seconds + this.reloadInterval = setInterval(() => { + this.fetchTelemetryMarkers(); + }, 30000); }, beforeUnmount() { + if (this.reloadInterval) clearInterval(this.reloadInterval); if (this.exportInterval) clearInterval(this.exportInterval); if (this.searchTimeout) clearTimeout(this.searchTimeout); document.removeEventListener("click", this.handleClickOutside); window.removeEventListener("resize", this.checkScreenSize); + WebSocketConnection.off("message", this.onWebsocketMessage); }, methods: { async getConfig() { @@ -733,6 +867,23 @@ export default { }), }); + // setup telemetry markers + this.markerSource = new VectorSource(); + this.markerLayer = new VectorLayer({ + source: this.markerSource, + zIndex: 100, + }); + this.map.addLayer(this.markerLayer); + + this.map.on("click", (evt) => { + const feature = this.map.forEachFeatureAtPixel(evt.pixel, (f) => f); + if (feature && feature.get("telemetry")) { + this.onMarkerClick(feature); + } else { + this.selectedMarker = null; + } + }); + this.currentCenter = [defaultLon, defaultLat]; this.currentZoom = defaultZoom; @@ -1307,6 +1458,108 @@ export default { checkScreenSize() { this.isMobileScreen = window.innerWidth < 640; }, + async fetchPeers() { + try { + const response = await window.axios.get("/api/v1/lxmf/conversations"); + const peers = {}; + for (const conv of response.data.conversations) { + peers[conv.destination_hash] = conv; + } + this.peers = peers; + } catch (e) { + console.error("Failed to fetch peers", e); + } + }, + async fetchTelemetryMarkers() { + try { + const response = await window.axios.get("/api/v1/telemetry/peers"); + this.telemetryList = response.data.telemetry; + this.updateMarkers(); + } catch (e) { + console.error("Failed to fetch telemetry", e); + } + }, + updateMarkers() { + if (!this.markerSource) return; + this.markerSource.clear(); + + for (const t of this.telemetryList) { + const loc = t.telemetry?.location; + if (!loc || loc.latitude === undefined || loc.longitude === undefined) continue; + + const peer = this.peers[t.destination_hash]; + const displayName = peer?.display_name || t.destination_hash.substring(0, 8); + + const feature = new Feature({ + geometry: new Point(fromLonLat([loc.longitude, loc.latitude])), + telemetry: t, + peer: peer, + }); + + // Default style + let iconColor = "#3b82f6"; + let bgColor = "#ffffff"; + + if (peer?.lxmf_user_icon) { + iconColor = peer.lxmf_user_icon.foreground_colour || iconColor; + bgColor = peer.lxmf_user_icon.background_colour || bgColor; + } + + feature.setStyle( + new Style({ + image: new CircleStyle({ + radius: 8, + fill: new Fill({ color: bgColor }), + stroke: new Stroke({ color: iconColor, width: 2 }), + }), + text: new Text({ + text: displayName, + offsetY: -15, + font: "bold 11px sans-serif", + fill: new Fill({ color: "#000" }), + stroke: new Stroke({ color: "#fff", width: 2 }), + }), + }) + ); + + this.markerSource.addFeature(feature); + } + }, + onMarkerClick(feature) { + this.selectedMarker = { + telemetry: feature.get("telemetry"), + peer: feature.get("peer"), + }; + }, + async onWebsocketMessage(message) { + const json = JSON.parse(message.data); + if (json.type === "lxmf.telemetry") { + // Find and update or add to telemetryList + const index = this.telemetryList.findIndex((t) => t.destination_hash === json.destination_hash); + const entry = { + destination_hash: json.destination_hash, + timestamp: json.timestamp, + telemetry: json.telemetry, + updated_at: new Date().toISOString(), + }; + + if (index !== -1) { + this.telemetryList.splice(index, 1, entry); + } else { + this.telemetryList.push(entry); + } + this.updateMarkers(); + } + }, + formatTimestamp(ts) { + return new Date(ts * 1000).toLocaleString(); + }, + openChat(hash) { + this.$router.push({ + name: "messages", + params: { destinationHash: hash }, + }); + }, }, };