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 @@