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:
Claude
2025-12-05 18:20:49 +00:00
parent e3ce1258a8
commit 0d14ed0ccc
3 changed files with 104 additions and 23 deletions
+55 -1
View File
@@ -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)",
)
+31 -22
View File
@@ -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 -->