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}")