From 973dcbfe510eb9322113213d0d8640901e103d0c Mon Sep 17 00:00:00 2001 From: pablorevilla-meshtastic Date: Thu, 5 Mar 2026 11:36:26 -0800 Subject: [PATCH] fixed the db and added more reporting --- add_relay_node_column.py | 24 --- ...1d8f2a9e3_add_relay_node_to_packet_seen.py | 39 ----- meshview/lang/en.json | 3 +- meshview/lang/es.json | 3 +- meshview/models.py | 1 - meshview/mqtt_store.py | 1 - meshview/templates/packet.html | 1 - meshview/templates/stats.html | 145 +++++++++++++++--- meshview/web_api/api.py | 47 +++--- 9 files changed, 154 insertions(+), 110 deletions(-) delete mode 100644 add_relay_node_column.py delete mode 100644 alembic/versions/c6b1d8f2a9e3_add_relay_node_to_packet_seen.py diff --git a/add_relay_node_column.py b/add_relay_node_column.py deleted file mode 100644 index e144f31..0000000 --- a/add_relay_node_column.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Migration script to add relay_node column to packet_seen table. -Run this once to update your database schema. -""" - -import asyncio - -from meshview import database - - -async def add_relay_node_column(): - """Add relay_node column to packet_seen table""" - async with database.async_session() as session: - # Add the column - await session.execute(""" - ALTER TABLE packet_seen - ADD COLUMN IF NOT EXISTS relay_node BIGINT - """) - await session.commit() - print("Successfully added relay_node column to packet_seen table") - - -if __name__ == "__main__": - asyncio.run(add_relay_node_column()) diff --git a/alembic/versions/c6b1d8f2a9e3_add_relay_node_to_packet_seen.py b/alembic/versions/c6b1d8f2a9e3_add_relay_node_to_packet_seen.py deleted file mode 100644 index 79013dd..0000000 --- a/alembic/versions/c6b1d8f2a9e3_add_relay_node_to_packet_seen.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Add relay_node column to packet_seen - -Revision ID: c6b1d8f2a9e3 -Revises: 4f1d2a9c8b71 -Create Date: 2026-03-05 00:00:00.000000 - -""" - -from collections.abc import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "c6b1d8f2a9e3" -down_revision: str | None = "4f1d2a9c8b71" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - conn = op.get_bind() - inspector = sa.inspect(conn) - packet_seen_columns = {col["name"] for col in inspector.get_columns("packet_seen")} - - if "relay_node" not in packet_seen_columns: - with op.batch_alter_table("packet_seen", schema=None) as batch_op: - batch_op.add_column(sa.Column("relay_node", sa.BigInteger(), nullable=True)) - - -def downgrade() -> None: - conn = op.get_bind() - inspector = sa.inspect(conn) - packet_seen_columns = {col["name"] for col in inspector.get_columns("packet_seen")} - - if "relay_node" in packet_seen_columns: - with op.batch_alter_table("packet_seen", schema=None) as batch_op: - batch_op.drop_column("relay_node") diff --git a/meshview/lang/en.json b/meshview/lang/en.json index 71d95e6..4ab6368 100644 --- a/meshview/lang/en.json +++ b/meshview/lang/en.json @@ -127,7 +127,8 @@ "total_gateways": "Total Gateways", "total_packets": "Total Packets", "total_packets_seen": "Total Packets Seen", - "daily_snapshot_histogram": "Daily Network Snapshot Histogram (Last 30 Days)", + "daily_snapshot_packets_history": "Daily Packet Snapshot History (All Available Days)", + "daily_snapshot_nodes_gateways_history": "Daily Node/Gateway Snapshot History (All Available Days)", "packets_per_day_all": "Packets per Day - All Ports (Last 14 Days)", "packets_per_day_text": "Packets per Day - Text Messages (Port 1, Last 14 Days)", "packets_per_hour_all": "Packets per Hour - All Ports", diff --git a/meshview/lang/es.json b/meshview/lang/es.json index 0c33e9a..9795aad 100644 --- a/meshview/lang/es.json +++ b/meshview/lang/es.json @@ -123,7 +123,8 @@ "total_gateways": "Gateways Totales", "total_packets": "Paquetes Totales", "total_packets_seen": "Paquetes Totales Vistos", - "daily_snapshot_histogram": "Histograma Diario de Instantáneas de Red (Últimos 30 Días)", + "daily_snapshot_packets_history": "Historial Diario de Instantáneas de Paquetes (Todos los Días Disponibles)", + "daily_snapshot_nodes_gateways_history": "Historial Diario de Instantáneas de Nodos/Gateways (Todos los Días Disponibles)", "packets_per_day_all": "Paquetes por Día - Todos los Puertos (Últimos 14 Días)", "packets_per_day_text": "Paquetes por Día - Mensajes de Texto (Puerto 1, Últimos 14 Días)", "packets_per_hour_all": "Paquetes por Hora - Todos los Puertos", diff --git a/meshview/models.py b/meshview/models.py index 21c1ab3..b450336 100644 --- a/meshview/models.py +++ b/meshview/models.py @@ -74,7 +74,6 @@ class PacketSeen(Base): rx_time: Mapped[int] = mapped_column(BigInteger, primary_key=True) hop_limit: Mapped[int] = mapped_column(nullable=True) hop_start: Mapped[int] = mapped_column(nullable=True) - relay_node: Mapped[int] = mapped_column(BigInteger, nullable=True) channel: Mapped[str] = mapped_column(nullable=True) rx_snr: Mapped[float] = mapped_column(nullable=True) rx_rssi: Mapped[int] = mapped_column(nullable=True) diff --git a/meshview/mqtt_store.py b/meshview/mqtt_store.py index abedc49..e197134 100644 --- a/meshview/mqtt_store.py +++ b/meshview/mqtt_store.py @@ -206,7 +206,6 @@ async def process_envelope(topic, env): "rx_rssi": env.packet.rx_rssi, "hop_limit": env.packet.hop_limit, "hop_start": env.packet.hop_start, - "relay_node": env.packet.relay_node if env.packet.relay_node else None, "topic": topic, "import_time_us": now_us, } diff --git a/meshview/templates/packet.html b/meshview/templates/packet.html index 48595d3..42e3122 100644 --- a/meshview/templates/packet.html +++ b/meshview/templates/packet.html @@ -469,7 +469,6 @@ seenTableBody.innerHTML = Object.keys(hopGroups) ${s.node_id}
HW: ${node?.hw_model ?? "—"}
Channel: ${s.channel ?? "—"}
- Relay Node: ${s.relay_node ?? "—"}
${ distanceKm !== null ? `Distance: diff --git a/meshview/templates/stats.html b/meshview/templates/stats.html index 74c7c0f..2182065 100644 --- a/meshview/templates/stats.html +++ b/meshview/templates/stats.html @@ -119,12 +119,21 @@
-

- Daily Network Snapshot Histogram (Last 30 Days) +

+ Daily Packet Snapshot History (All Available Days)

- - -
+ + +
+
+ +
+

+ Daily Node/Gateway Snapshot History (All Available Days) +

+ + +
@@ -256,9 +265,9 @@ async function fetchStats(period_type,length,portnum=null,channel=null){ }catch{return [];} } -async function fetchDailySnapshots(length=30){ +async function fetchDailySnapshots(){ try{ - const res=await fetch(`/api/snapshots/daily?length=${length}`); + const res=await fetch("/api/snapshots/daily"); if(!res.ok) return []; const json=await res.json(); return json.data||[]; @@ -285,6 +294,18 @@ async function fetchChannels(){ } } +function buildPacketTypeBreakdownFromSeries(allData, perPortSeries){ + const totalAll = (allData || []).reduce((sum,d)=>sum+(d.count??d.packet_count??0),0); + const results = perPortSeries.map(({portnum, data}) => ({ + portnum, + count: (data || []).reduce((sum,d)=>sum+(d.count??d.packet_count??0),0), + })); + 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; +} + function processCountField(nodes,field){ const counts={}; nodes.forEach(n=>{ @@ -383,19 +404,50 @@ function renderPieChart(elId,data,name){ return chart; } -function renderDailySnapshotChart(domId,data){ +function makeTelemetryLine(name, color, seriesData) { + return { + name, + type: "line", + smooth: true, + connectNulls: true, + showSymbol: true, + symbol: "circle", + symbolSize: 7, + lineStyle: { + width: 2, + color, + shadowColor: color.replace("1)", "0.35)"), + shadowBlur: 8, + shadowOffsetY: 3 + }, + itemStyle: { + color, + borderColor: "#000", + borderWidth: 1 + }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: color.replace("1)", "0.5)") }, + { offset: 0.6, color: color.replace("1)", "0.2)") }, + { offset: 1, color: "rgba(0,0,0,0)" } + ]) + }, + data: seriesData + }; +} + +function renderDailySnapshotPacketsChart(domId,data){ const el=document.getElementById(domId); if(!el) return; const chart=echarts.init(el); const labels=data.map(d=>d.date||""); - const nodeCounts=data.map(d=>d.node_count??0); const packetCounts=data.map(d=>d.packet_count??0); - const gatewayCounts=data.map(d=>d.gateway_count??0); + chart.setOption({ backgroundColor:'#272b2f', tooltip:{trigger:'axis'}, legend:{ - data:['Nodes','Packets','Gateways'], + data:['Packets'], textStyle:{color:'#ccc'} }, grid:{left:'6%', right:'6%', bottom:'18%'}, @@ -411,9 +463,42 @@ function renderDailySnapshotChart(domId,data){ axisLabel:{color:'#ccc'} }, series:[ - {name:'Nodes', type:'bar', data:nodeCounts, itemStyle:{color:'#5e81ac'}}, - {name:'Packets', type:'bar', data:packetCounts, itemStyle:{color:'#88c0d0'}}, - {name:'Gateways', type:'bar', data:gatewayCounts, itemStyle:{color:'#a3be8c'}} + makeTelemetryLine("Packets", "rgba(255,214,82,1)", packetCounts) + ] + }); + return chart; +} + +function renderDailySnapshotNodesGatewaysChart(domId,data){ + const el=document.getElementById(domId); + if(!el) return; + const chart=echarts.init(el); + const labels=data.map(d=>d.date||""); + const nodeCounts=data.map(d=>d.node_count??0); + const gatewayCounts=data.map(d=>d.gateway_count??0); + + chart.setOption({ + backgroundColor:'#272b2f', + tooltip:{trigger:'axis'}, + legend:{ + data:['Nodes','Gateways'], + textStyle:{color:'#ccc'} + }, + grid:{left:'6%', right:'6%', bottom:'18%'}, + xAxis:{ + type:'category', + data:labels, + axisLine:{lineStyle:{color:'#aaa'}}, + axisLabel:{rotate:45,color:'#ccc'} + }, + yAxis:{ + type:'value', + axisLine:{lineStyle:{color:'#aaa'}}, + axisLabel:{color:'#ccc'} + }, + series:[ + makeTelemetryLine("Nodes", "rgba(79,155,255,1)", nodeCounts), + makeTelemetryLine("Gateways", "rgba(138,255,108,1)", gatewayCounts) ] }); return chart; @@ -442,10 +527,11 @@ async function fetchPacketTypeBreakdown(channel=null) { // --- Init --- let chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71; let chartDailyAll, chartDailyPortnum1; -let chartDailySnapshot; +let chartDailySnapshotPackets, chartDailySnapshotNodesGateways; let chartHwModel, chartRole, chartChannel; let chartGatewayChannel, chartGatewayRole, chartGatewayFirmware; let chartPacketTypes; +let defaultPacketTypesData = []; async function init(){ // Channel selector @@ -459,8 +545,12 @@ async function init(){ }); // Daily snapshot histogram - const snapshots = await fetchDailySnapshots(30); - chartDailySnapshot = renderDailySnapshotChart("chart_daily_snapshot", snapshots); + const snapshots = await fetchDailySnapshots(); + chartDailySnapshotPackets = renderDailySnapshotPacketsChart("chart_daily_snapshot_packets", snapshots); + chartDailySnapshotNodesGateways = renderDailySnapshotNodesGatewaysChart( + "chart_daily_snapshot_nodes_gateways", + snapshots + ); // Daily all ports const dailyAllData=await fetchStats('day',14); @@ -522,7 +612,9 @@ async function init(){ } // Packet types pie - const packetTypesData = await fetchPacketTypeBreakdown(); + const packetSeries = portnums.map((pn, i) => ({ portnum: pn, data: allData[i] })); + const packetTypesData = buildPacketTypeBreakdownFromSeries(hourlyAllData, packetSeries); + defaultPacketTypesData = packetTypesData; const formatted = packetTypesData .filter(d=>d.count>0) .map(d=>({ @@ -561,7 +653,8 @@ window.addEventListener('resize',()=>{ chartPortnum67, chartPortnum70, chartPortnum71, - chartDailySnapshot, + chartDailySnapshotPackets, + chartDailySnapshotNodesGateways, chartDailyAll, chartDailyPortnum1, chartHwModel, @@ -646,7 +739,7 @@ document.querySelectorAll(".export-btn").forEach(btn=>{ document.getElementById("channelSelect").addEventListener("change", async (e)=>{ const channel = e.target.value; - const packetTypesData = await fetchPacketTypeBreakdown(channel); + const packetTypesData = channel ? await fetchPacketTypeBreakdown(channel) : defaultPacketTypesData; const formatted = packetTypesData .filter(d=>d.count>0) .map(d=>({ @@ -666,10 +759,14 @@ init(); async function loadConfigAndTranslations() { let langCode = "en"; try { - const resConfig = await fetch("/api/config"); - const cfg = await resConfig.json(); - window.site_config = cfg; - langCode = cfg?.site?.language || "en"; + if (window.site_config?.site) { + langCode = window.site_config.site.language || "en"; + } else { + const resConfig = await fetch("/api/config"); + const cfg = await resConfig.json(); + window.site_config = cfg; + langCode = cfg?.site?.language || "en"; + } } catch(err) { console.error("Failed to load /api/config:", err); window.site_config = { site: { language: "en" } }; diff --git a/meshview/web_api/api.py b/meshview/web_api/api.py index afa22b3..6c14d2d 100644 --- a/meshview/web_api/api.py +++ b/meshview/web_api/api.py @@ -454,30 +454,41 @@ async def api_stats_count(request): @routes.get("/api/snapshots/daily") async def api_daily_snapshots(request): - length_raw = request.query.get("length", "30") - try: - length = int(length_raw) - except ValueError: - return web.json_response({"error": "length must be an integer"}, status=400) - - # Keep query bounded. - length = max(1, min(length, 3650)) + length_raw = request.query.get("length") + length = None + if length_raw is not None: + try: + length = int(length_raw) + except ValueError: + return web.json_response({"error": "length must be an integer"}, status=400) + length = max(1, min(length, 3650)) try: async with database.async_session() as session: - rows = ( - ( - await session.execute( - select(DailySnapshot) - .order_by(DailySnapshot.snapshot_date.desc()) - .limit(length) + if length is not None: + rows = ( + ( + await session.execute( + select(DailySnapshot) + .order_by(DailySnapshot.snapshot_date.desc()) + .limit(length) + ) ) + .scalars() + .all() + ) + rows = list(reversed(rows)) + else: + rows = ( + ( + await session.execute( + select(DailySnapshot).order_by(DailySnapshot.snapshot_date.asc()) + ) + ) + .scalars() + .all() ) - .scalars() - .all() - ) - rows = list(reversed(rows)) data = [ { "date": row.snapshot_date.isoformat(),