diff --git a/meshview/store.py b/meshview/store.py index 6bfa225..d39daf5 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -263,7 +263,13 @@ async def get_node_traffic(node_id: int): return [] -async def get_nodes(role=None, channel=None, hw_model=None, days_active=None): +async def get_nodes( + role=None, + channel=None, + hw_model=None, + days_active=None, + active_within: timedelta | None = None, +): """ Fetches nodes from the database based on optional filtering criteria. @@ -271,6 +277,8 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None): role (str, optional): The role of the node (converted to uppercase for consistency). channel (str, optional): The communication channel associated with the node. hw_model (str, optional): The hardware model of the node. + days_active (int, optional): Legacy support for filtering by a number of days. + active_within (timedelta, optional): Filter nodes seen within the provided window. Returns: list: A list of Node objects that match the given criteria. @@ -290,8 +298,12 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None): if hw_model is not None: query = query.where(Node.hw_model == hw_model) - if days_active is not None: - query = query.where(Node.last_update > datetime.now() - timedelta(days_active)) + window = active_within + if window is None and days_active is not None: + window = timedelta(days=days_active) + + if window is not None: + query = query.where(Node.last_update > datetime.now() - window) # Exclude nodes where last_update is an empty string query = query.where(Node.last_update != "") @@ -360,6 +372,19 @@ async def get_packet_stats( } +async def get_all_channels(): + async with database.async_session() as session: + stmt = ( + select(Node.channel) + .where(Node.channel.is_not(None)) + .where(Node.channel != "") + .distinct() + .order_by(Node.channel.asc()) + ) + result = await session.execute(stmt) + return [row[0] for row in result] + + async def get_channels_in_period(period_type: str = "hour", length: int = 24): """ Returns a list of distinct channels used in packets over a given period. diff --git a/meshview/templates/map.html b/meshview/templates/map.html index e093963..4d6ee3c 100644 --- a/meshview/templates/map.html +++ b/meshview/templates/map.html @@ -23,10 +23,20 @@ #filter-container { text-align: center; margin-top: 10px; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 8px; } .filter-checkbox { margin: 0 10px; } + #activity-range { + padding: 4px 8px; + border-radius: 4px; + border: 1px solid #ccc; + } #share-button { margin-left: 20px; padding: 5px 15px; @@ -72,6 +82,12 @@ {% block body %}
+ + Show Routers Only
@@ -101,6 +117,18 @@ async function loadTranslations() { // Initialize map AFTER translations are loaded loadTranslations().then(() => { const t = window.mapTranslations || {}; + const activitySelect = document.getElementById("activity-range"); + const activityLabel = document.getElementById("activity-range-label"); + if (activityLabel) { + activityLabel.textContent = t.active_within || "Active in:"; + } + if (activitySelect) { + activitySelect.addEventListener("change", () => { + const url = new URL(window.location.href); + url.searchParams.set("active", activitySelect.value); + window.location.href = url.toString(); + }); + } // ---- Map Setup ---- var map = L.map('map'); @@ -135,6 +163,8 @@ loadTranslations().then(() => { }{{ "," if not loop.last else "" }} {% endfor %} ]; + const providedChannels = {{ all_channels | default([], true) | tojson }}; + const channelSet = new Set(providedChannels.filter(ch => ch)); const portMap = {1: "Text", 67: "Telemetry", 3: "Position", 70: "Traceroute", 4: "Node Info", 71: "Neighbour Info", 73: "Map Report"}; @@ -162,18 +192,24 @@ loadTranslations().then(() => { return color; } + function channelKey(channel) { + if (typeof channel === 'string' && channel.trim().length > 0) { + return channel; + } + return 'Unknown'; + } + const nodeMap = new Map(); nodes.forEach(n => nodeMap.set(n.id, n)); function isInvalidCoord(node) { return !node || !node.lat || !node.long || node.lat===0 || node.long===0 || Number.isNaN(node.lat) || Number.isNaN(node.long); } // ---- Marker Plotting ---- var bounds = L.latLngBounds(); - var channels = new Set(); nodes.forEach(node => { if (!isInvalidCoord(node)) { - let category = node.channel; - channels.add(category); + let category = channelKey(node.channel); + channelSet.add(category); let color = hashToColor(category); let popupContent = `${node.long_name} (${node.short_name})
@@ -209,6 +245,8 @@ loadTranslations().then(() => { if (customView) map.setView([customView.lat,customView.lng],customView.zoom); else map.fitBounds(areaBounds); + const channelList = Array.from(channelSet).sort(); + // ---- LocalStorage for Filter Preferences ---- const FILTER_STORAGE_KEY = 'meshview_map_filters'; @@ -225,7 +263,7 @@ loadTranslations().then(() => { channels: {} }; - channels.forEach(channel => { + channelList.forEach(channel => { let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`; let checkbox = document.getElementById(filterId); if (checkbox) { @@ -259,7 +297,7 @@ loadTranslations().then(() => { document.getElementById("filter-routers-only").checked = false; // Reset all channel filters to checked (default) - channels.forEach(channel => { + channelList.forEach(channel => { let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`; let checkbox = document.getElementById(filterId); if (checkbox) { @@ -286,7 +324,7 @@ loadTranslations().then(() => { filterLabel.textContent = t.show_routers_only || "Show Routers Only"; let filterContainer = document.getElementById("filter-container"); - channels.forEach(channel => { + channelList.forEach(channel => { let filterId = `filter-${channel.replace(/\s+/g,'-').toLowerCase()}`; let color = hashToColor(channel); let label = document.createElement('label'); @@ -302,7 +340,7 @@ loadTranslations().then(() => { document.getElementById("filter-routers-only").checked = savedFilters.routersOnly || false; // Apply channel filters - channels.forEach(channel => { + channelList.forEach(channel => { let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`; let checkbox = document.getElementById(filterId); if (checkbox && savedFilters.channels.hasOwnProperty(channel)) { @@ -314,15 +352,19 @@ loadTranslations().then(() => { function updateMarkers() { let showRoutersOnly = document.getElementById("filter-routers-only").checked; nodes.forEach(node => { - let category=node.channel; + let category = channelKey(node.channel); let checkbox=document.getElementById(`filter-${category.replace(/\s+/g,'-').toLowerCase()}`); - let shouldShow=checkbox.checked && (!showRoutersOnly || node.isRouter); + let shouldShow=(!checkbox || checkbox.checked) && (!showRoutersOnly || node.isRouter); let marker=markerById[node.id]; if(marker) marker.setStyle({fillOpacity:shouldShow?1:0}); }); // Save filters to localStorage whenever they change saveFiltersToLocalStorage(); + + if (!document.hidden) { + restartPacketFetcher(); + } } document.querySelectorAll(".filter-checkbox").forEach(input=>input.addEventListener("change",updateMarkers)); @@ -338,7 +380,11 @@ loadTranslations().then(() => { const zoom = map.getZoom(); const lat = center.lat.toFixed(6); const lng = center.lng.toFixed(6); - const shareUrl = `${window.location.origin}/map?lat=${lat}&lng=${lng}&zoom=${zoom}`; + const url = new URL(window.location.href); + url.searchParams.set('lat', lat); + url.searchParams.set('lng', lng); + url.searchParams.set('zoom', zoom); + const shareUrl = url.toString(); navigator.clipboard.writeText(shareUrl).then(()=>{ const orig = shareBtn.textContent; shareBtn.textContent = '✓ Link Copied!'; @@ -474,8 +520,25 @@ loadTranslations().then(() => { }).catch(err=>console.error(err)); } let packetInterval=null; - function startPacketFetcher(){ if(mapInterval<=0) return; if(!packetInterval){ fetchLatestPacket(); packetInterval=setInterval(fetchNewPackets,mapInterval*1000); } } + function startPacketFetcher(resetImportTime=true){ + if(mapInterval<=0) return; + if(!packetInterval){ + if(resetImportTime || !lastImportTime){ + fetchLatestPacket(); + } + packetInterval=setInterval(fetchNewPackets,mapInterval*1000); + if(!resetImportTime && lastImportTime){ + fetchNewPackets(); + } + } + } function stopPacketFetcher(){ if(packetInterval){ clearInterval(packetInterval); packetInterval=null; } } + function restartPacketFetcher(){ + if(mapInterval<=0) return; + stopPacketFetcher(); + if(document.hidden) return; + startPacketFetcher(false); + } document.addEventListener("visibilitychange",function(){ if(document.hidden) stopPacketFetcher(); else startPacketFetcher(); }); if(mapInterval>0) startPacketFetcher(); }); diff --git a/meshview/templates/nodegraph.html b/meshview/templates/nodegraph.html index 3d25625..e833a27 100644 --- a/meshview/templates/nodegraph.html +++ b/meshview/templates/nodegraph.html @@ -87,6 +87,12 @@
+ + @@ -123,6 +129,25 @@