From ed2cf09ff34fbbc647ba3a1b4efc761721fb398d Mon Sep 17 00:00:00 2001 From: Louis King Date: Sun, 11 Jan 2026 12:49:34 +0000 Subject: [PATCH] Improve admin UI and remove unused coordinate tag type - Replace node type badge with icon in admin tag editor - Add Edit/Add Tags button on node detail page (when admin enabled and authenticated) - Remove automatic seed container startup to prevent overwriting user changes - Remove unused 'coordinate' value type from node tags (only string, number, boolean remain) --- AGENTS.md | 4 +++- README.md | 13 +++++-------- docker-compose.yml | 13 +++++++------ example/seed/node_tags.yaml | 10 +++++----- src/meshcore_hub/collector/cli.py | 8 ++++---- src/meshcore_hub/collector/tag_import.py | 2 +- src/meshcore_hub/common/models/node_tag.py | 2 +- src/meshcore_hub/common/schemas/nodes.py | 4 ++-- src/meshcore_hub/web/routes/nodes.py | 6 ++++++ .../web/templates/admin/node_tags.html | 18 ++++++++---------- .../web/templates/node_detail.html | 11 ++++++++++- tests/test_collector/test_tag_import.py | 4 ++-- tests/test_common/test_models.py | 4 ++-- 13 files changed, 56 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 266c633..3994e74 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -487,7 +487,7 @@ The database can be seeded with node tags and network members from YAML files in - `node_tags.yaml` - Node tag definitions (keyed by public_key) - `members.yaml` - Network member definitions -Seeding is a separate process from the collector and must be run explicitly: +**Important:** Seeding is NOT automatic and must be run explicitly. This prevents seed files from overwriting user changes made via the admin UI. ```bash # Native CLI @@ -497,6 +497,8 @@ meshcore-hub collector seed docker compose --profile seed up ``` +**Note:** Once the admin UI is enabled (`WEB_ADMIN_ENABLED=true`), tags should be managed through the web interface rather than seed files. + ### Webhook Configuration The collector supports forwarding events to external HTTP endpoints: diff --git a/README.md b/README.md index 5d31699..4fee0b4 100644 --- a/README.md +++ b/README.md @@ -471,21 +471,18 @@ Tags are keyed by public key in YAML format: fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210: friendly_name: Oakland Repeater altitude: 150 - location: - value: "37.8044,-122.2712" - type: coordinate ``` Tag values can be: - **YAML primitives** (auto-detected type): strings, numbers, booleans -- **Explicit type** (for special types like coordinate): +- **Explicit type** (when you need to force a specific type): ```yaml - location: - value: "37.7749,-122.4194" - type: coordinate + altitude: + value: "150" + type: number ``` -Supported types: `string`, `number`, `boolean`, `coordinate` +Supported types: `string`, `number`, `boolean` ### Import Tags Manually diff --git a/docker-compose.yml b/docker-compose.yml index 2aece61..7137592 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -139,7 +139,7 @@ services: - core restart: unless-stopped depends_on: - seed: + db-migrate: condition: service_completed_successfully volumes: - ${DATA_HOME:-./data}:/data @@ -196,7 +196,7 @@ services: - core restart: unless-stopped depends_on: - seed: + db-migrate: condition: service_completed_successfully collector: condition: service_started @@ -295,7 +295,10 @@ services: command: ["db", "upgrade"] # ========================================================================== - # Seed Data - Import node_tags.json and members.json from SEED_HOME + # Seed Data - Import node_tags.yaml and members.yaml from SEED_HOME + # NOTE: This is NOT run automatically. Use --profile seed to run explicitly. + # Since tags are now managed via the admin UI, automatic seeding would + # overwrite user changes. # ========================================================================== seed: image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest} @@ -304,8 +307,6 @@ services: dockerfile: Dockerfile container_name: meshcore-seed profiles: - - all - - core - seed restart: "no" depends_on: @@ -322,7 +323,7 @@ services: - LOG_LEVEL=${LOG_LEVEL:-INFO} # Explicitly unset to use DATA_HOME-based default path - DATABASE_URL= - # Imports both node_tags.json and members.json if they exist + # Imports both node_tags.yaml and members.yaml if they exist command: ["collector", "seed"] # ========================================================================== diff --git a/example/seed/node_tags.yaml b/example/seed/node_tags.yaml index 7fb256a..bf7fddb 100644 --- a/example/seed/node_tags.yaml +++ b/example/seed/node_tags.yaml @@ -7,12 +7,12 @@ # elevation: 150 # number # is_online: true # boolean # -# - Explicit type (for special types like coordinate): -# location: -# value: "37.7749,-122.4194" -# type: coordinate +# - Explicit type (when you need to force a specific type): +# altitude: +# value: "150" +# type: number # -# Supported types: string, number, boolean, coordinate +# Supported types: string, number, boolean 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef: friendly_name: Gateway Node diff --git a/src/meshcore_hub/collector/cli.py b/src/meshcore_hub/collector/cli.py index 421e46e..6353b92 100644 --- a/src/meshcore_hub/collector/cli.py +++ b/src/meshcore_hub/collector/cli.py @@ -433,12 +433,12 @@ def import_tags_cmd( \b 0123456789abcdef...: friendly_name: My Node - location: - value: "52.0,1.0" - type: coordinate altitude: value: "150" type: number + active: + value: "true" + type: boolean Shorthand is also supported (string values with default type): @@ -447,7 +447,7 @@ def import_tags_cmd( friendly_name: My Node role: gateway - Supported types: string, number, boolean, coordinate + Supported types: string, number, boolean """ from pathlib import Path diff --git a/src/meshcore_hub/collector/tag_import.py b/src/meshcore_hub/collector/tag_import.py index a3f7ef7..1c9bf12 100644 --- a/src/meshcore_hub/collector/tag_import.py +++ b/src/meshcore_hub/collector/tag_import.py @@ -19,7 +19,7 @@ class TagValue(BaseModel): """Schema for a tag value with type.""" value: str | None = None - type: str = Field(default="string", pattern=r"^(string|number|boolean|coordinate)$") + type: str = Field(default="string", pattern=r"^(string|number|boolean)$") class NodeTags(BaseModel): diff --git a/src/meshcore_hub/common/models/node_tag.py b/src/meshcore_hub/common/models/node_tag.py index 969013d..ddf8867 100644 --- a/src/meshcore_hub/common/models/node_tag.py +++ b/src/meshcore_hub/common/models/node_tag.py @@ -21,7 +21,7 @@ class NodeTag(Base, UUIDMixin, TimestampMixin): node_id: Foreign key to nodes table key: Tag name/key value: Tag value (stored as text, can be JSON for typed values) - value_type: Type hint (string, number, boolean, coordinate) + value_type: Type hint (string, number, boolean) created_at: Record creation timestamp updated_at: Record update timestamp """ diff --git a/src/meshcore_hub/common/schemas/nodes.py b/src/meshcore_hub/common/schemas/nodes.py index 04ab6bc..b5609a5 100644 --- a/src/meshcore_hub/common/schemas/nodes.py +++ b/src/meshcore_hub/common/schemas/nodes.py @@ -19,7 +19,7 @@ class NodeTagCreate(BaseModel): default=None, description="Tag value", ) - value_type: Literal["string", "number", "boolean", "coordinate"] = Field( + value_type: Literal["string", "number", "boolean"] = Field( default="string", description="Value type hint", ) @@ -32,7 +32,7 @@ class NodeTagUpdate(BaseModel): default=None, description="Tag value", ) - value_type: Optional[Literal["string", "number", "boolean", "coordinate"]] = Field( + value_type: Optional[Literal["string", "number", "boolean"]] = Field( default=None, description="Value type hint", ) diff --git a/src/meshcore_hub/web/routes/nodes.py b/src/meshcore_hub/web/routes/nodes.py index 3a7f907..75f045c 100644 --- a/src/meshcore_hub/web/routes/nodes.py +++ b/src/meshcore_hub/web/routes/nodes.py @@ -118,12 +118,18 @@ async def node_detail(request: Request, public_key: str) -> HTMLResponse: logger.warning(f"Failed to fetch node details from API: {e}") context["api_error"] = str(e) + # Check if admin editing is available + admin_enabled = getattr(request.app.state, "admin_enabled", False) + auth_user = request.headers.get("X-Forwarded-User") + context.update( { "node": node, "advertisements": advertisements, "telemetry": telemetry, "public_key": public_key, + "admin_enabled": admin_enabled, + "is_authenticated": bool(auth_user), } ) diff --git a/src/meshcore_hub/web/templates/admin/node_tags.html b/src/meshcore_hub/web/templates/admin/node_tags.html index 00ded1e..c7f495f 100644 --- a/src/meshcore_hub/web/templates/admin/node_tags.html +++ b/src/meshcore_hub/web/templates/admin/node_tags.html @@ -64,12 +64,12 @@
-
-

{{ selected_node.name or 'Unnamed Node' }}

-

{{ selected_public_key }}

- {% if selected_node.adv_type %} - {{ selected_node.adv_type }} - {% endif %} +
+ {% if selected_node.adv_type and selected_node.adv_type|lower == 'chat' %}💬{% elif selected_node.adv_type and selected_node.adv_type|lower == 'repeater' %}📡{% elif selected_node.adv_type and selected_node.adv_type|lower == 'room' %}🪧{% else %}📍{% endif %} +
+

{{ selected_node.name or 'Unnamed Node' }}

+

{{ selected_public_key }}

+
View Node
@@ -158,8 +158,7 @@ - - +
@@ -202,8 +201,7 @@ - - +