diff --git a/src/meshcore_hub/api/routes/dashboard.py b/src/meshcore_hub/api/routes/dashboard.py index 3711255..f85f438 100644 --- a/src/meshcore_hub/api/routes/dashboard.py +++ b/src/meshcore_hub/api/routes/dashboard.py @@ -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, ) diff --git a/src/meshcore_hub/common/schemas/messages.py b/src/meshcore_hub/common/schemas/messages.py index 105b751..feeeb02 100644 --- a/src/meshcore_hub/common/schemas/messages.py +++ b/src/meshcore_hub/common/schemas/messages.py @@ -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)", + ) diff --git a/src/meshcore_hub/web/templates/network.html b/src/meshcore_hub/web/templates/network.html index 0e806a1..a590ae7 100644 --- a/src/meshcore_hub/web/templates/network.html +++ b/src/meshcore_hub/web/templates/network.html @@ -130,39 +130,48 @@ - + + {% if stats.channel_messages %}

- Channel Messages + Recent Channel Messages

- {% if stats.channel_message_counts %} -
- - - - - - - - - {% for channel, count in stats.channel_message_counts.items() %} - - - - +
+ {% for channel, messages in stats.channel_messages.items() %} +
+

+ CH{{ channel }} + Channel {{ channel }} +

+
+ {% for msg in messages %} +
+
+ + {% if msg.sender_friendly_name or msg.sender_name %} + {{ msg.sender_friendly_name or msg.sender_name }} + {% elif msg.pubkey_prefix %} + {{ msg.pubkey_prefix[:8] }} + {% else %} + Unknown + {% endif %} + + {{ msg.received_at.split('T')[1][:5] if msg.received_at else '' }} +
+
{{ msg.text }}
+
{% endfor %} -
-
ChannelCount
Channel {{ channel }}{{ count }}
+
+
+ {% endfor %}
- {% else %} -

No channel messages recorded yet.

- {% endif %} + {% endif %}