-
+
Total: 0
@@ -214,17 +227,123 @@ async function fetchStats(period_type,length,portnum=null,channel=null){
}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 fetchNodes(){
+ try{
+ const res=await fetch("/api/nodes");
+ const json=await res.json();
+ return json.nodes||[];
+ }catch{
+ return [];
+ }
+}
-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; }
+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=>{
+ 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; 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 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; 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) {
@@ -234,8 +353,10 @@ async function fetchPacketTypeBreakdown(channel=null) {
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);
@@ -250,40 +371,102 @@ let chartHwModel, chartRole, chartChannel;
let chartPacketTypes;
async function init(){
+ // Channel selector
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); });
+ channels.forEach(ch=>{
+ const opt = document.createElement("option");
+ opt.value = ch;
+ opt.textContent = ch;
+ select.appendChild(opt);
+ });
+ // Daily all ports
const dailyAllData=await fetchStats('day',14);
updateTotalCount('total_daily_all',dailyAllData);
chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a');
+ // Daily port 1
const dailyPort1Data=await fetchStats('day',14,1);
updateTotalCount('total_daily_portnum_1',dailyPort1Data);
chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722');
+ // Hourly all ports
const hourlyAllData=await fetchStats('hour',24);
updateTotalCount('total_hourly_all',hourlyAllData);
chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6');
+ // Hourly per port
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
fetchStats('hour',24,pn)));
+ for(let i=0;id.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count }));
+ 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)");
+
+ // Total packet + total seen from /api/stats/count
+ try {
+ const countsRes = await fetch("/api/stats/count");
+ if (countsRes.ok) {
+ const countsJson = await countsRes.json();
+ const elPackets = document.getElementById("summary_packets");
+ const elSeen = document.getElementById("summary_seen");
+ if (elPackets) {
+ elPackets.textContent = (countsJson.total_packets || 0).toLocaleString();
+ }
+ if (elSeen) {
+ elSeen.textContent = (countsJson.total_seen || 0).toLocaleString();
+ }
+ }
+ } catch (err) {
+ console.error("Failed to load /api/stats/count:", err);
+ }
}
-window.addEventListener('resize',()=>{ [chartHourlyAll,chartPortnum1,chartPortnum3,chartPortnum4,chartPortnum67,chartPortnum70,chartPortnum71, chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel,chartPacketTypes].forEach(c=>c?.resize()); });
+window.addEventListener('resize',()=>{
+ [
+ chartHourlyAll,
+ chartPortnum1,
+ chartPortnum3,
+ chartPortnum4,
+ chartPortnum67,
+ chartPortnum70,
+ chartPortnum71,
+ chartDailyAll,
+ chartDailyPortnum1,
+ chartHwModel,
+ chartRole,
+ chartChannel,
+ chartPacketTypes
+ ].forEach(c=>c?.resize());
+});
const modal=document.getElementById("chartModal");
const modalChartEl=document.getElementById("modalChart");
@@ -345,11 +528,19 @@ 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 }));
+ 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)");
});
+// Kick everything off
init();
// --- Load config and translations ---
@@ -383,6 +574,5 @@ async function loadConfigAndTranslations() {
// Call after init
loadConfigAndTranslations();
-
{% endblock %}
diff --git a/meshview/templates/traceroute.html b/meshview/templates/traceroute.html
deleted file mode 100644
index c2e329a..0000000
--- a/meshview/templates/traceroute.html
+++ /dev/null
@@ -1,94 +0,0 @@
-{% block head %}
-
-{% endblock %}
-
-{% block body %}
-
-
-
-{% endblock %}
diff --git a/meshview/web.py b/meshview/web.py
index b71816c..6cd61c7 100644
--- a/meshview/web.py
+++ b/meshview/web.py
@@ -269,6 +269,14 @@ async def top(request):
content_type="text/html",
)
+@routes.get("/stats")
+async def stats(request):
+ template = env.get_template("stats.html")
+ return web.Response(
+ text=template.render(),
+ content_type="text/html",
+ )
+
# Keep !!
@routes.get("/graph/traceroute/{packet_id}")
@@ -377,7 +385,7 @@ async def graph_traceroute(request):
content_type="image/svg+xml",
)
-
+'''
@routes.get("/stats")
async def stats(request):
try:
@@ -399,86 +407,7 @@ async def stats(request):
status=500,
content_type="text/plain",
)
-
-
'''
-@routes.get("/top")
-async def top(request):
- import time
-
- try:
- # Check if performance metrics should be displayed
- show_perf = request.query.get("perf", "").lower() in ("true", "1", "yes")
-
- # Start overall timing
- start_time = time.perf_counter()
- timing_data = None
-
- node_id = request.query.get("node_id") # Get node_id from the URL query parameters
-
- if node_id:
- # If node_id is provided, fetch traffic data for the specific node
- db_start = time.perf_counter()
- node_traffic = await store.get_node_traffic(int(node_id))
- db_time = time.perf_counter() - db_start
-
- template = env.get_template("node_traffic.html")
- html_content = template.render(
- traffic=node_traffic, node_id=node_id, site_config=CONFIG
- )
- else:
- # Otherwise, fetch top traffic nodes as usual
- db_start = time.perf_counter()
- top_nodes = await store.get_top_traffic_nodes()
- db_time = time.perf_counter() - db_start
-
- # Data processing timing
- process_start = time.perf_counter()
-
- # Count records processed
- total_packets = sum(node.get('total_packets_sent', 0) for node in top_nodes)
- total_seen = sum(node.get('total_times_seen', 0) for node in top_nodes)
-
- process_time = time.perf_counter() - process_start
-
- # Calculate total time
- total_time = time.perf_counter() - start_time
-
- # Only include timing_data if perf parameter is set
- if show_perf:
- timing_data = {
- 'db_query_ms': f"{db_time * 1000:.2f}",
- 'processing_ms': f"{process_time * 1000:.2f}",
- 'total_ms': f"{total_time * 1000:.2f}",
- 'node_count': len(top_nodes),
- 'total_packets': total_packets,
- 'total_seen': total_seen,
- }
-
- template = env.get_template("top.html")
- html_content = template.render(
- nodes=top_nodes,
- timing_data=timing_data,
- site_config=CONFIG,
- SOFTWARE_RELEASE=SOFTWARE_RELEASE,
- )
-
- return web.Response(
- text=html_content,
- content_type="text/html",
- )
- except Exception as e:
- logger.error(f"Error in /top: {e}")
- template = env.get_template("error.html")
- rendered = template.render(
- error_message="An error occurred in /top",
- error_details=traceback.format_exc(),
- site_config=CONFIG,
- SOFTWARE_RELEASE=SOFTWARE_RELEASE,
- )
- return web.Response(text=rendered, status=500, content_type="text/html")
-'''
-
async def run_server():
# Wait for database migrations to complete before starting web server