mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-07-31 05:53:17 +02:00
Added self updating firehose.html and chat.html
This commit is contained in:
+122
-17
@@ -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 %}
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 10000); // 10 secondd
|
||||
</script>
|
||||
<div class="container" id="chat">
|
||||
{% for packet in packets %}
|
||||
<div
|
||||
class="row chat-packet"
|
||||
data-packet-id="{{ packet.id }}"
|
||||
role="article"
|
||||
aria-label="Chat message from {{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}"
|
||||
>
|
||||
<span class="col-2 timestamp" title="{{ packet.import_time.isoformat() }}">
|
||||
{{ packet.import_time.strftime('%-I:%M:%S %p - %m/%d/%Y') }}
|
||||
</span>
|
||||
<span class="col-2 channel">
|
||||
<a href="/packet/{{ packet.id }}" title="View packet details">✉️</a> {{ packet.from_node.channel }}
|
||||
</span>
|
||||
<span class="col-2 username">
|
||||
<a href="/packet_list/{{ packet.from_node_id }}" title="View all packets from this node">
|
||||
{{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="col-5 message">
|
||||
{{ packet.payload }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
No packets found.
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="container" >
|
||||
{% for packet in packets %}
|
||||
{% include 'chat_packet.html' %}
|
||||
{% else %}
|
||||
No packets found.
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
let lastTime = null;
|
||||
// Initialize lastTime from DOM
|
||||
const firstPacket = document.querySelector(".chat-packet");
|
||||
if (firstPacket) {
|
||||
lastTime = firstPacket.querySelector(".timestamp")?.getAttribute("title");
|
||||
console.log("Initialized lastTime from DOM:", lastTime);
|
||||
}
|
||||
// Track rendered packets
|
||||
const renderedPacketIds = new Set();
|
||||
document.querySelectorAll(".chat-packet").forEach(div => {
|
||||
const id = div.dataset.packetId;
|
||||
if (id) renderedPacketIds.add(id);
|
||||
});
|
||||
|
||||
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function fetchUpdates() {
|
||||
try {
|
||||
const url = new URL("/chat/updates", window.location.origin);
|
||||
if (lastTime) {
|
||||
url.searchParams.set("last_time", lastTime);
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
const chatContainer = document.querySelector(".container");
|
||||
if (!chatContainer) return;
|
||||
|
||||
if (data.packets && data.packets.length > 0) {
|
||||
for (const packet of data.packets) {
|
||||
if (renderedPacketIds.has(packet.id)) continue;
|
||||
renderedPacketIds.add(packet.id);
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "row chat-packet";
|
||||
div.dataset.packetId = packet.id;
|
||||
|
||||
div.innerHTML = `
|
||||
<span class="col-2 timestamp" title="${packet.import_time}">
|
||||
${new Date(packet.import_time).toLocaleString()}
|
||||
</span>
|
||||
<span class="col-2 channel">
|
||||
<a href="/packet/${packet.id}" title="View packet details">✉️</a> ${packet.channel}
|
||||
</span>
|
||||
<span class="col-2 username">
|
||||
<a href="/packet_list/${packet.from_node_id}" title="View all packets from this node">
|
||||
${packet.long_name ? packet.long_name : parseInt(packet.from_node_id).toString(16)}
|
||||
</a>
|
||||
</span>
|
||||
<span class="col-5 message">${escapeHtml(packet.payload)}</span>
|
||||
`;
|
||||
|
||||
chatContainer.prepend(div);
|
||||
chatContainer.prepend(div);
|
||||
|
||||
// Trigger flash animation
|
||||
div.classList.add("flash");
|
||||
|
||||
// Optionally remove the class after animation ends
|
||||
setTimeout(() => div.classList.remove("flash"), 2500);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (data.latest_import_time) {
|
||||
lastTime = data.latest_import_time;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(fetchUpdates, 5000); // Poll every 5 seconds
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
<script>
|
||||
let refreshInterval;
|
||||
|
||||
function updateURLWithPort() {
|
||||
let selectedPort = document.querySelector('select[name="portnum"]').value;
|
||||
let url = new URL(window.location.href);
|
||||
url.searchParams.set('portnum', selectedPort);
|
||||
|
||||
// Save scroll position before refreshing
|
||||
localStorage.setItem("scrollPosition", window.scrollY);
|
||||
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
refreshInterval = setInterval(updateURLWithPort, 5000);
|
||||
localStorage.setItem("autoRefresh", "true");
|
||||
updateButtonState(true);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
clearInterval(refreshInterval);
|
||||
localStorage.setItem("autoRefresh", "false");
|
||||
updateButtonState(false);
|
||||
}
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
let isEnabled = localStorage.getItem("autoRefresh") === "true";
|
||||
if (isEnabled) {
|
||||
stopAutoRefresh();
|
||||
} else {
|
||||
startAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtonState(isEnabled) {
|
||||
let button = document.getElementById("auto-refresh-button");
|
||||
button.innerText = isEnabled ? "Disable Auto-Refresh" : "Enable Auto-Refresh";
|
||||
}
|
||||
|
||||
function restoreScrollPosition() {
|
||||
let scrollPosition = localStorage.getItem("scrollPosition");
|
||||
if (scrollPosition !== null) {
|
||||
window.scrollTo(0, parseInt(scrollPosition, 10));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelector('select[name="portnum"]').addEventListener('change', updateURLWithPort);
|
||||
document.getElementById("auto-refresh-button").addEventListener('click', toggleAutoRefresh);
|
||||
|
||||
// Restore auto-refresh state
|
||||
let isEnabled = localStorage.getItem("autoRefresh") === "true";
|
||||
updateButtonState(isEnabled);
|
||||
if (isEnabled) {
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
// Restore scroll position
|
||||
restoreScrollPosition();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<form class="row">
|
||||
<form class="d-flex align-items-center justify-content-between mb-2">
|
||||
{% set options = {
|
||||
1: "Text Message",
|
||||
3: "Position",
|
||||
@@ -84,14 +37,16 @@
|
||||
70: "Trace Route",
|
||||
}
|
||||
%}
|
||||
<select name="portnum" class="col-2 m-2">
|
||||
<select name="portnum" class="form-select form-select-sm w-auto">
|
||||
<option value="" {% if portnum not in options %}selected{% endif %}>All</option>
|
||||
{% for value, name in options.items() %}
|
||||
<option value="{{ value }}" {% if value == portnum %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" id="auto-refresh-button">Enable Auto-Refresh</button>
|
||||
|
||||
<button type="button" id="pause-button" class="btn btn-sm btn-outline-secondary">Pause</button>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs" id="packet_list">
|
||||
{% for packet in packets %}
|
||||
@@ -103,4 +58,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let lastTime = null;
|
||||
let portnum = "{{ portnum if portnum is not none else '' }}";
|
||||
let updatesPaused = false;
|
||||
|
||||
function fetchUpdates() {
|
||||
if (updatesPaused) return;
|
||||
|
||||
const url = new URL("/firehose/updates", window.location.origin);
|
||||
if (lastTime) url.searchParams.set("last_time", lastTime);
|
||||
if (portnum) url.searchParams.set("portnum", portnum);
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.packets && data.packets.length > 0) {
|
||||
lastTime = data.latest_import_time;
|
||||
const list = document.getElementById("packet_list");
|
||||
|
||||
for (const html of data.packets.reverse()) {
|
||||
list.insertAdjacentHTML("afterbegin", html);
|
||||
const firstChild = list.firstElementChild;
|
||||
if (firstChild) {
|
||||
firstChild.classList.add("packet-fade-in");
|
||||
firstChild.addEventListener("animationend", () => {
|
||||
firstChild.classList.remove("packet-fade-in");
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Update fetch failed:", err);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const pauseBtn = document.getElementById("pause-button");
|
||||
|
||||
document.querySelector('select[name="portnum"]').addEventListener("change", (e) => {
|
||||
const selected = e.target.value;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("portnum", selected);
|
||||
window.location.href = url;
|
||||
});
|
||||
|
||||
pauseBtn.addEventListener("click", () => {
|
||||
updatesPaused = !updatesPaused;
|
||||
pauseBtn.textContent = updatesPaused ? "Resume" : "Pause";
|
||||
});
|
||||
|
||||
// Start fetching updates
|
||||
fetchUpdates();
|
||||
setInterval(fetchUpdates, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,10 +24,30 @@
|
||||
<div class="container" > {{ site_config["site"]["weekly_net_message"] }} <br><br>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% for packet in packets %}
|
||||
{% include 'chat_packet.html' %}
|
||||
{% else %}
|
||||
No packets found.
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for packet in packets %}
|
||||
<div
|
||||
class="row chat-packet"
|
||||
data-packet-id="{{ packet.id }}"
|
||||
role="article"
|
||||
aria-label="Chat message from {{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}"
|
||||
>
|
||||
<span class="col-2 timestamp">
|
||||
{{ packet.import_time.strftime('%-I:%M:%S %p - %m-%d-%Y') }}
|
||||
</span>
|
||||
<span class="col-1 timestamp">
|
||||
<a href="/packet/{{ packet.id }}" title="View packet details">✉️</a> {{ packet.from_node.channel }}
|
||||
</span>
|
||||
<span class="col-2 username">
|
||||
<a href="/packet_list/{{ packet.from_node_id }}" title="View all packets from this node">
|
||||
{{ packet.from_node.long_name or (packet.from_node_id | node_id_to_hex) }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="col-5 message">
|
||||
{{ packet.payload }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
No packets found.
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span {% if from_me %} class="fw-bold" {% endif %}>
|
||||
{{packet.from_node.long_name}}(
|
||||
{%- if not from_me -%}
|
||||
<a hx-target="#node" href="/node_search?q={{packet.from_node_id|node_id_to_hex}}">
|
||||
<a href="/node_search?q={{packet.from_node_id|node_id_to_hex}}">
|
||||
{%- endif -%}
|
||||
{{packet.from_node_id|node_id_to_hex}}
|
||||
{%- if not from_me -%}
|
||||
|
||||
@@ -1,67 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="mynetwork" style="width: 100%; height: 600px;"></div>
|
||||
<div id="mynetwork" style="width: 100%; height: 800px;"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var chart = echarts.init(document.getElementById('mynetwork'));
|
||||
const chart = echarts.init(document.getElementById('mynetwork'));
|
||||
|
||||
// Define the nodes and edges passed from the backend
|
||||
var nodes = {{ chart_data['nodes'] | tojson }};
|
||||
var edges = {{ chart_data['edges'] | tojson }};
|
||||
const rawNodes = {{ chart_data['nodes'] | tojson }};
|
||||
const rawEdges = {{ chart_data['edges'] | tojson }};
|
||||
|
||||
var option = {
|
||||
backgroundColor: '#ffffff', // Set background color to white
|
||||
tooltip: {},
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
data: nodes,
|
||||
links: edges,
|
||||
roam: true,
|
||||
force: {
|
||||
repulsion: 500,
|
||||
edgeLength: [100, 200],
|
||||
gravity: 0.1
|
||||
// Build DAG layout
|
||||
const layers = {};
|
||||
const nodeDepth = {};
|
||||
|
||||
// Organize nodes into layers by hop count
|
||||
for (const edge of rawEdges) {
|
||||
const { source, target } = edge;
|
||||
if (!(source in nodeDepth)) nodeDepth[source] = 0;
|
||||
const nextDepth = nodeDepth[source] + 1;
|
||||
nodeDepth[target] = Math.max(nodeDepth[target] || 0, nextDepth);
|
||||
}
|
||||
|
||||
for (const node of rawNodes) {
|
||||
const depth = nodeDepth[node.name] || 0;
|
||||
if (!(depth in layers)) layers[depth] = [];
|
||||
layers[depth].push(node);
|
||||
}
|
||||
|
||||
// Position nodes manually
|
||||
const chartNodes = [];
|
||||
const layerKeys = Object.keys(layers).sort((a, b) => +a - +b);
|
||||
const verticalSpacing = 100;
|
||||
const horizontalSpacing = 180;
|
||||
|
||||
layerKeys.forEach((depth, layerIndex) => {
|
||||
const layer = layers[depth];
|
||||
const y = layerIndex * verticalSpacing;
|
||||
const xStart = -(layer.length - 1) * horizontalSpacing / 2;
|
||||
layer.forEach((node, i) => {
|
||||
chartNodes.push({
|
||||
...node,
|
||||
x: xStart + i * horizontalSpacing,
|
||||
y: y,
|
||||
itemStyle: {
|
||||
color: '#dddddd',
|
||||
borderColor: '#222',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#000',
|
||||
padding: [5, 10],
|
||||
formatter: function(params) { return params.data.value; },
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderColor: '#999',
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
z: 5 // Label z-index is now 5, to be below the edges
|
||||
fontSize: 12,
|
||||
formatter: node.short_name || node.name,
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderColor: '#1E1E1E',
|
||||
borderWidth: 2,
|
||||
}
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#ccc', // Edge color
|
||||
curveness: 0.1, // Slight curve for edges
|
||||
type: 'solid',
|
||||
z: 10 // Edge lines have a higher z-index than the labels
|
||||
},
|
||||
edgeSymbol: ['arrow', 'arrow'], // Both ends of the edge will have arrowheads
|
||||
edgeSymbolSize: [8, 8], // Size of the arrows
|
||||
z: 15 // Ensure edges (arrows) are on top of both the nodes and labels
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const chartEdges = rawEdges.map(edge => ({
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
lineStyle: {
|
||||
color: edge.originalColor || '#ccc',
|
||||
width: 2,
|
||||
type: 'solid',
|
||||
},
|
||||
}));
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#fff',
|
||||
tooltip: {},
|
||||
animation: false,
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'none',
|
||||
coordinateSystem: null,
|
||||
data: chartNodes,
|
||||
links: chartEdges,
|
||||
roam: true,
|
||||
edgeSymbol: ['none', 'arrow'],
|
||||
edgeSymbolSize: [0, 10],
|
||||
lineStyle: {
|
||||
curveness: 0,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+100
-2
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user