mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-10 19:03:14 +02:00
Updates stats with pie chart and report for channel
This commit is contained in:
@@ -340,3 +340,31 @@ async def get_packet_stats(
|
||||
"from_node": from_node,
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
period_type: "hour" or "day"
|
||||
length: number of hours or days to look back
|
||||
"""
|
||||
now = datetime.now()
|
||||
|
||||
if period_type == "hour":
|
||||
start_time = now - timedelta(hours=length)
|
||||
elif period_type == "day":
|
||||
start_time = now - timedelta(days=length)
|
||||
else:
|
||||
raise ValueError("period_type must be 'hour' or 'day'")
|
||||
|
||||
async with database.async_session() as session:
|
||||
q = (
|
||||
select(Packet.channel)
|
||||
.where(Packet.import_time >= start_time)
|
||||
.distinct()
|
||||
.order_by(Packet.channel)
|
||||
)
|
||||
|
||||
result = await session.execute(q)
|
||||
channels = [row[0] for row in result if row[0] is not None]
|
||||
return channels
|
||||
|
||||
+119
-72
@@ -76,6 +76,15 @@
|
||||
color: #66bb6a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#channelSelect {
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 6px;
|
||||
background:#444;
|
||||
color:#fff;
|
||||
border:none;
|
||||
border-radius:4px;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
@@ -84,26 +93,24 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="main-container">
|
||||
<h2 class="main-header">Mesh Statistics - Summary (all available in Database) </h2>
|
||||
<h2 class="main-header">Mesh Statistics - Summary (all available in Database)</h2>
|
||||
|
||||
<div class="summary-container" style="display:flex; justify-content:space-between; gap:10px; margin-bottom:20px;">
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p>Total Nodes</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_nodes) }}</div>
|
||||
<div class="summary-container" style="display:flex; justify-content:space-between; gap:10px; margin-bottom:20px;">
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p>Total Nodes</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_nodes) }}</div>
|
||||
</div>
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p>Total Packets</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_packets) }}</div>
|
||||
</div>
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p>Total Packets Seen</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_packets_seen) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p>Total Packets</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_packets) }}</div>
|
||||
</div>
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p>Total Packets Seen</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_packets_seen) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{# Daily Charts #}
|
||||
<!-- Daily Charts -->
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Day - All Ports (Last 14 Days)</p>
|
||||
<div id="total_daily_all" class="total-count">Total: 0</div>
|
||||
@@ -120,7 +127,18 @@
|
||||
<div id="chart_daily_portnum_1" class="chart"></div>
|
||||
</div>
|
||||
|
||||
{# Hourly Charts #}
|
||||
<!-- Packet Types Pie Chart with Channel Selector (moved here) -->
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packet Types - Last 24 Hours</p>
|
||||
<select id="channelSelect">
|
||||
<option value="">All Channels</option>
|
||||
</select>
|
||||
<button class="expand-btn" data-chart="chart_packet_types">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_packet_types">Export CSV</button>
|
||||
<div id="chart_packet_types" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hourly Charts -->
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Hour - All Ports</p>
|
||||
<div id="total_hourly_all" class="total-count">Total: 0</div>
|
||||
@@ -137,47 +155,7 @@
|
||||
<div id="chart_portnum_1" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Hour - Position (Port 3)</p>
|
||||
<div id="total_portnum_3" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_portnum_3">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_portnum_3">Export CSV</button>
|
||||
<div id="chart_portnum_3" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Hour - Node Info (Port 4)</p>
|
||||
<div id="total_portnum_4" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_portnum_4">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_portnum_4">Export CSV</button>
|
||||
<div id="chart_portnum_4" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Hour - Telemetry (Port 67)</p>
|
||||
<div id="total_portnum_67" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_portnum_67">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_portnum_67">Export CSV</button>
|
||||
<div id="chart_portnum_67" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Hour - Traceroute (Port 70)</p>
|
||||
<div id="total_portnum_70" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_portnum_70">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_portnum_70">Export CSV</button>
|
||||
<div id="chart_portnum_70" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header">Packets per Hour - Neighbor Info (Port 71)</p>
|
||||
<div id="total_portnum_71" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_portnum_71">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_portnum_71">Export CSV</button>
|
||||
<div id="chart_portnum_71" class="chart"></div>
|
||||
</div>
|
||||
|
||||
{# Node breakdown charts #}
|
||||
<!-- Node breakdown charts -->
|
||||
<div class="card-section">
|
||||
<p class="section-header">Hardware Breakdown</p>
|
||||
<button class="expand-btn" data-chart="chart_hw_model">Expand Chart</button>
|
||||
@@ -200,7 +178,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Modal for expanded charts #}
|
||||
<!-- Modal for expanded charts -->
|
||||
<div id="chartModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%;
|
||||
background:rgba(0,0,0,0.7); z-index:1000; justify-content:center; align-items:center;">
|
||||
<div style="position:relative; width:80%; max-width:1000px; height:80%;
|
||||
@@ -214,11 +192,21 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PORTNUM_LABELS = {
|
||||
1: "Text Messages",
|
||||
3: "Position",
|
||||
4: "Node Info",
|
||||
67: "Telemetry",
|
||||
70: "Traceroute",
|
||||
71: "Neighbor Info"
|
||||
};
|
||||
|
||||
// --- Fetch & Processing ---
|
||||
async function fetchStats(period_type,length,portnum=null){
|
||||
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=${channel}`;
|
||||
const res=await fetch(url);
|
||||
if(!res.ok) return [];
|
||||
const json=await res.json();
|
||||
@@ -234,6 +222,14 @@ async function fetchNodes(){
|
||||
}catch{return [];}
|
||||
}
|
||||
|
||||
async function fetchChannels(){
|
||||
try{
|
||||
const res = await fetch("/api/channels");
|
||||
const json = await res.json();
|
||||
return json.channels || [];
|
||||
}catch{return [];}
|
||||
}
|
||||
|
||||
function processCountField(nodes,field){
|
||||
const counts={};
|
||||
nodes.forEach(n=>{
|
||||
@@ -300,12 +296,43 @@ function renderPieChart(elId,data,name){
|
||||
return chart;
|
||||
}
|
||||
|
||||
// --- Packet Type Pie Chart ---
|
||||
async function fetchPacketTypeBreakdown(channel=null) {
|
||||
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 results = await Promise.all(requests);
|
||||
const trackedTotal = results.reduce((sum,d)=>sum+d.count,0);
|
||||
const other = Math.max(totalAll - trackedTotal,0);
|
||||
if(other>0) results.push({portnum:"other", count:other});
|
||||
return results;
|
||||
}
|
||||
|
||||
// --- Init ---
|
||||
let chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71;
|
||||
let chartDailyAll, chartDailyPortnum1;
|
||||
let chartHwModel, chartRole, chartChannel;
|
||||
let chartPacketTypes;
|
||||
|
||||
async function init(){
|
||||
// Populate channels
|
||||
const channels = await fetchChannels();
|
||||
const select = document.getElementById("channelSelect");
|
||||
channels.forEach(ch=>{
|
||||
const opt = document.createElement("option");
|
||||
opt.value = ch;
|
||||
opt.textContent = ch;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
// Daily
|
||||
const dailyAllData=await fetchStats('day',14);
|
||||
updateTotalCount('total_daily_all',dailyAllData);
|
||||
chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a',false);
|
||||
@@ -314,6 +341,7 @@ async function init(){
|
||||
updateTotalCount('total_daily_portnum_1',dailyPort1Data);
|
||||
chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722',false);
|
||||
|
||||
// Hourly
|
||||
const hourlyAllData=await fetchStats('hour',24);
|
||||
updateTotalCount('total_hourly_all',hourlyAllData);
|
||||
chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6',true);
|
||||
@@ -328,16 +356,25 @@ async function init(){
|
||||
window['chartPortnum'+portnums[i]]=renderChart(domIds[i],allData[i],'bar',colors[i],true);
|
||||
}
|
||||
|
||||
// Node Breakdown
|
||||
const nodes=await fetchNodes();
|
||||
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");
|
||||
|
||||
// Packet Type Pie Chart
|
||||
const packetTypesData = await fetchPacketTypeBreakdown();
|
||||
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)");
|
||||
}
|
||||
|
||||
// --- Resize ---
|
||||
window.addEventListener('resize',()=>{
|
||||
[chartHourlyAll,chartPortnum1,chartPortnum3,chartPortnum4,chartPortnum67,chartPortnum70,chartPortnum71,
|
||||
chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel].forEach(c=>c?.resize());
|
||||
chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel,chartPacketTypes].forEach(c=>c?.resize());
|
||||
});
|
||||
|
||||
// --- Modal ---
|
||||
@@ -348,25 +385,23 @@ let modalChart=null;
|
||||
document.querySelectorAll(".expand-btn").forEach(btn=>{
|
||||
btn.addEventListener("click",()=>{
|
||||
const chartId=btn.getAttribute("data-chart");
|
||||
const chartInstance=echarts.getInstanceByDom(document.getElementById(chartId));
|
||||
if(!chartInstance) return;
|
||||
const chartData=chartInstance.getOption();
|
||||
const sourceChart=echarts.getInstanceByDom(document.getElementById(chartId));
|
||||
if(!sourceChart)return;
|
||||
modal.style.display="flex";
|
||||
if(modalChart) modalChart.dispose();
|
||||
modalChart=echarts.init(modalChartEl);
|
||||
modalChart.setOption(chartData);
|
||||
modalChart.resize();
|
||||
modalChart.setOption(sourceChart.getOption());
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("closeModal").addEventListener("click",()=>{
|
||||
modal.style.display="none";
|
||||
if(modalChart) modalChart.dispose();
|
||||
modalChart?.dispose();
|
||||
modalChart=null;
|
||||
});
|
||||
|
||||
// --- CSV Export ---
|
||||
function downloadCSV(filename,rows){
|
||||
const csvContent=rows.map(e=>e.join(",")).join("\n");
|
||||
const csvContent=rows.map(r=>r.map(v=>`"${v}"`).join(",")).join("\n");
|
||||
const blob=new Blob([csvContent],{type:"text/csv;charset=utf-8;"});
|
||||
const link=document.createElement("a");
|
||||
link.href=URL.createObjectURL(blob);
|
||||
@@ -380,13 +415,13 @@ document.querySelectorAll(".export-btn").forEach(btn=>{
|
||||
btn.addEventListener("click",()=>{
|
||||
const chartId=btn.getAttribute("data-chart");
|
||||
const chart=echarts.getInstanceByDom(document.getElementById(chartId));
|
||||
if(!chart) return;
|
||||
if(!chart)return;
|
||||
const option=chart.getOption();
|
||||
let rows=[];
|
||||
if(option.series[0].type==="bar"||option.series[0].type==="line"){
|
||||
rows.push(["Period","Count"]);
|
||||
const xData=option.xAxis[0].data;
|
||||
const yData=option.series[0].data;
|
||||
rows.push(["Period","Count"]);
|
||||
for(let i=0;i<xData.length;i++) rows.push([xData[i],yData[i]]);
|
||||
}
|
||||
if(option.series[0].type==="pie"){
|
||||
@@ -401,6 +436,18 @@ document.querySelectorAll(".export-btn").forEach(btn=>{
|
||||
});
|
||||
});
|
||||
|
||||
// --- Channel filter for Packet Types ---
|
||||
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();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1340,6 +1340,17 @@ async def get_config(request):
|
||||
# The response includes "latest_import_time" for frontend to keep track of the newest message timestamp.
|
||||
# The backend fetches extra packets (limit*5) to account for filtering messages like "seq N" and since filtering.
|
||||
|
||||
@routes.get("/api/channels")
|
||||
async def api_channels(request: web.Request):
|
||||
period_type = request.query.get("period_type", "hour")
|
||||
length = int(request.query.get("length", 24))
|
||||
|
||||
try:
|
||||
channels = await store.get_channels_in_period(period_type, length)
|
||||
return web.json_response({"channels": channels})
|
||||
except Exception as e:
|
||||
return web.json_response({"channels": [], "error": str(e)})
|
||||
|
||||
|
||||
@routes.get("/api/chat")
|
||||
async def api_chat(request):
|
||||
|
||||
Reference in New Issue
Block a user