diff --git a/meshview/templates/chat.html b/meshview/templates/chat.html index ae6a538..4e54db3 100644 --- a/meshview/templates/chat.html +++ b/meshview/templates/chat.html @@ -5,33 +5,138 @@ min-width:10em; } .chat-packet:nth-of-type(odd){ - background-color: #272b2f; /* Lighter than #2a2a2a */ + background-color: #3a3a3a; /* Lighter than #2a2a2a */ } .chat-packet { - border: 1px solid #474b4e; + border-bottom: 1px solid #555; padding: 8px; - margin-bottom: 4px; border-radius: 8px; /* Adjust the value to make the corners more or less rounded */ } .chat-packet:nth-of-type(even){ - background-color: #212529; /* Slightly lighter than the previous #181818 */ + background-color: #333333; /* Slightly lighter than the previous #181818 */ +} + +@keyframes flash { + 0% { background-color: #ffe066; } + 100% { background-color: inherit; } +} + +.chat-packet.flash { + animation: flash 3.5s ease-out; } {% endblock %} - {% block body %} - +
+ {% for packet in packets %} +
+ + {{ packet.import_time.strftime('%-I:%M:%S %p - %m/%d/%Y') }} + + + ✉️ {{ packet.from_node.channel }} + + + + {{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }} + + + + {{ packet.payload }} + +
+ {% else %} + No packets found. + {% endfor %} +
-
- {% for packet in packets %} - {% include 'chat_packet.html' %} - {% else %} - No packets found. - {% endfor %} -
+ {% endblock %} diff --git a/meshview/templates/firehose.html b/meshview/templates/firehose.html index 7795451..c0abc8f 100644 --- a/meshview/templates/firehose.html +++ b/meshview/templates/firehose.html @@ -1,80 +1,33 @@ {% extends "base.html" %} -{% block css %} - /* Set the maximum width of the page to 900px */ +{% block css %} .container { max-width: 900px; - margin: 0 auto; /* Center the content horizontally */ + margin: 0 auto; + } + + .packet-fade-in { + animation: fadeIn 0.5s ease forwards; + opacity: 0; + } + + @keyframes fadeIn { + to { + opacity: 1; + } + } + + #pause-button { + white-space: nowrap; + padding: 2px 8px; + font-size: 0.85rem; } {% endblock %} {% block body %} - -
-
+ {% set options = { 1: "Text Message", 3: "Position", @@ -84,14 +37,16 @@ 70: "Trace Route", } %} - {% for value, name in options.items() %} {% endfor %} - + +
+
{% for packet in packets %} @@ -103,4 +58,61 @@
+ + {% endblock %} diff --git a/meshview/templates/net.html b/meshview/templates/net.html index 5222753..5ed7498 100644 --- a/meshview/templates/net.html +++ b/meshview/templates/net.html @@ -24,10 +24,30 @@
{{ site_config["site"]["weekly_net_message"] }}

- {% for packet in packets %} - {% include 'chat_packet.html' %} - {% else %} - No packets found. - {% endfor %} -
+ {% for packet in packets %} +
+ + {{ packet.import_time.strftime('%-I:%M:%S %p - %m-%d-%Y') }} + + + ✉️ {{ packet.from_node.channel }} + + + + {{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }} + + + + {{ packet.payload }} + +
+ {% else %} + No packets found. + {% endfor %} +
{% endblock %} diff --git a/meshview/templates/packet.html b/meshview/templates/packet.html index ca65f79..731e793 100644 --- a/meshview/templates/packet.html +++ b/meshview/templates/packet.html @@ -5,7 +5,7 @@ {{packet.from_node.long_name}}( {%- if not from_me -%} - + {%- endif -%} {{packet.from_node_id|node_id_to_hex}} {%- if not from_me -%} diff --git a/meshview/templates/traceroute.html b/meshview/templates/traceroute.html index 8e94a01..c2e329a 100644 --- a/meshview/templates/traceroute.html +++ b/meshview/templates/traceroute.html @@ -1,67 +1,94 @@ -{% extends "base.html" %} - {% block head %} - + {% endblock %} {% block body %} -
+
- {% endblock %} diff --git a/meshview/web.py b/meshview/web.py index 78d40cd..b76a895 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -1,5 +1,6 @@ import asyncio import datetime +from datetime import timedelta import json import os import re @@ -20,7 +21,10 @@ from meshview import models from meshview import store from meshview.store import get_total_node_count from aiohttp import web -SOFTWARE_RELEASE= "2.0.2" +import re + +SEQ_REGEX = re.compile(r"seq \d+") +SOFTWARE_RELEASE= "2.0.3" CONFIG = config.CONFIG env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape()) @@ -384,6 +388,55 @@ async def packet_details(request): content_type="text/html", ) +@routes.get("/firehose/updates") +async def firehose_updates(request): + try: + last_time_str = request.query.get("last_time", None) + if last_time_str: + try: + last_time = datetime.datetime.fromisoformat(last_time_str) + except Exception as e: + print(f"Failed to parse last_time '{last_time_str}': {e}") + last_time = datetime.datetime.min + else: + last_time = datetime.datetime.min + + portnum = request.query.get("portnum") + if portnum is not None and portnum != "": + portnum = int(portnum) + else: + portnum = None + + packets = await store.get_packets( + portnum=portnum, + limit=20, + ) + ui_packets = [Packet.from_model(p) for p in packets] + + # Filter packets newer than last_time + new_packets = [p for p in ui_packets if p.import_time > last_time] + + template = env.get_template("packet.html") + # Replace YOUR_NODE_ID with your current node ID (integer) + YOUR_NODE_ID = 0x12345678 # example, replace with actual + + rendered_packets = [template.render(packet=p, node_id=YOUR_NODE_ID) for p in new_packets] + + response = { + "packets": rendered_packets, + } + + if new_packets: + latest_import_time = max(p.import_time for p in new_packets) + response["latest_import_time"] = latest_import_time.isoformat() + + return web.json_response(response) + + except Exception as e: + print("Error in /firehose/updates:", e) + return web.json_response({"error": "Failed to fetch updates"}, status=500) + + @routes.get("/packet/{packet_id}") async def packet(request): packet = await store.get_packet(int(request.match_info["packet_id"])) @@ -1000,7 +1053,7 @@ async def net(request): try: # Fetch packets for the given node ID and port number packets = await store.get_packets( - node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP, limit=1000 + node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP, since=timedelta(days=3) ) # Convert packets to UI packets @@ -1139,6 +1192,51 @@ async def chat(request): status=500, content_type="text/plain", ) +@routes.get("/chat/updates") +async def chat_updates(request): + try: + last_time_str = request.query.get("last_time", None) + if last_time_str: + try: + last_time = datetime.datetime.fromisoformat(last_time_str) + except Exception as e: + print(f"Failed to parse last_time '{last_time_str}': {e}") + last_time = datetime.datetime.min + else: + last_time = datetime.datetime.min + + packets = await store.get_packets( + node_id=0xFFFFFFFF, + portnum=PortNum.TEXT_MESSAGE_APP, + limit=5, + ) + ui_packets = [Packet.from_model(p) for p in packets] + filtered_packets = [p for p in ui_packets if not SEQ_REGEX.fullmatch(p.payload)] + new_packets = [p for p in filtered_packets if p.import_time > last_time] + + packets_data = [{ + "id": p.id, + "import_time": p.import_time.isoformat(), + "channel": p.from_node.channel if p.from_node else "", + "from_node_id": p.from_node_id, + "long_name": p.from_node.long_name if p.from_node else "", + "payload": p.payload, + } for p in new_packets] + + response = { + "packets": packets_data + } + + if new_packets: + latest_import_time = max(p.import_time for p in new_packets) + response["latest_import_time"] = latest_import_time.isoformat() + + return web.json_response(response) + + except Exception as e: + print("Error in /chat/updates:", e) + return web.json_response({"error": "Failed to fetch updates"}, status=500) + # Assuming the route URL structure is /nodegraph @routes.get("/nodegraph")