diff --git a/meshview/store.py b/meshview/store.py index d39daf5..8060469 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -24,7 +24,14 @@ async def get_fuzzy_nodes(query): return result.scalars() -async def get_packets(node_id=None, portnum=None, after=None, before=None, limit=None, channel: str | None = None): +async def get_packets( + node_id=None, + portnum=None, + after=None, + before=None, + limit=None, + channel: str | list[str] | tuple[str, ...] | None = None, +): async with database.async_session() as session: q = select(Packet) @@ -37,7 +44,12 @@ async def get_packets(node_id=None, portnum=None, after=None, before=None, limit if before: q = q.where(Packet.import_time < before) if channel: - q = q.where(func.lower(Packet.channel) == channel.lower()) + if isinstance(channel, (list, tuple, set)): + lowered = [c.lower() for c in channel if isinstance(c, str) and c] + if lowered: + q = q.where(func.lower(Packet.channel).in_(lowered)) + elif isinstance(channel, str): + q = q.where(func.lower(Packet.channel) == channel.lower()) q = q.order_by(Packet.import_time.desc()) @@ -372,19 +384,6 @@ 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 4d6ee3c..6c455e0 100644 --- a/meshview/templates/map.html +++ b/meshview/templates/map.html @@ -115,7 +115,7 @@ async function loadTranslations() { } // Initialize map AFTER translations are loaded -loadTranslations().then(() => { +loadTranslations().then(async () => { const t = window.mapTranslations || {}; const activitySelect = document.getElementById("activity-range"); const activityLabel = document.getElementById("activity-range-label"); @@ -163,8 +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 channelSet = new Set(); + let channelList = []; const portMap = {1: "Text", 67: "Telemetry", 3: "Position", 70: "Traceroute", 4: "Node Info", 71: "Neighbour Info", 73: "Map Report"}; @@ -199,6 +199,19 @@ loadTranslations().then(() => { return 'Unknown'; } + async function fetchAdditionalChannels() { + try { + const res = await fetch('/api/channels?period_type=day&length=30'); + if (!res.ok) return []; + const data = await res.json(); + if (!data || !Array.isArray(data.channels)) return []; + return data.channels.filter(ch => typeof ch === 'string' && ch.trim().length > 0); + } catch (err) { + console.error('Channel list fetch failed:', err); + return []; + } + } + 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); } @@ -245,7 +258,9 @@ loadTranslations().then(() => { if (customView) map.setView([customView.lat,customView.lng],customView.zoom); else map.fitBounds(areaBounds); - const channelList = Array.from(channelSet).sort(); + const extraChannels = await fetchAdditionalChannels(); + extraChannels.forEach(raw => channelSet.add(channelKey(raw))); + channelList = Array.from(channelSet).sort(); // ---- LocalStorage for Filter Preferences ---- const FILTER_STORAGE_KEY = 'meshview_map_filters'; @@ -272,16 +287,13 @@ loadTranslations().then(() => { }); localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify(filters)); - console.log('Filters saved to localStorage:', filters); } function loadFiltersFromLocalStorage() { try { const stored = localStorage.getItem(FILTER_STORAGE_KEY); if (stored) { - const filters = JSON.parse(stored); - console.log('Filters loaded from localStorage:', filters); - return filters; + return JSON.parse(stored); } } catch (error) { console.error('Error loading filters from localStorage:', error); @@ -289,25 +301,34 @@ loadTranslations().then(() => { return null; } + function renderChannelFilters(savedFilters) { + const filterContainer = document.getElementById("filter-container"); + filterContainer.querySelectorAll('label[data-channel-filter="true"]').forEach(el => el.remove()); + channelList.forEach(channel => { + let filterId = `filter-${channel.replace(/\s+/g,'-').toLowerCase()}`; + let color = hashToColor(channel); + let label = document.createElement('label'); + label.style.color = color; + label.setAttribute('data-channel-filter', 'true'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'filter-checkbox'; + checkbox.id = filterId; + const shouldCheck = savedFilters ? savedFilters.channels?.[channel] !== false : true; + checkbox.checked = shouldCheck; + checkbox.addEventListener("change", updateMarkers); + label.appendChild(checkbox); + label.append(` ${channel}`); + filterContainer.appendChild(label); + }); + } + function resetFiltersToDefaults() { localStorage.removeItem(FILTER_STORAGE_KEY); - console.log('Filters reset to defaults'); - - // Reset routers only filter document.getElementById("filter-routers-only").checked = false; - - // Reset all channel filters to checked (default) - channelList.forEach(channel => { - let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`; - let checkbox = document.getElementById(filterId); - if (checkbox) { - checkbox.checked = true; - } - }); - + renderChannelFilters(null); updateMarkers(); - // Show feedback to user const button = document.getElementById('reset-filters-button'); const originalText = button.textContent; button.textContent = '✓ Filters Reset!'; @@ -319,35 +340,18 @@ loadTranslations().then(() => { }, 2000); } + window.resetFiltersToDefaults = resetFiltersToDefaults; + // ---- Filters ---- const filterLabel = document.getElementById("filter-routers-label"); filterLabel.textContent = t.show_routers_only || "Show Routers Only"; - - let filterContainer = document.getElementById("filter-container"); - channelList.forEach(channel => { - let filterId = `filter-${channel.replace(/\s+/g,'-').toLowerCase()}`; - let color = hashToColor(channel); - let label = document.createElement('label'); - label.style.color=color; - label.innerHTML=` ${channel}`; - filterContainer.appendChild(label); - }); - - // Load saved filters from localStorage + const routersOnlyCheckbox = document.getElementById("filter-routers-only"); const savedFilters = loadFiltersFromLocalStorage(); if (savedFilters) { - // Apply routers only filter - document.getElementById("filter-routers-only").checked = savedFilters.routersOnly || false; - - // Apply channel filters - channelList.forEach(channel => { - let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`; - let checkbox = document.getElementById(filterId); - if (checkbox && savedFilters.channels.hasOwnProperty(channel)) { - checkbox.checked = savedFilters.channels[channel]; - } - }); + routersOnlyCheckbox.checked = savedFilters.routersOnly || false; } + routersOnlyCheckbox.addEventListener("change", updateMarkers); + renderChannelFilters(savedFilters); function updateMarkers() { let showRoutersOnly = document.getElementById("filter-routers-only").checked; @@ -363,11 +367,17 @@ loadTranslations().then(() => { saveFiltersToLocalStorage(); if (!document.hidden) { - restartPacketFetcher(); + restartPacketFetcher(true); } } - document.querySelectorAll(".filter-checkbox").forEach(input=>input.addEventListener("change",updateMarkers)); + function getActiveChannels() { + return channelList.filter(channel => { + if (channel === 'Unknown') return false; + let checkbox = document.getElementById(`filter-${channel.replace(/\s+/g,'-').toLowerCase()}`); + return checkbox ? checkbox.checked : true; + }); + } // Apply initial filters (from localStorage or defaults) updateMarkers(); @@ -497,50 +507,78 @@ loadTranslations().then(() => { // ---- Packet fetching ---- let lastImportTime=null; const mapInterval={{ site_config["site"]["map_interval"]|default(3) }}; + function buildPacketsUrl(base){ + const active = getActiveChannels(); + const url = new URL(base, window.location.origin); + url.searchParams.delete('channel'); + if (active.length) { + active.forEach(ch => url.searchParams.append('channel', ch)); + } + if (url.origin === window.location.origin) { + return url.pathname + (url.search || '') + (url.hash || ''); + } + return url.toString(); + } function fetchLatestPacket(){ - fetch(`/api/packets?limit=1`).then(r=>r.json()).then(data=>{ - if(data.packets && data.packets.length>0) lastImportTime=data.packets[0].import_time; - else lastImportTime=new Date().toISOString(); - }).catch(err=>console.error(err)); + return fetch(buildPacketsUrl(`/api/packets?limit=1`)) + .then(r=>r.json()) + .then(data=>{ + if(data.packets && data.packets.length>0){ + lastImportTime=data.packets[0].import_time; + } else { + lastImportTime=new Date().toISOString(); + } + }) + .catch(err=>{ + console.error('fetchLatestPacket failed:', err); + }); } function fetchNewPackets(){ if(!lastImportTime) return; - fetch(`/api/packets?since=${lastImportTime}`).then(r=>r.json()).then(data=>{ - if(!data.packets||data.packets.length===0) return; - let latestSeen=lastImportTime; - data.packets.forEach(packet=>{ - if(packet.import_time && (!latestSeen || packet.import_time>latestSeen)) latestSeen=packet.import_time; - let marker=markerById[packet.from_node_id]; - if(marker){ - let nodeData=nodeMap.get(packet.from_node_id); - if(nodeData) blinkNode(marker,nodeData.long_name,packet.portnum); - } + const baseUrl = `/api/packets?since=${encodeURIComponent(lastImportTime)}`; + return fetch(buildPacketsUrl(baseUrl)) + .then(r=>r.json()) + .then(data=>{ + if(!data.packets||data.packets.length===0) return; + let latestSeen=lastImportTime; + data.packets.forEach(packet=>{ + if(packet.import_time && (!latestSeen || packet.import_time>latestSeen)) latestSeen=packet.import_time; + let marker=markerById[packet.from_node_id]; + if(marker){ + let nodeData=nodeMap.get(packet.from_node_id); + if(nodeData) blinkNode(marker,nodeData.long_name,packet.portnum); + } + }); + if(latestSeen) lastImportTime=latestSeen; + }) + .catch(err=>{ + console.error('fetchNewPackets failed:', err); }); - if(latestSeen) lastImportTime=latestSeen; - }).catch(err=>console.error(err)); } let packetInterval=null; - function startPacketFetcher(resetImportTime=true){ - if(mapInterval<=0) return; - if(!packetInterval){ - if(resetImportTime || !lastImportTime){ - fetchLatestPacket(); - } - packetInterval=setInterval(fetchNewPackets,mapInterval*1000); - if(!resetImportTime && lastImportTime){ - fetchNewPackets(); - } + async function startPacketFetcher(resetImportTime=true){ + if (mapInterval <= 0) return; + stopPacketFetcher(); + if (resetImportTime) { + lastImportTime = null; } + if (!lastImportTime) { + await fetchLatestPacket(); + } + await fetchNewPackets(); + packetInterval = setInterval(()=>{ fetchNewPackets(); }, mapInterval*1000); } function stopPacketFetcher(){ if(packetInterval){ clearInterval(packetInterval); packetInterval=null; } } - function restartPacketFetcher(){ + async function restartPacketFetcher(resetImportTime=false){ if(mapInterval<=0) return; - stopPacketFetcher(); if(document.hidden) return; - startPacketFetcher(false); + await startPacketFetcher(resetImportTime); } - document.addEventListener("visibilitychange",function(){ if(document.hidden) stopPacketFetcher(); else startPacketFetcher(); }); - if(mapInterval>0) startPacketFetcher(); + document.addEventListener("visibilitychange",function(){ + if(document.hidden) stopPacketFetcher(); + else restartPacketFetcher(false); + }); + if(mapInterval>0) startPacketFetcher(true); }); {% endblock %} diff --git a/meshview/templates/nodegraph.html b/meshview/templates/nodegraph.html index e833a27..f71cf58 100644 --- a/meshview/templates/nodegraph.html +++ b/meshview/templates/nodegraph.html @@ -130,10 +130,8 @@ {% endblock %} diff --git a/meshview/templates/stats.html b/meshview/templates/stats.html index 38bd3fe..2b63d8c 100644 --- a/meshview/templates/stats.html +++ b/meshview/templates/stats.html @@ -77,6 +77,69 @@ font-weight: bold; } +.table-wrapper { + max-height: 400px; + overflow: auto; + border: 1px solid #3a3d42; + border-radius: 6px; + margin-top: 10px; +} + +.stats-table { + width: 100%; + border-collapse: collapse; + color: #ddd; +} + +.stats-table th, +.stats-table td { + padding: 8px 10px; + text-align: left; + border-bottom: 1px solid #3a3d42; + font-size: 13px; + white-space: nowrap; +} + +.stats-table th { + cursor: pointer; + position: relative; + user-select: none; + color: #f0f0f0; +} + +.stats-table th.sorted-asc::after, +.stats-table th.sorted-desc::after { + content: ""; + position: absolute; + right: 8px; + top: 50%; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; +} + +.stats-table th.sorted-asc::after { + border-bottom: 6px solid #66bb6a; + transform: translateY(-75%); +} + +.stats-table th.sorted-desc::after { + border-top: 6px solid #66bb6a; + transform: translateY(-25%); +} + +.stats-table tbody tr:hover { + background-color: #2f3338; +} + +.empty-table { + padding: 16px; + text-align: center; + color: #aaa; + font-size: 14px; +} + .filter-bar { display: flex; justify-content: flex-end; @@ -190,9 +253,29 @@
Channel Breakdown
- +Nodes Overview
+| Long Name | +Short Name | +Role | +Hardware | +Channel | +Last Seen | +
|---|