From 3469278fba5e6834fba1324fda957160774ecd2b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Dec 2025 17:58:52 +0000 Subject: [PATCH] Add node name lookups to advertisements list - Join with Node table to get node names and tags for both source and receiver nodes - Display friendly_name (from tags), node_name, or advertised name with priority in that order - Show name with public key preview for both Node and Received By columns --- src/meshcore_hub/api/routes/advertisements.py | 107 +++++++++++++++--- src/meshcore_hub/common/schemas/messages.py | 10 ++ .../web/templates/advertisements.html | 13 ++- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/src/meshcore_hub/api/routes/advertisements.py b/src/meshcore_hub/api/routes/advertisements.py index 44fb44e..3f75696 100644 --- a/src/meshcore_hub/api/routes/advertisements.py +++ b/src/meshcore_hub/api/routes/advertisements.py @@ -5,7 +5,7 @@ from typing import Optional from fastapi import APIRouter, HTTPException, Query from sqlalchemy import func, select -from sqlalchemy.orm import aliased +from sqlalchemy.orm import aliased, selectinload from meshcore_hub.api.auth import RequireRead from meshcore_hub.api.dependencies import DbSession @@ -15,6 +15,16 @@ from meshcore_hub.common.schemas.messages import AdvertisementList, Advertisemen router = APIRouter() +def _get_friendly_name(node: Optional[Node]) -> Optional[str]: + """Extract friendly_name tag from a node's tags.""" + if not node or not node.tags: + return None + for tag in node.tags: + if tag.key == "friendly_name": + return tag.value + return None + + @router.get("", response_model=AdvertisementList) async def list_advertisements( _: RequireRead, @@ -29,13 +39,23 @@ async def list_advertisements( offset: int = Query(0, ge=0, description="Page offset"), ) -> AdvertisementList: """List advertisements with filtering and pagination.""" - # Alias for receiver node join + # Aliases for node joins ReceiverNode = aliased(Node) + SourceNode = aliased(Node) - # Build query with receiver node join - query = select( - Advertisement, ReceiverNode.public_key.label("receiver_pk") - ).outerjoin(ReceiverNode, Advertisement.receiver_node_id == ReceiverNode.id) + # Build query with both receiver and source node joins + query = ( + select( + Advertisement, + ReceiverNode.public_key.label("receiver_pk"), + ReceiverNode.name.label("receiver_name"), + ReceiverNode.id.label("receiver_id"), + SourceNode.name.label("source_name"), + SourceNode.id.label("source_id"), + ) + .outerjoin(ReceiverNode, Advertisement.receiver_node_id == ReceiverNode.id) + .outerjoin(SourceNode, Advertisement.node_id == SourceNode.id) + ) if public_key: query = query.where(Advertisement.public_key == public_key) @@ -59,16 +79,38 @@ async def list_advertisements( # Execute results = session.execute(query).all() - # Build response with received_by + # Collect node IDs to fetch tags + node_ids = set() + for row in results: + if row.receiver_id: + node_ids.add(row.receiver_id) + if row.source_id: + node_ids.add(row.source_id) + + # Fetch nodes with tags + nodes_by_id: dict[str, Node] = {} + if node_ids: + nodes_query = ( + select(Node).where(Node.id.in_(node_ids)).options(selectinload(Node.tags)) + ) + nodes = session.execute(nodes_query).scalars().all() + nodes_by_id = {n.id: n for n in nodes} + + # Build response with node details items = [] - for adv, receiver_pk in results: + for row in results: + adv = row[0] + receiver_node = nodes_by_id.get(row.receiver_id) if row.receiver_id else None + source_node = nodes_by_id.get(row.source_id) if row.source_id else None + data = { - "id": adv.id, - "receiver_node_id": adv.receiver_node_id, - "received_by": receiver_pk, - "node_id": adv.node_id, + "received_by": row.receiver_pk, + "receiver_name": row.receiver_name, + "receiver_friendly_name": _get_friendly_name(receiver_node), "public_key": adv.public_key, "name": adv.name, + "node_name": row.source_name, + "node_friendly_name": _get_friendly_name(source_node), "adv_type": adv.adv_type, "flags": adv.flags, "received_at": adv.received_at, @@ -92,9 +134,18 @@ async def get_advertisement( ) -> AdvertisementRead: """Get a single advertisement by ID.""" ReceiverNode = aliased(Node) + SourceNode = aliased(Node) query = ( - select(Advertisement, ReceiverNode.public_key.label("receiver_pk")) + select( + Advertisement, + ReceiverNode.public_key.label("receiver_pk"), + ReceiverNode.name.label("receiver_name"), + ReceiverNode.id.label("receiver_id"), + SourceNode.name.label("source_name"), + SourceNode.id.label("source_id"), + ) .outerjoin(ReceiverNode, Advertisement.receiver_node_id == ReceiverNode.id) + .outerjoin(SourceNode, Advertisement.node_id == SourceNode.id) .where(Advertisement.id == advertisement_id) ) result = session.execute(query).one_or_none() @@ -102,14 +153,34 @@ async def get_advertisement( if not result: raise HTTPException(status_code=404, detail="Advertisement not found") - adv, receiver_pk = result + adv = result[0] + + # Fetch nodes with tags for friendly names + node_ids = [] + if result.receiver_id: + node_ids.append(result.receiver_id) + if result.source_id: + node_ids.append(result.source_id) + + nodes_by_id: dict[str, Node] = {} + if node_ids: + nodes_query = ( + select(Node).where(Node.id.in_(node_ids)).options(selectinload(Node.tags)) + ) + nodes = session.execute(nodes_query).scalars().all() + nodes_by_id = {n.id: n for n in nodes} + + receiver_node = nodes_by_id.get(result.receiver_id) if result.receiver_id else None + source_node = nodes_by_id.get(result.source_id) if result.source_id else None + data = { - "id": adv.id, - "receiver_node_id": adv.receiver_node_id, - "received_by": receiver_pk, - "node_id": adv.node_id, + "received_by": result.receiver_pk, + "receiver_name": result.receiver_name, + "receiver_friendly_name": _get_friendly_name(receiver_node), "public_key": adv.public_key, "name": adv.name, + "node_name": result.source_name, + "node_friendly_name": _get_friendly_name(source_node), "adv_type": adv.adv_type, "flags": adv.flags, "received_at": adv.received_at, diff --git a/src/meshcore_hub/common/schemas/messages.py b/src/meshcore_hub/common/schemas/messages.py index b57a526..0ab0a63 100644 --- a/src/meshcore_hub/common/schemas/messages.py +++ b/src/meshcore_hub/common/schemas/messages.py @@ -84,8 +84,18 @@ class AdvertisementRead(BaseModel): received_by: Optional[str] = Field( default=None, description="Receiving interface node public key" ) + receiver_name: Optional[str] = Field(default=None, description="Receiver node name") + receiver_friendly_name: Optional[str] = Field( + default=None, description="Receiver friendly name from tags" + ) public_key: str = Field(..., description="Advertised public key") name: Optional[str] = Field(default=None, description="Advertised name") + node_name: Optional[str] = Field( + default=None, description="Node name from nodes table" + ) + node_friendly_name: Optional[str] = Field( + default=None, description="Node friendly name from tags" + ) adv_type: Optional[str] = Field(default=None, description="Node type") flags: Optional[int] = Field(default=None, description="Capability flags") received_at: datetime = Field(..., description="When received") diff --git a/src/meshcore_hub/web/templates/advertisements.html b/src/meshcore_hub/web/templates/advertisements.html index a0c2979..b32f7dd 100644 --- a/src/meshcore_hub/web/templates/advertisements.html +++ b/src/meshcore_hub/web/templates/advertisements.html @@ -48,8 +48,8 @@ - {% if ad.name %} -
{{ ad.name }}
+ {% if ad.node_friendly_name or ad.node_name or ad.name %} +
{{ ad.node_friendly_name or ad.node_name or ad.name }}
{{ ad.public_key[:16] }}...
{% else %} {{ ad.public_key[:16] }}... @@ -58,8 +58,13 @@ {% if ad.received_by %} -
- {{ ad.received_by[:16] }}... + + {% if ad.receiver_friendly_name or ad.receiver_name %} +
{{ ad.receiver_friendly_name or ad.receiver_name }}
+
{{ ad.received_by[:16] }}...
+ {% else %} + {{ ad.received_by[:16] }}... + {% endif %}
{% else %} -