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 @@