Merge branch 'pablorevilla-meshtastic:master' into master

This commit is contained in:
madeofstown
2025-01-26 17:45:44 -08:00
committed by GitHub
5 changed files with 639 additions and 36 deletions
+3 -1
View File
@@ -9,7 +9,7 @@ from sqlalchemy import ForeignKey, BigInteger
class Base(AsyncAttrs, DeclarativeBase):
pass
# Node
class Node(Base):
__tablename__ = "node"
id: Mapped[str] = mapped_column(primary_key=True)
@@ -20,6 +20,7 @@ class Node(Base):
role: Mapped[str] = mapped_column(nullable=True)
last_lat: Mapped[int] = mapped_column(BigInteger, nullable=True)
last_long: Mapped[int] = mapped_column(BigInteger, nullable=True)
channel: Mapped[str]
class Packet(Base):
@@ -36,6 +37,7 @@ class Packet(Base):
)
payload: Mapped[bytes]
import_time: Mapped[datetime]
channel: Mapped[str]
class PacketSeen(Base):
+231 -26
View File
@@ -12,30 +12,6 @@ from meshview.models import Packet, PacketSeen, Node, Traceroute
from meshview import notify
# We count the total amount of packages
# This is to be used by /stats in web.py
async def get_total_packet_count():
async with database.async_session() as session:
q = select(func.count(Packet.id)) # Use SQLAlchemy's func to count packets
result = await session.execute(q)
return result.scalar() # Return the total count of packets
# We count the total amount of nodes
async def get_total_node_count():
async with database.async_session() as session:
q = select(func.count(Node.id)) # Use SQLAlchemy's func to count nodes
result = await session.execute(q)
return result.scalar() # Return the total count of nodes
# We count the total amount of seen packets
async def get_total_packet_seen_count():
async with database.async_session() as session:
q = select(func.count(PacketSeen.node_id)) # Use SQLAlchemy's func to count nodes
result = await session.execute(q)
return result.scalar() # Return the total count of seen packets
async def process_envelope(topic, env):
if not env.packet.id:
return
@@ -54,6 +30,7 @@ async def process_envelope(topic, env):
payload=env.packet.SerializeToString(),
# p.r. Here seems to be where the packet is imported on the Database and import time is set.
import_time=datetime.datetime.now(),
channel=env.channel_id,
)
session.add(packet)
@@ -111,7 +88,7 @@ async def process_envelope(topic, env):
node.short_name = user.short_name
node.hw_model = hw_model
node.role = role
# if need to update time of last update it may be here
# if need to update time of last update it may be here
else:
node = Node(
@@ -121,7 +98,8 @@ async def process_envelope(topic, env):
short_name=user.short_name,
hw_model=hw_model,
role=role,
# if need to update time of last update it may be here
channel=env.channel_id,
# if need to update time of last update it may be here
)
session.add(node)
@@ -293,6 +271,233 @@ async def get_mqtt_neighbors(since):
)
return result
# In order to provide separate network graphs for LongFast and MediumSlow, I am duplicating the procedures.
# 3 procedures are needed. These would have to be replicated for any other network that we may need to use graphs.
#
# get_traceroutes_longfast
# get_packets_longfast
# get_mqtt_neighbors_longfast
#
# p.r.
#
# Get Traceroute for LongFast only
async def get_traceroutes_longfast(since):
async with database.async_session() as session:
result = await session.execute(
select(Traceroute)
.join(Packet)
.where(
(Traceroute.import_time > (datetime.datetime.now() - since))
& (Packet.channel == "LongFast")
)
.order_by(Traceroute.import_time)
)
return result.scalars()
# Get MQTT Neighbors for LongFast only
# p.r.
async def get_mqtt_neighbors_longfast(since):
async with database.async_session() as session:
result = await session.execute(select(PacketSeen, Packet)
.join(Packet)
.where(
(PacketSeen.hop_limit == PacketSeen.hop_start)
& (PacketSeen.hop_start != 0)
& (Packet.channel == "LongFast")
)
.options(
lazyload(Packet.from_node),
lazyload(Packet.to_node),
)
)
return result
# Get Packets for LongFast only
# p.r.
async def get_packets_longfast(node_id=None, portnum=None, since=None, limit=500, before=None, after=None):
async with database.async_session() as session:
q = select(Packet)
# Add condition for channel being "LongFast"
q = q.where(Packet.channel == "LongFast")
if node_id:
q = q.where(
(Packet.from_node_id == node_id) | (Packet.to_node_id == node_id)
)
if portnum:
q = q.where(Packet.portnum == portnum)
if since:
q = q.where(Packet.import_time > (datetime.datetime.now() - since))
if before:
q = q.where(Packet.import_time < before)
if after:
q = q.where(Packet.import_time > after)
if limit is not None:
q = q.limit(limit)
result = await session.execute(q.order_by(Packet.import_time.desc()))
return result.scalars()
# Get Traceroute for mediumslow only
# p.r.
async def get_traceroutes_mediumslow(since):
async with database.async_session() as session:
result = await session.execute(
select(Traceroute)
.join(Packet)
.where(
(Traceroute.import_time > (datetime.datetime.now() - since))
& (Packet.channel == "MediumSlow")
)
.order_by(Traceroute.import_time)
)
return result.scalars()
# Get MQTT Neighbors for mediumslow only
# p.r.
async def get_mqtt_neighbors_mediumslow(since):
async with database.async_session() as session:
result = await session.execute(select(PacketSeen, Packet)
.join(Packet)
.where(
(PacketSeen.hop_limit == PacketSeen.hop_start)
& (PacketSeen.hop_start != 0)
& (Packet.channel == "MediumSlow")
)
.options(
lazyload(Packet.from_node),
lazyload(Packet.to_node),
)
)
return result
# Get Packets for MediumSlow only
# p.r.
async def get_packets_mediumslow(node_id=None, portnum=None, since=None, limit=500, before=None, after=None):
async with database.async_session() as session:
q = select(Packet)
# Add condition for channel being "MediumSlow"
q = q.where(Packet.channel == "MediumSlow")
if node_id:
q = q.where(
(Packet.from_node_id == node_id) | (Packet.to_node_id == node_id)
)
if portnum:
q = q.where(Packet.portnum == portnum)
if since:
q = q.where(Packet.import_time > (datetime.datetime.now() - since))
if before:
q = q.where(Packet.import_time < before)
if after:
q = q.where(Packet.import_time > after)
if limit is not None:
q = q.limit(limit)
result = await session.execute(q.order_by(Packet.import_time.desc()))
return result.scalars()
# We count the total amount of packages
# This is to be used by /stats in web.py
async def get_total_packet_count():
async with database.async_session() as session:
q = select(func.count(Packet.id)) # Use SQLAlchemy's func to count packets
result = await session.execute(q)
return result.scalar() # Return the total count of packets
# We count the total amount of nodes
async def get_total_node_count():
async with database.async_session() as session:
q = select(func.count(Node.id)) # Use SQLAlchemy's func to count nodes
result = await session.execute(q)
return result.scalar() # Return the total count of nodes
# We count the total amount of seen packets
async def get_total_packet_seen_count():
async with database.async_session() as session:
q = select(func.count(PacketSeen.node_id)) # Use SQLAlchemy's func to count nodes
result = await session.execute(q)
return result.scalar() # Return the total count of seen packets
async def get_total_node_count_longfast() -> int:
"""
Retrieves the total count of nodes where the channel is equal to 'LongFast'.
This function queries the database asynchronously to count the number of nodes
in the `Node` table that meet the condition `channel == 'LongFast'`. It uses
SQLAlchemy's asynchronous session management and query construction.
Returns:
int: The total count of nodes with `channel == 'LongFast'`.
Raises:
Exception: If an error occurs during the database query execution.
"""
try:
# Open an asynchronous session with the database
async with database.async_session() as session:
# Build the query to count nodes where channel == 'LongFast'
q = select(func.count(Node.id)).filter(Node.channel == 'LongFast')
# Execute the query asynchronously and fetch the result
result = await session.execute(q)
# Return the scalar value (the count of nodes)
return result.scalar()
except Exception as e:
# Log or handle the exception if needed (optional, replace with logging if necessary)
print(f"An error occurred: {e}")
return 0 # Return 0 or an appropriate fallback value in case of an error
async def get_total_node_count_mediumslow() -> int:
"""
Retrieves the total count of nodes where the channel is equal to 'MediumSlow'.
This function queries the database asynchronously to count the number of nodes
in the `Node` table that meet the condition `channel == 'MediumSlow'`. It uses
SQLAlchemy's asynchronous session management and query construction.
Returns:
int: The total count of nodes with `channel == 'MediumSlow'`.
Raises:
Exception: If an error occurs during the database query execution.
"""
try:
# Open an asynchronous session with the database
async with database.async_session() as session:
# Build the query to count nodes where channel == 'LongFast'
q = select(func.count(Node.id)).filter(Node.channel == 'MediumSlow')
# Execute the query asynchronously and fetch the result
result = await session.execute(q)
# Return the scalar value (the count of nodes)
return result.scalar()
except Exception as e:
# Log or handle the exception if needed (optional, replace with logging if necessary)
print(f"An error occurred: {e}")
return 0 # Return 0 or an appropriate fallback value in case of an error
# Get Nodes for mediumslow only
# p.r.
async def get_nodes_mediumslow():
async with database.async_session() as session:
result = await session.execute(
select(Node)
.where(
(Node.channel == "MediumSlow")
)
)
return result.scalars()
+2 -2
View File
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<title>MeshView - Bay Area Mesh - http://bayme.sh {% if node and node.short_name %}-- {{node.short_name}}{% endif %}</title>
<title>MeshView - Bay Area Mesh - http://meshview.bayme.sh {% if node and node.short_name %}-- {{node.short_name}}{% endif %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/htmx.org@1.9.11" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous"></script>
@@ -34,7 +34,7 @@
</head>
<body hx-indicator="#spinner">
<br><div style="text-align:center"><strong>Bay Area Mesh - http://bayme.sh</strong></div>
<div style="text-align:center">Quick Links:&nbsp;&nbsp;<a href="/">Search for a node </a>&nbsp;-&nbsp;<a href="/chat">Conversations</a>&nbsp;-&nbsp;<a href="/firehose">See <strong>everything</strong> </a>&nbsp;-&nbsp;<a href="/graph/network">Mesh Graph </a>&nbsp;-&nbsp;<a href="/stats">Stats </a></div><br>
<div style="text-align:center">Quick Links:&nbsp;&nbsp;<a href="/">Search for a node </a>&nbsp;-&nbsp;<a href="/chat">Conversations</a>&nbsp;-&nbsp;<a href="/firehose">See <strong>everything</strong> </a>&nbsp;-&nbsp;Mesh Graph <a href="/graph/longfast">LG</a>&nbsp;-&nbsp;<a href="/graph/mediumslow">MS</a>&nbsp;-&nbsp;<a href="/stats">Stats </a></div><br>
<div id="spinner" class="spinner-border secondary-primary htmx-indicator position-absolute top-50 start-50" role="status">
<span class="visually-hidden">Loading...</span>
</div>
+46 -4
View File
@@ -8,10 +8,52 @@
{% endblock %}
{% block body %}
<div style="font-family: Arial, sans-serif; color: #333; background-color: #f9f9f9; padding: 15px; border-radius: 8px; max-width: 300px; margin: auto;">
<p style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">Total Nodes: <span style="font-weight: normal; color: #007bff;">{{ total_nodes }}</span></p>
<p style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">Total Packets: <span style="font-weight: normal; color: #007bff;">{{ total_packets }}</span></p>
<p style="font-size: 18px; font-weight: bold;">Total MQTT Reports: <span style="font-weight: normal; color: #007bff;">{{ total_packets_seen }}</span></p>
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; color: #fff; background: linear-gradient(135deg, #6a11cb, #2575fc); padding: 25px; border-radius: 15px; max-width: 350px; margin: auto; text-align: center; box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);">
<h2 style="font-size: 22px; margin-bottom: 20px; font-weight: 600;">
Mesh Statistics
</h2>
<!-- Section for Total Nodes -->
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 15px;">
<p style="font-size: 16px; margin: 0; font-weight: 500;">
Total Nodes:
<span style="font-weight: 700; color: #ffeb3b;">{{ total_nodes }}</span>
</p>
</div>
<!-- Section for Total Nodes LongFast -->
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 10px;">
<p style="font-size: 14px; margin: 0; font-weight: 500;">
Total Nodes LongFast:
<span style="font-weight: 700; color: #03dac6;">{{ total_nodes_longfast }}</span>
<span style="font-size: 12px; color: #ffeb3b; font-weight: 400;">
({{ (total_nodes_longfast / total_nodes * 100) | round(2) }}%)
</span>
</p>
</div>
<!-- Section for Total Nodes MediumSlow -->
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 10px;">
<p style="font-size: 14px; margin: 0; font-weight: 500;">
Total Nodes MediumSlow:
<span style="font-weight: 700; color: #03dac6;">{{ total_nodes_mediumslow }}</span>
<span style="font-size: 12px; color: #ffeb3b; font-weight: 400;">
({{ (total_nodes_mediumslow / total_nodes * 100) | round(2) }}%)
</span>
</p>
</div>
<!-- Section for Total Packets -->
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px; margin-bottom: 10px;">
<p style="font-size: 14px; margin: 0; font-weight: 500;">
Total Packets:
<span style="font-weight: 700; color: #03dac6;">{{ total_packets }}</span>
</p>
</div>
<!-- Section for Total MQTT Reports -->
<div style="background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 10px;">
<p style="font-size: 14px; margin: 0; font-weight: 500;">
Total MQTT Reports:
<span style="font-weight: 700; color: #03dac6;">{{ total_packets_seen }}</span>
</p>
</div>
</div>
+357 -3
View File
@@ -1086,18 +1086,372 @@ async def stats(request):
total_packets = await store.get_total_packet_count()
total_nodes = await store.get_total_node_count()
total_packets_seen = await store.get_total_packet_seen_count()
total_nodes_longfast = await store.get_total_node_count_longfast()
total_nodes_mediumslow = await store.get_total_node_count_mediumslow()
# Render the stats template with the total packet count
template = env.get_template("stats.html")
return web.Response(
text=template.render(total_packets=total_packets, total_nodes=total_nodes,total_packets_seen=total_packets_seen ),
text=template.render(total_packets=total_packets, total_nodes=total_nodes,total_packets_seen=total_packets_seen,total_nodes_longfast=total_nodes_longfast, total_nodes_mediumslow=total_nodes_mediumslow ),
content_type="text/html",
)
# In the stats.html template, you would include something like:
# <p>Total Packets: {{ total_packets }}</p>
@routes.get("/graph/longfast")
async def graph_network_longfast(request):
try:
root = request.query.get("root")
depth = int(request.query.get("depth", 5))
hours = int(request.query.get("hours", 24))
minutes = int(request.query.get("minutes", 0))
# Validation
if root and not root.isdigit():
return web.Response(status=400, text="Invalid root node ID.")
if depth < 1:
return web.Response(status=400, text="Depth must be at least 1.")
since = datetime.timedelta(hours=hours, minutes=minutes)
nodes = {}
node_ids = set()
traceroutes = []
# Fetch traceroutes
for tr in await store.get_traceroutes_longfast(since):
node_ids.add(tr.gateway_node_id)
node_ids.add(tr.packet.from_node_id)
node_ids.add(tr.packet.to_node_id)
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
node_ids.update(route.route)
path = [tr.packet.from_node_id]
path.extend(route.route)
if tr.done:
path.append(tr.packet.to_node_id)
else:
if path[-1] != tr.gateway_node_id:
path.append(tr.gateway_node_id)
traceroutes.append((tr, path))
edges = Counter()
edge_type = {}
used_nodes = set()
# Fetch MQTT neighbors
for ps, p in await store.get_mqtt_neighbors_longfast(since):
node_ids.add(ps.node_id)
node_ids.add(p.from_node_id)
used_nodes.add(ps.node_id)
used_nodes.add(p.from_node_id)
edges[(p.from_node_id, ps.node_id)] += 1
edge_type[(p.from_node_id, ps.node_id)] = 'sni'
# Fetch NeighborInfo packets
for packet in await store.get_packets_longfast(portnum=PortNum.NEIGHBORINFO_APP, since=since):
try:
_, neighbor_info = decode_payload.decode(packet)
node_ids.add(packet.from_node_id)
used_nodes.add(packet.from_node_id)
for node in neighbor_info.neighbors:
node_ids.add(node.node_id)
used_nodes.add(node.node_id)
edges[(node.node_id, packet.from_node_id)] += 1
edge_type[(node.node_id, packet.from_node_id)] = 'ni'
except Exception as e:
print(f"Error decoding NeighborInfo packet: {e}")
# Retrieve node details concurrently
async with asyncio.TaskGroup() as tg:
for node_id in node_ids:
nodes[node_id] = tg.create_task(store.get_node(node_id))
tr_done = set()
for tr, path in traceroutes:
if tr.done:
if tr.packet_id in tr_done:
continue
else:
tr_done.add(tr.packet_id)
for src, dest in zip(path, path[1:]):
used_nodes.add(src)
used_nodes.add(dest)
edges[(src, dest)] += 1
edge_type[(src, dest)] = 'tr'
# Helper to get node name
async def get_node_name(node_id):
try:
node = await nodes[node_id]
if node:
return f'[{node.short_name}] {node.long_name}\n{node_id_to_hex(node_id)}'
return node_id_to_hex(node_id)
except Exception as e:
return f"Error retrieving node: {str(e)}"
# Filter nodes and edges if root is specified
if root:
new_used_nodes = set()
new_edges = Counter()
edge_map = {}
for src, dest in edges:
edge_map.setdefault(dest, []).append(src)
queue = [int(root)]
for _ in range(depth):
next_queue = []
for node in queue:
new_used_nodes.add(node)
for dest in edge_map.get(node, []):
new_used_nodes.add(dest)
new_edges[(dest, node)] += 1
next_queue.append(dest)
queue = next_queue
used_nodes = new_used_nodes
edges = new_edges
# Create graph
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
for node_id in used_nodes:
node = await nodes[node_id]
color = '#000000'
node_name = await get_node_name(node_id)
if node and node.role in ('ROUTER', 'ROUTER_CLIENT', 'REPEATER'):
color = '#0000FF'
elif node and node.role == 'CLIENT_MUTE':
color = '#00FF00'
graph.add_node(pydot.Node(
str(node_id),
label=node_name,
shape='box',
color=color,
href=f"/graph/network?root={node_id}&amp;depth={depth-1}",
))
# Adjust edge visualization
if edges:
max_edge_count = edges.most_common(1)[0][1]
else:
max_edge_count = 1
size_ratio = 2. / max_edge_count
edge_added = set()
for (src, dest), edge_count in edges.items():
size = max(size_ratio * edge_count, .25)
arrowsize = max(size_ratio * edge_count, .5)
if edge_type[(src, dest)] in ('ni'):
color = '#FF0000'
elif edge_type[(src, dest)] in ('sni'):
color = '#00FF00'
else:
color = '#000000'
edge_dir = "forward"
if (dest, src) in edges and edge_type[(src, dest)] == edge_type[(dest, src)]:
edge_dir = "both"
edge_added.add((dest, src))
if (src, dest) not in edge_added:
edge_added.add((src, dest))
graph.add_edge(pydot.Edge(
str(src),
str(dest),
color=color,
tooltip=f'{await get_node_name(src)} -> {await get_node_name(dest)}',
penwidth=1.85,
dir=edge_dir,
))
return web.Response(
body=graph.create_svg(),
content_type="image/svg+xml",
)
except Exception as e:
print(f"Error in graph_network_longfast: {e}")
return web.Response(status=500, text="Internal Server Error")
@routes.get("/graph/mediumslow")
async def graph_network_mediumslow(request):
try:
root = request.query.get("root")
depth = int(request.query.get("depth", 5))
hours = int(request.query.get("hours", 24))
minutes = int(request.query.get("minutes", 0))
# Validation
if root and not root.isdigit():
return web.Response(status=400, text="Invalid root node ID.")
if depth < 1:
return web.Response(status=400, text="Depth must be at least 1.")
since = datetime.timedelta(hours=hours, minutes=minutes)
nodes = {}
node_ids = set()
traceroutes = []
# Fetch traceroutes
for tr in await store.get_traceroutes_mediumslow(since):
node_ids.add(tr.gateway_node_id)
node_ids.add(tr.packet.from_node_id)
node_ids.add(tr.packet.to_node_id)
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
node_ids.update(route.route)
path = [tr.packet.from_node_id]
path.extend(route.route)
if tr.done:
path.append(tr.packet.to_node_id)
else:
if path[-1] != tr.gateway_node_id:
path.append(tr.gateway_node_id)
traceroutes.append((tr, path))
edges = Counter()
edge_type = {}
used_nodes = set()
# Fetch MQTT neighbors
for ps, p in await store.get_mqtt_neighbors_mediumslow(since):
node_ids.add(ps.node_id)
node_ids.add(p.from_node_id)
used_nodes.add(ps.node_id)
used_nodes.add(p.from_node_id)
edges[(p.from_node_id, ps.node_id)] += 1
edge_type[(p.from_node_id, ps.node_id)] = 'sni'
# Fetch NeighborInfo packets
for packet in await store.get_packets_mediumslow(portnum=PortNum.NEIGHBORINFO_APP, since=since):
try:
_, neighbor_info = decode_payload.decode(packet)
node_ids.add(packet.from_node_id)
used_nodes.add(packet.from_node_id)
for node in neighbor_info.neighbors:
node_ids.add(node.node_id)
used_nodes.add(node.node_id)
edges[(node.node_id, packet.from_node_id)] += 1
edge_type[(node.node_id, packet.from_node_id)] = 'ni'
except Exception as e:
print(f"Error decoding NeighborInfo packet: {e}")
# Retrieve node details concurrently
async with asyncio.TaskGroup() as tg:
for node_id in node_ids:
nodes[node_id] = tg.create_task(store.get_node(node_id))
tr_done = set()
for tr, path in traceroutes:
if tr.done:
if tr.packet_id in tr_done:
continue
else:
tr_done.add(tr.packet_id)
for src, dest in zip(path, path[1:]):
used_nodes.add(src)
used_nodes.add(dest)
edges[(src, dest)] += 1
edge_type[(src, dest)] = 'tr'
# Helper to get node name
async def get_node_name(node_id):
try:
node = await nodes[node_id]
if node:
return f'[{node.short_name}] {node.long_name}\n{node_id_to_hex(node_id)}'
return node_id_to_hex(node_id)
except Exception as e:
return f"Error retrieving node: {str(e)}"
# Filter nodes and edges if root is specified
if root:
new_used_nodes = set()
new_edges = Counter()
edge_map = {}
for src, dest in edges:
edge_map.setdefault(dest, []).append(src)
queue = [int(root)]
for _ in range(depth):
next_queue = []
for node in queue:
new_used_nodes.add(node)
for dest in edge_map.get(node, []):
new_used_nodes.add(dest)
new_edges[(dest, node)] += 1
next_queue.append(dest)
queue = next_queue
used_nodes = new_used_nodes
edges = new_edges
# Create graph
graph = pydot.Dot('network', graph_type="digraph", layout="neato", overlap="false", model='subset', esep="+5")
for node_id in used_nodes:
node = await nodes[node_id]
color = '#000000'
node_name = await get_node_name(node_id)
if node and node.role in ('ROUTER', 'ROUTER_CLIENT', 'REPEATER'):
color = '#0000FF'
elif node and node.role == 'CLIENT_MUTE':
color = '#00FF00'
graph.add_node(pydot.Node(
str(node_id),
label=node_name,
shape='box',
color=color,
href=f"/graph/mediumslow?root={node_id}&amp;depth={depth-1}",
))
# Adjust edge visualization
if edges:
max_edge_count = edges.most_common(1)[0][1]
else:
max_edge_count = 1
size_ratio = 2. / max_edge_count
edge_added = set()
for (src, dest), edge_count in edges.items():
size = max(size_ratio * edge_count, .25)
arrowsize = max(size_ratio * edge_count, .5)
if edge_type[(src, dest)] in ('ni'):
color = '#FF0000'
elif edge_type[(src, dest)] in ('sni'):
color = '#00FF00'
else:
color = '#000000'
edge_dir = "forward"
if (dest, src) in edges and edge_type[(src, dest)] == edge_type[(dest, src)]:
edge_dir = "both"
edge_added.add((dest, src))
if (src, dest) not in edge_added:
edge_added.add((src, dest))
graph.add_edge(pydot.Edge(
str(src),
str(dest),
color=color,
tooltip=f'{await get_node_name(src)} -> {await get_node_name(dest)}',
penwidth=1.85,
dir=edge_dir,
))
return web.Response(
body=graph.create_svg(),
content_type="image/svg+xml",
)
except Exception as e:
print(f"Error in graph_network_longfast: {e}")
return web.Response(status=500, text="Internal Server Error")