diff --git a/meshview/templates/chat.html b/meshview/templates/chat.html
index 2f0e167..6c3a57b 100644
--- a/meshview/templates/chat.html
+++ b/meshview/templates/chat.html
@@ -87,7 +87,19 @@ document.addEventListener("DOMContentLoaded", async () => {
renderedPacketIds.add(packet.id);
packetMap.set(packet.id, packet);
- const date = new Date(packet.import_time_us / 1000);
+ // 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.
+ // Fall back to import_time if import_time_us is 0, null, or undefined
+ let date;
+ if (packet.import_time_us && packet.import_time_us > 0) {
+ date = new Date(packet.import_time_us / 1000);
+ } else if (packet.import_time) {
+ date = new Date(packet.import_time);
+ } else {
+ // Last resort: use current time
+ date = new Date();
+ }
const formattedTime = date.toLocaleTimeString([], { hour:"numeric", minute:"2-digit", second:"2-digit", hour12:true });
const formattedDate = `${(date.getMonth()+1).toString().padStart(2,"0")}/${date.getDate().toString().padStart(2,"0")}/${date.getFullYear()}`;
const formattedTimestamp = `${formattedTime} - ${formattedDate}`;
@@ -136,7 +148,17 @@ document.addEventListener("DOMContentLoaded", async () => {
function renderPacketsEnsureDescending(packets, highlight=false) {
if (!Array.isArray(packets) || packets.length===0) return;
- const sortedDesc = packets.slice().sort((a,b)=>b.import_time_us - a.import_time_us);
+ const sortedDesc = packets.slice().sort((a,b)=>{
+ // 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.
+ // Sort by import_time_us with fallback to import_time
+ const aTime = (a.import_time_us && a.import_time_us > 0) ? a.import_time_us :
+ (a.import_time ? new Date(a.import_time).getTime() * 1000 : 0);
+ const bTime = (b.import_time_us && b.import_time_us > 0) ? b.import_time_us :
+ (b.import_time ? new Date(b.import_time).getTime() * 1000 : 0);
+ return bTime - aTime;
+ });
for (let i=sortedDesc.length-1; i>=0; i--) renderPacket(sortedDesc[i], highlight);
}
diff --git a/meshview/web_api/api.py b/meshview/web_api/api.py
index ac77647..fffc490 100644
--- a/meshview/web_api/api.py
+++ b/meshview/web_api/api.py
@@ -121,6 +121,10 @@ 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", ""),
}
@@ -175,7 +179,10 @@ 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 ---
- ui_packets.sort(key=lambda p: p.import_time_us, reverse=True)
+ # 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
+ )
ui_packets = ui_packets[:limit]
# --- Prepare output ---
@@ -184,6 +191,10 @@ async def api_packets(request):
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,
"to_node_id": p.to_node_id,
@@ -202,7 +213,32 @@ async def api_packets(request):
packets_data.append(packet_dict)
- return web.json_response({"packets": packets_data})
+ # 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 = 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"]
+ 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):
+ pass
+
+ response = {"packets": packets_data}
+ if latest_import_time is not None:
+ response["latest_import_time"] = latest_import_time
+
+ return web.json_response(response)
except Exception as e:
logger.error(f"Error in /api/packets: {e}")