From ffe839869e1ca78adab0c290864e0dd583f13df7 Mon Sep 17 00:00:00 2001 From: iceice400 Date: Fri, 12 Jun 2026 03:17:16 -0400 Subject: [PATCH 1/3] feat(dashboard): operator status strips, MQTT broker health, map hint Add console-style status footers on Stats, live MQTT runtime card on Configuration, map overlay GPS hint, and Home Assistant cookbook docs. Co-authored-by: Cursor --- docs/HOME-ASSISTANT-COOKBOOK.md | 125 +++++++++++++++++++++++++ docs/plans/v0.7.8-operator-polish.md | 87 +++++++++++++++++ frontend/css/status_strip.css | 105 +++++++++++++++++++++ frontend/index.html | 2 + frontend/js/components/node_map.js | 30 +++++- frontend/js/configuration/mqtt_card.js | 80 ++++++++++++++++ frontend/js/stats_tab.js | 34 +++++++ frontend/js/status_strip.js | 65 +++++++++++++ src/api/routes/mqtt_config_routes.py | 55 ++++++++++- src/api/server.py | 5 +- src/coordinator.py | 4 + src/relay/mqtt_publisher.py | 38 ++++++++ tests/test_mqtt_config_routes.py | 43 +++++++++ tests/test_mqtt_publisher.py | 12 +++ 14 files changed, 678 insertions(+), 7 deletions(-) create mode 100644 docs/HOME-ASSISTANT-COOKBOOK.md create mode 100644 docs/plans/v0.7.8-operator-polish.md create mode 100644 frontend/css/status_strip.css create mode 100644 frontend/js/status_strip.js diff --git a/docs/HOME-ASSISTANT-COOKBOOK.md b/docs/HOME-ASSISTANT-COOKBOOK.md new file mode 100644 index 00000000..43630a93 --- /dev/null +++ b/docs/HOME-ASSISTANT-COOKBOOK.md @@ -0,0 +1,125 @@ +# Home Assistant Cookbook + +Copy-paste recipes for Meshpoint LAN automation. Enable the API first +(see [API-AUTOMATION.md](./API-AUTOMATION.md)), then add these snippets +to `configuration.yaml` or package files. + +All examples assume: + +- Meshpoint dashboard: `http://192.168.1.50:8080` +- Token stored in `secrets.yaml` as `meshpoint_automation_token` + +## Node count sensor + +```yaml +rest: + - resource: "http://192.168.1.50:8080/api/automation/nodes?limit=500" + headers: + X-Meshpoint-Token: !secret meshpoint_automation_token + scan_interval: 60 + sensor: + - name: "Mesh Nodes" + unique_id: meshpoint_node_count + value_template: "{{ value_json | length }}" + unit_of_measurement: "nodes" + icon: mdi:access-point-network +``` + +## Meshpoint health binary + +Alerts when the automation API stops responding (power loss, service crash). + +```yaml +rest: + - resource: "http://192.168.1.50:8080/api/automation/status" + headers: + X-Meshpoint-Token: !secret meshpoint_automation_token + scan_interval: 30 + binary_sensor: + - name: "Meshpoint Online" + unique_id: meshpoint_online + value_template: "{{ value_json.uptime_seconds is defined }}" + device_class: connectivity +``` + +## Relay activity sensor + +```yaml +rest: + - resource: "http://192.168.1.50:8080/api/automation/status" + headers: + X-Meshpoint-Token: !secret meshpoint_automation_token + scan_interval: 60 + sensor: + - name: "Meshpoint Relayed" + unique_id: meshpoint_relayed_total + value_template: "{{ value_json.relay.relayed | default(0) }}" + unit_of_measurement: "packets" + - name: "Meshpoint Relay Rejected" + unique_id: meshpoint_relay_rejected + value_template: "{{ value_json.relay.rejected | default(0) }}" + unit_of_measurement: "packets" +``` + +## Broadcast from an automation + +```yaml +rest_command: + meshpoint_broadcast: + url: "http://192.168.1.50:8080/api/automation/send" + method: POST + headers: + Content-Type: "application/json" + X-Meshpoint-Token: !secret meshpoint_automation_token + payload: '{"text":"{{ message }}","channel":0,"destination":"broadcast"}' + +automation: + - alias: "Mesh morning check-in" + trigger: + - platform: time + at: "08:00:00" + action: + - service: rest_command.meshpoint_broadcast + data: + message: "Good morning from Home Assistant" +``` + +## Alert when node count drops + +Useful when a backbone node disappears from the mesh. + +```yaml +automation: + - alias: "Mesh node count drop" + trigger: + - platform: numeric_state + entity_id: sensor.mesh_nodes + below: 5 + for: "00:05:00" + action: + - service: notify.mobile_app + data: + title: "Meshpoint alert" + message: "Node count fell below 5 for 5 minutes" +``` + +## MQTT + Home Assistant (packet stream) + +If `mqtt.enabled` is true, Meshpoint can publish decoded packets to your +broker. Enable **Home Assistant auto-discovery** under Configuration β†’ +MQTT, or subscribe manually to `msh//2/json/...` topics. + +The Configuration MQTT card shows live broker health: connection state, +publish count, disconnect count, and topic prefix. + +## Node-RED + +Use an **http request** node pointed at `/api/automation/status` or +`/api/automation/nodes` with header `X-Meshpoint-Token`. Poll every 30–60 s +for dashboards; use inject + POST for one-shot messages. + +## Security notes + +- Keep `automation.token` at 32+ random characters. +- Do not expose port 8080 to the internet. +- MQTT credentials live in `config/local.yaml`; treat that file like a secret. diff --git a/docs/plans/v0.7.8-operator-polish.md b/docs/plans/v0.7.8-operator-polish.md new file mode 100644 index 00000000..d6e561f0 --- /dev/null +++ b/docs/plans/v0.7.8-operator-polish.md @@ -0,0 +1,87 @@ +# v0.7.8 Operator Polish + +**Purpose:** Small, green-tier improvements that help field operators trust +dashboard data and integrate Meshpoint with LAN automation. Submit **after** +Tier A (v0.7.7) PRs begin merging; **one PR at a time**. + +**Risk:** 🟒 Frontend + read-only backend surfaces. + +--- + +## Queue + +| # | Branch | Title | Depends on | Status | +|---|--------|-------|------------|--------| +| 17 | `feat/operator-polish` | Operator status strips + MQTT broker health + map hint | Tier A baseline | βœ… Ready | +| 18 | `feat/stats-traffic-heatmap` | SVG traffic heatmap (extends #69 histogram data) | **#69 merged** | ⬜ Not started | +| 19 | `feat/analytics-status-strips` | RF + Unknown RF + Topology tab footers | **#72, #79, #80 merged** | πŸ”„ Partial (#72) | +| 20 | `docs/home-assistant-cookbook` | Expanded HA recipes (ships with #17 docs) | β€” | βœ… In #17 | + +--- + +## PR 17 β€” Operator polish (green) + +| | | +|---|---| +| **Branch** | `feat/operator-polish` | +| **Title** | `feat(dashboard): operator status strips and MQTT broker health` | +| **Risk** | 🟒 | + +### What ships + +1. **Status strip component** (`status_strip.js`, `status_strip.css`) + - Console-style footer: `TRAFFIC Β· concentrator Β· 8,247 pkts Β· updated 14s ago` + - Wired on Stats tab (more tabs in PR 19 after their parent PRs merge) + +2. **MQTT broker health** (`GET /api/config/mqtt/runtime`) + - Live connection, publish count, disconnect count, topic prefix + - Configuration β†’ MQTT card banner (no restart required to view) + +3. **Map topology GPS hint** + - Amber banner when Topology Links overlay is on but no lines draw + - Points operators to Topology tab for logical graph + +4. **Home Assistant cookbook** (`docs/HOME-ASSISTANT-COOKBOOK.md`) + +### Test plan + +- [ ] Stats tab footer updates on refresh +- [ ] MQTT card shows connected/disabled/offline states +- [ ] Map hint appears when links exist without GPS on both ends +- [ ] `python -m unittest tests.test_mqtt_publisher tests.test_mqtt_config_routes` + +--- + +## PR 18 β€” Traffic heatmap (green, follow-up to #69) + +Do **not** rewrite #69 mid-merge. After #69 lands, add optional SVG +hour Γ— protocol heatmap beside the 24h bar chart using the same SQL. + +--- + +## PR 19 β€” Analytics tab footers (green) + +After #72 (topology), #79 (RF), and #80 (unknown RF) merge, add +`StatusStrip` footers: + +| Tab | Label | Example | +|-----|-------|---------| +| Topology | `TOPOLOGY` | `47 nodes Β· 23 links Β· neighborinfo+routing Β· updated 2m ago` | +| RF Environment | `RF ENV` | `noise -112 dBm Β· live scan Β· 48 samples Β· updated 5s ago` | +| Unknown RF | `UNKNOWN RF` | `12 frames Β· 3 SF buckets Β· updated 1m ago` | + +Rebase each tab's branch and add ~20 lines; or land as one PR on top of +merged analytics tabs. + +--- + +## Submission order + +``` +Tier A merges (v0.7.7) + β†’ PR 17 operator-polish + β†’ wait for #69 merge β†’ PR 18 heatmap + β†’ wait for #72/#79/#80 β†’ PR 19 tab footers +``` + +Never open PR 17 and PR 18 simultaneously. diff --git a/frontend/css/status_strip.css b/frontend/css/status_strip.css new file mode 100644 index 00000000..aa3dc0c4 --- /dev/null +++ b/frontend/css/status_strip.css @@ -0,0 +1,105 @@ +/* Operator status footer β€” console-style telemetry in panel chrome. */ + +.status-strip { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 1.25rem; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-sm); + border: 1px solid rgba(255, 255, 255, 0.04); + background: rgba(6, 182, 212, 0.03); + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.04em; + color: var(--text-muted); +} + +.status-strip__label { + color: var(--text-secondary); + font-weight: 600; + letter-spacing: 0.1em; + font-size: 0.62rem; +} + +.status-strip__sep { + opacity: 0.35; +} + +.status-strip__items { + color: var(--accent-cyan); + text-shadow: 0 0 6px rgba(6, 182, 212, 0.12); +} + +.status-strip__updated { + color: var(--text-muted); + font-style: italic; +} + +.status-strip--quiet .status-strip__items { + color: var(--text-muted); + text-shadow: none; + font-style: italic; +} + +.map-topology-hint { + position: absolute; + left: 50%; + bottom: 2.75rem; + transform: translateX(-50%); + z-index: 800; + max-width: min(92%, 28rem); + padding: 0.45rem 0.75rem; + border-radius: var(--radius-sm); + border: 1px solid rgba(245, 158, 11, 0.35); + background: rgba(15, 23, 42, 0.92); + color: var(--accent-amber); + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.03em; + text-align: center; + pointer-events: none; + box-shadow: var(--glow-amber); +} + +.mqtt-runtime { + margin: 0 0 1rem; + padding: 0.55rem 0.75rem; + border-radius: var(--radius-sm); + border: 1px solid rgba(255, 255, 255, 0.05); + background: rgba(0, 229, 160, 0.04); + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.03em; + color: var(--text-muted); +} + +.mqtt-runtime--offline { + background: rgba(239, 68, 68, 0.06); + border-color: rgba(239, 68, 68, 0.25); +} + +.mqtt-runtime--disabled { + background: rgba(100, 116, 139, 0.08); +} + +.mqtt-runtime__label { + color: var(--text-secondary); + font-weight: 600; + letter-spacing: 0.1em; + font-size: 0.62rem; + margin-right: 0.35rem; +} + +.mqtt-runtime__state { + color: var(--accent-green); +} + +.mqtt-runtime--offline .mqtt-runtime__state { + color: var(--accent-red); +} + +.mqtt-runtime--disabled .mqtt-runtime__state { + color: var(--text-muted); +} diff --git a/frontend/index.html b/frontend/index.html index 239398cc..69a41a12 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -21,6 +21,7 @@ + @@ -747,6 +748,7 @@

Service actions

+ diff --git a/frontend/js/components/node_map.js b/frontend/js/components/node_map.js index c8a88af8..72564281 100644 --- a/frontend/js/components/node_map.js +++ b/frontend/js/components/node_map.js @@ -404,12 +404,33 @@ class NodeMap { }, 200); } + _showTopologyMapHint(show) { + let el = document.getElementById('map-topology-hint'); + if (!el && this._map) { + el = document.createElement('div'); + el.id = 'map-topology-hint'; + el.className = 'map-topology-hint'; + el.hidden = true; + this._map.getContainer().appendChild(el); + } + if (!el) return; + if (show) { + el.textContent = + 'Links need GPS on both endpoints. Open the Topology tab for the logical graph.'; + el.hidden = false; + } else { + el.hidden = true; + } + } + async _loadTopology() { try { - const res = await fetch('/api/analytics/topology'); - const links = await res.json(); + const res = await fetch('/api/analytics/topology?hours=24'); + const data = await res.json(); + const links = Array.isArray(data) ? data : (data.edges || []); this._topologyLayer.clearLayers(); + let drawn = 0; for (const link of links) { const srcMarker = this._markers[link.source]; const tgtMarker = this._markers[link.target]; @@ -434,9 +455,14 @@ class NodeMap { line.bindTooltip(tooltip); this._topologyLayer.addLayer(line); + drawn += 1; } + this._showTopologyMapHint( + this._topologyVisible && links.length > 0 && drawn === 0, + ); } catch (e) { console.error('Topology load failed:', e); + this._showTopologyMapHint(false); } } diff --git a/frontend/js/configuration/mqtt_card.js b/frontend/js/configuration/mqtt_card.js index 2678d460..c7738e4c 100644 --- a/frontend/js/configuration/mqtt_card.js +++ b/frontend/js/configuration/mqtt_card.js @@ -11,6 +11,7 @@ class MqttConfigCard { this._root = null; this._passwordDirty = false; this._unsubDisplayUnits = null; + this._runtimeInterval = null; } mount(root) { @@ -25,6 +26,10 @@ class MqttConfigCard { are published. Undecrypted packets never leave the device.

+