@@ -115,20 +99,8 @@ async function loadTranslations() {
}
// Initialize map AFTER translations are loaded
-loadTranslations().then(async () => {
+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');
@@ -163,8 +135,6 @@ loadTranslations().then(async () => {
}{{ "," if not loop.last else "" }}
{% endfor %}
];
- 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"};
@@ -192,37 +162,18 @@ loadTranslations().then(async () => {
return color;
}
- function channelKey(channel) {
- if (typeof channel === 'string' && channel.trim().length > 0) {
- return channel;
- }
- 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); }
// ---- Marker Plotting ----
var bounds = L.latLngBounds();
+ var channels = new Set();
nodes.forEach(node => {
if (!isInvalidCoord(node)) {
- let category = channelKey(node.channel);
- channelSet.add(category);
+ let category = node.channel;
+ channels.add(category);
let color = hashToColor(category);
let popupContent = `
@@ -258,10 +209,6 @@ loadTranslations().then(async () => {
if (customView) map.setView([customView.lat,customView.lng],customView.zoom);
else map.fitBounds(areaBounds);
- 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';
@@ -287,13 +234,16 @@ loadTranslations().then(async () => {
});
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) {
- return JSON.parse(stored);
+ const filters = JSON.parse(stored);
+ console.log('Filters loaded from localStorage:', filters);
+ return filters;
}
} catch (error) {
console.error('Error loading filters from localStorage:', error);
@@ -325,8 +275,20 @@ loadTranslations().then(async () => {
function resetFiltersToDefaults() {
localStorage.removeItem(FILTER_STORAGE_KEY);
+ console.log('Filters reset to defaults');
+
+ // Reset routers only filter
document.getElementById("filter-routers-only").checked = false;
- renderChannelFilters(null);
+
+ // Reset all channel filters to checked (default)
+ channels.forEach(channel => {
+ let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
+ let checkbox = document.getElementById(filterId);
+ if (checkbox) {
+ checkbox.checked = true;
+ }
+ });
+
updateMarkers();
const button = document.getElementById('reset-filters-button');
@@ -340,35 +302,48 @@ loadTranslations().then(async () => {
}, 2000);
}
- window.resetFiltersToDefaults = resetFiltersToDefaults;
-
// ---- Filters ----
const filterLabel = document.getElementById("filter-routers-label");
filterLabel.textContent = t.show_routers_only || "Show Routers Only";
- const routersOnlyCheckbox = document.getElementById("filter-routers-only");
+
+ let filterContainer = document.getElementById("filter-container");
+ channels.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 savedFilters = loadFiltersFromLocalStorage();
if (savedFilters) {
- routersOnlyCheckbox.checked = savedFilters.routersOnly || false;
+ // Apply routers only filter
+ document.getElementById("filter-routers-only").checked = savedFilters.routersOnly || false;
+
+ // Apply channel filters
+ channels.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.addEventListener("change", updateMarkers);
- renderChannelFilters(savedFilters);
function updateMarkers() {
let showRoutersOnly = document.getElementById("filter-routers-only").checked;
nodes.forEach(node => {
- let category = channelKey(node.channel);
+ let category=node.channel;
let checkbox=document.getElementById(`filter-${category.replace(/\s+/g,'-').toLowerCase()}`);
- let shouldShow=(!checkbox || checkbox.checked) && (!showRoutersOnly || node.isRouter);
+ let shouldShow=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(true);
- }
}
function getActiveChannels() {
@@ -390,11 +365,7 @@ loadTranslations().then(async () => {
const zoom = map.getZoom();
const lat = center.lat.toFixed(6);
const lng = center.lng.toFixed(6);
- 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();
+ const shareUrl = `${window.location.origin}/map?lat=${lat}&lng=${lng}&zoom=${zoom}`;
navigator.clipboard.writeText(shareUrl).then(()=>{
const orig = shareBtn.textContent;
shareBtn.textContent = '✓ Link Copied!';
@@ -507,78 +478,33 @@ loadTranslations().then(async () => {
// ---- 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(){
- 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);
- });
+ 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));
}
function fetchNewPackets(){
if(!lastImportTime) return;
- 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);
+ 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);
+ }
});
+ if(latestSeen) lastImportTime=latestSeen;
+ }).catch(err=>console.error(err));
}
let packetInterval=null;
- 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 startPacketFetcher(){ if(mapInterval<=0) return; if(!packetInterval){ fetchLatestPacket(); packetInterval=setInterval(fetchNewPackets,mapInterval*1000); } }
function stopPacketFetcher(){ if(packetInterval){ clearInterval(packetInterval); packetInterval=null; } }
- async function restartPacketFetcher(resetImportTime=false){
- if(mapInterval<=0) return;
- if(document.hidden) return;
- await startPacketFetcher(resetImportTime);
- }
- document.addEventListener("visibilitychange",function(){
- if(document.hidden) stopPacketFetcher();
- else restartPacketFetcher(false);
- });
- if(mapInterval>0) startPacketFetcher(true);
+ document.addEventListener("visibilitychange",function(){ if(document.hidden) stopPacketFetcher(); else startPacketFetcher(); });
+ if(mapInterval>0) startPacketFetcher();
});
{% endblock %}
\ No newline at end of file
diff --git a/meshview/templates/nodegraph.html b/meshview/templates/nodegraph.html
index f71cf58..3d25625 100644
--- a/meshview/templates/nodegraph.html
+++ b/meshview/templates/nodegraph.html
@@ -87,12 +87,6 @@
{% endblock %}
diff --git a/meshview/templates/stats.html b/meshview/templates/stats.html
index 2b63d8c..e045371 100644
--- a/meshview/templates/stats.html
+++ b/meshview/templates/stats.html
@@ -77,90 +77,14 @@
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;
- align-items: center;
- gap: 10px;
- margin-bottom: 20px;
-}
-
-.filter-label {
- color: #ccc;
- font-size: 14px;
-}
-
#channelSelect {
+ margin-bottom: 8px;
padding: 4px 6px;
background:#444;
color:#fff;
border:none;
border-radius:4px;
}
-
{% endblock %}
{% block head %}
@@ -171,25 +95,18 @@
Mesh Statistics - Summary (all available in Database)
-
-
-
-
-
Total Nodes
-
{{ "{:,}".format(total_nodes) }}
+
{{ "{:,}".format(total_nodes) }}
Total Packets
-
{{ "{:,}".format(total_packets) }}
+
{{ "{:,}".format(total_packets) }}
Total Packets Seen
-
{{ "{:,}".format(total_packets_seen) }}
+
{{ "{:,}".format(total_packets_seen) }}
@@ -205,6 +122,9 @@
+
@@ -253,29 +173,9 @@
-
+
-
-
-
-
-
-
-
- | Long Name |
- Short Name |
- Role |
- Hardware |
- Channel |
- Last Seen |
-
-
-
-
-
No nodes found for the selected channel.
-
-
@@ -301,28 +201,12 @@ const PORTNUM_LABELS = {
71: "Neighbor Info"
};
-const PORT_CONFIG = [
- { port: 1, color: '#ff5722', domId: 'chart_portnum_1', totalId: 'total_portnum_1' },
- { port: 3, color: '#2196f3', domId: 'chart_portnum_3', totalId: 'total_portnum_3' },
- { port: 4, color: '#9c27b0', domId: 'chart_portnum_4', totalId: 'total_portnum_4' },
- { port: 67, color: '#ffeb3b', domId: 'chart_portnum_67', totalId: 'total_portnum_67' },
- { port: 70, color: '#795548', domId: 'chart_portnum_70', totalId: 'total_portnum_70' },
- { port: 71, color: '#4caf50', domId: 'chart_portnum_71', totalId: 'total_portnum_71' },
-];
-
-const CHANNEL_PRESETS = ["LongFast", "MediumSlow"];
-
-let currentChannel = "";
-let nodeTableData = [];
-let nodeTableSortKey = "last_update";
-let nodeTableSortDirection = "desc";
-
// --- Fetch & Processing ---
async function fetchStats(period_type,length,portnum=null,channel=null){
try{
let url=`/api/stats?period_type=${period_type}&length=${length}`;
if(portnum!==null) url+=`&portnum=${portnum}`;
- if(channel) url+=`&channel=${encodeURIComponent(channel)}`;
+ if(channel) url+=`&channel=${channel}`;
const res=await fetch(url);
if(!res.ok) return [];
const json=await res.json();
@@ -330,212 +214,28 @@ async function fetchStats(period_type,length,portnum=null,channel=null){
}catch{return [];}
}
-async function fetchNodes(channel=null){
- try{
- const base="/api/nodes";
- const url=channel?`${base}?channel=${encodeURIComponent(channel)}`:base;
- const res=await fetch(url);
- const json=await res.json();
- return json.nodes||[];
- }catch{return [];}
-}
+async function fetchNodes(){ try{ const res=await fetch("/api/nodes"); const json=await res.json(); return json.nodes||[];}catch{return [];} }
+async function fetchChannels(){ try{ const res = await fetch("/api/channels"); const json = await res.json(); return json.channels || [];}catch{return [];} }
-async function fetchChannels(){
- try{
- const res = await fetch("/api/channels");
- const json = await res.json();
- return json.channels || [];
- }catch{return [];}
-}
-
-async function fetchSummary(channel=null){
- try{
- const base="/api/stats/summary";
- const url=channel?`${base}?channel=${encodeURIComponent(channel)}`:base;
- const res=await fetch(url);
- if(!res.ok) return null;
- return await res.json();
- }catch{return null;}
-}
-
-function processCountField(nodes,field){
- const counts={};
- (nodes||[]).forEach(n=>{
- const key=n?.[field]||"Unknown";
- counts[key]=(counts[key]||0)+1;
- });
- return Object.entries(counts).map(([name,value])=>({name,value}));
-}
-
-function updateTotalCount(domId,data){
- const el=document.getElementById(domId);
- if(!el) return;
- const dataset=Array.isArray(data)?data:[];
- const total=dataset.reduce((acc,d)=>acc+(d?.count??d?.packet_count??0),0);
- el.textContent=`Total: ${total.toLocaleString()}`;
-}
-
-function prepareTopN(data=[],n=20){
- const sorted=[...(data||[])].sort((a,b)=>b.value-a.value);
- const top=sorted.slice(0,n);
- if(sorted.length>n){
- const otherValue=sorted.slice(n).reduce((sum,item)=>sum+item.value,0);
- top.push({name:"Other", value:otherValue});
- }
- return top;
-}
-
-function formatDateString(value){
- if(!value) return "—";
- const date = new Date(value);
- if(Number.isNaN(date.getTime())) return value;
- return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
-}
-
-function normalizeString(value){
- return (value ?? "").toString().toLowerCase();
-}
-
-function applyNodeTableSort(render=true){
- const dir = nodeTableSortDirection === "asc" ? 1 : -1;
- nodeTableData.sort((a,b)=>{
- let lhs=a[nodeTableSortKey];
- let rhs=b[nodeTableSortKey];
- if(nodeTableSortKey==="last_update"){
- lhs = lhs ?? -Infinity;
- rhs = rhs ?? -Infinity;
- return (lhs - rhs) * dir;
- }
- const left = normalizeString(lhs);
- const right = normalizeString(rhs);
- if(left===right) return 0;
- return left > right ? dir : -dir;
- });
- if(render) renderNodeTableRows();
-}
-
-function renderNodeTableRows(){
- const tbody=document.querySelector("#nodesTable tbody");
- const emptyMessage=document.getElementById("nodesTableEmpty");
- if(!tbody) return;
- tbody.innerHTML="";
- if(!nodeTableData.length){
- if(emptyMessage) emptyMessage.style.display="block";
- return;
- }
- if(emptyMessage) emptyMessage.style.display="none";
- nodeTableData.forEach(node=>{
- const tr=document.createElement("tr");
- tr.innerHTML=`
-
${node.long_name} |
-
${node.short_name} |
-
${node.role} |
-
${node.hw_model} |
-
${node.channel || "—"} |
-
${node.last_update_display} |
- `;
- tbody.appendChild(tr);
- });
- updateSortIndicators();
-}
-
-function setNodeTableData(rawNodes){
- nodeTableData = (rawNodes||[]).map(n=>{
- const lastUpdateRaw = n?.last_update ?? null;
- const lastUpdateDate = lastUpdateRaw ? new Date(lastUpdateRaw) : null;
- return {
- long_name: n?.long_name || "—",
- short_name: n?.short_name || "—",
- role: n?.role || "Unknown",
- hw_model: n?.hw_model || "Unknown",
- channel: n?.channel || "",
- last_update: lastUpdateDate ? lastUpdateDate.getTime() : null,
- last_update_display: formatDateString(lastUpdateRaw),
- };
- });
- applyNodeTableSort(false);
- renderNodeTableRows();
-}
-
-function updateSortIndicators(){
- document.querySelectorAll("#nodesTable thead th[data-sort-key]").forEach(th=>{
- th.classList.remove("sorted-asc","sorted-desc");
- if(th.dataset.sortKey === nodeTableSortKey){
- th.classList.add(nodeTableSortDirection === "asc" ? "sorted-asc" : "sorted-desc");
- }
- });
-}
-
-function handleNodeTableSort(event){
- const key=event.currentTarget?.dataset?.sortKey;
- if(!key) return;
- if(nodeTableSortKey===key){
- nodeTableSortDirection = nodeTableSortDirection === "asc" ? "desc" : "asc";
- }else{
- nodeTableSortKey = key;
- nodeTableSortDirection = key === "last_update" ? "desc" : "asc";
- }
- applyNodeTableSort();
-}
+function processCountField(nodes,field){ const counts={}; nodes.forEach(n=>{ const key=n[field]||"Unknown"; counts[key]=(counts[key]||0)+1; }); return Object.entries(counts).map(([name,value])=>({name,value})); }
+function updateTotalCount(domId,data){ const el=document.getElementById(domId); if(!el||!data.length) return; const total=data.reduce((acc,d)=>acc+(d.count??d.packet_count??0),0); el.textContent=`Total: ${total.toLocaleString()}`; }
+function prepareTopN(data,n=20){ data.sort((a,b)=>b.value-a.value); let top=data.slice(0,n); if(data.length>n){ const otherValue=data.slice(n).reduce((sum,item)=>sum+item.value,0); top.push({name:"Other", value:otherValue}); } return top; }
// --- Chart Rendering ---
-function renderChart(domId,data,type,color){
- const el=document.getElementById(domId);
- if(!el) return null;
- const existing=echarts.getInstanceByDom(el);
- if(existing) existing.dispose();
- const chart=echarts.init(el);
- const source=Array.isArray(data)?data:[];
- const periods=source.map(d=>{
- const periodValue=d?.period;
- return (periodValue||periodValue===0) ? String(periodValue) : '';
- });
- const counts=source.map(d=>d?.count??d?.packet_count??0);
- chart.setOption({
- backgroundColor:'#272b2f',
- tooltip:{trigger:'axis'},
- grid:{left:'6%', right:'6%', bottom:'18%'},
- xAxis:{type:'category', data:periods, axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{rotate:45,color:'#ccc'}},
- yAxis:{type:'value', axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{color:'#ccc'}},
- series:[{data:counts,type:type,smooth:type==='line',itemStyle:{color:color}, areaStyle:type==='line'?{}:undefined}]
- });
- return chart;
-}
+function renderChart(domId,data,type,color){ const el=document.getElementById(domId); if(!el) return; const chart=echarts.init(el); const periods=data.map(d=>(d.period??d.period===0)?d.period.toString():''); const counts=data.map(d=>d.count??d.packet_count??0); chart.setOption({backgroundColor:'#272b2f', tooltip:{trigger:'axis'}, grid:{left:'6%', right:'6%', bottom:'18%'}, xAxis:{type:'category', data:periods, axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{rotate:45,color:'#ccc'}}, yAxis:{type:'value', axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{color:'#ccc'}}, series:[{data:counts,type:type,smooth:type==='line',itemStyle:{color:color}, areaStyle:type==='line'?{}:undefined}]}); return chart; }
-function renderPieChart(elId,data,name){
- const el=document.getElementById(elId);
- if(!el) return null;
- const existing=echarts.getInstanceByDom(el);
- if(existing) existing.dispose();
- const chart=echarts.init(el);
- const top20=prepareTopN(data,20);
- chart.setOption({
- backgroundColor:"#272b2f",
- tooltip:{trigger:"item", formatter: params=>`${params.name}: ${Math.round(params.percent)}% (${params.value})`},
- series:[{
- name:name,
- type:"pie",
- radius:["30%","70%"],
- center:["50%","50%"],
- avoidLabelOverlap:true,
- itemStyle:{borderRadius:6,borderColor:"#272b2f",borderWidth:2},
- label:{show:true,formatter:"{b}\n{d}%", color:"#ccc", fontSize:10},
- labelLine:{show:true,length:10,length2:6},
- data:top20
- }]
- });
- return chart;
-}
+function renderPieChart(elId,data,name){ const el=document.getElementById(elId); if(!el) return; const chart=echarts.init(el); const top20=prepareTopN(data,20); chart.setOption({backgroundColor:"#272b2f", tooltip:{trigger:"item", formatter: params=>`${params.name}: ${Math.round(params.percent)}% (${params.value})`}, series:[{name:name, type:"pie", radius:["30%","70%"], center:["50%","50%"], avoidLabelOverlap:true, itemStyle:{borderRadius:6,borderColor:"#272b2f",borderWidth:2}, label:{show:true,formatter:"{b}\n{d}%", color:"#ccc", fontSize:10}, labelLine:{show:true,length:10,length2:6}, data:top20}]}); return chart; }
// --- Packet Type Pie Chart ---
async function fetchPacketTypeBreakdown(channel=null) {
- const requests = PORT_CONFIG.map(async ({port}) => {
- const data = await fetchStats('hour',24,port,channel);
- const total = (data || []).reduce((sum,d)=>sum+(d?.count??d?.packet_count??0),0);
- return {portnum: port, count: total};
+ const portnums = [1,3,4,67,70,71];
+ const requests = portnums.map(async pn => {
+ const data = await fetchStats('hour',24,pn,channel);
+ const total = (data || []).reduce((sum,d)=>sum+(d.count??d.packet_count??0),0);
+ return {portnum: pn, count: total};
});
const allData = await fetchStats('hour',24,null,channel);
- const totalAll = (allData||[]).reduce((sum,d)=>sum+(d?.count??d?.packet_count??0),0);
+ const totalAll = allData.reduce((sum,d)=>sum+(d.count??d.packet_count??0),0);
const results = await Promise.all(requests);
const trackedTotal = results.reduce((sum,d)=>sum+d.count,0);
const other = Math.max(totalAll - trackedTotal,0);
@@ -543,137 +243,44 @@ async function fetchPacketTypeBreakdown(channel=null) {
return results;
}
-function updateSummaryCards(summary){
- if(!summary) return;
- const nodesEl=document.getElementById("summaryTotalNodes");
- const packetsEl=document.getElementById("summaryTotalPackets");
- const packetsSeenEl=document.getElementById("summaryTotalPacketsSeen");
- if(nodesEl) nodesEl.textContent=Number(summary.total_nodes||0).toLocaleString();
- if(packetsEl) packetsEl.textContent=Number(summary.total_packets||0).toLocaleString();
- if(packetsSeenEl) packetsSeenEl.textContent=Number(summary.total_packets_seen||0).toLocaleString();
-}
-
-function populateChannelSelect(channels){
- const select=document.getElementById("channelSelect");
- if(!select) return;
- const unique=Array.from(new Set((channels||[]).filter(ch=>typeof ch==="string" && ch.trim().length>0))).sort((a,b)=>a.localeCompare(b));
- const prioritized=[];
- CHANNEL_PRESETS.forEach(preset=>{
- const idx=unique.indexOf(preset);
- if(idx>=0){
- prioritized.push(unique[idx]);
- unique.splice(idx,1);
- }
- });
- const ordered=[...new Set([...prioritized, ...unique])];
- select.innerHTML="";
- const allOption=document.createElement("option");
- allOption.value="";
- allOption.textContent="All Channels";
- allOption.setAttribute("data-translate-lang","all_channels");
- select.appendChild(allOption);
- ordered.forEach(ch=>{
- const opt=document.createElement("option");
- opt.value=ch;
- opt.textContent=ch;
- select.appendChild(opt);
- });
- let preferred=currentChannel;
- if(!preferred){
- preferred=CHANNEL_PRESETS.find(preset=>ordered.includes(preset))||"";
- }
- if(preferred && !ordered.includes(preferred)){
- preferred="";
- }
- select.value=preferred;
- currentChannel=select.value;
-}
-
-function setPortChart(port, chart){
- switch(port){
- case 1: chartPortnum1 = chart; break;
- case 3: chartPortnum3 = chart; break;
- case 4: chartPortnum4 = chart; break;
- case 67: chartPortnum67 = chart; break;
- case 70: chartPortnum70 = chart; break;
- case 71: chartPortnum71 = chart; break;
- default: break;
- }
-}
-
// --- Init ---
let chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71;
let chartDailyAll, chartDailyPortnum1;
let chartHwModel, chartRole, chartChannel;
let chartPacketTypes;
-let isRefreshing=false;
-
-async function refreshDashboard(){
- if(isRefreshing) return;
- isRefreshing=true;
- const channel=currentChannel||null;
- try{
- const [summary, dailyAllData, dailyPort1Data, hourlyAllData, portDataSets, nodes, packetTypesData] = await Promise.all([
- fetchSummary(channel),
- fetchStats('day',14,null,channel),
- fetchStats('day',14,1,channel),
- fetchStats('hour',24,null,channel),
- Promise.all(PORT_CONFIG.map(cfg=>fetchStats('hour',24,cfg.port,channel))),
- fetchNodes(channel),
- fetchPacketTypeBreakdown(channel)
- ]);
-
- updateSummaryCards(summary);
-
- updateTotalCount('total_daily_all',dailyAllData);
- chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a');
-
- updateTotalCount('total_daily_portnum_1',dailyPort1Data);
- chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722');
-
- updateTotalCount('total_hourly_all',hourlyAllData);
- chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6');
-
- PORT_CONFIG.forEach((cfg,idx)=>{
- const data=portDataSets?.[idx]||[];
- updateTotalCount(cfg.totalId,data);
- const chart=renderChart(cfg.domId,data,'bar',cfg.color);
- setPortChart(cfg.port,chart);
- });
-
- chartHwModel=renderPieChart("chart_hw_model",processCountField(nodes,"hw_model"),"Hardware");
- chartRole=renderPieChart("chart_role",processCountField(nodes,"role"),"Role");
- chartChannel=renderPieChart("chart_channel",processCountField(nodes,"channel"),"Channel");
- setNodeTableData(nodes);
-
- const formatted=(packetTypesData||[]).filter(d=>d.count>0).map(d=>({
- name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`),
- value: d.count
- }));
- chartPacketTypes=renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)");
- } finally {
- isRefreshing=false;
- }
-}
async function init(){
const channels = await fetchChannels();
- populateChannelSelect(channels);
- const select=document.getElementById("channelSelect");
- if(select && !select.dataset.listenerAttached){
- select.addEventListener("change",async e=>{
- currentChannel=e.target.value;
- await refreshDashboard();
- });
- select.dataset.listenerAttached="true";
- }
- document.querySelectorAll("#nodesTable thead th[data-sort-key]").forEach(th=>{
- if(!th.dataset.listenerAttached){
- th.addEventListener("click",handleNodeTableSort);
- th.dataset.listenerAttached="true";
- }
- });
- await refreshDashboard();
+ const select = document.getElementById("channelSelect");
+ channels.forEach(ch=>{ const opt = document.createElement("option"); opt.value = ch; opt.textContent = ch; select.appendChild(opt); });
+
+ const dailyAllData=await fetchStats('day',14);
+ updateTotalCount('total_daily_all',dailyAllData);
+ chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a');
+
+ const dailyPort1Data=await fetchStats('day',14,1);
+ updateTotalCount('total_daily_portnum_1',dailyPort1Data);
+ chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722');
+
+ const hourlyAllData=await fetchStats('hour',24);
+ updateTotalCount('total_hourly_all',hourlyAllData);
+ chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6');
+
+ const portnums=[1,3,4,67,70,71];
+ const colors=['#ff5722','#2196f3','#9c27b0','#ffeb3b','#795548','#4caf50'];
+ const domIds=['chart_portnum_1','chart_portnum_3','chart_portnum_4','chart_portnum_67','chart_portnum_70','chart_portnum_71'];
+ const totalIds=['total_portnum_1','total_portnum_3','total_portnum_4','total_portnum_67','total_portnum_70','total_portnum_71'];
+ const allData=await Promise.all(portnums.map(pn=>fetchStats('hour',24,pn)));
+ for(let i=0;i
d.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count }));
+ chartPacketTypes = renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)");
}
window.addEventListener('resize',()=>{ [chartHourlyAll,chartPortnum1,chartPortnum3,chartPortnum4,chartPortnum67,chartPortnum70,chartPortnum71, chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel,chartPacketTypes].forEach(c=>c?.resize()); });
@@ -735,6 +342,14 @@ document.querySelectorAll(".export-btn").forEach(btn=>{
});
});
+document.getElementById("channelSelect").addEventListener("change", async (e)=>{
+ const channel = e.target.value;
+ const packetTypesData = await fetchPacketTypeBreakdown(channel);
+ const formatted = packetTypesData.filter(d=>d.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count }));
+ chartPacketTypes?.dispose();
+ chartPacketTypes = renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)");
+});
+
init();
// --- Translation Loader ---
diff --git a/meshview/web.py b/meshview/web.py
index 728ab45..54f015b 100644
--- a/meshview/web.py
+++ b/meshview/web.py
@@ -33,17 +33,6 @@ SEQ_REGEX = re.compile(r"seq \d+")
SOFTWARE_RELEASE = "2.0.7 ~ 09-17-25"
CONFIG = config.CONFIG
-ACTIVITY_FILTERS = [
- ("1h", "Last 1 hour", timedelta(hours=1)),
- ("8h", "Last 8 hours", timedelta(hours=8)),
- ("1d", "Last 1 day", timedelta(days=1)),
- ("3d", "Last 3 days", timedelta(days=3)),
- ("7d", "Last 7 days", timedelta(days=7)),
- ("total", "All time", None),
-]
-ACTIVITY_OPTIONS = {value: window for value, _label, window in ACTIVITY_FILTERS}
-DEFAULT_ACTIVITY_OPTION = "1d"
-
env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape())
# Start Database
@@ -197,17 +186,6 @@ def format_timestamp(timestamp):
env.filters["node_id_to_hex"] = node_id_to_hex
env.filters["format_timestamp"] = format_timestamp
-
-def resolve_activity_window(
- raw_value: str | None, default_key: str = DEFAULT_ACTIVITY_OPTION
-):
- default_key = default_key if default_key in ACTIVITY_OPTIONS else DEFAULT_ACTIVITY_OPTION
- if raw_value:
- normalized = raw_value.strip().lower()
- if normalized in ACTIVITY_OPTIONS:
- return normalized, ACTIVITY_OPTIONS[normalized]
- return default_key, ACTIVITY_OPTIONS[default_key]
-
routes = web.RouteTableDef()
@@ -447,27 +425,15 @@ async def packet_details(request):
@routes.get("/firehose")
async def packet_details_firehose(request):
- portnum_value = request.query.get("portnum")
- channel = request.query.get("channel")
-
- portnum = None
- if portnum_value:
- try:
- portnum = int(portnum_value)
- except ValueError:
- logger.warning("Invalid portnum '%s' provided to /firehose", portnum_value)
-
- packets = await store.get_packets(portnum=portnum, limit=10, channel=channel)
- ui_packets = [Packet.from_model(p) for p in packets]
- latest_time = ui_packets[0].import_time.isoformat() if ui_packets else None
-
+ portnum = request.query.get("portnum")
+ if portnum:
+ portnum = int(portnum)
+ packets = await store.get_packets(portnum=portnum, limit=10)
template = env.get_template("firehose.html")
return web.Response(
text=template.render(
- packets=ui_packets,
+ packets=(Packet.from_model(p) for p in packets),
portnum=portnum,
- channel=channel,
- last_time=latest_time,
site_config=CONFIG,
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
),
@@ -488,18 +454,8 @@ async def firehose_updates(request):
logger.error(f"Failed to parse last_time '{last_time_str}': {e}")
last_time = None
- portnum_value = request.query.get("portnum")
- channel = request.query.get("channel")
-
- portnum = None
- if portnum_value:
- try:
- portnum = int(portnum_value)
- except ValueError:
- logger.warning("Invalid portnum '%s' provided to /firehose/updates", portnum_value)
-
# Query packets after last_time (microsecond precision)
- packets = await store.get_packets(after=last_time, limit=10, portnum=portnum, channel=channel)
+ packets = await store.get_packets(after=last_time, limit=10)
# Convert to UI model
ui_packets = [Packet.from_model(p) for p in packets]
@@ -1203,15 +1159,7 @@ async def net(request):
@routes.get("/map")
async def map(request):
try:
- activity_param = request.query.get("active")
- if not activity_param:
- legacy_days = request.query.get("days_active")
- if legacy_days and legacy_days.isdigit():
- activity_param = "total" if legacy_days == "0" else f"{legacy_days}d"
-
- selected_activity, activity_window = resolve_activity_window(activity_param)
-
- nodes = await store.get_nodes(active_within=activity_window)
+ nodes = await store.get_nodes(days_active=3)
# Filter out nodes with no latitude
nodes = [node for node in nodes if node.last_lat is not None]
@@ -1244,8 +1192,6 @@ async def map(request):
text=template.render(
nodes=nodes,
custom_view=custom_view,
- activity_filters=ACTIVITY_FILTERS,
- selected_activity=selected_activity,
site_config=CONFIG,
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
),
@@ -1384,26 +1330,22 @@ async def chat(request):
# Assuming the route URL structure is /nodegraph
@routes.get("/nodegraph")
async def nodegraph(request):
- activity_param = request.query.get("active")
- if not activity_param:
- legacy_days = request.query.get("days_active")
- if legacy_days and legacy_days.isdigit():
- activity_param = "total" if legacy_days == "0" else f"{legacy_days}d"
-
- selected_activity, activity_window = resolve_activity_window(activity_param)
-
- nodes = await store.get_nodes(active_within=activity_window)
-
- active_node_ids = {node.node_id for node in nodes}
+ nodes = await store.get_nodes(days_active=3) # Fetch nodes for the given channel
+ node_ids = set()
edges_map = defaultdict(
lambda: {"weight": 0, "type": None}
) # weight is based on the number of traceroutes and neighbor info packets
+ used_nodes = set() # This will track nodes involved in edges (including traceroutes)
since = datetime.timedelta(hours=48)
traceroutes = []
# Fetch traceroutes
async for tr in store.get_traceroutes(since):
+ node_ids.add(tr.gateway_node_id)
+ node_ids.add(tr.packet.from_node_id)
+ node_ids.add(tr.packet.to_node_id)
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
+ node_ids.update(route.route)
path = [tr.packet.from_node_id]
path.extend(route.route)
@@ -1419,12 +1361,18 @@ async def nodegraph(request):
edge_pair = (path[i], path[i + 1])
edges_map[edge_pair]["weight"] += 1
edges_map[edge_pair]["type"] = "traceroute"
+ used_nodes.add(path[i]) # Add all nodes in the traceroute path
+ used_nodes.add(path[i + 1]) # Add all nodes in the traceroute path
# Fetch NeighborInfo packets
for packet in await store.get_packets(portnum=PortNum.NEIGHBORINFO_APP, after=since):
try:
_, neighbor_info = decode_payload.decode(packet)
+ node_ids.add(packet.from_node_id)
+ used_nodes.add(packet.from_node_id)
for node in neighbor_info.neighbors:
+ node_ids.add(node.node_id)
+ used_nodes.add(node.node_id)
edge_pair = (node.node_id, packet.from_node_id)
edges_map[edge_pair]["weight"] += 1
@@ -1433,16 +1381,7 @@ async def nodegraph(request):
logger.error(f"Error decoding NeighborInfo packet: {e}")
# Convert edges_map to a list of dicts with colors
- filtered_edge_items = [
- ((frm, to), info)
- for (frm, to), info in edges_map.items()
- if frm in active_node_ids and to in active_node_ids
- ]
- max_weight = (
- max(info["weight"] for _, info in filtered_edge_items)
- if filtered_edge_items
- else 1
- )
+ max_weight = max(i['weight'] for i in edges_map.values()) if edges_map else 1
edges = [
{
"from": frm,
@@ -1450,22 +1389,17 @@ async def nodegraph(request):
"type": info["type"],
"weight": max([info['weight'] / float(max_weight) * 10, 1]),
}
- for (frm, to), info in filtered_edge_items
+ for (frm, to), info in edges_map.items()
]
# Filter nodes to only include those involved in edges (including traceroutes)
- connected_node_ids = {
- node_id for edge in edges for node_id in (edge["from"], edge["to"])
- }
- nodes_with_edges = [node for node in nodes if node.node_id in connected_node_ids]
+ nodes_with_edges = [node for node in nodes if node.node_id in used_nodes]
template = env.get_template("nodegraph.html")
return web.Response(
text=template.render(
nodes=nodes_with_edges,
edges=edges, # Pass edges with color info
- activity_filters=ACTIVITY_FILTERS,
- selected_activity=selected_activity,
site_config=CONFIG,
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
),
@@ -1524,7 +1458,6 @@ async def api_chat(request):
# Parse query params
limit_str = request.query.get("limit", "20")
since_str = request.query.get("since")
- channel_filter = request.query.get("channel")
# Clamp limit between 1 and 200
try:
@@ -1544,9 +1477,7 @@ async def api_chat(request):
packets = await store.get_packets(
node_id=0xFFFFFFFF,
portnum=PortNum.TEXT_MESSAGE_APP,
- after=since,
limit=limit,
- channel=channel_filter,
)
ui_packets = [Packet.from_model(p) for p in packets]
@@ -1616,20 +1547,17 @@ async def api_nodes(request):
role = request.query.get("role")
channel = request.query.get("channel")
hw_model = request.query.get("hw_model")
- activity_param = request.query.get("active")
- if not activity_param:
- legacy_days = request.query.get("days_active")
- if legacy_days and legacy_days.isdigit():
- activity_param = "total" if legacy_days == "0" else f"{legacy_days}d"
+ days_active = request.query.get("days_active")
- _, activity_window = resolve_activity_window(activity_param, default_key="total")
+ if days_active:
+ try:
+ days_active = int(days_active)
+ except ValueError:
+ days_active = None
# Fetch nodes from database using your get_nodes function
nodes = await store.get_nodes(
- role=role,
- channel=channel,
- hw_model=hw_model,
- active_within=activity_window,
+ role=role, channel=channel, hw_model=hw_model, days_active=days_active
)
# Prepare the JSON response
@@ -1665,19 +1593,6 @@ async def api_packets(request):
limit = int(request.query.get("limit", 50))
since_str = request.query.get("since")
since_time = None
- channel_values = []
-
- # Support repeated ?channel=foo&channel=bar and comma-separated values
- if "channel" in request.query:
- raw_channels = request.query.getall("channel", [])
- if not raw_channels:
- raw_value = request.query.get("channel")
- if raw_value:
- raw_channels = [raw_value]
- for raw in raw_channels:
- if raw:
- parts = [part.strip() for part in raw.split(",") if part.strip()]
- channel_values.extend(parts)
# Parse 'since' timestamp if provided
if since_str:
@@ -1687,14 +1602,7 @@ async def api_packets(request):
logger.error(f"Failed to parse 'since' timestamp '{since_str}': {e}")
# Fetch last N packets
- if not channel_values:
- channel_filter = None
- elif len(channel_values) == 1:
- channel_filter = channel_values[0]
- else:
- channel_filter = channel_values
-
- packets = await store.get_packets(limit=limit, after=since_time, channel=channel_filter)
+ packets = await store.get_packets(limit=limit, after=since_time)
packets = [Packet.from_model(p) for p in packets]
# Build JSON response (no raw_payload)
@@ -1770,22 +1678,6 @@ async def api_stats(request):
return web.json_response(stats)
-@routes.get("/api/stats/summary")
-async def api_stats_summary(request):
- channel = request.query.get("channel") or None
- total_packets = await store.get_total_packet_count(channel=channel)
- total_nodes = await store.get_total_node_count(channel=channel)
- total_packets_seen = await store.get_total_packet_seen_count(channel=channel)
-
- return web.json_response(
- {
- "total_packets": total_packets,
- "total_nodes": total_nodes,
- "total_packets_seen": total_packets_seen,
- }
- )
-
-
@routes.get("/api/config")
async def api_config(request):
try: