Removed friendly name support and tidied tags

This commit is contained in:
Louis King
2025-12-07 23:02:19 +00:00
parent 86bff07f7d
commit fbd29ff78e
13 changed files with 227 additions and 125 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']}")

View File

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

View File

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

View File

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

View File

@@ -49,8 +49,8 @@
<tr class="hover">
<td>
<a href="/nodes/{{ ad.public_key }}" class="link link-hover">
{% 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>
{% if ad.node_tag_name or ad.node_name or ad.name %}
<div class="font-medium">{{ ad.node_tag_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>
@@ -80,7 +80,7 @@
{% for recv in ad.receivers %}
<li>
<a href="/nodes/{{ recv.public_key }}" class="text-sm">
{{ recv.friendly_name or recv.name or recv.public_key[:12] + '...' }}
{{ recv.tag_name or recv.name or recv.public_key[:12] + '...' }}
</a>
</li>
{% endfor %}
@@ -88,8 +88,8 @@
</div>
{% elif ad.receivers and ad.receivers|length == 1 %}
<a href="/nodes/{{ ad.receivers[0].public_key }}" class="link link-hover">
{% if ad.receivers[0].friendly_name or ad.receivers[0].name %}
<div class="font-medium">{{ ad.receivers[0].friendly_name or ad.receivers[0].name }}</div>
{% if ad.receivers[0].tag_name or ad.receivers[0].name %}
<div class="font-medium">{{ ad.receivers[0].tag_name or ad.receivers[0].name }}</div>
<div class="text-xs font-mono opacity-70">{{ ad.receivers[0].public_key[:16] }}...</div>
{% else %}
<span class="font-mono text-sm">{{ ad.receivers[0].public_key[:16] }}...</span>
@@ -97,8 +97,8 @@
</a>
{% elif ad.received_by %}
<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>
{% if ad.receiver_tag_name or ad.receiver_name %}
<div class="font-medium">{{ ad.receiver_tag_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>

View File

@@ -78,8 +78,8 @@
{% if msg.message_type == 'channel' %}
<span class="font-mono">CH{{ msg.channel_idx }}</span>
{% else %}
{% if msg.sender_friendly_name or msg.sender_name %}
<span class="font-medium">{{ msg.sender_friendly_name or msg.sender_name }}</span>
{% if msg.sender_tag_name or msg.sender_name %}
<span class="font-medium">{{ msg.sender_tag_name or msg.sender_name }}</span>
{% else %}
<span class="font-mono text-xs">{{ (msg.pubkey_prefix or '-')[:12] }}</span>
{% endif %}
@@ -96,7 +96,7 @@
{% for recv in msg.receivers %}
<li>
<a href="/nodes/{{ recv.public_key }}" class="text-sm">
<span class="flex-1">{{ recv.friendly_name or recv.name or recv.public_key[:12] + '...' }}</span>
<span class="flex-1">{{ recv.tag_name or recv.name or recv.public_key[:12] + '...' }}</span>
{% if recv.snr is not none %}
<span class="badge badge-ghost badge-xs">{{ "%.1f"|format(recv.snr) }}</span>
{% endif %}
@@ -107,8 +107,8 @@
</div>
{% elif msg.receivers and msg.receivers|length == 1 %}
<a href="/nodes/{{ msg.receivers[0].public_key }}" class="link link-hover">
{% if msg.receivers[0].friendly_name or msg.receivers[0].name %}
<div class="font-medium">{{ msg.receivers[0].friendly_name or msg.receivers[0].name }}</div>
{% if msg.receivers[0].tag_name or msg.receivers[0].name %}
<div class="font-medium">{{ msg.receivers[0].tag_name or msg.receivers[0].name }}</div>
<div class="text-xs font-mono opacity-70">{{ msg.receivers[0].public_key[:16] }}...</div>
{% else %}
<span class="font-mono text-sm">{{ msg.receivers[0].public_key[:16] }}...</span>
@@ -116,8 +116,8 @@
</a>
{% elif msg.received_by %}
<a href="/nodes/{{ msg.received_by }}" class="link link-hover">
{% if msg.receiver_friendly_name or msg.receiver_name %}
<div class="font-medium">{{ msg.receiver_friendly_name or msg.receiver_name }}</div>
{% if msg.receiver_tag_name or msg.receiver_name %}
<div class="font-medium">{{ msg.receiver_tag_name or msg.receiver_name }}</div>
<div class="text-xs font-mono opacity-70">{{ msg.received_by[:16] }}...</div>
{% else %}
<span class="font-mono text-sm">{{ msg.received_by[:16] }}...</span>

View File

@@ -8,13 +8,13 @@
<li><a href="/">Home</a></li>
<li><a href="/nodes">Nodes</a></li>
{% 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 %}
<li>{{ ns.friendly_name or node.name or public_key[:12] + '...' }}</li>
<li>{{ ns.tag_name or node.name or public_key[:12] + '...' }}</li>
{% else %}
<li>Not Found</li>
{% 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 %}
<!-- Node Info Card -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h1 class="card-title text-2xl">
{{ ns.friendly_name or node.name or 'Unnamed Node' }}
{{ ns.tag_name or node.name or 'Unnamed Node' }}
{% if node.adv_type %}
<span class="badge badge-secondary">{{ node.adv_type }}</span>
{% endif %}
@@ -125,8 +125,8 @@
<td>
{% if adv.received_by %}
<a href="/nodes/{{ adv.received_by }}" class="link link-hover">
{% if adv.receiver_friendly_name or adv.receiver_name %}
<div class="font-medium text-sm">{{ adv.receiver_friendly_name or adv.receiver_name }}</div>
{% if adv.receiver_tag_name or adv.receiver_name %}
<div class="font-medium text-sm">{{ adv.receiver_tag_name or adv.receiver_name }}</div>
<div class="text-xs font-mono opacity-70">{{ adv.received_by[:16] }}...</div>
{% else %}
<span class="font-mono text-xs">{{ adv.received_by[:16] }}...</span>
@@ -175,8 +175,8 @@
<td>
{% if tel.received_by %}
<a href="/nodes/{{ tel.received_by }}" class="link link-hover">
{% if tel.receiver_friendly_name or tel.receiver_name %}
<div class="font-medium text-sm">{{ tel.receiver_friendly_name or tel.receiver_name }}</div>
{% if tel.receiver_tag_name or tel.receiver_name %}
<div class="font-medium text-sm">{{ tel.receiver_tag_name or tel.receiver_name }}</div>
<div class="text-xs font-mono opacity-70">{{ tel.received_by[:16] }}...</div>
{% else %}
<span class="font-mono text-xs">{{ tel.received_by[:16] }}...</span>

View File

@@ -57,17 +57,17 @@
</thead>
<tbody>
{% 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 %}
<tr class="hover">
<td>
<a href="/nodes/{{ node.public_key }}" class="link link-hover">
{% if ns.friendly_name or node.name %}
<div class="font-medium">{{ ns.friendly_name or node.name }}</div>
{% if ns.tag_name or node.name %}
<div class="font-medium">{{ ns.tag_name or node.name }}</div>
<div class="text-xs font-mono opacity-70">{{ node.public_key[:16] }}...</div>
{% else %}
<span class="font-mono text-sm">{{ node.public_key[:16] }}...</span>

View File

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