mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-06-22 02:55:03 +02:00
fixed the db and added more reporting
This commit is contained in:
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -469,7 +469,6 @@ seenTableBody.innerHTML = Object.keys(hopGroups)
|
||||
<a href="/node/${s.node_id}">${s.node_id}</a><br>
|
||||
HW: ${node?.hw_model ?? "—"}<br>
|
||||
<span data-translate-lang="channel">Channel</span>: ${s.channel ?? "—"}<br>
|
||||
Relay Node: ${s.relay_node ?? "—"}<br>
|
||||
${
|
||||
distanceKm !== null
|
||||
? `<span data-translate-lang="distance">Distance</span>:
|
||||
|
||||
+121
-24
@@ -119,12 +119,21 @@
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="daily_snapshot_histogram">
|
||||
Daily Network Snapshot Histogram (Last 30 Days)
|
||||
<p class="section-header" data-translate-lang="daily_snapshot_packets_history">
|
||||
Daily Packet Snapshot History (All Available Days)
|
||||
</p>
|
||||
<button class="expand-btn" data-chart="chart_daily_snapshot" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_daily_snapshot" data-translate-lang="export_csv">Export CSV</button>
|
||||
<div id="chart_daily_snapshot" class="chart"></div>
|
||||
<button class="expand-btn" data-chart="chart_daily_snapshot_packets" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_daily_snapshot_packets" data-translate-lang="export_csv">Export CSV</button>
|
||||
<div id="chart_daily_snapshot_packets" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="daily_snapshot_nodes_gateways_history">
|
||||
Daily Node/Gateway Snapshot History (All Available Days)
|
||||
</p>
|
||||
<button class="expand-btn" data-chart="chart_daily_snapshot_nodes_gateways" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_daily_snapshot_nodes_gateways" data-translate-lang="export_csv">Export CSV</button>
|
||||
<div id="chart_daily_snapshot_nodes_gateways" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- Daily Charts -->
|
||||
@@ -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" } };
|
||||
|
||||
+29
-18
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user