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
This commit is contained in:
Claude
2025-12-05 17:58:52 +00:00
parent 89c81630c9
commit 3469278fba
3 changed files with 108 additions and 22 deletions
+89 -18
View File
@@ -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,
@@ -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")
@@ -48,8 +48,8 @@
<tr class="hover">
<td>
<a href="/nodes/{{ ad.public_key }}" class="link link-hover">
{% if ad.name %}
<div class="font-medium">{{ ad.name }}</div>
{% if ad.node_friendly_name or ad.node_name or ad.name %}
<div class="font-medium">{{ ad.node_friendly_name or ad.node_name or ad.name }}</div>
<div class="text-xs font-mono opacity-70">{{ ad.public_key[:16] }}...</div>
{% else %}
<span class="font-mono text-sm">{{ ad.public_key[:16] }}...</span>
@@ -58,8 +58,13 @@
</td>
<td>
{% if ad.received_by %}
<a href="/nodes/{{ ad.received_by }}" class="link link-hover font-mono text-sm" title="{{ ad.received_by }}">
{{ ad.received_by[:16] }}...
<a href="/nodes/{{ ad.received_by }}" class="link link-hover">
{% if ad.receiver_friendly_name or ad.receiver_name %}
<div class="font-medium">{{ ad.receiver_friendly_name or ad.receiver_name }}</div>
<div class="text-xs font-mono opacity-70">{{ ad.received_by[:16] }}...</div>
{% else %}
<span class="font-mono text-sm">{{ ad.received_by[:16] }}...</span>
{% endif %}
</a>
{% else %}
<span class="opacity-50">-</span>