From fbd29ff78e96451cd443b237fdc8c9503be0cc47 Mon Sep 17 00:00:00 2001 From: Louis King Date: Sun, 7 Dec 2025 23:02:19 +0000 Subject: [PATCH] Removed friendly name support and tidied tags --- src/meshcore_hub/api/routes/advertisements.py | 26 +++---- src/meshcore_hub/api/routes/dashboard.py | 30 ++++---- src/meshcore_hub/api/routes/members.py | 18 ++--- src/meshcore_hub/api/routes/messages.py | 40 +++++------ src/meshcore_hub/collector/cli.py | 16 ++++- src/meshcore_hub/collector/tag_import.py | 69 ++++++++++++++----- src/meshcore_hub/common/schemas/members.py | 4 +- src/meshcore_hub/common/schemas/messages.py | 26 ++++--- .../web/templates/advertisements.html | 14 ++-- src/meshcore_hub/web/templates/messages.html | 14 ++-- .../web/templates/node_detail.html | 24 +++---- src/meshcore_hub/web/templates/nodes.html | 10 +-- tests/test_collector/test_tag_import.py | 61 ++++++++++++++++ 13 files changed, 227 insertions(+), 125 deletions(-) diff --git a/src/meshcore_hub/api/routes/advertisements.py b/src/meshcore_hub/api/routes/advertisements.py index e36974b..ae6e487 100644 --- a/src/meshcore_hub/api/routes/advertisements.py +++ b/src/meshcore_hub/api/routes/advertisements.py @@ -19,12 +19,12 @@ from meshcore_hub.common.schemas.messages import ( router = APIRouter() -def _get_friendly_name(node: Optional[Node]) -> Optional[str]: - """Extract friendly_name tag from a node's tags.""" +def _get_tag_name(node: Optional[Node]) -> Optional[str]: + """Extract 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": + if tag.key == "name": return tag.value return None @@ -57,15 +57,15 @@ def _fetch_receivers_for_events( receivers_by_hash: dict[str, list[ReceiverInfo]] = {} node_ids = [r.node_id for r in results] - friendly_names: dict[str, str] = {} + tag_names: dict[str, str] = {} if node_ids: - fn_query = ( + tag_query = ( select(NodeTag.node_id, NodeTag.value) .where(NodeTag.node_id.in_(node_ids)) - .where(NodeTag.key == "friendly_name") + .where(NodeTag.key == "name") ) - for node_id, value in session.execute(fn_query).all(): - friendly_names[node_id] = value + for node_id, value in session.execute(tag_query).all(): + tag_names[node_id] = value for row in results: if row.event_hash not in receivers_by_hash: @@ -76,7 +76,7 @@ def _fetch_receivers_for_events( node_id=row.node_id, public_key=row.public_key, name=row.name, - friendly_name=friendly_names.get(row.node_id), + tag_name=tag_names.get(row.node_id), snr=row.snr, received_at=row.received_at, ) @@ -173,11 +173,11 @@ async def list_advertisements( data = { "received_by": row.receiver_pk, "receiver_name": row.receiver_name, - "receiver_friendly_name": _get_friendly_name(receiver_node), + "receiver_tag_name": _get_tag_name(receiver_node), "public_key": adv.public_key, "name": adv.name, "node_name": row.source_name, - "node_friendly_name": _get_friendly_name(source_node), + "node_tag_name": _get_tag_name(source_node), "adv_type": adv.adv_type or row.source_adv_type, "flags": adv.flags, "received_at": adv.received_at, @@ -255,11 +255,11 @@ async def get_advertisement( data = { "received_by": result.receiver_pk, "receiver_name": result.receiver_name, - "receiver_friendly_name": _get_friendly_name(receiver_node), + "receiver_tag_name": _get_tag_name(receiver_node), "public_key": adv.public_key, "name": adv.name, "node_name": result.source_name, - "node_friendly_name": _get_friendly_name(source_node), + "node_tag_name": _get_tag_name(source_node), "adv_type": adv.adv_type or result.source_adv_type, "flags": adv.flags, "received_at": adv.received_at, diff --git a/src/meshcore_hub/api/routes/dashboard.py b/src/meshcore_hub/api/routes/dashboard.py index 9afecdb..621d576 100644 --- a/src/meshcore_hub/api/routes/dashboard.py +++ b/src/meshcore_hub/api/routes/dashboard.py @@ -82,11 +82,11 @@ async def get_stats( .all() ) - # Get node names, adv_types, and friendly_name tags for the advertised nodes + # Get node names, adv_types, and name tags for the advertised nodes ad_public_keys = [ad.public_key for ad in recent_ads] node_names: dict[str, str] = {} node_adv_types: dict[str, str] = {} - friendly_names: dict[str, str] = {} + tag_names: dict[str, str] = {} if ad_public_keys: # Get node names and adv_types from Node table node_query = select(Node.public_key, Node.name, Node.adv_type).where( @@ -98,21 +98,21 @@ async def get_stats( if adv_type: node_adv_types[public_key] = adv_type - # Get friendly_name tags - friendly_name_query = ( + # Get name tags + tag_name_query = ( select(Node.public_key, NodeTag.value) .join(NodeTag, Node.id == NodeTag.node_id) .where(Node.public_key.in_(ad_public_keys)) - .where(NodeTag.key == "friendly_name") + .where(NodeTag.key == "name") ) - for public_key, value in session.execute(friendly_name_query).all(): - friendly_names[public_key] = value + for public_key, value in session.execute(tag_name_query).all(): + tag_names[public_key] = value recent_advertisements = [ RecentAdvertisement( public_key=ad.public_key, name=ad.name or node_names.get(ad.public_key), - friendly_name=friendly_names.get(ad.public_key), + tag_name=tag_names.get(ad.public_key), adv_type=ad.adv_type or node_adv_types.get(ad.public_key), received_at=ad.received_at, ) @@ -146,7 +146,7 @@ async def get_stats( # 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] = {} + msg_tag_names: dict[str, str] = {} if msg_prefixes: for prefix in set(msg_prefixes): sender_node_query = select(Node.public_key, Node.name).where( @@ -156,14 +156,14 @@ async def get_stats( if name: msg_sender_names[public_key[:12]] = name - sender_friendly_query = ( + sender_tag_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") + .where(NodeTag.key == "name") ) - for public_key, value in session.execute(sender_friendly_query).all(): - msg_friendly_names[public_key[:12]] = value + for public_key, value in session.execute(sender_tag_query).all(): + msg_tag_names[public_key[:12]] = value channel_messages[int(channel_idx)] = [ ChannelMessage( @@ -171,8 +171,8 @@ async def get_stats( 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 + sender_tag_name=( + msg_tag_names.get(m.pubkey_prefix) if m.pubkey_prefix else None ), pubkey_prefix=m.pubkey_prefix, received_at=m.received_at, diff --git a/src/meshcore_hub/api/routes/members.py b/src/meshcore_hub/api/routes/members.py index 809aff7..f817afd 100644 --- a/src/meshcore_hub/api/routes/members.py +++ b/src/meshcore_hub/api/routes/members.py @@ -41,7 +41,7 @@ def _enrich_member_nodes( updated_at=mn.updated_at, node_name=info.get("name"), node_adv_type=info.get("adv_type"), - friendly_name=info.get("friendly_name"), + tag_name=info.get("tag_name"), ) ) return enriched_nodes @@ -100,15 +100,15 @@ async def list_members( ) nodes = session.execute(node_query).scalars().all() for node in nodes: - friendly_name = None + tag_name = None for tag in node.tags: - if tag.key == "friendly_name": - friendly_name = tag.value + if tag.key == "name": + tag_name = tag.value break node_info[node.public_key] = { "name": node.name, "adv_type": node.adv_type, - "friendly_name": friendly_name, + "tag_name": tag_name, } return MemberList( @@ -145,15 +145,15 @@ async def get_member( ) nodes = session.execute(node_query).scalars().all() for node in nodes: - friendly_name = None + tag_name = None for tag in node.tags: - if tag.key == "friendly_name": - friendly_name = tag.value + if tag.key == "name": + tag_name = tag.value break node_info[node.public_key] = { "name": node.name, "adv_type": node.adv_type, - "friendly_name": friendly_name, + "tag_name": tag_name, } return _member_to_read(member, node_info) diff --git a/src/meshcore_hub/api/routes/messages.py b/src/meshcore_hub/api/routes/messages.py index 811ad53..d4a2e45 100644 --- a/src/meshcore_hub/api/routes/messages.py +++ b/src/meshcore_hub/api/routes/messages.py @@ -15,12 +15,12 @@ from meshcore_hub.common.schemas.messages import MessageList, MessageRead, Recei router = APIRouter() -def _get_friendly_name(node: Optional[Node]) -> Optional[str]: - """Extract friendly_name tag from a node's tags.""" +def _get_tag_name(node: Optional[Node]) -> Optional[str]: + """Extract 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": + if tag.key == "name": return tag.value return None @@ -64,17 +64,17 @@ def _fetch_receivers_for_events( # Group by event_hash receivers_by_hash: dict[str, list[ReceiverInfo]] = {} - # Get friendly names for receiver nodes + # Get tag names for receiver nodes node_ids = [r.node_id for r in results] - friendly_names: dict[str, str] = {} + tag_names: dict[str, str] = {} if node_ids: - fn_query = ( + tag_query = ( select(NodeTag.node_id, NodeTag.value) .where(NodeTag.node_id.in_(node_ids)) - .where(NodeTag.key == "friendly_name") + .where(NodeTag.key == "name") ) - for node_id, value in session.execute(fn_query).all(): - friendly_names[node_id] = value + for node_id, value in session.execute(tag_query).all(): + tag_names[node_id] = value for row in results: if row.event_hash not in receivers_by_hash: @@ -85,7 +85,7 @@ def _fetch_receivers_for_events( node_id=row.node_id, public_key=row.public_key, name=row.name, - friendly_name=friendly_names.get(row.node_id), + tag_name=tag_names.get(row.node_id), snr=row.snr, received_at=row.received_at, ) @@ -153,10 +153,10 @@ async def list_messages( # Execute results = session.execute(query).all() - # Look up sender names and friendly_names for senders with pubkey_prefix + # Look up sender names and tag names for senders with pubkey_prefix pubkey_prefixes = [r[0].pubkey_prefix for r in results if r[0].pubkey_prefix] sender_names: dict[str, str] = {} - friendly_names: dict[str, str] = {} + sender_tag_names: dict[str, str] = {} if pubkey_prefixes: # Find nodes whose public_key starts with any of these prefixes for prefix in set(pubkey_prefixes): @@ -168,15 +168,15 @@ async def list_messages( if name: sender_names[public_key[:12]] = name - # Get friendly_name tag - friendly_name_query = ( + # Get name tag + tag_name_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") + .where(NodeTag.key == "name") ) - for public_key, value in session.execute(friendly_name_query).all(): - friendly_names[public_key[:12]] = value + for public_key, value in session.execute(tag_name_query).all(): + sender_tag_names[public_key[:12]] = value # Collect receiver node IDs to fetch tags receiver_ids = set() @@ -214,14 +214,14 @@ async def list_messages( "receiver_node_id": m.receiver_node_id, "received_by": receiver_pk, "receiver_name": receiver_name, - "receiver_friendly_name": _get_friendly_name(receiver_node), + "receiver_tag_name": _get_tag_name(receiver_node), "message_type": m.message_type, "pubkey_prefix": m.pubkey_prefix, "sender_name": ( sender_names.get(m.pubkey_prefix) if m.pubkey_prefix else None ), - "sender_friendly_name": ( - friendly_names.get(m.pubkey_prefix) if m.pubkey_prefix else None + "sender_tag_name": ( + sender_tag_names.get(m.pubkey_prefix) if m.pubkey_prefix else None ), "channel_idx": m.channel_idx, "text": m.text, diff --git a/src/meshcore_hub/collector/cli.py b/src/meshcore_hub/collector/cli.py index 6de7bc9..9a61e14 100644 --- a/src/meshcore_hub/collector/cli.py +++ b/src/meshcore_hub/collector/cli.py @@ -383,8 +383,11 @@ def _run_seed_import( file_path=str(node_tags_file), db=db, create_nodes=create_nodes, + clear_existing=True, ) if verbose: + if stats["deleted"]: + click.echo(f" Deleted {stats['deleted']} existing tags") click.echo( f" Tags: {stats['created']} created, {stats['updated']} updated" ) @@ -428,16 +431,24 @@ def _run_seed_import( default=False, help="Skip tags for nodes that don't exist (default: create nodes)", ) +@click.option( + "--clear-existing", + is_flag=True, + default=False, + help="Delete all existing tags before importing", +) @click.pass_context def import_tags_cmd( ctx: click.Context, file: str | None, no_create_nodes: bool, + clear_existing: bool, ) -> None: """Import node tags from a YAML file. Reads a YAML file containing tag definitions and upserts them - into the database. Existing tags are updated, new tags are created. + into the database. By default, existing tags are updated and new tags are created. + Use --clear-existing to delete all tags before importing. FILE is the path to the YAML file containing tags. If not provided, defaults to {SEED_HOME}/node_tags.yaml. @@ -492,11 +503,14 @@ def import_tags_cmd( file_path=tags_file, db=db, create_nodes=not no_create_nodes, + clear_existing=clear_existing, ) # Report results click.echo("") click.echo("Import complete:") + if stats["deleted"]: + click.echo(f" Tags deleted: {stats['deleted']}") click.echo(f" Total tags in file: {stats['total']}") click.echo(f" Tags created: {stats['created']}") click.echo(f" Tags updated: {stats['updated']}") diff --git a/src/meshcore_hub/collector/tag_import.py b/src/meshcore_hub/collector/tag_import.py index 9c1d68e..a3f7ef7 100644 --- a/src/meshcore_hub/collector/tag_import.py +++ b/src/meshcore_hub/collector/tag_import.py @@ -7,7 +7,7 @@ from typing import Any import yaml from pydantic import BaseModel, Field, model_validator -from sqlalchemy import select +from sqlalchemy import delete, func, select from meshcore_hub.common.database import DatabaseManager from meshcore_hub.common.models import Node, NodeTag @@ -151,16 +151,19 @@ def import_tags( file_path: str | Path, db: DatabaseManager, create_nodes: bool = True, + clear_existing: bool = False, ) -> dict[str, Any]: """Import tags from a YAML file into the database. Performs upsert operations - existing tags are updated, new tags are created. + Optionally clears all existing tags before import. Args: file_path: Path to the tags YAML file db: Database manager instance create_nodes: If True, create nodes that don't exist. If False, skip tags for non-existent nodes. + clear_existing: If True, delete all existing tags before importing. Returns: Dictionary with import statistics: @@ -169,6 +172,7 @@ def import_tags( - updated: Number of existing tags updated - skipped: Number of tags skipped (node not found and create_nodes=False) - nodes_created: Number of new nodes created + - deleted: Number of existing tags deleted (if clear_existing=True) - errors: List of error messages """ stats: dict[str, Any] = { @@ -177,6 +181,7 @@ def import_tags( "updated": 0, "skipped": 0, "nodes_created": 0, + "deleted": 0, "errors": [], } @@ -194,6 +199,15 @@ def import_tags( now = datetime.now(timezone.utc) with db.session_scope() as session: + # Clear all existing tags if requested + if clear_existing: + delete_count = ( + session.execute(select(func.count()).select_from(NodeTag)).scalar() or 0 + ) + session.execute(delete(NodeTag)) + stats["deleted"] = delete_count + logger.info(f"Deleted {delete_count} existing tags") + # Cache nodes by public_key to reduce queries node_cache: dict[str, Node] = {} @@ -232,24 +246,8 @@ def import_tags( tag_value = tag_data.get("value") tag_type = tag_data.get("type", "string") - # Find or create tag - tag_query = select(NodeTag).where( - NodeTag.node_id == node.id, - NodeTag.key == tag_key, - ) - existing_tag = session.execute(tag_query).scalar_one_or_none() - - if existing_tag: - # Update existing tag - existing_tag.value = tag_value - existing_tag.value_type = tag_type - stats["updated"] += 1 - logger.debug( - f"Updated tag {tag_key}={tag_value} " - f"for {public_key[:12]}..." - ) - else: - # Create new tag + if clear_existing: + # When clearing, always create new tags new_tag = NodeTag( node_id=node.id, key=tag_key, @@ -262,6 +260,39 @@ def import_tags( f"Created tag {tag_key}={tag_value} " f"for {public_key[:12]}..." ) + else: + # Find or create tag + tag_query = select(NodeTag).where( + NodeTag.node_id == node.id, + NodeTag.key == tag_key, + ) + existing_tag = session.execute( + tag_query + ).scalar_one_or_none() + + if existing_tag: + # Update existing tag + existing_tag.value = tag_value + existing_tag.value_type = tag_type + stats["updated"] += 1 + logger.debug( + f"Updated tag {tag_key}={tag_value} " + f"for {public_key[:12]}..." + ) + else: + # Create new tag + new_tag = NodeTag( + node_id=node.id, + key=tag_key, + value=tag_value, + value_type=tag_type, + ) + session.add(new_tag) + stats["created"] += 1 + logger.debug( + f"Created tag {tag_key}={tag_value} " + f"for {public_key[:12]}..." + ) except Exception as e: error_msg = f"Error processing tag {tag_key} for {public_key[:12]}...: {e}" diff --git a/src/meshcore_hub/common/schemas/members.py b/src/meshcore_hub/common/schemas/members.py index 28bf660..1748f28 100644 --- a/src/meshcore_hub/common/schemas/members.py +++ b/src/meshcore_hub/common/schemas/members.py @@ -35,9 +35,7 @@ class MemberNodeRead(BaseModel): node_adv_type: Optional[str] = Field( default=None, description="Node's advertisement type" ) - friendly_name: Optional[str] = Field( - default=None, description="Node's friendly name tag" - ) + tag_name: Optional[str] = Field(default=None, description="Node's name tag") class Config: from_attributes = True diff --git a/src/meshcore_hub/common/schemas/messages.py b/src/meshcore_hub/common/schemas/messages.py index c19a876..72ef361 100644 --- a/src/meshcore_hub/common/schemas/messages.py +++ b/src/meshcore_hub/common/schemas/messages.py @@ -12,9 +12,7 @@ class ReceiverInfo(BaseModel): node_id: str = Field(..., description="Receiver node UUID") public_key: str = Field(..., description="Receiver node public key") name: Optional[str] = Field(default=None, description="Receiver node name") - friendly_name: Optional[str] = Field( - default=None, description="Receiver friendly name from tags" - ) + tag_name: Optional[str] = Field(default=None, description="Receiver name from tags") snr: Optional[float] = Field( default=None, description="Signal-to-noise ratio at this receiver" ) @@ -31,8 +29,8 @@ class MessageRead(BaseModel): 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" + receiver_tag_name: Optional[str] = Field( + default=None, description="Receiver name from tags" ) message_type: str = Field(..., description="Message type (contact, channel)") pubkey_prefix: Optional[str] = Field( @@ -41,8 +39,8 @@ class MessageRead(BaseModel): sender_name: Optional[str] = Field( default=None, description="Sender's advertised node name" ) - sender_friendly_name: Optional[str] = Field( - default=None, description="Sender's friendly name from node tags" + sender_tag_name: Optional[str] = Field( + default=None, description="Sender's name from node tags" ) channel_idx: Optional[int] = Field(default=None, description="Channel index") text: str = Field(..., description="Message content") @@ -110,16 +108,16 @@ class AdvertisementRead(BaseModel): 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" + receiver_tag_name: Optional[str] = Field( + default=None, description="Receiver 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" + node_tag_name: Optional[str] = Field( + default=None, description="Node name from tags" ) adv_type: Optional[str] = Field(default=None, description="Node type") flags: Optional[int] = Field(default=None, description="Capability flags") @@ -215,7 +213,7 @@ class RecentAdvertisement(BaseModel): public_key: str = Field(..., description="Node public key") name: Optional[str] = Field(default=None, description="Node name") - friendly_name: Optional[str] = Field(default=None, description="Friendly name tag") + tag_name: Optional[str] = Field(default=None, description="Name tag") adv_type: Optional[str] = Field(default=None, description="Node type") received_at: datetime = Field(..., description="When received") @@ -225,8 +223,8 @@ class ChannelMessage(BaseModel): 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" + sender_tag_name: Optional[str] = Field( + default=None, description="Sender name from tags" ) pubkey_prefix: Optional[str] = Field( default=None, description="Sender public key prefix" diff --git a/src/meshcore_hub/web/templates/advertisements.html b/src/meshcore_hub/web/templates/advertisements.html index 724f5f2..b750431 100644 --- a/src/meshcore_hub/web/templates/advertisements.html +++ b/src/meshcore_hub/web/templates/advertisements.html @@ -49,8 +49,8 @@ - {% if ad.node_friendly_name or ad.node_name or ad.name %} -
{{ ad.node_friendly_name or ad.node_name or ad.name }}
+ {% if ad.node_tag_name or ad.node_name or ad.name %} +
{{ ad.node_tag_name or ad.node_name or ad.name }}
{{ ad.public_key[:16] }}...
{% else %} {{ ad.public_key[:16] }}... @@ -80,7 +80,7 @@ {% for recv in ad.receivers %}
  • - {{ recv.friendly_name or recv.name or recv.public_key[:12] + '...' }} + {{ recv.tag_name or recv.name or recv.public_key[:12] + '...' }}
  • {% endfor %} @@ -88,8 +88,8 @@ {% elif ad.receivers and ad.receivers|length == 1 %} - {% if ad.receivers[0].friendly_name or ad.receivers[0].name %} -
    {{ ad.receivers[0].friendly_name or ad.receivers[0].name }}
    + {% if ad.receivers[0].tag_name or ad.receivers[0].name %} +
    {{ ad.receivers[0].tag_name or ad.receivers[0].name }}
    {{ ad.receivers[0].public_key[:16] }}...
    {% else %} {{ ad.receivers[0].public_key[:16] }}... @@ -97,8 +97,8 @@
    {% elif ad.received_by %} - {% if ad.receiver_friendly_name or ad.receiver_name %} -
    {{ ad.receiver_friendly_name or ad.receiver_name }}
    + {% if ad.receiver_tag_name or ad.receiver_name %} +
    {{ ad.receiver_tag_name or ad.receiver_name }}
    {{ ad.received_by[:16] }}...
    {% else %} {{ ad.received_by[:16] }}... diff --git a/src/meshcore_hub/web/templates/messages.html b/src/meshcore_hub/web/templates/messages.html index cfd2555..008767f 100644 --- a/src/meshcore_hub/web/templates/messages.html +++ b/src/meshcore_hub/web/templates/messages.html @@ -78,8 +78,8 @@ {% if msg.message_type == 'channel' %} CH{{ msg.channel_idx }} {% else %} - {% if msg.sender_friendly_name or msg.sender_name %} - {{ msg.sender_friendly_name or msg.sender_name }} + {% if msg.sender_tag_name or msg.sender_name %} + {{ msg.sender_tag_name or msg.sender_name }} {% else %} {{ (msg.pubkey_prefix or '-')[:12] }} {% endif %} @@ -96,7 +96,7 @@ {% for recv in msg.receivers %}
  • - {{ recv.friendly_name or recv.name or recv.public_key[:12] + '...' }} + {{ recv.tag_name or recv.name or recv.public_key[:12] + '...' }} {% if recv.snr is not none %} {{ "%.1f"|format(recv.snr) }} {% endif %} @@ -107,8 +107,8 @@ {% elif msg.receivers and msg.receivers|length == 1 %} - {% if msg.receivers[0].friendly_name or msg.receivers[0].name %} -
    {{ msg.receivers[0].friendly_name or msg.receivers[0].name }}
    + {% if msg.receivers[0].tag_name or msg.receivers[0].name %} +
    {{ msg.receivers[0].tag_name or msg.receivers[0].name }}
    {{ msg.receivers[0].public_key[:16] }}...
    {% else %} {{ msg.receivers[0].public_key[:16] }}... @@ -116,8 +116,8 @@
    {% elif msg.received_by %} - {% if msg.receiver_friendly_name or msg.receiver_name %} -
    {{ msg.receiver_friendly_name or msg.receiver_name }}
    + {% if msg.receiver_tag_name or msg.receiver_name %} +
    {{ msg.receiver_tag_name or msg.receiver_name }}
    {{ msg.received_by[:16] }}...
    {% else %} {{ msg.received_by[:16] }}... diff --git a/src/meshcore_hub/web/templates/node_detail.html b/src/meshcore_hub/web/templates/node_detail.html index ca08a53..02b8326 100644 --- a/src/meshcore_hub/web/templates/node_detail.html +++ b/src/meshcore_hub/web/templates/node_detail.html @@ -8,13 +8,13 @@
  • Home
  • Nodes
  • {% if node %} - {% set ns = namespace(friendly_name=none) %} + {% set ns = namespace(tag_name=none) %} {% for tag in node.tags or [] %} - {% if tag.key == 'friendly_name' %} - {% set ns.friendly_name = tag.value %} + {% if tag.key == 'name' %} + {% set ns.tag_name = tag.value %} {% endif %} {% endfor %} -
  • {{ ns.friendly_name or node.name or public_key[:12] + '...' }}
  • +
  • {{ ns.tag_name or node.name or public_key[:12] + '...' }}
  • {% else %}
  • Not Found
  • {% endif %} @@ -31,17 +31,17 @@ {% endif %} {% if node %} -{% set ns = namespace(friendly_name=none) %} +{% set ns = namespace(tag_name=none) %} {% for tag in node.tags or [] %} - {% if tag.key == 'friendly_name' %} - {% set ns.friendly_name = tag.value %} + {% if tag.key == 'name' %} + {% set ns.tag_name = tag.value %} {% endif %} {% endfor %}

    - {{ ns.friendly_name or node.name or 'Unnamed Node' }} + {{ ns.tag_name or node.name or 'Unnamed Node' }} {% if node.adv_type %} {{ node.adv_type }} {% endif %} @@ -125,8 +125,8 @@ {% if adv.received_by %} - {% if adv.receiver_friendly_name or adv.receiver_name %} -
    {{ adv.receiver_friendly_name or adv.receiver_name }}
    + {% if adv.receiver_tag_name or adv.receiver_name %} +
    {{ adv.receiver_tag_name or adv.receiver_name }}
    {{ adv.received_by[:16] }}...
    {% else %} {{ adv.received_by[:16] }}... @@ -175,8 +175,8 @@ {% if tel.received_by %}
    - {% if tel.receiver_friendly_name or tel.receiver_name %} -
    {{ tel.receiver_friendly_name or tel.receiver_name }}
    + {% if tel.receiver_tag_name or tel.receiver_name %} +
    {{ tel.receiver_tag_name or tel.receiver_name }}
    {{ tel.received_by[:16] }}...
    {% else %} {{ tel.received_by[:16] }}... diff --git a/src/meshcore_hub/web/templates/nodes.html b/src/meshcore_hub/web/templates/nodes.html index 1636fca..7221f5f 100644 --- a/src/meshcore_hub/web/templates/nodes.html +++ b/src/meshcore_hub/web/templates/nodes.html @@ -57,17 +57,17 @@ {% for node in nodes %} - {% set ns = namespace(friendly_name=none) %} + {% set ns = namespace(tag_name=none) %} {% for tag in node.tags or [] %} - {% if tag.key == 'friendly_name' %} - {% set ns.friendly_name = tag.value %} + {% if tag.key == 'name' %} + {% set ns.tag_name = tag.value %} {% endif %} {% endfor %}
    - {% if ns.friendly_name or node.name %} -
    {{ ns.friendly_name or node.name }}
    + {% if ns.tag_name or node.name %} +
    {{ ns.tag_name or node.name }}
    {{ node.public_key[:16] }}...
    {% else %} {{ node.public_key[:16] }}... diff --git a/tests/test_collector/test_tag_import.py b/tests/test_collector/test_tag_import.py index 8d0c7e0..44ba953 100644 --- a/tests/test_collector/test_tag_import.py +++ b/tests/test_collector/test_tag_import.py @@ -390,3 +390,64 @@ class TestImportTags: assert tag_dict["is_disabled"].value_type == "boolean" Path(f.name).unlink() + + def test_import_with_clear_existing(self, db_manager): + """Test that clear_existing deletes all tags before importing.""" + # Create initial tags + initial_data = { + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef": { + "old_tag": "old_value", + "shared_tag": "old_value", + }, + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef": { + "another_old_tag": "value", + }, + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(initial_data, f) + f.flush() + initial_file = f.name + + stats1 = import_tags(initial_file, db_manager, create_nodes=True) + assert stats1["created"] == 3 + assert stats1["deleted"] == 0 + + # Verify initial tags exist + with db_manager.session_scope() as session: + tags = session.execute(select(NodeTag)).scalars().all() + assert len(tags) == 3 + + # Import new tags with clear_existing=True + new_data = { + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef": { + "new_tag": "new_value", + "shared_tag": "new_value", + } + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(new_data, f) + f.flush() + new_file = f.name + + stats2 = import_tags( + new_file, db_manager, create_nodes=True, clear_existing=True + ) + assert stats2["deleted"] == 3 # All 3 old tags deleted + assert stats2["created"] == 2 # 2 new tags created + assert stats2["updated"] == 0 # No updates when clearing + + # Verify only new tags exist + with db_manager.session_scope() as session: + tags = session.execute(select(NodeTag)).scalars().all() + tag_dict = {t.key: t for t in tags} + assert len(tags) == 2 + assert "new_tag" in tag_dict + assert "shared_tag" in tag_dict + assert tag_dict["shared_tag"].value == "new_value" + assert "old_tag" not in tag_dict + assert "another_old_tag" not in tag_dict + + Path(initial_file).unlink() + Path(new_file).unlink()