diff --git a/meshview/static/api-chat.html b/meshview/static/api-chat.html new file mode 100644 index 0000000..fe9dd81 --- /dev/null +++ b/meshview/static/api-chat.html @@ -0,0 +1,127 @@ + + + + + API Documentation - Chat + + + + +
+ + + + + diff --git a/meshview/static/api-edges.html b/meshview/static/api-edges.html new file mode 100644 index 0000000..e1fcec2 --- /dev/null +++ b/meshview/static/api-edges.html @@ -0,0 +1,109 @@ + + + + + API Documentation - Edges + + + + +
+ + + + + diff --git a/meshview/static/api-nodes.html b/meshview/static/api-nodes.html new file mode 100644 index 0000000..9dc7d11 --- /dev/null +++ b/meshview/static/api-nodes.html @@ -0,0 +1,134 @@ + + + + + API Documentation - Nodes + + + + +
+ + + + + diff --git a/meshview/templates/chat.html b/meshview/templates/chat.html index f72fbf4..d44d8b1 100644 --- a/meshview/templates/chat.html +++ b/meshview/templates/chat.html @@ -23,135 +23,152 @@ animation: flash 3.5s ease-out; } -/* New CSS for smaller Replying to text */ +/* Nested reply style below the message */ .replying-to { - font-size: 0.85em; - color: #ccc; + background-color: #444; + padding: 4px 8px; + margin-top: 4px; + border-left: 3px solid #ffe066; + border-radius: 4px; + font-size: 0.9em; + color: #fff; } {% 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 }} - {% if packet.raw_mesh_packet.decoded.reply_id %} - - (Replying to: {{ packet.raw_mesh_packet.decoded.reply_id }}) - - {% endif %} - -
- {% else %} -

No packets found.

- {% endfor %}
+ + + + {% endblock %} diff --git a/meshview/web.py b/meshview/web.py index 0504606..3abce95 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -1279,18 +1279,9 @@ async def top(request): @routes.get("/chat") async def chat(request): try: - packets = await store.get_packets( - node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP, limit=100 - ) - - ui_packets = [Packet.from_model(p) for p in packets] - filtered_packets = [ - p for p in ui_packets if not re.fullmatch(r"seq \d+", p.payload) - ] template = env.get_template("chat.html") return web.Response( text=template.render( - packets=filtered_packets, site_config=CONFIG, SOFTWARE_RELEASE=SOFTWARE_RELEASE ), @@ -1307,63 +1298,6 @@ async def chat(request): ) return web.Response(text=rendered, status=500, content_type="text/html") -@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 = [] - for p in new_packets: - reply_id = None - if getattr(p, 'raw_mesh_packet', None): - decoded = getattr(p.raw_mesh_packet, 'decoded', None) - if decoded: - reply_id = getattr(decoded, 'reply_id', None) - - packet_dict = { - "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, - } - if reply_id: - packet_dict["reply_id"] = reply_id - - packets_data.append(packet_dict) - - 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") @@ -1476,41 +1410,48 @@ async def get_config(request): # The response includes "latest_import_time" for frontend to keep track of the newest message timestamp. # The backend fetches extra packets (limit*5) to account for filtering messages like "seq N" and since filtering. + @routes.get("/api/chat") async def api_chat(request): try: # Parse query params - limit_str = request.query.get("limit", "100") - since_str = request.query.get("since", None) + limit_str = request.query.get("limit", "20") + since_str = request.query.get("since") + # Clamp limit between 1 and 200 try: - limit = min(max(int(limit_str), 1), 200) # Limit between 1 and 200 + limit = min(max(int(limit_str), 1), 100) except ValueError: - limit = 100 + limit = 50 + # Parse "since" timestamp if provided + since = None if since_str: try: since = datetime.datetime.fromisoformat(since_str) except Exception as e: print(f"Failed to parse since '{since_str}': {e}") - since = None - else: - since = None # Fetch packets from store packets = await store.get_packets( node_id=0xFFFFFFFF, portnum=PortNum.TEXT_MESSAGE_APP, - limit=limit*5, # Fetch extra to filter out seq messages and since filter + limit=limit, ) - SEQ_REGEX = re.compile(r"seq \d+") ui_packets = [Packet.from_model(p) for p in packets] - filtered_packets = [p for p in ui_packets if p.payload and not SEQ_REGEX.fullmatch(p.payload)] - # Filter by 'since' timestamp if provided + # Filter out "seq N" and missing payloads + filtered_packets = [ + p for p in ui_packets + if p.payload and not SEQ_REGEX.fullmatch(p.payload) + ] + + # Apply "since" filter if since: - filtered_packets = [p for p in filtered_packets if p.import_time > since] + filtered_packets = [ + p for p in filtered_packets if p.import_time > since + ] # Sort by import_time descending (latest first) filtered_packets.sort(key=lambda p: p.import_time, reverse=True) @@ -1518,16 +1459,36 @@ async def api_chat(request): # Trim to requested limit filtered_packets = filtered_packets[:limit] - packets_data = [{ - "id": p.id, - "import_time": p.import_time.isoformat(), - "channel": getattr(p.from_node, "channel", ""), - "from_node_id": p.from_node_id, - "long_name": getattr(p.from_node, "long_name", ""), - "payload": p.payload, - } for p in filtered_packets] + # Build response data + packets_data = [] + for p in filtered_packets: + reply_id = getattr( + getattr(getattr(p, "raw_mesh_packet", None), "decoded", None), + "reply_id", + None + ) - latest_import_time = filtered_packets[0].import_time.isoformat() if filtered_packets else since_str or None + packet_dict = { + "id": p.id, + "import_time": p.import_time.isoformat(), + "channel": getattr(p.from_node, "channel", ""), + "from_node_id": p.from_node_id, + "long_name": getattr(p.from_node, "long_name", ""), + "payload": p.payload, + } + + if reply_id: # ✅ only include if not None/0 + packet_dict["reply_id"] = reply_id + + packets_data.append(packet_dict) + + # Pick latest import time for clients to use in next request + if filtered_packets: + latest_import_time = filtered_packets[0].import_time.isoformat() + elif since: + latest_import_time = since.isoformat() + else: + latest_import_time = None return web.json_response({ "packets": packets_data, @@ -1536,7 +1497,11 @@ async def api_chat(request): except Exception as e: print("Error in /api/chat:", e) - return web.json_response({"error": "Failed to fetch chat data"}, status=500) + return web.json_response( + {"error": "Failed to fetch chat data", "details": str(e)}, + status=500 + ) + # Client to pass ?hours=1 or ?days=7 to filter