diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f12b077d..9f72748d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -477,6 +477,19 @@ storage: Packets are stored in a local SQLite database. Old packets are pruned automatically based on `max_packets_retained`. +### Stray frames (Unknown RF) + +When a LoRa frame fails **both** Meshtastic and MeshCore decode, Meshpoint can log RF metadata only (no relay, no re-encode). View results in the dashboard **Unknown RF** tab or export via `GET /api/stray_frames?format=csv`. + +```yaml +stray_frames: + enabled: true + max_retained: 10000 # hard cap on SQLite rows + retention_hours: 168 # drop rows older than this (7 days) +``` + +Pruning runs in the background during the normal storage cleanup loop and does not block the receive path. + --- ## Dashboard @@ -490,6 +503,10 @@ dashboard: Access at `http://:8080`. Bind to `127.0.0.1` to restrict to local access only. +### Unknown RF tab + +Open **Unknown RF** in the sidebar to browse undecodable frames logged by `stray_frames` (see Storage). Filter by time window and minimum RSSI; use **Export CSV** to download the current filter via `/api/stray_frames?format=csv`. Disable logging with `stray_frames.enabled: false` in `local.yaml`. + --- ## Device Identity @@ -744,6 +761,11 @@ storage: # local SQLite packet store max_packets_retained: 100000 cleanup_interval_seconds: 3600 +stray_frames: # undecodable RF metadata (Unknown RF tab) + enabled: true + max_retained: 10000 + retention_hours: 168 + dashboard: # local web UI host: "0.0.0.0" port: 8080 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/css/unknown_rf.css b/frontend/css/unknown_rf.css new file mode 100644 index 00000000..6b24f08f --- /dev/null +++ b/frontend/css/unknown_rf.css @@ -0,0 +1,113 @@ +.section[data-section="unknown-rf"] { + overflow-y: auto; +} + +.unknown-rf-panel { + padding: 1.25rem; + max-width: 1100px; + margin: 0 auto; +} + +.unknown-rf-panel__header { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.unknown-rf-panel__title { + margin: 0; + font-size: 1.1rem; + color: var(--text-primary, #f1f5f9); +} + +.unknown-rf-panel__desc { + margin: 0.35rem 0 0; + font-size: 0.78rem; + color: var(--text-secondary, #94a3b8); + max-width: 40rem; +} + +.unknown-rf-controls { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.unknown-rf-field label { + display: block; + font-size: 0.68rem; + color: var(--text-muted, #64748b); + margin-bottom: 0.2rem; +} + +.unknown-rf-field input, +.unknown-rf-field select { + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(51, 65, 85, 0.8); + border-radius: 6px; + color: var(--text-primary, #e2e8f0); + padding: 0.35rem 0.5rem; + font-size: 0.78rem; +} + +.unknown-rf-btn { + background: rgba(6, 182, 212, 0.15); + border: 1px solid rgba(6, 182, 212, 0.45); + color: var(--accent-cyan); + border-radius: 6px; + padding: 0.4rem 0.75rem; + font-size: 0.75rem; + cursor: pointer; +} + +.unknown-rf-btn:hover { + background: rgba(6, 182, 212, 0.25); +} + +.unknown-rf-meta { + font-size: 0.72rem; + color: var(--text-secondary, #94a3b8); + margin-bottom: 0.75rem; +} + +.unknown-rf-table-wrap { + overflow-x: auto; + border: 1px solid rgba(51, 65, 85, 0.6); + border-radius: 8px; +} + +.unknown-rf-table { + width: 100%; + border-collapse: collapse; + font-size: 0.75rem; +} + +.unknown-rf-table th, +.unknown-rf-table td { + padding: 0.45rem 0.6rem; + text-align: left; + border-bottom: 1px solid rgba(51, 65, 85, 0.35); +} + +.unknown-rf-table th { + color: var(--text-muted, #64748b); + font-weight: 600; + background: rgba(15, 23, 42, 0.6); +} + +.unknown-rf-table td { + color: var(--text-primary, #e2e8f0); + font-variant-numeric: tabular-nums; +} + +.unknown-rf-empty { + padding: 2rem; + text-align: center; + color: var(--text-secondary, #94a3b8); + font-size: 0.8rem; +} diff --git a/frontend/index.html b/frontend/index.html index 239398cc..de1cd63b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -21,6 +21,7 @@ + @@ -33,6 +34,7 @@ + @@ -102,6 +104,24 @@ Stats +