IFM Relay — Mesh Operator Dashboard Web App
Status: ✅ Core demo implemented (
packages/sdk/examples/relay/) — this document is the full design spec for the production app.Implemented today: a live observer dashboard that boots the IFM web core (the Rust protocol core in WASM) as a passive mesh observer and joins the SAME mesh the desktop apps run on (entry = rendezvous manifest, exactly like production). It shows stations on the discovery plane, connected peers, per-frequency traffic, packets-by-type, the shared station-agnostic relay pool, counters and a live traffic feed. Relays are passive infrastructure run by operators — the dashboard only observes; it never spawns relays.
The Relay app is a live operational dashboard for running an IFM relay node. It provides real-time mesh visibility, traffic analytics, health monitoring, and operational controls — think Datadog + Grafana + Netdata purpose-built for the IFM mesh.
This is the only app with a live dashboard — Station and Listener are consumer/producer apps; Relay is the operator's mission control.
Target Audience
- Relay operators — Running
ifm-relaynodes (bootstrap, regional, edge) - Network admins — Monitoring mesh health, capacity planning
- DevOps/SRE — Alerting, incident response, post-mortem replay
- Protocol developers — Debugging gossip, DHT, transport behavior
The Live Dashboard (Core Differentiator)
The dashboard is a real-time, multi-panel operational view updating at sub-second intervals. It's not a static admin page — it's a live cockpit.
Dashboard Layout (Desktop / Wide)
Panel Specifications
| Panel | Refresh | Data Source | Interactions |
|---|---|---|---|
| Mesh Topology | 1s | peers.list + peers.get (latency) | Pan/zoom, click peer → detail drawer, geo/logical toggle, filter by region/transport/state |
| Frequency Table | 2s | frequencies.list | Sort any column, filter (type, listeners, broadcaster), search, export CSV, click → frequency detail |
| Traffic Sparklines | 500ms | packets.stream (aggregated) | Time range selector, pause, voice/text/video/control breakdown, packets/sec + bytes/sec |
| Health Gauges | 2s | node.health | Click gauge → detail modal (sparklines, thresholds), configurable warn/crit levels |
| Alert Feed | Event-driven | alerts.stream (SSE) | Acknowledge, dismiss, filter by severity, webhook test button, silence rules |
Full Feature Set
1. Mesh Topology (/relay/topology)
Real-time peer graph with geographic intelligence
- Force-directed graph (D3.js / Canvas) — Peers as nodes, connections as edges
- Layout modes:
- Geographic — Nodes positioned by GeoIP (lat/long), edges curved by great-circle
- Logical — Force-directed by connection strength, clusters = sub-meshes
- Hierarchical — Bootstrap → regional → edge layers
- Node encoding:
- Size = bandwidth (total in+out)
- Color = health (green=healthy, yellow=degraded, red=critical, gray=disconnected)
- Border = transport (solid=QUIC, dashed=WebRTC, dotted=TCP)
- Label = peer ID prefix + region flag
- Edge encoding:
- Width = traffic volume
- Color = latency (green<50ms, yellow<150ms, red>150ms)
- Animation = packet flow direction (subtle pulse)
- Controls: Pan/zoom, search peer ID, filter by region/transport/state, time-travel slider (historical replay)
- Peer detail drawer (click node): Full
PeerInfo, connection history, per-frequency traffic, actions (disconnect, ban, force reconnect)
2. Frequencies (/relay/frequencies)
Registry of all known frequencies with operational controls
- Columns: Name, namespace, channel, type (public/protected/hidden), listener count, broadcaster count, bandwidth (in/out), codec, bitrate, last activity, local subscription state
- Actions per row: Announce (inject into DHT), Hide (remove from DHT), Config (access type, capacity, TTL), Delete (force purge), Subscribe (relay joins frequency)
- Bulk actions: Multi-select → announce/hide/delete/set-access-type
- Create frequency modal: Name, type, initial TTL, capacity limit, access control list (for protected)
3. Traffic Analytics (/relay/traffic)
Deep-dive packet flow analysis
- Real-time charts (500ms update):
- Packets/sec (in/out, stacked by type: voice/text/video/control/plugin)
- Bytes/sec (in/out)
- Unique peers/sec (in/out)
- Frequency count active
- Aggregation windows: 1m, 5m, 15m, 1h, 24h (pre-aggregated in node)
- Breakdown tables (drill-down):
- By frequency: top 20 by packets, bytes, peers
- By peer: top 20 producers/consumers
- By type: voice vs text vs video vs control ratios
- By TTL: distribution histogram (0-255)
- By hops: distribution histogram
- Anomaly detection: Auto-flag spikes (3σ), new frequency bursts, peer traffic anomalies
- Export: CSV (current view), PCAP (raw packets via
packets.stream+ local buffer)
4. Health Monitoring (/relay/health)
System + node metrics with thresholds
| Gauge | Source | Warn | Crit | Detail Modal |
|---|---|---|---|---|
| CPU % | node.health.cpu_percent | 70% | 90% | Per-core, process vs system, sparkline 1h |
| Memory % | node.health.memory_bytes / total | 75% | 90% | Heap, RSS, cache, GC pauses |
| Network In | node.health.network.bytes_in_per_sec | 80% NIC | 95% NIC | Per-interface, peer breakdown |
| Network Out | node.health.network.bytes_out_per_sec | 80% NIC | 95% NIC | Per-interface, peer breakdown |
| Connections | node.health.network.connections | 80% max | 95% max | By transport, by state |
| Gossip Hit Rate | node.health.gossip.hit_rate | <80% | <60% | Cache size, dup rate, TTL dist |
| Gossip Cache | node.health.gossip.cache_size | 80% max | 95% max | Eviction rate, age distribution |
| DHT Routing Table | node.health.dht.routing_table_size | <500 | <200 | Bucket distribution, lookup latency |
| Audio Encoders | node.health.audio.encoder_active | N/A | N/A | Bitrate, complexity, FEC, packets |
| Audio Decoders | node.health.audio.decoder_active | N/A | N/A | Jitter buffer, underruns/overruns |
- Threshold config: Per-gauge warn/crit, notification channels (webhook, email, PagerDuty, Slack)
- SLO dashboard: Availability (99.9%), latency p50/p95/p99, peer churn rate
5. Peer Management (/relay/peers)
Operational peer control plane
- Actions: Disconnect (graceful), Ban (add to blocklist, persist), Force Reconnect (drop + dial), Delete (forget peer)
- Bulk: Select multiple → disconnect/ban
- Peer detail: Full connection history, per-frequency traffic, capability negotiation, transport fallback history
- Blocklist management: View/edit persistent ban list, CIDR ranges, expiry
6. Frequency Control (/relay/frequency-control)
Operator control over frequency lifecycle
- Announce frequency — Inject into DHT with TTL, capacity, access type
- Hide frequency — Remove from DHT (graceful: notify subscribers first)
- Set access type — Public / Protected (ACL) / Hidden (invite-only)
- Capacity limits — Max listeners, max broadcasters, bandwidth cap per frequency
- TTL management — Default TTL for gossip, per-frequency override
- Bridge config — Relay ↔ external (RTMP/SRT/ICEcast) — future
7. Alerting (/relay/alerts)
Threshold + anomaly alerting with notification routing
Alert Rules (Configurable)
| Rule | Condition | Severity | Cooldown |
|---|---|---|---|
| Peer churn | >10 peers disconnect in 1m | Warning | 5m |
| Gossip hit rate | <70% for 5m | Warning | 10m |
| Gossip hit rate | <50% for 2m | Critical | 5m |
| CPU sustained | >85% for 10m | Warning | 15m |
| Memory leak | RSS growth >10%/hr for 1h | Warning | 30m |
| Bandwidth saturation | >90% NIC for 5m | Critical | 5m |
| DHT routing table | <200 peers | Warning | 10m |
| Audio underruns | >5/min sustained | Warning | 5m |
| New frequency burst | >20 new freq in 1m | Info | 1m |
Notification Channels
- Webhook (generic JSON POST) — Slack, Discord, PagerDuty, Opsgenie, custom
- Email (SMTP) — Digest + immediate
- In-app — Alert feed panel, browser notification API
- SSE stream —
/api/alerts/streamfor external consumers
Alert Feed UI
- Live streaming list (newest top), color-coded by severity
- Group by rule, expand for context (metric values, peer list, graph snapshot)
- Actions: Acknowledge (assign to user), Silence (1h/24h/custom), Dismiss, Create silencing rule
- History: Searchable, filterable, exportable
8. Historical Replay (/relay/replay)
Time-travel debugging — the killer feature
- Timeline scrubber — Date/time picker + relative (last 1h/6h/24h/7d)
- Synchronized panels — All dashboard panels (topology, frequencies, traffic, health) show state at selected timestamp
- Playback controls — Play/pause, speed (0.25x/0.5x/1x/2x/5x), loop range
- Event markers — Alerts, peer joins/leaves, frequency announcements, config changes
- Export — Snapshot (JSON), video (WebM screen capture), report (PDF)
- Use cases:
- Post-mortem: "What happened at 14:23 when traffic dropped?"
- Capacity planning: "Show me peak hour last Tuesday"
- Debug: "Replay the cascade when peer X disconnected"
Architecture
JSON-RPC API (Relay Node → Dashboard)
| Method | Params | Returns | Description |
|---|---|---|---|
node.info | {} | NodeInfo | Identity, version, uptime, listen addrs |
node.health | {} | HealthMetrics | CPU, mem, net, gossip, DHT, audio |
peers.list | {} | PeerInfo[] | All peers with connection state |
peers.get | {peer_id} | PeerInfo | Detailed peer info + history |
peers.connect | {peer_id, addrs[]} | bool | Dial peer |
peers.disconnect | {peer_id} | bool | Graceful disconnect |
peers.ban | {peer_id, duration?, reason?} | bool | Add to blocklist |
frequencies.list | {} | FrequencyInfo[] | All known frequencies |
frequencies.announce | {freq, type, ttl, capacity, acl?} | bool | Inject into DHT |
frequencies.hide | {freq, graceful?} | bool | Remove from DHT |
frequencies.set_access | {freq, type, acl?} | bool | Change access type |
packets.stream | {filter?, aggregate?} | PacketStream | SSE: aggregated or raw packets |
metrics.stream | {interval_ms?} | MetricsStream | SSE: health metrics push |
alerts.stream | {severity?} | AlertStream | SSE: alert events |
replay.query | {start_ms, end_ms, panels[]} | ReplaySnapshot | Historical state at timestamp |
config.get | {} | RelayConfig | Current runtime config |
config.set | {key, value} | bool | Update config (hot-reload) |
Project Structure
packages/sdk/examples/relay/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── public/
│ ├── wasm/ # @ifm/sdk (for types only)
│ └── icons/
├── src/
│ ├── main.tsx
│ ├── App.tsx # Router + providers + WebSocket manager
│ ├── styles/globals.css
│ ├── components/
│ │ ├── ui/ # Radix primitives
│ │ ├── layout/ # Header, Sidebar, TabBar, StatusBar
│ │ ├── topology/
│ │ │ ├── MeshGraph.tsx # D3/Canvas force-directed graph
│ │ │ ├── PeerNode.tsx # Node rendering (color, size, border)
│ │ │ ├── PeerEdge.tsx # Edge rendering (width, color, pulse)
│ │ │ ├── PeerDetailDrawer.tsx # Click peer → slide-out panel
│ │ │ └── TopologyControls.tsx # Layout toggle, filters, time-travel
│ │ ├── frequencies/
│ │ │ ├── FrequencyTable.tsx # TanStack Table + virtualization
│ │ │ ├── FrequencyActions.tsx # Announce/Hide/Config/Delete buttons
│ │ │ ├── CreateFrequencyModal.tsx
│ │ │ └── BulkActionsBar.tsx
│ │ ├── traffic/
│ │ │ ├── TrafficCharts.tsx # Recharts/Visx real-time charts
│ │ │ ├── BreakdownTables.tsx # Drill-down tables
│ │ │ ├── TimeRangeSelector.tsx
│ │ │ └── ExportButtons.tsx
│ │ ├── health/
│ │ │ ├── GaugeGrid.tsx # Gauge components (react-gauge-chart)
│ │ │ ├── GaugeDetailModal.tsx # Click gauge → sparkline + thresholds
│ │ │ ├── ThresholdConfig.tsx # Warn/crit per metric
│ │ │ └── SLOPanel.tsx # Availability, latency SLOs
│ │ ├── peers/
│ │ │ ├── PeerTable.tsx
│ │ │ ├── PeerDetailModal.tsx
│ │ │ ├── BlocklistManager.tsx
│ │ │ └── BulkPeerActions.tsx
│ │ ├── alerts/
│ │ │ ├── AlertFeed.tsx # Live streaming list (SSE)
│ │ │ ├── AlertRuleEditor.tsx
│ │ │ ├── NotificationConfig.tsx
│ │ │ └── AlertHistory.tsx
│ │ ├── replay/
│ │ │ ├── TimelineScrubber.tsx
│ │ │ ├── PlaybackControls.tsx
│ │ │ ├── SyncedPanels.tsx # All panels at timestamp
│ │ │ ├── EventMarkers.tsx
│ │ │ └── ExportReplay.tsx
│ │ └── config/
│ │ ├── ConfigEditor.tsx # TOML editor + validation
│ │ └── HotReloadIndicator.tsx
│ ├── pages/
│ │ ├── Topology.tsx
│ │ ├── Frequencies.tsx
│ │ ├── Traffic.tsx
│ │ ├── Health.tsx
│ │ ├── Peers.tsx
│ │ ├── Alerts.tsx
│ │ ├── Replay.tsx
│ │ └── Config.tsx
│ ├── hooks/
│ │ ├── useJSONRPC.ts # RPC client + caching (TanStack Query)
│ │ ├── useSSE.ts # SSE connection manager
│ │ ├── useMesh.ts # Peer list + topology computed
│ │ ├── useFrequencies.ts # Frequency list + actions
│ │ ├── useTraffic.ts # Aggregated traffic metrics
│ │ ├── useHealth.ts # Health gauges + thresholds
│ │ ├── useAlerts.ts # Alert feed + rules
│ │ ├── useReplay.ts # Historical query + playback
│ │ └── useConfig.ts # Config get/set + validation
│ ├── services/
│ │ ├── jsonrpc.ts # WebSocket JSON-RPC client
│ │ ├── sse.ts # EventSource wrapper + reconnect
│ │ ├── replay.ts # IndexedDB + Parquet (WASM) storage
│ │ └── notifications.ts # Web Push + browser notifications
│ ├── stores/
│ │ ├── topologyStore.ts # Peer positions, layout mode
│ │ ├── frequencyStore.ts
│ │ ├── trafficStore.ts # Time-series buffers
│ │ ├── healthStore.ts
│ │ ├── alertStore.ts
│ │ └── replayStore.ts
│ └── types/
│ ├── rpc.ts # JSON-RPC types
│ ├── topology.ts
│ ├── frequency.ts
│ ├── traffic.ts
│ ├── health.ts
│ ├── alerts.ts
│ └── replay.tsReal-Time Data Flow
JSON-RPC (Request/Response + Polling)
// Polling for state that changes at known intervals
const peerQuery = useQuery({
queryKey: ['peers'],
queryFn: () => rpc.call('peers.list', {}),
refetchInterval: 1000, // 1s for topology
});
// Push for high-frequency data
const trafficSSE = useSSE('/api/packets/stream', {
filter: { aggregate: true }, // Server aggregates to 500ms buckets
});
trafficSSE.on('data', (point) => trafficStore.push(point));SSE (Server-Sent Events) for Streams
packets.stream→ aggregated traffic metrics (500ms)metrics.stream→ health gauges (2s)alerts.stream→ alert events (immediate)logs.stream→ structured logs (optional, high volume)
Replay Engine (Local-First)
- Browser stores last 24h of aggregated metrics in IndexedDB (Parquet via WASM
parquet-wasm) replay.queryRPC fetches historical snapshots from node (node retains 7d Parquet)- Timeline scrubber reads from local IndexedDB (instant) → falls back to RPC for older data
- Playback: animates through local buffer at configurable speed
Key User Flows
1. Daily Operations Check
2. Incident Response
3. Capacity Planning
4. Frequency Lifecycle
Responsive Breakpoints
| Breakpoint | Width | Layout |
|---|---|---|
| Mobile | < 640px | Single panel view, bottom tab bar, swipe between tabs; topology = peer list |
| Tablet | 640–1024px | 2-col: main panel + sidebar (alerts/health); topology graph simplified |
| Desktop | 1024–1440px | Full 5-panel dashboard grid (as shown above) |
| Wide / Ops Wall | > 1440px | All panels visible, larger graphs, additional: SLO panel, replay minimap |
Designed for operations walls — Relay dashboard is the app you put on a 4K monitor in the NOC.
Configuration
# relay-dashboard.config.toml
[dashboard]
refresh_intervals = { topology = 1000, frequencies = 2000, traffic = 500, health = 2000 }
timezone = "UTC"
theme = "dark" # "dark" | "light" | "auto"
[topology]
layout = "geo" # "geo" | "logical" | "hierarchical"
max_peers_render = 500
show_disconnected = false
geoip_database = "public/geoip/mmdb"
[frequencies]
default_sort = "listeners_desc"
page_size = 50
show_hidden = true
[traffic]
aggregation_windows = [60, 300, 900, 3600, 86400] # seconds
chart_retention_ms = 3600000 # 1h in browser
[health]
thresholds = {
cpu_warn = 70, cpu_crit = 90,
mem_warn = 75, mem_crit = 90,
net_in_warn = 80, net_in_crit = 95,
net_out_warn = 80, net_out_crit = 95,
gossip_hit_warn = 80, gossip_hit_crit = 60,
dht_size_warn = 500, dht_size_crit = 200,
}
[alerts]
rules = [
{ name = "peer_churn", metric = "peer_disconnects_per_min", condition = ">10", severity = "warning", cooldown = 300 },
{ name = "gossip_degraded", metric = "gossip_hit_rate", condition = "<70", severity = "warning", cooldown = 600 },
{ name = "gossip_critical", metric = "gossip_hit_rate", condition = "<50", severity = "critical", cooldown = 300 },
{ name = "bandwidth_sat", metric = "nic_utilization", condition = ">90", severity = "critical", cooldown = 300 },
]
notifications = {
webhook = { url = "", secret = "" },
email = { smtp = "", from = "", to = [] },
slack = { webhook_url = "" },
pagerduty = { integration_key = "" },
}
[replay]
local_retention_hours = 24
remote_retention_days = 7
parquet_compression = "zstd"
[node_connection]
ws_url = "ws://localhost:8080/ifm/jsonrpc"
reconnect_interval_ms = 2000
max_reconnect_attempts = 10
request_timeout_ms = 5000Dependencies (Key)
| Package | Purpose |
|---|---|
react, react-dom | UI framework |
react-router-dom | Routing |
@tanstack/react-query | RPC caching, polling |
zustand | Client state |
@radix-ui/* | Accessible primitives |
tailwindcss | Styling |
d3-force, d3-geo, d3-scale | Topology graph |
canvas / roughjs | High-perf graph rendering |
recharts / visx | Real-time charts |
react-gauge-chart | Health gauges |
date-fns / date-fns-tz | Time handling |
parquet-wasm | Replay storage (IndexedDB) |
eventsource-parser | SSE parsing |
vite | Build tool |
vitest, playwright | Testing |
Implementation Phases
Phase 1: Foundation
- [ ] Vite + React + TypeScript + Tailwind scaffold
- [ ] JSON-RPC WebSocket client with reconnection
- [ ] SSE client for streams
- [ ] Basic routing + tab layout
- [ ] Node connection config + status indicator
Phase 2: Core Dashboard Panels
- [ ] Topology: Peer list → D3 force graph (logical layout first)
- [ ] Frequencies: Table with sort/filter/actions
- [ ] Traffic: Real-time charts (Recharts) from SSE
- [ ] Health: Gauge grid from SSE metrics
- [ ] Alerts: Feed from SSE + rule editor
Phase 3: Operational Controls
- [ ] Peer management (disconnect/ban/reconnect)
- [ ] Frequency control (announce/hide/set-access)
- [ ] Config editor (TOML + validation + hot-reload)
- [ ] Blocklist manager
Phase 4: Historical Replay
- [ ] IndexedDB + Parquet storage
- [ ] Timeline scrubber + playback controls
- [ ] Synced panel rendering at timestamp
- [ ] Node replay RPC integration
- [ ] Export (snapshot, video, report)
Phase 5: Geo + Advanced
- [ ] Geographic topology layout (GeoIP + D3 geo)
- [ ] Anomaly detection on traffic/health
- [ ] SLO dashboard + burn rate alerts
- [ ] Multi-node federation view (cluster of relays)
- [ ] Mobile/tablet responsive polish
Phase 6: Polish & Hardening
- [ ] Themes, keyboard shortcuts, accessibility
- [ ] Web Push notifications for critical alerts
- [ ] Performance: virtualized tables, canvas graph, WebWorkers for Parquet
- [ ] E2E tests with simulated node
- [ ] Documentation + runbooks
Related Docs
- Station App — Broadcaster studio (content originates here, appears in Relay topology/frequencies)
- Listener App — End-user consumption (consumes frequencies Relay manages)
- Node UI — Terminal equivalent for SSH/local access
- CLI —
ifm relaysubcommands for automation (native core, onifm-core) - SDK API — Core protocol (Relay dashboard connects to native node via JSON-RPC)