mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-07 09:22:49 +02:00
More changes... almost ready for release.
Ranamed 2 pages for easy or reading.
This commit is contained in:
+56
-26
@@ -1,9 +1,9 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import func, nullslast, select, text
|
||||
from sqlalchemy import and_, func, or_, select, text
|
||||
from sqlalchemy.orm import lazyload
|
||||
|
||||
from meshview import database
|
||||
from meshview import database, models
|
||||
from meshview.models import Node, Packet, PacketSeen, Traceroute
|
||||
|
||||
|
||||
@@ -24,38 +24,68 @@ async def get_fuzzy_nodes(query):
|
||||
return result.scalars()
|
||||
|
||||
|
||||
|
||||
async def get_packets(
|
||||
node_id=None, portnum=None, after=None, before=None, limit=None, packet_id=None
|
||||
from_node_id=None,
|
||||
to_node_id=None,
|
||||
node_id=None, # legacy: match either from/to
|
||||
portnum=None,
|
||||
after=None,
|
||||
limit=50,
|
||||
):
|
||||
"""
|
||||
SQLAlchemy 2.0 async ORM version.
|
||||
Supports strict from/to/node filtering, portnum, since, limit.
|
||||
"""
|
||||
|
||||
async with database.async_session() as session:
|
||||
# --- Fast path: fetch by packet_id (uses primary key lookup) ---
|
||||
if packet_id is not None:
|
||||
packet = await session.get(Packet, packet_id)
|
||||
return [packet] if packet else []
|
||||
|
||||
# --- Normal query path ---
|
||||
q = select(Packet)
|
||||
# Start select
|
||||
stmt = select(models.Packet)
|
||||
|
||||
if node_id:
|
||||
q = q.where((Packet.from_node_id == node_id) | (Packet.to_node_id == node_id))
|
||||
if portnum:
|
||||
q = q.where(Packet.portnum == portnum)
|
||||
if after:
|
||||
q = q.where(Packet.import_time_us > after)
|
||||
if before:
|
||||
q = q.where(Packet.import_time_us < before)
|
||||
conditions = []
|
||||
|
||||
# Order by import_time_us when available, fallback to import_time for old data
|
||||
# This handles databases where import_time_us may be NULL (old backups)
|
||||
# First sort by import_time_us (NULLs last), then by import_time for those rows
|
||||
q = q.order_by(nullslast(Packet.import_time_us.desc()), Packet.import_time.desc())
|
||||
# Strict FROM filter
|
||||
if from_node_id is not None:
|
||||
conditions.append(models.Packet.from_node_id == from_node_id)
|
||||
|
||||
if limit is not None:
|
||||
q = q.limit(limit)
|
||||
# Strict TO filter
|
||||
if to_node_id is not None:
|
||||
conditions.append(models.Packet.to_node_id == to_node_id)
|
||||
|
||||
# Legacy "node_id = match either direction"
|
||||
if node_id is not None:
|
||||
conditions.append(
|
||||
or_(
|
||||
models.Packet.from_node_id == node_id,
|
||||
models.Packet.to_node_id == node_id
|
||||
)
|
||||
)
|
||||
|
||||
# Port filter
|
||||
if portnum is not None:
|
||||
conditions.append(models.Packet.portnum == portnum)
|
||||
|
||||
# Time filter
|
||||
if after is not None:
|
||||
conditions.append(models.Packet.import_time_us > after)
|
||||
|
||||
# Apply WHERE clause if needed
|
||||
if conditions:
|
||||
stmt = stmt.where(and_(*conditions))
|
||||
|
||||
# Sort newest → oldest
|
||||
stmt = stmt.order_by(models.Packet.import_time_us.desc())
|
||||
|
||||
# Limit
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
# Execute
|
||||
result = await session.execute(stmt)
|
||||
|
||||
# Convert ORM rows to models
|
||||
return result.scalars().all()
|
||||
|
||||
result = await session.execute(q)
|
||||
packets = list(result.scalars())
|
||||
return packets
|
||||
|
||||
|
||||
async def get_packets_from(node_id=None, portnum=None, since=None, limit=500):
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11/dist/ext/sse.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Styles -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
|
||||
@@ -131,7 +131,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
replyHtml = `
|
||||
<div class="replying-to">
|
||||
${replyPrefix}
|
||||
<a href="/new_packet/${packet.reply_id}">${packet.reply_id}</a>
|
||||
<a href="/packet/${packet.reply_id}">${packet.reply_id}</a>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
div.innerHTML = `
|
||||
<span class="col-2 timestamp" title="${packet.import_time_us}">${formattedTimestamp}</span>
|
||||
<span class="col-2 channel">
|
||||
<a href="/new_packet/${packet.id}" title="${chatLang.view_packet_details || 'View details'}">🔎</a>
|
||||
<a href="/packet/${packet.id}" title="${chatLang.view_packet_details || 'View details'}">🔎</a>
|
||||
${escapeHtml(packet.channel || "")}
|
||||
</span>
|
||||
<span class="col-3 nodename">
|
||||
|
||||
@@ -265,7 +265,7 @@ async function fetchUpdates() {
|
||||
const html = `
|
||||
<tr class="packet-row" data-id="${pkt.id}">
|
||||
<td>${localTime}</td>
|
||||
<td><span class="toggle-btn">▶</span> <a href="/new_packet/${pkt.id}" style="text-decoration:underline; color:inherit;">${pkt.id}</a></td>
|
||||
<td><span class="toggle-btn">▶</span> <a href="/packet/${pkt.id}" style="text-decoration:underline; color:inherit;">${pkt.id}</a></td>
|
||||
<td>${from}</td>
|
||||
<td>${to}</td>
|
||||
<td>${portLabel(pkt.portnum, pkt.payload, inlineLinks)}</td>
|
||||
|
||||
@@ -572,7 +572,7 @@ async function loadPackets(){
|
||||
list.insertAdjacentHTML("afterbegin", `
|
||||
<tr class="packet-row">
|
||||
<td>${localTime}</td>
|
||||
<td><span class="toggle-btn">▶</span> <a href="/new_packet/${pkt.id}" style="text-decoration:underline; color:inherit;">${pkt.id}</a></td>
|
||||
<td><span class="toggle-btn">▶</span> <a href="/packet/${pkt.id}" style="text-decoration:underline; color:inherit;">${pkt.id}</a></td>
|
||||
<td>${fromCell}</td>
|
||||
<td>${toCell}</td>
|
||||
<td>${portLabel(pkt.portnum)}${inlineLinks}</td>
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- Source Marker: 2px bigger than gateway (26px vs 24px) --- */
|
||||
/* --- SOURCE MARKER (slightly bigger) --- */
|
||||
.source-marker {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(255,0,0,0.55);
|
||||
border: 3px solid #ff0000;
|
||||
border-radius: 50%;
|
||||
@@ -135,13 +135,17 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const seenTableBody = document.getElementById("seen-table-body");
|
||||
const seenCountSpan = document.getElementById("seen-count");
|
||||
|
||||
const match = window.location.pathname.match(/\/new_packet\/(\d+)/);
|
||||
/* ---------------------------------------------
|
||||
Identify packet ID
|
||||
----------------------------------------------*/
|
||||
const match = window.location.pathname.match(/\/packet\/(\d+)/);
|
||||
if (!match) {
|
||||
loading.textContent = "Invalid packet URL";
|
||||
return;
|
||||
}
|
||||
const packetId = match[1];
|
||||
|
||||
/* PORT NAME MAP */
|
||||
const PORT_NAMES = {
|
||||
0:"UNKNOWN APP",
|
||||
1:"Text",
|
||||
@@ -154,9 +158,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
71:"Neighbor"
|
||||
};
|
||||
|
||||
/* -------------------------
|
||||
/* ---------------------------------------------
|
||||
Fetch packet
|
||||
-------------------------- */
|
||||
----------------------------------------------*/
|
||||
const packetRes = await fetch(`/api/packets?packet_id=${packetId}`);
|
||||
const packetData = await packetRes.json();
|
||||
if (!packetData.packets.length) {
|
||||
@@ -165,24 +169,24 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
const p = packetData.packets[0];
|
||||
|
||||
/* -------------------------
|
||||
Fetch nodes
|
||||
-------------------------- */
|
||||
/* ---------------------------------------------
|
||||
Fetch all nodes
|
||||
----------------------------------------------*/
|
||||
const nodesRes = await fetch("/api/nodes");
|
||||
const nodesData = await nodesRes.json();
|
||||
const nodeLookup = {};
|
||||
(nodesData.nodes || []).forEach(n => nodeLookup[n.node_id] = n);
|
||||
|
||||
const fromNodeObj = nodeLookup[p.from_node_id];
|
||||
const fromNodeLabel = fromNodeObj?.long_name || p.from_node_id;
|
||||
const toNodeObj = nodeLookup[p.to_node_id];
|
||||
|
||||
const toNodeObj = nodeLookup[p.to_node_id];
|
||||
const fromNodeLabel = fromNodeObj?.long_name || p.from_node_id;
|
||||
const toNodeLabel =
|
||||
p.to_node_id == 4294967295 ? "All" : (toNodeObj?.long_name || p.to_node_id);
|
||||
|
||||
/* -------------------------
|
||||
Parse payload
|
||||
-------------------------- */
|
||||
/* ---------------------------------------------
|
||||
Parse payload for lat/lon if this *packet* is a position packet
|
||||
----------------------------------------------*/
|
||||
let lat = null, lon = null;
|
||||
const parsed = {};
|
||||
|
||||
@@ -197,19 +201,19 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------
|
||||
Render packet header & details
|
||||
----------------------------------------------*/
|
||||
const time = p.import_time_us
|
||||
? new Date(p.import_time_us / 1000).toLocaleString()
|
||||
: "—";
|
||||
|
||||
const telemetryExtras = [];
|
||||
if (parsed.PDOP) telemetryExtras.push(`PDOP: ${parsed.PDOP}`);
|
||||
if (parsed.sats_in_view) telemetryExtras.push(`Sats: ${parsed.sats_in_view}`);
|
||||
if (parsed.ground_speed) telemetryExtras.push(`Speed: ${parsed.ground_speed}`);
|
||||
if (parsed.altitude) telemetryExtras.push(`Altitude: ${parsed.altitude}`);
|
||||
|
||||
const time = p.import_time_us
|
||||
? new Date(p.import_time_us / 1000).toLocaleString()
|
||||
: "—";
|
||||
|
||||
/* -------------------------
|
||||
Render packet card
|
||||
-------------------------- */
|
||||
packetCard.innerHTML = `
|
||||
<div class="card-header">
|
||||
<span>Packet ID: <i>${p.id}</i></span>
|
||||
@@ -256,9 +260,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
loading.classList.add("d-none");
|
||||
packetCard.classList.remove("d-none");
|
||||
|
||||
/* -------------------------
|
||||
Map init
|
||||
-------------------------- */
|
||||
/* ---------------------------------------------
|
||||
Map initialization
|
||||
----------------------------------------------*/
|
||||
const map = L.map("map");
|
||||
mapDiv.style.display = "block";
|
||||
|
||||
@@ -268,11 +272,22 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
const allBounds = [];
|
||||
|
||||
/* --------------------------------------------------------------------
|
||||
SOURCE MARKER (26px vs 24px)
|
||||
--------------------------------------------------------------------- */
|
||||
if (lat && lon) {
|
||||
allBounds.push([lat, lon]);
|
||||
/* ---------------------------------------------
|
||||
ALWAYS SHOW SOURCE POSITION
|
||||
Priority:
|
||||
1) position from packet payload
|
||||
2) fallback: last_lat/last_long from /api/nodes
|
||||
----------------------------------------------*/
|
||||
let srcLat = lat;
|
||||
let srcLon = lon;
|
||||
|
||||
if ((!srcLat || !srcLon) && fromNodeObj?.last_lat && fromNodeObj?.last_long) {
|
||||
srcLat = fromNodeObj.last_lat / 1e7;
|
||||
srcLon = fromNodeObj.last_long / 1e7;
|
||||
}
|
||||
|
||||
if (srcLat && srcLon) {
|
||||
allBounds.push([srcLat, srcLon]);
|
||||
|
||||
const sourceIcon = L.divIcon({
|
||||
html: `<div class="source-marker"></div>`,
|
||||
@@ -281,7 +296,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
iconAnchor: [13, 13]
|
||||
});
|
||||
|
||||
const sourceMarker = L.marker([lat, lon], {
|
||||
const sourceMarker = L.marker([srcLat, srcLon], {
|
||||
icon: sourceIcon,
|
||||
zIndexOffset: 9999
|
||||
}).addTo(map);
|
||||
@@ -289,34 +304,32 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
sourceMarker.bindPopup(`
|
||||
<div style="font-size:0.9em">
|
||||
<b>Packet Source</b><br>
|
||||
Lat: ${lat.toFixed(6)}<br>
|
||||
Lon: ${lon.toFixed(6)}<br>
|
||||
Lat: ${srcLat.toFixed(6)}<br>
|
||||
Lon: ${srcLon.toFixed(6)}<br>
|
||||
From Node: ${fromNodeLabel}<br>
|
||||
Channel: ${p.channel ?? "—"}<br>
|
||||
Port: ${PORT_NAMES[p.portnum] || "UNKNOWN"} (${p.portnum})
|
||||
</div>
|
||||
`);
|
||||
|
||||
} else {
|
||||
map.setView([0, 0], 2);
|
||||
map.setView([0,0], 2);
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Helpers
|
||||
-------------------------- */
|
||||
|
||||
/* 0 = warmest → 7 = coldest */
|
||||
/* ---------------------------------------------
|
||||
Color for hop indicator markers (warm → cold)
|
||||
----------------------------------------------*/
|
||||
function hopColor(hopValue){
|
||||
const colors = [
|
||||
"#ff3b30", // 0 red
|
||||
"#ff3b30",
|
||||
"#ff6b22",
|
||||
"#ff9f0c",
|
||||
"#ffd60a",
|
||||
"#87d957",
|
||||
"#57d9c4",
|
||||
"#3db2ff",
|
||||
"#1e63ff" // 7 deep cold blue
|
||||
"#1e63ff"
|
||||
];
|
||||
|
||||
let h = Number(hopValue);
|
||||
if (isNaN(h)) return "#aaa";
|
||||
if (h < 0) h = 0;
|
||||
@@ -324,6 +337,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
return colors[h];
|
||||
}
|
||||
|
||||
/* Distance helper */
|
||||
function haversine(lat1,lon1,lat2,lon2){
|
||||
const R=6371;
|
||||
const dLat=(lat2-lat1)*Math.PI/180;
|
||||
@@ -335,13 +349,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
return R*(2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)));
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Fetch seen list
|
||||
-------------------------- */
|
||||
/* ---------------------------------------------
|
||||
Fetch packets_seen
|
||||
----------------------------------------------*/
|
||||
const seenRes = await fetch(`/api/packets_seen/${packetId}`);
|
||||
const seenData = await seenRes.json();
|
||||
const seenList = seenData.seen ?? [];
|
||||
|
||||
/* sort by hop_start descending (warm → cold) */
|
||||
const seenSorted = seenList.slice().sort((a,b)=>{
|
||||
const A=a.hop_start??-999;
|
||||
const B=b.hop_start??-999;
|
||||
@@ -353,9 +368,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
seenCountSpan.textContent=`(${seenSorted.length} gateways)`;
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Gateway markers + table
|
||||
-------------------------- */
|
||||
/* ---------------------------------------------
|
||||
Gateway markers and seen table
|
||||
----------------------------------------------*/
|
||||
seenTableBody.innerHTML = seenSorted.map(s=>{
|
||||
const node=nodeLookup[s.node_id];
|
||||
const label=node?(node.long_name||node.node_id):s.node_id;
|
||||
@@ -401,8 +416,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}).addTo(map);
|
||||
|
||||
let distKm=null,distMi=null;
|
||||
if(lat&&lon){
|
||||
distKm=haversine(lat,lon,rlat,rlon);
|
||||
if(srcLat&&srcLon){
|
||||
distKm=haversine(srcLat,srcLon,rlat,rlon);
|
||||
distMi=distKm*0.621371;
|
||||
}
|
||||
|
||||
@@ -437,10 +452,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
</tr>`;
|
||||
}).join("");
|
||||
|
||||
/* ---------------------------------------------
|
||||
Fit map to all markers
|
||||
----------------------------------------------*/
|
||||
if(allBounds.length>0){
|
||||
map.fitBounds(allBounds,{padding:[40,40]});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------
|
||||
Escape HTML
|
||||
----------------------------------------------*/
|
||||
function escapeHtml(unsafe) {
|
||||
return (unsafe??"").replace(/[&<"'>]/g,m=>({
|
||||
"&":"&",
|
||||
+2
-8
@@ -200,12 +200,6 @@ async def redirect_packet_list(request):
|
||||
raise web.HTTPFound(location=f"/node/{packet_id}")
|
||||
|
||||
|
||||
# redirect for backwards compatibility
|
||||
@routes.get("/packet/{packet_id}")
|
||||
async def redirect_packet(request):
|
||||
packet_id = request.match_info["packet_id"]
|
||||
raise web.HTTPFound(location=f"/packet/{packet_id}")
|
||||
|
||||
@routes.get("/net")
|
||||
async def net(request):
|
||||
return web.Response(
|
||||
@@ -246,9 +240,9 @@ async def chat(request):
|
||||
)
|
||||
|
||||
|
||||
@routes.get("/new_packet/{packet_id}")
|
||||
@routes.get("/packet/{packet_id}")
|
||||
async def new_packet(request):
|
||||
template = env.get_template("new_packet.html")
|
||||
template = env.get_template("packet.html")
|
||||
return web.Response(
|
||||
text=template.render(),
|
||||
content_type="text/html",
|
||||
|
||||
+45
-37
@@ -100,9 +100,13 @@ async def api_packets(request):
|
||||
since_str = request.query.get("since")
|
||||
portnum_str = request.query.get("portnum")
|
||||
contains = request.query.get("contains")
|
||||
from_node_id_str = request.query.get("from_node_id")
|
||||
|
||||
# --- If a packet_id is provided, fetch just that one ---
|
||||
# NEW — explicit filters
|
||||
from_node_id_str = request.query.get("from_node_id")
|
||||
to_node_id_str = request.query.get("to_node_id")
|
||||
node_id_str = request.query.get("node_id") # legacy: match either from/to
|
||||
|
||||
# --- If a packet_id is provided, return only that packet ---
|
||||
if packet_id_str:
|
||||
try:
|
||||
packet_id = int(packet_id_str)
|
||||
@@ -111,7 +115,7 @@ async def api_packets(request):
|
||||
|
||||
packet = await store.get_packet(packet_id)
|
||||
if not packet:
|
||||
return web.json_response({"packets": []}) # consistent shape
|
||||
return web.json_response({"packets": []})
|
||||
|
||||
p = Packet.from_model(packet)
|
||||
data = {
|
||||
@@ -121,24 +125,19 @@ async def api_packets(request):
|
||||
"portnum": int(p.portnum) if p.portnum is not None else None,
|
||||
"payload": (p.payload or "").strip(),
|
||||
"import_time_us": p.import_time_us,
|
||||
# NOTE: Temporary stopgap - include import_time as fallback until old data
|
||||
# with import_time_us=0 is migrated/cleaned up. Can be removed once all
|
||||
# legacy records have been updated.
|
||||
"import_time": p.import_time.isoformat() if p.import_time else None,
|
||||
"channel": getattr(p.from_node, "channel", ""),
|
||||
"long_name": getattr(p.from_node, "long_name", ""),
|
||||
}
|
||||
return web.json_response({"packets": [data]}) # unified key
|
||||
return web.json_response({"packets": [data]})
|
||||
|
||||
# --- Otherwise: multi-packet listing mode ---
|
||||
|
||||
# Limit validation
|
||||
# --- Parse limit ---
|
||||
try:
|
||||
limit = min(max(int(limit_str), 1), 100)
|
||||
except ValueError:
|
||||
limit = 50
|
||||
|
||||
# Parse 'since' timestamp
|
||||
# --- Parse since timestamp ---
|
||||
since = None
|
||||
if since_str:
|
||||
try:
|
||||
@@ -146,15 +145,7 @@ async def api_packets(request):
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid 'since' value (expected microseconds): {since_str}")
|
||||
|
||||
# Parse from_node_id (decimal or hex)
|
||||
node_id = None
|
||||
if from_node_id_str:
|
||||
try:
|
||||
node_id = int(from_node_id_str, 0)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid from_node_id: {from_node_id_str}")
|
||||
|
||||
# Parse portnum safely
|
||||
# --- Parse portnum ---
|
||||
portnum = None
|
||||
if portnum_str:
|
||||
try:
|
||||
@@ -162,9 +153,34 @@ async def api_packets(request):
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid portnum: {portnum_str}")
|
||||
|
||||
# --- Fetch packets ---
|
||||
# --- Parse node filters ---
|
||||
from_node_id = None
|
||||
to_node_id = None
|
||||
node_id = None # legacy: match either from/to
|
||||
|
||||
if from_node_id_str:
|
||||
try:
|
||||
from_node_id = int(from_node_id_str, 0)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid from_node_id: {from_node_id_str}")
|
||||
|
||||
if to_node_id_str:
|
||||
try:
|
||||
to_node_id = int(to_node_id_str, 0)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid to_node_id: {to_node_id_str}")
|
||||
|
||||
if node_id_str:
|
||||
try:
|
||||
node_id = int(node_id_str, 0)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid node_id: {node_id_str}")
|
||||
|
||||
# --- Fetch packets using explicit filters ---
|
||||
packets = await store.get_packets(
|
||||
node_id=node_id,
|
||||
from_node_id=from_node_id,
|
||||
to_node_id=to_node_id,
|
||||
node_id=node_id, # old behavior: match from OR to
|
||||
portnum=portnum,
|
||||
after=since,
|
||||
limit=limit,
|
||||
@@ -179,21 +195,18 @@ async def api_packets(request):
|
||||
ui_packets = [p for p in ui_packets if contains.lower() in p.payload.lower()]
|
||||
|
||||
# --- Sort descending by import_time_us ---
|
||||
# Handle None values by treating them as smallest (will be sorted last)
|
||||
ui_packets.sort(
|
||||
key=lambda p: (p.import_time_us is not None, p.import_time_us or 0), reverse=True
|
||||
key=lambda p: (p.import_time_us is not None, p.import_time_us or 0),
|
||||
reverse=True
|
||||
)
|
||||
ui_packets = ui_packets[:limit]
|
||||
|
||||
# --- Prepare output ---
|
||||
# --- Build JSON output ---
|
||||
packets_data = []
|
||||
for p in ui_packets:
|
||||
packet_dict = {
|
||||
"id": p.id,
|
||||
"import_time_us": p.import_time_us,
|
||||
# NOTE: Temporary stopgap - include import_time as fallback until old data
|
||||
# with import_time_us=0 is migrated/cleaned up. Can be removed once all
|
||||
# legacy records have been updated.
|
||||
"import_time": p.import_time.isoformat() if p.import_time else None,
|
||||
"channel": getattr(p.from_node, "channel", ""),
|
||||
"from_node_id": p.from_node_id,
|
||||
@@ -213,25 +226,19 @@ async def api_packets(request):
|
||||
|
||||
packets_data.append(packet_dict)
|
||||
|
||||
# Calculate latest_import_time for incremental updates
|
||||
# NOTE: Temporary stopgap - fallback to import_time until old data with
|
||||
# import_time_us=0 is migrated/cleaned up. Can be simplified once all
|
||||
# legacy records have been updated.
|
||||
# Use the highest import_time_us, with fallback to import_time
|
||||
# --- Latest import_time for incremental fetch ---
|
||||
latest_import_time = None
|
||||
if packets_data:
|
||||
for p in packets_data:
|
||||
if p.get("import_time_us") and p["import_time_us"] > 0:
|
||||
if latest_import_time is None or p["import_time_us"] > latest_import_time:
|
||||
latest_import_time = p["import_time_us"]
|
||||
latest_import_time = max(latest_import_time or 0, p["import_time_us"])
|
||||
elif p.get("import_time") and latest_import_time is None:
|
||||
# Fallback: convert ISO string to microseconds if import_time_us is missing
|
||||
try:
|
||||
dt = datetime.datetime.fromisoformat(
|
||||
p["import_time"].replace("Z", "+00:00")
|
||||
)
|
||||
latest_import_time = int(dt.timestamp() * 1_000_000)
|
||||
except (ValueError, AttributeError):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
response = {"packets": packets_data}
|
||||
@@ -245,6 +252,7 @@ async def api_packets(request):
|
||||
return web.json_response({"error": "Failed to fetch packets"}, status=500)
|
||||
|
||||
|
||||
|
||||
@routes.get("/api/stats")
|
||||
async def api_stats(request):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user