mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-08 09:53:05 +02:00
Add latest channel messages to Network dashboard
Replace the channel counts table with actual recent messages per channel: - Added ChannelMessage schema for channel message summaries - Dashboard API now fetches latest 5 messages for each channel with sender name lookups - Network page displays messages grouped by channel with sender names and timestamps - Only shows channels that have messages
This commit is contained in:
@@ -9,7 +9,11 @@ from sqlalchemy import func, select
|
||||
from meshcore_hub.api.auth import RequireRead
|
||||
from meshcore_hub.api.dependencies import DbSession
|
||||
from meshcore_hub.common.models import Advertisement, Message, Node, NodeTag
|
||||
from meshcore_hub.common.schemas.messages import DashboardStats, RecentAdvertisement
|
||||
from meshcore_hub.common.schemas.messages import (
|
||||
ChannelMessage,
|
||||
DashboardStats,
|
||||
RecentAdvertisement,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -123,6 +127,55 @@ async def get_stats(
|
||||
int(channel): int(count) for channel, count in channel_results
|
||||
}
|
||||
|
||||
# Get latest 5 messages for each channel that has messages
|
||||
channel_messages: dict[int, list[ChannelMessage]] = {}
|
||||
for channel_idx, _ in channel_results:
|
||||
messages_query = (
|
||||
select(Message)
|
||||
.where(Message.message_type == "channel")
|
||||
.where(Message.channel_idx == channel_idx)
|
||||
.order_by(Message.received_at.desc())
|
||||
.limit(5)
|
||||
)
|
||||
channel_msgs = session.execute(messages_query).scalars().all()
|
||||
|
||||
# Look up sender names for these messages
|
||||
msg_prefixes = [m.pubkey_prefix for m in channel_msgs if m.pubkey_prefix]
|
||||
msg_sender_names: dict[str, str] = {}
|
||||
msg_friendly_names: dict[str, str] = {}
|
||||
if msg_prefixes:
|
||||
for prefix in set(msg_prefixes):
|
||||
sender_node_query = select(Node.public_key, Node.name).where(
|
||||
Node.public_key.startswith(prefix)
|
||||
)
|
||||
for public_key, name in session.execute(sender_node_query).all():
|
||||
if name:
|
||||
msg_sender_names[public_key[:12]] = name
|
||||
|
||||
sender_friendly_query = (
|
||||
select(Node.public_key, NodeTag.value)
|
||||
.join(NodeTag, Node.id == NodeTag.node_id)
|
||||
.where(Node.public_key.startswith(prefix))
|
||||
.where(NodeTag.key == "friendly_name")
|
||||
)
|
||||
for public_key, value in session.execute(sender_friendly_query).all():
|
||||
msg_friendly_names[public_key[:12]] = value
|
||||
|
||||
channel_messages[int(channel_idx)] = [
|
||||
ChannelMessage(
|
||||
text=m.text,
|
||||
sender_name=(
|
||||
msg_sender_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
),
|
||||
sender_friendly_name=(
|
||||
msg_friendly_names.get(m.pubkey_prefix) if m.pubkey_prefix else None
|
||||
),
|
||||
pubkey_prefix=m.pubkey_prefix,
|
||||
received_at=m.received_at,
|
||||
)
|
||||
for m in channel_msgs
|
||||
]
|
||||
|
||||
return DashboardStats(
|
||||
total_nodes=total_nodes,
|
||||
active_nodes=active_nodes,
|
||||
@@ -132,6 +185,7 @@ async def get_stats(
|
||||
advertisements_24h=advertisements_24h,
|
||||
recent_advertisements=recent_advertisements,
|
||||
channel_message_counts=channel_message_counts,
|
||||
channel_messages=channel_messages,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -187,6 +187,20 @@ class RecentAdvertisement(BaseModel):
|
||||
received_at: datetime = Field(..., description="When received")
|
||||
|
||||
|
||||
class ChannelMessage(BaseModel):
|
||||
"""Schema for a channel message summary."""
|
||||
|
||||
text: str = Field(..., description="Message text")
|
||||
sender_name: Optional[str] = Field(default=None, description="Sender name")
|
||||
sender_friendly_name: Optional[str] = Field(
|
||||
default=None, description="Sender friendly name"
|
||||
)
|
||||
pubkey_prefix: Optional[str] = Field(
|
||||
default=None, description="Sender public key prefix"
|
||||
)
|
||||
received_at: datetime = Field(..., description="When received")
|
||||
|
||||
|
||||
class DashboardStats(BaseModel):
|
||||
"""Schema for dashboard statistics."""
|
||||
|
||||
@@ -205,3 +219,7 @@ class DashboardStats(BaseModel):
|
||||
default_factory=dict,
|
||||
description="Message count per channel",
|
||||
)
|
||||
channel_messages: dict[int, list[ChannelMessage]] = Field(
|
||||
default_factory=dict,
|
||||
description="Recent messages per channel (up to 5 each)",
|
||||
)
|
||||
|
||||
@@ -130,39 +130,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel Stats -->
|
||||
<!-- Channel Messages -->
|
||||
{% if stats.channel_messages %}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
Channel Messages
|
||||
Recent Channel Messages
|
||||
</h2>
|
||||
{% if stats.channel_message_counts %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-compact w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th class="text-right">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for channel, count in stats.channel_message_counts.items() %}
|
||||
<tr>
|
||||
<td>Channel {{ channel }}</td>
|
||||
<td class="text-right font-mono">{{ count }}</td>
|
||||
</tr>
|
||||
<div class="space-y-4">
|
||||
{% for channel, messages in stats.channel_messages.items() %}
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-2 flex items-center gap-2">
|
||||
<span class="badge badge-info badge-sm">CH{{ channel }}</span>
|
||||
Channel {{ channel }}
|
||||
</h3>
|
||||
<div class="space-y-1 pl-2 border-l-2 border-base-300">
|
||||
{% for msg in messages %}
|
||||
<div class="text-sm">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-medium text-xs opacity-70 whitespace-nowrap">
|
||||
{% if msg.sender_friendly_name or msg.sender_name %}
|
||||
{{ msg.sender_friendly_name or msg.sender_name }}
|
||||
{% elif msg.pubkey_prefix %}
|
||||
<span class="font-mono">{{ msg.pubkey_prefix[:8] }}</span>
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="text-xs opacity-50">{{ msg.received_at.split('T')[1][:5] if msg.received_at else '' }}</span>
|
||||
</div>
|
||||
<div class="break-words" style="white-space: pre-wrap;">{{ msg.text }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-70">No channel messages recorded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
|
||||
Reference in New Issue
Block a user