mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-03-28 17:42:56 +01:00
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)
This commit is contained in:
@@ -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:
|
||||
|
||||
13
README.md
13
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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
# ==========================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -64,12 +64,12 @@
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="card-title">{{ selected_node.name or 'Unnamed Node' }}</h2>
|
||||
<p class="text-sm opacity-70 font-mono">{{ selected_public_key }}</p>
|
||||
{% if selected_node.adv_type %}
|
||||
<span class="badge badge-outline mt-2">{{ selected_node.adv_type }}</span>
|
||||
{% endif %}
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-2xl" title="{{ selected_node.adv_type or 'Unknown' }}">{% 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 %}</span>
|
||||
<div>
|
||||
<h2 class="card-title">{{ selected_node.name or 'Unnamed Node' }}</h2>
|
||||
<p class="text-sm opacity-70 font-mono">{{ selected_public_key }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/nodes/{{ selected_public_key }}" class="btn btn-ghost btn-sm">View Node</a>
|
||||
</div>
|
||||
@@ -158,8 +158,7 @@
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="boolean">boolean</option>
|
||||
<option value="coordinate">coordinate</option>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
@@ -202,8 +201,7 @@
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="boolean">boolean</option>
|
||||
<option value="coordinate">coordinate</option>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
|
||||
@@ -97,9 +97,10 @@
|
||||
|
||||
<div class="grid grid-cols-1 {% if ns_map.lat and ns_map.lon %}lg:grid-cols-2{% endif %} gap-6 mt-6">
|
||||
<!-- Tags -->
|
||||
{% if node.tags %}
|
||||
{% if node.tags or (admin_enabled and is_authenticated) %}
|
||||
<div>
|
||||
<h3 class="font-semibold opacity-70 mb-2">Tags</h3>
|
||||
{% if node.tags %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-compact w-full">
|
||||
<thead>
|
||||
@@ -120,6 +121,14 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-70 mb-2">No tags defined.</p>
|
||||
{% endif %}
|
||||
{% if admin_enabled and is_authenticated %}
|
||||
<div class="mt-3">
|
||||
<a href="/a/node-tags?public_key={{ node.public_key }}" class="btn btn-sm btn-outline">{% if node.tags %}Edit Tags{% else %}Add Tags{% endif %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class TestLoadTagsFile:
|
||||
"""Test loading file with full format (value and type)."""
|
||||
data = {
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef": {
|
||||
"location": {"value": "52.0,1.0", "type": "coordinate"},
|
||||
"is_active": {"value": "true", "type": "boolean"},
|
||||
"altitude": {"value": "150", "type": "number"},
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class TestLoadTagsFile:
|
||||
|
||||
result = load_tags_file(f.name)
|
||||
key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
assert result[key]["location"]["type"] == "coordinate"
|
||||
assert result[key]["is_active"]["type"] == "boolean"
|
||||
assert result[key]["altitude"]["type"] == "number"
|
||||
|
||||
Path(f.name).unlink()
|
||||
|
||||
@@ -55,14 +55,14 @@ class TestNodeModel:
|
||||
def test_node_tags_relationship(self, db_session) -> None:
|
||||
"""Test node-tag relationship."""
|
||||
node = Node(public_key="b" * 64, name="Tagged Node")
|
||||
tag = NodeTag(key="location", value="51.5,-0.1", value_type="coordinate")
|
||||
tag = NodeTag(key="altitude", value="150", value_type="number")
|
||||
node.tags.append(tag)
|
||||
|
||||
db_session.add(node)
|
||||
db_session.commit()
|
||||
|
||||
assert len(node.tags) == 1
|
||||
assert node.tags[0].key == "location"
|
||||
assert node.tags[0].key == "altitude"
|
||||
|
||||
|
||||
class TestMessageModel:
|
||||
|
||||
Reference in New Issue
Block a user