mirror of
https://github.com/dmduran12/pymc_console-dist.git
synced 2026-07-06 09:51:50 +02:00
b55320b83d
Automated sync from private repository. Commit: 183deb7a619e896fa8d09edc117486562118dfcd
2724 lines
99 KiB
JSON
2724 lines
99 KiB
JSON
{
|
||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||
"title": "pyMC Console WebSocket API Design Specification",
|
||
"version": "2.0.0",
|
||
"description": "WebSocket-first API specification for the pyMC Console backend. This document defines a real-time, bidirectional communication protocol for packet streaming, topology updates, statistics, and mesh network monitoring. Designed for delivery to a backend specialist implementing these features in pyMC_Repeater.",
|
||
"meta": {
|
||
"author": "pyMC Console Team",
|
||
"created": "2026-01-15",
|
||
"target_backend": "pyMC_Repeater (CherryPy + SQLite + ws4py or websockets)",
|
||
"frontend_framework": "React + TypeScript + Zustand",
|
||
"intended_audience": "Backend/Database Specialist",
|
||
"protocol": "WebSocket (RFC 6455)"
|
||
},
|
||
|
||
"architecture": {
|
||
"overview": "This specification defines a WebSocket-first architecture where the primary data flow is through persistent, bidirectional connections. REST endpoints are retained only for initial data hydration and one-off queries. All real-time updates (packets, stats, topology changes, neighbor events) flow through WebSocket channels, eliminating polling overhead and enabling instant UI updates.",
|
||
|
||
"design_principles": [
|
||
{
|
||
"principle": "WebSocket-First, REST-Second",
|
||
"rationale": "WebSocket provides true real-time communication with minimal overhead. REST polling wastes bandwidth, increases server load, and introduces latency. Use WebSocket for all streaming data; retain REST only for initial hydration, historical queries, and actions (POST/DELETE)."
|
||
},
|
||
{
|
||
"principle": "Server-Side Computation",
|
||
"rationale": "All heavy computation (topology analysis, prefix disambiguation, airtime calculation, time bucketing) should occur server-side. The server pushes computed results; the frontend is a thin display layer."
|
||
},
|
||
{
|
||
"principle": "Subscription-Based Data Flow",
|
||
"rationale": "Clients subscribe to specific data channels (packets, stats, topology, neighbors). Server pushes only subscribed data, reducing bandwidth. Clients can dynamically adjust subscriptions."
|
||
},
|
||
{
|
||
"principle": "Delta Updates",
|
||
"rationale": "After initial state hydration, server sends only changes (new packets, updated edges, changed stats). Never re-send full state unless explicitly requested."
|
||
},
|
||
{
|
||
"principle": "Heartbeat & Reconnection",
|
||
"rationale": "Maintain connection health with periodic pings. Clients must handle disconnection gracefully and re-subscribe on reconnect. Server should support session resumption."
|
||
},
|
||
{
|
||
"principle": "Message Sequencing",
|
||
"rationale": "All messages include sequence numbers for ordering and gap detection. Clients can request replay of missed messages after reconnection."
|
||
}
|
||
],
|
||
|
||
"current_client_side_computations_to_offload": [
|
||
"Prefix disambiguation (4-factor scoring, collision detection)",
|
||
"Topology edge building from packet paths",
|
||
"Viterbi HMM path decoding for ghost node discovery",
|
||
"Airtime calculation using Semtech formula",
|
||
"Time bucketing for charts (received/transmitted/forwarded/dropped)",
|
||
"LBT statistics computation (retry rates, backoff analysis)",
|
||
"Sparkline data generation per node",
|
||
"Path health scoring",
|
||
"Mobile node detection",
|
||
"Edge betweenness centrality",
|
||
"Zero-hop neighbor detection"
|
||
],
|
||
|
||
"connection_lifecycle": {
|
||
"1_connect": "Client opens WebSocket to ws://host:8000/ws",
|
||
"2_authenticate": "Client sends 'auth' message with session token (if required)",
|
||
"3_subscribe": "Client sends 'subscribe' messages for desired channels",
|
||
"4_hydrate": "Server sends initial state snapshot for subscribed channels",
|
||
"5_stream": "Server pushes delta updates as events occur",
|
||
"6_heartbeat": "Periodic ping/pong to maintain connection (every 30s)",
|
||
"7_reconnect": "On disconnect, client reconnects and re-subscribes with last_seq for replay"
|
||
}
|
||
},
|
||
|
||
"websocket_protocol": {
|
||
"endpoint": "ws://host:8000/ws",
|
||
"subprotocol": "pymc-console-v2",
|
||
|
||
"message_format": {
|
||
"description": "All messages are JSON objects with a standard envelope",
|
||
"schema": {
|
||
"type": { "type": "string", "required": true, "description": "Message type identifier" },
|
||
"channel": { "type": "string", "description": "Channel name for subscriptions and events" },
|
||
"seq": { "type": "integer", "description": "Server sequence number (for server→client messages)" },
|
||
"req_id": { "type": "string", "description": "Request ID for client→server messages (for correlation)" },
|
||
"payload": { "type": "object", "description": "Message-specific data" },
|
||
"timestamp": { "type": "integer", "description": "Unix timestamp (milliseconds)" },
|
||
"error": { "type": "string", "description": "Error message if applicable" }
|
||
},
|
||
"example_client_message": {
|
||
"type": "subscribe",
|
||
"channel": "packets",
|
||
"req_id": "abc123",
|
||
"payload": { "filters": { "types": [4] } }
|
||
},
|
||
"example_server_message": {
|
||
"type": "packet",
|
||
"channel": "packets",
|
||
"seq": 12345,
|
||
"timestamp": 1736934849000,
|
||
"payload": { "packet": "(full Packet object - see Packet schema)" }
|
||
}
|
||
},
|
||
|
||
"client_message_types": {
|
||
"subscribe": {
|
||
"description": "Subscribe to a data channel",
|
||
"payload": {
|
||
"channel": "string (required) - Channel to subscribe to",
|
||
"filters": "object (optional) - Channel-specific filters",
|
||
"last_seq": "integer (optional) - Last received sequence for replay"
|
||
},
|
||
"channels": ["packets", "stats", "topology", "neighbors", "lbt", "logs", "hardware", "noise_floor", "config"]
|
||
},
|
||
"unsubscribe": {
|
||
"description": "Unsubscribe from a data channel",
|
||
"payload": {
|
||
"channel": "string (required)"
|
||
}
|
||
},
|
||
"request": {
|
||
"description": "One-off data request (query/action)",
|
||
"payload": {
|
||
"action": "string (required) - Action identifier",
|
||
"params": "object - Action-specific parameters"
|
||
}
|
||
},
|
||
"ping": {
|
||
"description": "Client heartbeat",
|
||
"payload": {}
|
||
}
|
||
},
|
||
|
||
"server_message_types": {
|
||
"subscribed": {
|
||
"description": "Confirmation of subscription with initial state",
|
||
"payload": {
|
||
"channel": "string",
|
||
"state": "object - Initial state snapshot",
|
||
"seq": "integer - Starting sequence number"
|
||
}
|
||
},
|
||
"unsubscribed": {
|
||
"description": "Confirmation of unsubscription",
|
||
"payload": { "channel": "string" }
|
||
},
|
||
"event": {
|
||
"description": "Channel event (delta update)",
|
||
"payload": "varies by channel"
|
||
},
|
||
"response": {
|
||
"description": "Response to a request message",
|
||
"payload": {
|
||
"req_id": "string - Correlation ID from request",
|
||
"data": "object - Response data",
|
||
"error": "string (optional)"
|
||
}
|
||
},
|
||
"pong": {
|
||
"description": "Server heartbeat response",
|
||
"payload": { "server_time": "integer" }
|
||
},
|
||
"error": {
|
||
"description": "Error notification",
|
||
"payload": {
|
||
"code": "integer",
|
||
"message": "string",
|
||
"req_id": "string (optional)"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"channels": {
|
||
"packets": {
|
||
"description": "Real-time packet stream with filtering",
|
||
"rationale": "Replace 3-second polling with instant push. Each packet is sent as it arrives, enabling true real-time monitoring.",
|
||
|
||
"subscribe_options": {
|
||
"filters": {
|
||
"types": { "type": "array", "description": "Payload types to include (0-15)" },
|
||
"routes": { "type": "array", "description": "Route types to include (0-3)" },
|
||
"transmitted_only": { "type": "boolean", "description": "Only TX packets" },
|
||
"received_only": { "type": "boolean", "description": "Only RX packets" },
|
||
"min_rssi": { "type": "integer", "description": "Minimum RSSI threshold" },
|
||
"src_prefixes": { "type": "array", "description": "Source hash prefixes to include" },
|
||
"exclude_duplicates": { "type": "boolean", "default": false }
|
||
},
|
||
"include_path_analysis": { "type": "boolean", "default": false, "description": "Include server-side disambiguation for each packet" }
|
||
},
|
||
|
||
"initial_state": {
|
||
"description": "Sent on subscription",
|
||
"payload": {
|
||
"recent_packets": "array - Last 100 packets matching filters",
|
||
"counts": {
|
||
"total_rx": "integer",
|
||
"total_tx": "integer",
|
||
"total_forwarded": "integer",
|
||
"total_dropped": "integer"
|
||
}
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"packet_received": {
|
||
"description": "New packet received from radio",
|
||
"payload": {
|
||
"packet": "Full Packet object",
|
||
"path_analysis": "optional - Resolved path with confidence scores"
|
||
}
|
||
},
|
||
"packet_transmitted": {
|
||
"description": "Packet transmitted by this node",
|
||
"payload": {
|
||
"packet": "Full Packet object"
|
||
}
|
||
},
|
||
"duplicate_detected": {
|
||
"description": "Duplicate packet received",
|
||
"payload": {
|
||
"packet_hash": "string",
|
||
"duplicate_count": "integer",
|
||
"rssi": "integer",
|
||
"snr": "number"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"stats": {
|
||
"description": "Real-time statistics updates",
|
||
"rationale": "Push stats changes as they occur rather than polling every 3 seconds. Supports multiple aggregation windows.",
|
||
|
||
"subscribe_options": {
|
||
"windows": { "type": "array", "default": ["live", "hourly"], "description": "Time windows to receive: live, hourly, daily" },
|
||
"include_airtime": { "type": "boolean", "default": true },
|
||
"bucket_count": { "type": "integer", "default": 20, "description": "Number of buckets for live window" }
|
||
},
|
||
|
||
"initial_state": {
|
||
"description": "Full stats snapshot on subscription",
|
||
"payload": {
|
||
"node": {
|
||
"name": "string",
|
||
"local_hash": "string",
|
||
"public_key": "string",
|
||
"uptime_seconds": "integer"
|
||
},
|
||
"counts": {
|
||
"rx_count": "integer",
|
||
"tx_count": "integer",
|
||
"forwarded_count": "integer",
|
||
"dropped_count": "integer"
|
||
},
|
||
"rates": {
|
||
"rx_per_hour": "number",
|
||
"tx_per_hour": "number",
|
||
"forwarded_per_hour": "number"
|
||
},
|
||
"radio": {
|
||
"noise_floor_dbm": "number",
|
||
"duty_cycle_percent": "number",
|
||
"airtime_used_ms": "integer",
|
||
"airtime_remaining_ms": "integer"
|
||
},
|
||
"buckets": {
|
||
"live": "array of { bucket, start, end, rx, tx, forwarded, dropped, airtime_ms }",
|
||
"hourly": "array of hourly aggregates (last 24h)"
|
||
}
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"counts_updated": {
|
||
"description": "Packet counts changed",
|
||
"payload": {
|
||
"rx_count": "integer",
|
||
"tx_count": "integer",
|
||
"forwarded_count": "integer",
|
||
"dropped_count": "integer",
|
||
"delta": { "rx": "integer", "tx": "integer", "forwarded": "integer", "dropped": "integer" }
|
||
}
|
||
},
|
||
"rates_updated": {
|
||
"description": "Rate calculations updated (every minute)",
|
||
"payload": {
|
||
"rx_per_hour": "number",
|
||
"tx_per_hour": "number",
|
||
"forwarded_per_hour": "number"
|
||
}
|
||
},
|
||
"bucket_updated": {
|
||
"description": "Time bucket stats updated",
|
||
"payload": {
|
||
"window": "live|hourly|daily",
|
||
"bucket": "BucketData object"
|
||
}
|
||
},
|
||
"noise_floor_updated": {
|
||
"description": "Noise floor reading changed",
|
||
"payload": {
|
||
"noise_floor_dbm": "number",
|
||
"trend": "up|down|stable"
|
||
}
|
||
},
|
||
"duty_cycle_updated": {
|
||
"description": "Duty cycle changed",
|
||
"payload": {
|
||
"duty_cycle_percent": "number",
|
||
"airtime_used_ms": "integer",
|
||
"airtime_remaining_ms": "integer"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"topology": {
|
||
"description": "Real-time mesh topology updates",
|
||
"rationale": "Push topology changes as packets arrive. Server maintains edge state and pushes deltas. Eliminates need for client-side topology computation.",
|
||
|
||
"subscribe_options": {
|
||
"confidence_threshold": { "type": "number", "default": 0.4, "description": "Minimum confidence for edge inclusion" },
|
||
"include_weak_edges": { "type": "boolean", "default": true },
|
||
"include_centrality": { "type": "boolean", "default": true },
|
||
"include_mobile_detection": { "type": "boolean", "default": true },
|
||
"include_path_registry": { "type": "boolean", "default": false },
|
||
"include_ghost_nodes": { "type": "boolean", "default": true }
|
||
},
|
||
|
||
"initial_state": {
|
||
"description": "Full topology snapshot on subscription",
|
||
"payload": {
|
||
"edges": "array of TopologyEdge objects",
|
||
"hub_nodes": "array of node hashes with high centrality",
|
||
"gateway_nodes": "array of gateway node hashes",
|
||
"mobile_nodes": "array of volatile node hashes",
|
||
"loops": "array of NetworkLoop objects",
|
||
"centrality": "map of hash -> centrality score",
|
||
"ghost_clusters": "array of GhostCluster objects",
|
||
"disambiguation_stats": {
|
||
"total_prefixes": "integer",
|
||
"collision_prefixes": "integer",
|
||
"collision_rate_percent": "number",
|
||
"avg_confidence": "number"
|
||
},
|
||
"local_prefix": "string",
|
||
"packet_count_analyzed": "integer",
|
||
"computed_at": "timestamp"
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"edge_added": {
|
||
"description": "New edge discovered",
|
||
"payload": {
|
||
"edge": "TopologyEdge object",
|
||
"is_certain": "boolean"
|
||
}
|
||
},
|
||
"edge_updated": {
|
||
"description": "Edge observation count or confidence changed",
|
||
"payload": {
|
||
"edge_key": "string",
|
||
"updates": {
|
||
"observation_count": "integer",
|
||
"certain_count": "integer",
|
||
"avg_confidence": "number",
|
||
"last_seen": "timestamp"
|
||
}
|
||
}
|
||
},
|
||
"edge_removed": {
|
||
"description": "Edge fell below threshold or expired",
|
||
"payload": {
|
||
"edge_key": "string",
|
||
"reason": "threshold|expired|invalid"
|
||
}
|
||
},
|
||
"hub_detected": {
|
||
"description": "Node classified as hub",
|
||
"payload": {
|
||
"hash": "string",
|
||
"centrality_score": "number",
|
||
"traffic_percent": "number"
|
||
}
|
||
},
|
||
"hub_demoted": {
|
||
"description": "Node no longer qualifies as hub",
|
||
"payload": { "hash": "string" }
|
||
},
|
||
"gateway_detected": {
|
||
"description": "Node classified as gateway",
|
||
"payload": { "hash": "string", "traffic_percent": "number" }
|
||
},
|
||
"mobile_detected": {
|
||
"description": "Node identified as mobile/volatile",
|
||
"payload": {
|
||
"hash": "string",
|
||
"volatility_score": "number"
|
||
}
|
||
},
|
||
"loop_detected": {
|
||
"description": "Redundant path loop discovered",
|
||
"payload": {
|
||
"loop": "NetworkLoop object"
|
||
}
|
||
},
|
||
"ghost_cluster_updated": {
|
||
"description": "Ghost node cluster changed",
|
||
"payload": {
|
||
"prefix": "string",
|
||
"observation_count": "integer",
|
||
"is_likely_real": "boolean",
|
||
"estimated_location": { "lat": "number", "lon": "number" }
|
||
}
|
||
},
|
||
"disambiguation_updated": {
|
||
"description": "Prefix disambiguation confidence changed",
|
||
"payload": {
|
||
"prefix": "string",
|
||
"best_match": "string or null",
|
||
"confidence": "number",
|
||
"candidates": "array of { hash, score }"
|
||
}
|
||
},
|
||
"path_registered": {
|
||
"description": "New unique path observed",
|
||
"payload": {
|
||
"hops": "array of prefixes",
|
||
"resolved_nodes": "array of { prefix, hash, confidence }",
|
||
"is_canonical": "boolean"
|
||
}
|
||
},
|
||
"tx_delay_recommendation": {
|
||
"description": "TX delay recommendation updated for a node",
|
||
"payload": {
|
||
"hash": "string",
|
||
"network_role": "edge|relay|hub|backbone",
|
||
"flood_delay_sec": "number",
|
||
"direct_delay_sec": "number",
|
||
"collision_risk": "number",
|
||
"confidence": "string"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"neighbors": {
|
||
"description": "Real-time neighbor/contact updates",
|
||
"rationale": "Push neighbor changes as ADVERTs arrive. Server detects zero-hop contacts from path analysis.",
|
||
|
||
"subscribe_options": {
|
||
"types": { "type": "array", "default": ["all"], "description": "Contact types: repeater, companion, room_server, all" },
|
||
"include_expired": { "type": "boolean", "default": false },
|
||
"include_signal_history": { "type": "boolean", "default": false }
|
||
},
|
||
|
||
"initial_state": {
|
||
"description": "Full neighbor list on subscription",
|
||
"payload": {
|
||
"neighbors": "array of NeighborInfo objects",
|
||
"zero_hop_contacts": "array of QuickNeighbor objects (direct RF)",
|
||
"stats": {
|
||
"total": "integer",
|
||
"repeaters": "integer",
|
||
"companions": "integer",
|
||
"room_servers": "integer",
|
||
"zero_hop_count": "integer"
|
||
}
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"neighbor_discovered": {
|
||
"description": "New neighbor seen for first time",
|
||
"payload": {
|
||
"neighbor": "NeighborInfo object",
|
||
"is_zero_hop": "boolean"
|
||
}
|
||
},
|
||
"neighbor_updated": {
|
||
"description": "Neighbor info changed (ADVERT received)",
|
||
"payload": {
|
||
"hash": "string",
|
||
"updates": {
|
||
"advert_count": "integer",
|
||
"last_seen": "timestamp",
|
||
"rssi": "integer (if zero-hop)",
|
||
"snr": "number (if zero-hop)"
|
||
}
|
||
}
|
||
},
|
||
"neighbor_status_changed": {
|
||
"description": "Neighbor status changed (active/stale/expired)",
|
||
"payload": {
|
||
"hash": "string",
|
||
"old_status": "string",
|
||
"new_status": "string"
|
||
}
|
||
},
|
||
"zero_hop_detected": {
|
||
"description": "Direct RF contact detected from packet path",
|
||
"payload": {
|
||
"hash": "string",
|
||
"count": "integer",
|
||
"avg_rssi": "number",
|
||
"avg_snr": "number"
|
||
}
|
||
},
|
||
"neighbor_removed": {
|
||
"description": "Neighbor hidden by user",
|
||
"payload": { "hash": "string" }
|
||
}
|
||
}
|
||
},
|
||
|
||
"lbt": {
|
||
"description": "Listen Before Talk statistics stream",
|
||
"rationale": "Push LBT metrics as transmitted packets accumulate. Server computes retry rates, backoff stats, collision risk.",
|
||
|
||
"subscribe_options": {
|
||
"include_hourly_breakdown": { "type": "boolean", "default": true }
|
||
},
|
||
|
||
"initial_state": {
|
||
"description": "LBT stats snapshot on subscription",
|
||
"payload": {
|
||
"total_packets_with_lbt": "integer",
|
||
"packets_with_retries": "integer",
|
||
"retry_rate_percent": "number",
|
||
"avg_retries": "number",
|
||
"channel_busy_count": "integer",
|
||
"channel_busy_rate_percent": "number",
|
||
"collision_risk_percent": "number",
|
||
"backoff": {
|
||
"avg_ms": "number",
|
||
"max_ms": "number",
|
||
"total_ms": "number"
|
||
},
|
||
"hourly_breakdown": "array of hourly LBT stats"
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"lbt_stats_updated": {
|
||
"description": "LBT statistics changed",
|
||
"payload": {
|
||
"retry_rate_percent": "number",
|
||
"channel_busy_rate_percent": "number",
|
||
"collision_risk_percent": "number",
|
||
"delta": {
|
||
"packets_with_retries": "integer",
|
||
"channel_busy_count": "integer"
|
||
}
|
||
}
|
||
},
|
||
"channel_busy": {
|
||
"description": "Channel busy event occurred on TX",
|
||
"payload": {
|
||
"packet_hash": "string",
|
||
"attempts": "integer",
|
||
"total_backoff_ms": "number"
|
||
}
|
||
},
|
||
"high_collision_risk": {
|
||
"description": "Collision risk exceeded threshold",
|
||
"payload": {
|
||
"collision_risk_percent": "number",
|
||
"threshold": "number"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"logs": {
|
||
"description": "Real-time log streaming",
|
||
"rationale": "Stream logs as they're written. More efficient than polling log files.",
|
||
|
||
"subscribe_options": {
|
||
"level": { "type": "string", "default": "INFO", "enum": ["DEBUG", "INFO", "WARNING", "ERROR"] },
|
||
"max_initial": { "type": "integer", "default": 100, "description": "Max log lines in initial state" }
|
||
},
|
||
|
||
"initial_state": {
|
||
"payload": {
|
||
"logs": "array of { timestamp, level, message }",
|
||
"current_level": "string"
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"log_entry": {
|
||
"description": "New log entry",
|
||
"payload": {
|
||
"timestamp": "integer",
|
||
"level": "string",
|
||
"message": "string",
|
||
"source": "string (optional)"
|
||
}
|
||
},
|
||
"log_level_changed": {
|
||
"description": "Log level changed via API",
|
||
"payload": { "level": "string" }
|
||
}
|
||
}
|
||
},
|
||
|
||
"hardware": {
|
||
"description": "Hardware statistics stream",
|
||
"rationale": "Push hardware stats periodically (every 5s) rather than polling.",
|
||
|
||
"subscribe_options": {
|
||
"interval_seconds": { "type": "integer", "default": 5, "min": 1, "max": 60 }
|
||
},
|
||
|
||
"initial_state": {
|
||
"payload": {
|
||
"cpu_percent": "number",
|
||
"memory_percent": "number",
|
||
"disk_percent": "number",
|
||
"temperatures": "array of { label, celsius }",
|
||
"load_average": "array of 3 numbers"
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"hardware_stats": {
|
||
"description": "Periodic hardware stats update",
|
||
"payload": {
|
||
"cpu_percent": "number",
|
||
"memory_percent": "number",
|
||
"disk_percent": "number",
|
||
"temperatures": "array",
|
||
"load_average": "array"
|
||
}
|
||
},
|
||
"temperature_alert": {
|
||
"description": "Temperature exceeded threshold",
|
||
"payload": {
|
||
"label": "string",
|
||
"celsius": "number",
|
||
"threshold": "number"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"noise_floor": {
|
||
"description": "Real-time noise floor updates with anomaly detection",
|
||
"rationale": "Push noise floor readings as they're sampled. Server-side anomaly detection identifies interference patterns that correlate with reduced packet activity.",
|
||
|
||
"subscribe_options": {
|
||
"include_anomalies": { "type": "boolean", "default": true, "description": "Include anomaly detection events" },
|
||
"anomaly_config": {
|
||
"baseline_dbm": { "type": "number", "default": -107, "description": "Baseline noise floor threshold (dBm)" },
|
||
"spike_dbm": { "type": "number", "default": -100, "description": "Spike threshold (dBm)" },
|
||
"min_sequence_length": { "type": "integer", "default": 16, "description": "Minimum consecutive samples for anomaly" }
|
||
}
|
||
},
|
||
|
||
"initial_state": {
|
||
"description": "Noise floor history and current anomalies on subscription",
|
||
"payload": {
|
||
"current_dbm": "number",
|
||
"history_24h": "array of { timestamp, noise_floor_dbm }",
|
||
"thresholds": {
|
||
"median": "number",
|
||
"p90": "number",
|
||
"p95": "number",
|
||
"p99": "number"
|
||
},
|
||
"active_anomalies": "array of NoiseFloorAnomaly objects"
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"noise_floor_reading": {
|
||
"description": "New noise floor reading",
|
||
"payload": {
|
||
"timestamp": "integer",
|
||
"noise_floor_dbm": "number",
|
||
"trend": "up|down|stable"
|
||
}
|
||
},
|
||
"anomaly_started": {
|
||
"description": "New noise floor anomaly detected (elevated interference)",
|
||
"payload": {
|
||
"start_ts": "integer",
|
||
"peak_value": "number",
|
||
"severity": "moderate|severe|critical"
|
||
}
|
||
},
|
||
"anomaly_ended": {
|
||
"description": "Noise floor anomaly ended (returned to baseline)",
|
||
"payload": {
|
||
"start_ts": "integer",
|
||
"end_ts": "integer",
|
||
"duration_seconds": "integer",
|
||
"avg_value": "number",
|
||
"severity": "string"
|
||
}
|
||
},
|
||
"thresholds_updated": {
|
||
"description": "Statistical thresholds recalculated (hourly)",
|
||
"payload": {
|
||
"median": "number",
|
||
"p90": "number",
|
||
"p95": "number",
|
||
"p99": "number"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"config": {
|
||
"description": "Real-time configuration change notifications",
|
||
"rationale": "Push config changes made via other clients or CLI. Keeps all connected dashboards in sync.",
|
||
|
||
"subscribe_options": {},
|
||
|
||
"initial_state": {
|
||
"payload": {
|
||
"radio": "current radio configuration",
|
||
"repeater": "current repeater settings (mode, delays, etc.)",
|
||
"duty_cycle": "duty cycle configuration",
|
||
"web": "web server configuration"
|
||
}
|
||
},
|
||
|
||
"events": {
|
||
"radio_config_changed": {
|
||
"description": "Radio configuration updated",
|
||
"payload": {
|
||
"changed_fields": "array of field names",
|
||
"new_values": "object with new field values",
|
||
"requires_restart": "boolean"
|
||
}
|
||
},
|
||
"mode_changed": {
|
||
"description": "Operating mode changed (forward/monitor)",
|
||
"payload": {
|
||
"old_mode": "string",
|
||
"new_mode": "string"
|
||
}
|
||
},
|
||
"duty_cycle_changed": {
|
||
"description": "Duty cycle settings changed",
|
||
"payload": {
|
||
"enforcement_enabled": "boolean",
|
||
"max_airtime_percent": "number"
|
||
}
|
||
},
|
||
"identity_changed": {
|
||
"description": "Identity created/updated/deleted",
|
||
"payload": {
|
||
"action": "created|updated|deleted",
|
||
"identity_name": "string",
|
||
"identity_type": "repeater|room_server"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"request_actions": {
|
||
"description": "One-off requests via WebSocket (alternative to REST for actions)",
|
||
|
||
"get_packet": {
|
||
"description": "Fetch single packet by hash",
|
||
"params": {
|
||
"hash": "string (required)",
|
||
"include_duplicates": "boolean",
|
||
"include_path_analysis": "boolean"
|
||
},
|
||
"response": {
|
||
"packet": "Packet object",
|
||
"duplicates": "array (optional)",
|
||
"path_analysis": "object (optional)"
|
||
}
|
||
},
|
||
|
||
"get_packets_history": {
|
||
"description": "Fetch historical packets (for initial hydration or pagination)",
|
||
"params": {
|
||
"limit": { "type": "integer", "default": 100 },
|
||
"before_timestamp": "integer (optional)",
|
||
"after_timestamp": "integer (optional)",
|
||
"filters": "object (same as subscribe filters)"
|
||
},
|
||
"response": {
|
||
"packets": "array",
|
||
"has_more": "boolean",
|
||
"oldest_timestamp": "integer"
|
||
}
|
||
},
|
||
|
||
"resolve_path": {
|
||
"description": "Resolve packet path using Viterbi HMM",
|
||
"params": {
|
||
"path": "array of 2-char prefixes",
|
||
"packet_hash": "string (optional)",
|
||
"enable_ghost_detection": "boolean"
|
||
},
|
||
"response": {
|
||
"resolved_nodes": "array of { prefix, hash, confidence, is_ghost }",
|
||
"total_cost": "number",
|
||
"path_confidence": "number"
|
||
}
|
||
},
|
||
|
||
"lookup_prefix": {
|
||
"description": "Disambiguate a single prefix",
|
||
"params": {
|
||
"prefix": "string (required)",
|
||
"position": "integer (optional)",
|
||
"adjacent_prefixes": "array (optional)"
|
||
},
|
||
"response": {
|
||
"prefix": "string",
|
||
"best_match": "string or null",
|
||
"confidence": "number",
|
||
"candidates": "array"
|
||
}
|
||
},
|
||
|
||
"trigger_deep_analysis": {
|
||
"description": "Trigger full topology recomputation",
|
||
"params": {
|
||
"packet_hours": { "type": "integer", "default": 168 },
|
||
"force_full": { "type": "boolean", "default": false }
|
||
},
|
||
"response": {
|
||
"job_id": "string",
|
||
"status": "queued|computing"
|
||
}
|
||
},
|
||
|
||
"get_deep_analysis_status": {
|
||
"description": "Check status of deep analysis job",
|
||
"params": { "job_id": "string" },
|
||
"response": {
|
||
"status": "queued|computing|complete|failed",
|
||
"progress_percent": "number",
|
||
"topology": "TopologySnapshot (if complete)"
|
||
}
|
||
},
|
||
|
||
"calculate_airtime": {
|
||
"description": "Calculate airtime for packet configuration",
|
||
"params": {
|
||
"payload_bytes": "integer (required)",
|
||
"spreading_factor": "integer (optional)",
|
||
"bandwidth_hz": "integer (optional)",
|
||
"coding_rate": "integer (optional)"
|
||
},
|
||
"response": {
|
||
"airtime_ms": "number",
|
||
"symbol_time_ms": "number"
|
||
}
|
||
},
|
||
|
||
"get_sparklines": {
|
||
"description": "Fetch sparkline data for nodes",
|
||
"params": {
|
||
"hashes": "array of strings",
|
||
"hours": { "type": "integer", "default": 24 },
|
||
"points": { "type": "integer", "default": 24 }
|
||
},
|
||
"response": {
|
||
"sparklines": "map of hash -> array of { timestamp, rx, tx, rssi, snr }"
|
||
}
|
||
},
|
||
|
||
"get_noise_floor_history": {
|
||
"description": "Fetch noise floor history for charts and anomaly detection",
|
||
"params": {
|
||
"hours": { "type": "integer", "default": 24 },
|
||
"include_anomalies": { "type": "boolean", "default": true }
|
||
},
|
||
"response": {
|
||
"history": "array of { timestamp, noise_floor_dbm }",
|
||
"anomalies": "array of NoiseFloorAnomaly (if include_anomalies=true)",
|
||
"thresholds": "statistical thresholds object"
|
||
}
|
||
},
|
||
|
||
"get_packet_type_stats": {
|
||
"description": "Get packet type distribution for treemap/pie charts",
|
||
"params": {
|
||
"hours": { "type": "integer", "default": 24 },
|
||
"include_route_breakdown": { "type": "boolean", "default": false }
|
||
},
|
||
"response": {
|
||
"types": "map of type_id -> { name, count, percent }",
|
||
"route_breakdown": "optional nested breakdown"
|
||
}
|
||
},
|
||
|
||
"get_network_composition": {
|
||
"description": "Get breakdown of contact types in the mesh",
|
||
"params": {},
|
||
"response": {
|
||
"repeaters": "integer",
|
||
"companions": "integer",
|
||
"room_servers": "integer",
|
||
"total": "integer",
|
||
"active_24h": "integer"
|
||
}
|
||
},
|
||
|
||
"get_path_health": {
|
||
"description": "Get health metrics for observed paths",
|
||
"params": {
|
||
"source_hash": "string (optional)",
|
||
"dest_hash": "string (optional)",
|
||
"min_observations": { "type": "integer", "default": 3 }
|
||
},
|
||
"response": {
|
||
"paths": "array of { hops, health_score, weakest_link, observation_count, trend, estimated_latency_ms }",
|
||
"canonical_paths": "map of endpoint_pair -> best path"
|
||
}
|
||
},
|
||
|
||
"send_advert": {
|
||
"description": "Trigger ADVERT broadcast",
|
||
"params": {},
|
||
"response": { "success": "boolean" }
|
||
},
|
||
|
||
"set_mode": {
|
||
"description": "Set forward/monitor mode",
|
||
"params": { "mode": "forward|monitor" },
|
||
"response": { "success": "boolean", "mode": "string" }
|
||
},
|
||
|
||
"set_duty_cycle": {
|
||
"description": "Enable/disable duty cycle enforcement",
|
||
"params": { "enabled": "boolean" },
|
||
"response": { "success": "boolean" }
|
||
},
|
||
|
||
"set_log_level": {
|
||
"description": "Change log level",
|
||
"params": { "level": "DEBUG|INFO|WARNING|ERROR" },
|
||
"response": { "success": "boolean" }
|
||
},
|
||
|
||
"hide_neighbor": {
|
||
"description": "Hide a neighbor from display",
|
||
"params": { "hash": "string" },
|
||
"response": { "success": "boolean" }
|
||
},
|
||
|
||
"ping_neighbor": {
|
||
"description": "Send ping to neighbor",
|
||
"params": { "hash": "string", "timeout": "integer (default 10)" },
|
||
"response": { "success": "boolean", "rtt_ms": "number", "snr_db": "number", "rssi": "integer", "path": "array" }
|
||
},
|
||
|
||
"update_radio_config": {
|
||
"description": "Update radio configuration (frequency, SF, bandwidth, power, delays)",
|
||
"params": {
|
||
"frequency_mhz": "number (optional)",
|
||
"bandwidth_khz": "number (optional)",
|
||
"spreading_factor": "integer (optional, 5-12)",
|
||
"coding_rate": "integer (optional, 5-8)",
|
||
"tx_power": "integer (optional, dBm)",
|
||
"tx_delay_factor": "number (optional, 0.0-5.0)",
|
||
"direct_tx_delay_factor": "number (optional)",
|
||
"rx_delay_base": "number (optional)",
|
||
"node_name": "string (optional)",
|
||
"latitude": "number (optional)",
|
||
"longitude": "number (optional)",
|
||
"max_flood_hops": "integer (optional, 0-64)",
|
||
"advert_interval_minutes": "integer (optional, 0 or 60-240)"
|
||
},
|
||
"response": { "applied": "array of field names", "persisted": "boolean", "live_update": "boolean", "warnings": "array (optional)" }
|
||
},
|
||
|
||
"restart_service": {
|
||
"description": "Restart the pymc-repeater systemd service",
|
||
"params": {},
|
||
"response": { "success": "boolean", "message": "string" }
|
||
}
|
||
},
|
||
|
||
"rest_endpoints_retained": {
|
||
"description": "REST endpoints retained for specific use cases (initial hydration, file downloads, compatibility)",
|
||
|
||
"GET /api/stats": {
|
||
"description": "Initial stats fetch (for non-WebSocket fallback)",
|
||
"status": "EXISTING - Unchanged"
|
||
},
|
||
|
||
"GET /api/recent_packets": {
|
||
"description": "Fallback for initial packet load",
|
||
"status": "EXISTING - Unchanged"
|
||
},
|
||
|
||
"GET /api/radio_presets": {
|
||
"description": "Radio preset list",
|
||
"status": "EXISTING - Unchanged"
|
||
},
|
||
|
||
"POST /api/update_radio_config": {
|
||
"description": "Update radio configuration",
|
||
"status": "EXISTING - Unchanged (actions via REST are fine)"
|
||
},
|
||
|
||
"GET /api/identities": {
|
||
"description": "Identity management",
|
||
"status": "EXISTING - Unchanged"
|
||
},
|
||
|
||
"note": "All other existing REST endpoints remain available for backward compatibility and non-WebSocket clients.",
|
||
|
||
"identity_management": {
|
||
"description": "Identity CRUD operations (repeater + room servers)",
|
||
"endpoints": {
|
||
"GET /api/identities": "List all identities",
|
||
"GET /api/identity?name=X": "Get specific identity by name",
|
||
"POST /api/create_identity": "Create new identity (auto-generates key if not provided)",
|
||
"PUT /api/update_identity": "Update existing identity",
|
||
"DELETE /api/delete_identity?name=X": "Delete an identity",
|
||
"POST /api/send_room_server_advert": "Send ADVERT for a room server identity"
|
||
},
|
||
"status": "EXISTING - feat/identity branch"
|
||
},
|
||
|
||
"acl_management": {
|
||
"description": "Access Control List for authenticated clients",
|
||
"endpoints": {
|
||
"GET /api/acl_info": "ACL config and stats for all identities",
|
||
"GET /api/acl_clients": "List authenticated clients (optionally by identity)",
|
||
"POST /api/acl_remove_client": "Remove client from ACL",
|
||
"GET /api/acl_stats": "Overall ACL statistics"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"room_server": {
|
||
"description": "Room server message and client management",
|
||
"endpoints": {
|
||
"GET /api/room_messages": "Get messages from a room",
|
||
"POST /api/room_post_message": "Post message to a room",
|
||
"GET /api/room_stats": "Room statistics (one or all rooms)",
|
||
"GET /api/room_clients": "Clients synced to a room",
|
||
"DELETE /api/room_message": "Delete specific message",
|
||
"DELETE /api/room_messages": "Clear all messages in room"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"transport_keys": {
|
||
"description": "Transport key management for encrypted channels",
|
||
"endpoints": {
|
||
"GET /api/transport_keys": "List all transport keys",
|
||
"POST /api/transport_keys": "Create new transport key",
|
||
"GET /api/transport_key/:id": "Get specific transport key",
|
||
"PUT /api/transport_key/:id": "Update transport key",
|
||
"DELETE /api/transport_key/:id": "Delete transport key"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"api_tokens": {
|
||
"description": "Machine-to-machine authentication tokens",
|
||
"endpoints": {
|
||
"GET /auth/tokens": "List all API tokens",
|
||
"POST /auth/tokens": "Create new API token",
|
||
"DELETE /auth/tokens/:id": "Revoke API token"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"mesh_policy": {
|
||
"description": "Mesh routing policies",
|
||
"endpoints": {
|
||
"POST /api/global_flood_policy": "Set global flood allow/deny policy"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"duty_cycle_config": {
|
||
"description": "Duty cycle enforcement configuration",
|
||
"endpoints": {
|
||
"POST /api/update_duty_cycle_config": "Update max_airtime_percent and enforcement_enabled"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"web_config": {
|
||
"description": "Web server configuration",
|
||
"endpoints": {
|
||
"POST /api/update_web_config": "Update CORS and web_path settings",
|
||
"GET /api/check_pymc_console": "Check if pyMC Console is installed",
|
||
"GET /api/check_default_frontend": "Check if default Vue.js frontend exists"
|
||
},
|
||
"status": "EXISTING"
|
||
},
|
||
|
||
"neighbor_management": {
|
||
"description": "Neighbor/advert operations",
|
||
"endpoints": {
|
||
"DELETE /api/advert/:id": "Delete/hide a neighbor",
|
||
"POST /api/ping_neighbor": "Ping neighbor for RTT/SNR",
|
||
"GET /api/adverts_by_contact_type": "Filter adverts by type"
|
||
},
|
||
"status": "EXISTING"
|
||
}
|
||
},
|
||
|
||
"database_schema_additions": {
|
||
"description": "SQLite schema additions to support server-side computation and WebSocket state",
|
||
|
||
"tables": {
|
||
"stats_hourly": {
|
||
"description": "Pre-aggregated hourly statistics for efficient historical queries",
|
||
"columns": {
|
||
"hour_timestamp": "INTEGER PRIMARY KEY",
|
||
"rx_count": "INTEGER",
|
||
"tx_count": "INTEGER",
|
||
"forwarded_count": "INTEGER",
|
||
"dropped_count": "INTEGER",
|
||
"avg_rssi": "REAL",
|
||
"avg_snr": "REAL",
|
||
"noise_floor_avg": "REAL",
|
||
"total_airtime_ms": "INTEGER"
|
||
},
|
||
"triggers": "INSERT trigger on packets table to update hourly bucket"
|
||
},
|
||
|
||
"topology_edges": {
|
||
"description": "Materialized topology edges updated incrementally",
|
||
"columns": {
|
||
"edge_key": "TEXT PRIMARY KEY (sorted hash pair)",
|
||
"from_hash": "TEXT",
|
||
"to_hash": "TEXT",
|
||
"observation_count": "INTEGER",
|
||
"certain_count": "INTEGER",
|
||
"confidence_sum": "REAL",
|
||
"forward_count": "INTEGER",
|
||
"reverse_count": "INTEGER",
|
||
"flood_count": "INTEGER",
|
||
"direct_count": "INTEGER",
|
||
"min_hop_distance": "INTEGER",
|
||
"first_seen": "INTEGER",
|
||
"last_seen": "INTEGER",
|
||
"is_hub_connection": "INTEGER"
|
||
},
|
||
"indexes": [
|
||
"CREATE INDEX idx_edges_from ON topology_edges(from_hash)",
|
||
"CREATE INDEX idx_edges_to ON topology_edges(to_hash)",
|
||
"CREATE INDEX idx_edges_count ON topology_edges(observation_count DESC)"
|
||
]
|
||
},
|
||
|
||
"path_registry": {
|
||
"description": "Observed packet paths for health analysis",
|
||
"columns": {
|
||
"path_signature": "TEXT PRIMARY KEY",
|
||
"source_hash": "TEXT",
|
||
"dest_hash": "TEXT",
|
||
"hops": "TEXT (JSON array)",
|
||
"hop_count": "INTEGER",
|
||
"observation_count": "INTEGER",
|
||
"first_seen": "INTEGER",
|
||
"last_seen": "INTEGER",
|
||
"is_canonical": "INTEGER"
|
||
},
|
||
"indexes": [
|
||
"CREATE INDEX idx_paths_endpoints ON path_registry(source_hash, dest_hash)"
|
||
]
|
||
},
|
||
|
||
"ghost_clusters": {
|
||
"description": "Discovered ghost node clusters",
|
||
"columns": {
|
||
"prefix": "TEXT PRIMARY KEY",
|
||
"observation_count": "INTEGER",
|
||
"first_seen": "INTEGER",
|
||
"last_seen": "INTEGER",
|
||
"is_likely_real": "INTEGER",
|
||
"estimated_lat": "REAL",
|
||
"estimated_lon": "REAL",
|
||
"location_variance": "REAL",
|
||
"anchor_nodes": "TEXT (JSON array)"
|
||
}
|
||
},
|
||
|
||
"disambiguation_cache": {
|
||
"description": "Cached prefix disambiguation results",
|
||
"columns": {
|
||
"prefix": "TEXT PRIMARY KEY",
|
||
"best_match_hash": "TEXT",
|
||
"confidence": "REAL",
|
||
"is_unambiguous": "INTEGER",
|
||
"candidates_json": "TEXT",
|
||
"computed_at": "INTEGER"
|
||
}
|
||
},
|
||
|
||
"ws_sessions": {
|
||
"description": "WebSocket session tracking for reconnection support",
|
||
"columns": {
|
||
"session_id": "TEXT PRIMARY KEY",
|
||
"connected_at": "INTEGER",
|
||
"last_seen": "INTEGER",
|
||
"subscriptions": "TEXT (JSON array)",
|
||
"last_seq": "INTEGER"
|
||
}
|
||
},
|
||
|
||
"event_log": {
|
||
"description": "Event log for replay on reconnection (circular buffer)",
|
||
"columns": {
|
||
"seq": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
||
"channel": "TEXT",
|
||
"event_type": "TEXT",
|
||
"payload": "TEXT (JSON)",
|
||
"timestamp": "INTEGER"
|
||
},
|
||
"retention": "Keep last 10000 events per channel"
|
||
},
|
||
|
||
"noise_floor_history": {
|
||
"description": "Noise floor readings for anomaly detection and heatmaps",
|
||
"columns": {
|
||
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
||
"timestamp": "INTEGER NOT NULL",
|
||
"noise_floor_dbm": "REAL NOT NULL"
|
||
},
|
||
"indexes": [
|
||
"CREATE INDEX idx_noise_floor_ts ON noise_floor_history(timestamp DESC)"
|
||
],
|
||
"note": "May already exist - verify with existing schema"
|
||
},
|
||
|
||
"sparkline_cache": {
|
||
"description": "Pre-computed sparkline data per node (updated on packet arrival)",
|
||
"columns": {
|
||
"node_hash": "TEXT NOT NULL",
|
||
"bucket_timestamp": "INTEGER NOT NULL",
|
||
"rx_count": "INTEGER DEFAULT 0",
|
||
"tx_count": "INTEGER DEFAULT 0",
|
||
"avg_rssi": "REAL",
|
||
"avg_snr": "REAL"
|
||
},
|
||
"primary_key": "(node_hash, bucket_timestamp)",
|
||
"indexes": [
|
||
"CREATE INDEX idx_sparkline_node ON sparkline_cache(node_hash)"
|
||
],
|
||
"note": "Bucket size = 1 hour, keep 24 hours per node"
|
||
}
|
||
}
|
||
},
|
||
|
||
"time_series_architecture": {
|
||
"description": "Comprehensive time series data handling for historical analysis, charting, and data export",
|
||
|
||
"design_principles": [
|
||
{
|
||
"principle": "Multi-Resolution Storage",
|
||
"rationale": "Store data at multiple resolutions (raw, 1min, 5min, hourly, daily) to enable efficient queries at any time scale without computing aggregates on-the-fly."
|
||
},
|
||
{
|
||
"principle": "Continuous Downsampling",
|
||
"rationale": "Aggregate raw data into coarser buckets on a rolling basis. Keep 24h of raw data, 7d of 1min, 30d of hourly, unlimited daily. Reduces storage while preserving long-term trends."
|
||
},
|
||
{
|
||
"principle": "Time-Based Partitioning",
|
||
"rationale": "Partition large tables by time period (e.g., packets_2026_01, packets_2026_02) to enable efficient pruning and fast queries within time ranges."
|
||
},
|
||
{
|
||
"principle": "Streaming Aggregation",
|
||
"rationale": "Update aggregate buckets incrementally as data arrives rather than batch-computing. Enables real-time charts without query latency."
|
||
}
|
||
],
|
||
|
||
"retention_policy": {
|
||
"raw_packets": {
|
||
"retention": "7 days",
|
||
"note": "Full packet data including payload for deep analysis"
|
||
},
|
||
"minute_buckets": {
|
||
"retention": "30 days",
|
||
"fields": ["rx_count", "tx_count", "forwarded_count", "dropped_count", "avg_rssi", "avg_snr", "noise_floor"]
|
||
},
|
||
"hourly_buckets": {
|
||
"retention": "365 days (1 year)",
|
||
"fields": ["rx_count", "tx_count", "forwarded_count", "dropped_count", "avg_rssi", "avg_snr", "noise_floor_avg", "noise_floor_min", "noise_floor_max", "airtime_ms", "unique_nodes_seen"]
|
||
},
|
||
"daily_buckets": {
|
||
"retention": "unlimited",
|
||
"fields": ["rx_count", "tx_count", "forwarded_count", "dropped_count", "avg_rssi", "avg_snr", "noise_floor_avg", "airtime_ms", "unique_nodes_seen", "peak_hour"]
|
||
},
|
||
"topology_snapshots": {
|
||
"retention": "90 days",
|
||
"frequency": "Daily at midnight",
|
||
"note": "Compressed snapshots of topology state for historical playback"
|
||
},
|
||
"neighbor_history": {
|
||
"retention": "90 days",
|
||
"note": "Signal quality trends per neighbor over time"
|
||
}
|
||
},
|
||
|
||
"aggregation_windows": {
|
||
"live": {
|
||
"bucket_size": "5 seconds",
|
||
"retention": "10 minutes",
|
||
"use_case": "Real-time dashboard (last 10min at high resolution)"
|
||
},
|
||
"recent": {
|
||
"bucket_size": "1 minute",
|
||
"retention": "24 hours",
|
||
"use_case": "Recent activity charts, fine-grained analysis"
|
||
},
|
||
"short_term": {
|
||
"bucket_size": "5 minutes",
|
||
"retention": "7 days",
|
||
"use_case": "Week view charts, pattern detection"
|
||
},
|
||
"medium_term": {
|
||
"bucket_size": "1 hour",
|
||
"retention": "365 days",
|
||
"use_case": "Monthly/yearly trends, seasonal analysis"
|
||
},
|
||
"long_term": {
|
||
"bucket_size": "1 day",
|
||
"retention": "unlimited",
|
||
"use_case": "Historical overview, capacity planning"
|
||
}
|
||
},
|
||
|
||
"time_series_queries": {
|
||
"get_time_series": {
|
||
"description": "Flexible time series query for any metric",
|
||
"params": {
|
||
"metric": "string (required) - One of: rx_count, tx_count, forwarded_count, dropped_count, noise_floor, rssi, snr, airtime",
|
||
"start_time": "integer (required) - Unix timestamp",
|
||
"end_time": "integer (required) - Unix timestamp",
|
||
"resolution": "string - auto|5s|1m|5m|1h|1d (default: auto)",
|
||
"aggregation": "string - sum|avg|min|max|count (default: sum for counts, avg for metrics)",
|
||
"node_hash": "string (optional) - Filter to specific node",
|
||
"fill_gaps": "boolean - Fill missing buckets with 0/null (default: true)"
|
||
},
|
||
"response": {
|
||
"metric": "string",
|
||
"resolution": "string (actual resolution used)",
|
||
"buckets": "array of { timestamp, value, sample_count }",
|
||
"summary": {
|
||
"total": "number",
|
||
"avg": "number",
|
||
"min": "number",
|
||
"max": "number"
|
||
}
|
||
},
|
||
"notes": [
|
||
"Resolution 'auto' selects appropriate bucket size based on time range",
|
||
"< 1 hour: 5s buckets, < 24h: 1m buckets, < 7d: 5m buckets, < 30d: 1h buckets, else: 1d buckets"
|
||
]
|
||
},
|
||
|
||
"get_multi_series": {
|
||
"description": "Query multiple metrics in one request for correlated charts",
|
||
"params": {
|
||
"metrics": "array of metric names (required)",
|
||
"start_time": "integer (required)",
|
||
"end_time": "integer (required)",
|
||
"resolution": "string",
|
||
"node_hash": "string (optional)"
|
||
},
|
||
"response": {
|
||
"resolution": "string",
|
||
"series": "map of metric_name -> array of { timestamp, value }"
|
||
},
|
||
"use_case": "Overlay RX/TX/forwarded on same chart with aligned timestamps"
|
||
},
|
||
|
||
"get_heatmap_data": {
|
||
"description": "2D time series for heatmap visualization",
|
||
"params": {
|
||
"metric": "string (required)",
|
||
"start_time": "integer",
|
||
"end_time": "integer",
|
||
"x_resolution": "string - Hour of day (0-23)",
|
||
"y_resolution": "string - Day of week (0-6)"
|
||
},
|
||
"response": {
|
||
"grid": "2D array [day][hour] of values",
|
||
"min_value": "number",
|
||
"max_value": "number"
|
||
},
|
||
"use_case": "Activity heatmap showing busiest hours/days"
|
||
},
|
||
|
||
"get_comparison_series": {
|
||
"description": "Compare time periods (this week vs last week)",
|
||
"params": {
|
||
"metric": "string",
|
||
"current_start": "integer",
|
||
"current_end": "integer",
|
||
"compare_start": "integer",
|
||
"compare_end": "integer",
|
||
"resolution": "string"
|
||
},
|
||
"response": {
|
||
"current": "array of { timestamp, value }",
|
||
"comparison": "array of { timestamp, value }",
|
||
"change_percent": "number (overall change)"
|
||
}
|
||
},
|
||
|
||
"get_anomaly_detection": {
|
||
"description": "Detect anomalies in time series using statistical methods",
|
||
"params": {
|
||
"metric": "string",
|
||
"start_time": "integer",
|
||
"end_time": "integer",
|
||
"sensitivity": "number (0-1, default 0.5) - Higher = more anomalies detected",
|
||
"method": "string - zscore|iqr|mad (default: zscore)"
|
||
},
|
||
"response": {
|
||
"anomalies": "array of { timestamp, value, expected_value, deviation_score, type: spike|dip }",
|
||
"baseline": { "mean": "number", "std": "number" }
|
||
}
|
||
}
|
||
},
|
||
|
||
"database_tables": {
|
||
"stats_5s": {
|
||
"description": "Live 5-second buckets (rolling 10 minutes)",
|
||
"columns": {
|
||
"bucket_ts": "INTEGER PRIMARY KEY",
|
||
"rx_count": "INTEGER",
|
||
"tx_count": "INTEGER",
|
||
"forwarded_count": "INTEGER",
|
||
"dropped_count": "INTEGER"
|
||
},
|
||
"retention_trigger": "DELETE WHERE bucket_ts < (now - 600)"
|
||
},
|
||
"stats_1m": {
|
||
"description": "1-minute buckets (rolling 24 hours)",
|
||
"columns": {
|
||
"bucket_ts": "INTEGER PRIMARY KEY",
|
||
"rx_count": "INTEGER",
|
||
"tx_count": "INTEGER",
|
||
"forwarded_count": "INTEGER",
|
||
"dropped_count": "INTEGER",
|
||
"avg_rssi": "REAL",
|
||
"avg_snr": "REAL",
|
||
"noise_floor": "REAL"
|
||
},
|
||
"retention_trigger": "DELETE WHERE bucket_ts < (now - 86400)"
|
||
},
|
||
"stats_5m": {
|
||
"description": "5-minute buckets (rolling 7 days)",
|
||
"columns": {
|
||
"bucket_ts": "INTEGER PRIMARY KEY",
|
||
"rx_count": "INTEGER",
|
||
"tx_count": "INTEGER",
|
||
"forwarded_count": "INTEGER",
|
||
"dropped_count": "INTEGER",
|
||
"avg_rssi": "REAL",
|
||
"avg_snr": "REAL",
|
||
"noise_floor_avg": "REAL",
|
||
"noise_floor_min": "REAL",
|
||
"noise_floor_max": "REAL"
|
||
},
|
||
"retention_trigger": "DELETE WHERE bucket_ts < (now - 604800)"
|
||
},
|
||
"stats_hourly": {
|
||
"description": "Hourly buckets (365 days)",
|
||
"note": "Already defined in database_schema_additions"
|
||
},
|
||
"stats_daily": {
|
||
"description": "Daily buckets (unlimited)",
|
||
"columns": {
|
||
"day_ts": "INTEGER PRIMARY KEY",
|
||
"rx_count": "INTEGER",
|
||
"tx_count": "INTEGER",
|
||
"forwarded_count": "INTEGER",
|
||
"dropped_count": "INTEGER",
|
||
"avg_rssi": "REAL",
|
||
"avg_snr": "REAL",
|
||
"noise_floor_avg": "REAL",
|
||
"airtime_total_ms": "INTEGER",
|
||
"unique_nodes": "INTEGER",
|
||
"peak_hour": "INTEGER"
|
||
}
|
||
},
|
||
"neighbor_signal_history": {
|
||
"description": "Per-neighbor signal quality over time",
|
||
"columns": {
|
||
"node_hash": "TEXT NOT NULL",
|
||
"hour_ts": "INTEGER NOT NULL",
|
||
"packet_count": "INTEGER",
|
||
"avg_rssi": "REAL",
|
||
"avg_snr": "REAL",
|
||
"rssi_variance": "REAL",
|
||
"snr_variance": "REAL"
|
||
},
|
||
"primary_key": "(node_hash, hour_ts)",
|
||
"use_case": "Signal degradation detection, link quality trends"
|
||
},
|
||
"topology_daily_snapshot": {
|
||
"description": "Compressed daily topology snapshots for historical playback",
|
||
"columns": {
|
||
"day_ts": "INTEGER PRIMARY KEY",
|
||
"edges_json": "TEXT (compressed JSON)",
|
||
"nodes_json": "TEXT (compressed JSON)",
|
||
"hub_nodes": "TEXT (JSON array)",
|
||
"gateway_nodes": "TEXT (JSON array)",
|
||
"snapshot_size_bytes": "INTEGER"
|
||
},
|
||
"use_case": "Topology playback slider, network evolution visualization"
|
||
}
|
||
},
|
||
|
||
"downsampling_triggers": {
|
||
"description": "Background jobs to aggregate and prune data",
|
||
"jobs": [
|
||
{
|
||
"name": "aggregate_5s_to_1m",
|
||
"frequency": "Every 1 minute",
|
||
"logic": "Sum 5s buckets into 1m bucket, prune 5s buckets older than 10min"
|
||
},
|
||
{
|
||
"name": "aggregate_1m_to_5m",
|
||
"frequency": "Every 5 minutes",
|
||
"logic": "Sum 1m buckets into 5m bucket, prune 1m buckets older than 24h"
|
||
},
|
||
{
|
||
"name": "aggregate_5m_to_hourly",
|
||
"frequency": "Every hour (on the hour)",
|
||
"logic": "Sum 5m buckets into hourly bucket, prune 5m buckets older than 7d"
|
||
},
|
||
{
|
||
"name": "aggregate_hourly_to_daily",
|
||
"frequency": "Daily at 00:05 UTC",
|
||
"logic": "Sum hourly buckets into daily bucket"
|
||
},
|
||
{
|
||
"name": "snapshot_topology",
|
||
"frequency": "Daily at 00:00 UTC",
|
||
"logic": "Save compressed topology state to topology_daily_snapshot"
|
||
},
|
||
{
|
||
"name": "prune_hourly",
|
||
"frequency": "Daily at 03:00 UTC",
|
||
"logic": "Delete hourly buckets older than 365 days"
|
||
}
|
||
]
|
||
},
|
||
|
||
"export_formats": {
|
||
"csv": {
|
||
"endpoint": "GET /api/export/time_series",
|
||
"params": {
|
||
"metric": "string",
|
||
"start_time": "integer",
|
||
"end_time": "integer",
|
||
"resolution": "string",
|
||
"format": "csv"
|
||
},
|
||
"response": "text/csv with headers: timestamp,value,sample_count"
|
||
},
|
||
"json": {
|
||
"endpoint": "GET /api/export/time_series",
|
||
"params": { "format": "json" },
|
||
"response": "application/json array"
|
||
},
|
||
"prometheus": {
|
||
"endpoint": "GET /metrics",
|
||
"format": "Prometheus exposition format",
|
||
"metrics": [
|
||
"pymc_packets_received_total",
|
||
"pymc_packets_transmitted_total",
|
||
"pymc_packets_forwarded_total",
|
||
"pymc_packets_dropped_total",
|
||
"pymc_noise_floor_dbm",
|
||
"pymc_airtime_used_ms",
|
||
"pymc_neighbors_count"
|
||
],
|
||
"note": "Enables integration with Grafana/Prometheus ecosystem"
|
||
}
|
||
}
|
||
},
|
||
|
||
"pagination_architecture": {
|
||
"description": "Consistent pagination across all list endpoints for efficient data retrieval",
|
||
|
||
"design_principles": [
|
||
{
|
||
"principle": "Cursor-Based Pagination",
|
||
"rationale": "Use opaque cursors (base64-encoded timestamps/IDs) instead of offset/limit. Handles real-time data insertion without skipping or duplicating items. More efficient for large datasets."
|
||
},
|
||
{
|
||
"principle": "Consistent Response Envelope",
|
||
"rationale": "All paginated endpoints return the same metadata structure for predictable client handling."
|
||
},
|
||
{
|
||
"principle": "Bidirectional Navigation",
|
||
"rationale": "Support both forward (newer) and backward (older) pagination for timeline navigation."
|
||
},
|
||
{
|
||
"principle": "Stable Ordering",
|
||
"rationale": "Always include a tie-breaker (e.g., ID) when sorting by timestamp to ensure deterministic ordering."
|
||
}
|
||
],
|
||
|
||
"pagination_params": {
|
||
"limit": {
|
||
"type": "integer",
|
||
"default": 50,
|
||
"max": 500,
|
||
"description": "Maximum items per page"
|
||
},
|
||
"cursor": {
|
||
"type": "string",
|
||
"description": "Opaque cursor from previous response (base64-encoded)"
|
||
},
|
||
"direction": {
|
||
"type": "string",
|
||
"enum": ["forward", "backward"],
|
||
"default": "backward",
|
||
"description": "forward = newer items, backward = older items"
|
||
},
|
||
"sort_by": {
|
||
"type": "string",
|
||
"default": "timestamp",
|
||
"description": "Field to sort by (endpoint-specific options)"
|
||
},
|
||
"sort_order": {
|
||
"type": "string",
|
||
"enum": ["asc", "desc"],
|
||
"default": "desc"
|
||
}
|
||
},
|
||
|
||
"pagination_response": {
|
||
"items": "array - The requested items",
|
||
"pagination": {
|
||
"has_more": "boolean - More items available in this direction",
|
||
"next_cursor": "string|null - Cursor for next page (if has_more)",
|
||
"prev_cursor": "string|null - Cursor for previous page (if not at start)",
|
||
"total_count": "integer|null - Total items matching filters (expensive, optional)",
|
||
"returned_count": "integer - Items in this response"
|
||
}
|
||
},
|
||
|
||
"cursor_encoding": {
|
||
"format": "base64(JSON)",
|
||
"example_decoded": {
|
||
"ts": 1736934849000,
|
||
"id": "abc123",
|
||
"dir": "b"
|
||
},
|
||
"note": "Cursor encodes position (timestamp + ID) and direction for stateless server"
|
||
},
|
||
|
||
"paginated_endpoints": {
|
||
"GET /api/packets": {
|
||
"sort_options": ["timestamp", "rssi", "snr", "type"],
|
||
"filter_params": {
|
||
"types": "array of payload types",
|
||
"routes": "array of route types",
|
||
"min_rssi": "integer",
|
||
"max_rssi": "integer",
|
||
"src_hash": "string (prefix match)",
|
||
"transmitted": "boolean",
|
||
"start_time": "integer",
|
||
"end_time": "integer"
|
||
},
|
||
"example": "GET /api/packets?limit=100&types=4&start_time=1736848449&cursor=eyJ0cyI6MTczNjkzNDg0OTAwMH0="
|
||
},
|
||
|
||
"GET /api/logs": {
|
||
"sort_options": ["timestamp", "level"],
|
||
"filter_params": {
|
||
"level": "string (DEBUG|INFO|WARNING|ERROR)",
|
||
"search": "string (text search)",
|
||
"module": "string (logger name)",
|
||
"start_time": "integer",
|
||
"end_time": "integer"
|
||
}
|
||
},
|
||
|
||
"GET /api/neighbors": {
|
||
"sort_options": ["last_seen", "name", "rssi", "snr", "distance"],
|
||
"filter_params": {
|
||
"contact_type": "string (repeater|companion|room_server)",
|
||
"status": "string (active|stale|expired)",
|
||
"zero_hop_only": "boolean",
|
||
"has_location": "boolean"
|
||
}
|
||
},
|
||
|
||
"GET /api/topology_edges": {
|
||
"sort_options": ["observation_count", "last_seen", "confidence"],
|
||
"filter_params": {
|
||
"min_confidence": "number",
|
||
"node_hash": "string (edges involving this node)",
|
||
"is_certain": "boolean"
|
||
}
|
||
},
|
||
|
||
"GET /api/path_registry": {
|
||
"sort_options": ["observation_count", "last_seen", "hop_count"],
|
||
"filter_params": {
|
||
"source_hash": "string",
|
||
"dest_hash": "string",
|
||
"is_canonical": "boolean",
|
||
"min_observations": "integer"
|
||
}
|
||
},
|
||
|
||
"GET /api/room_messages": {
|
||
"sort_options": ["timestamp"],
|
||
"filter_params": {
|
||
"room_id": "string (required)",
|
||
"sender_hash": "string",
|
||
"search": "string"
|
||
}
|
||
}
|
||
},
|
||
|
||
"websocket_pagination": {
|
||
"description": "Pagination via WebSocket for initial hydration and history fetch",
|
||
"request_type": "request",
|
||
"action": "get_page",
|
||
"params": {
|
||
"resource": "string (packets|logs|neighbors|edges|paths|messages)",
|
||
"limit": "integer",
|
||
"cursor": "string",
|
||
"direction": "string",
|
||
"filters": "object"
|
||
},
|
||
"response": {
|
||
"req_id": "string",
|
||
"items": "array",
|
||
"pagination": "PaginationResponse object"
|
||
},
|
||
"example_request": {
|
||
"type": "request",
|
||
"req_id": "page-1",
|
||
"payload": {
|
||
"action": "get_page",
|
||
"params": {
|
||
"resource": "packets",
|
||
"limit": 100,
|
||
"cursor": "eyJ0cyI6MTczNjkzNDg0OTAwMH0=",
|
||
"direction": "backward",
|
||
"filters": { "types": [4] }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"infinite_scroll_support": {
|
||
"description": "Optimizations for infinite scroll UX",
|
||
"recommendations": [
|
||
"Pre-fetch next page when user scrolls to 80% of current page",
|
||
"Cache previous pages in memory for instant back-navigation",
|
||
"Use virtual scrolling for lists > 1000 items",
|
||
"Debounce scroll events (100ms) to avoid excessive requests"
|
||
],
|
||
"frontend_hooks": {
|
||
"useInfiniteQuery": "React Query / TanStack Query pattern for infinite lists",
|
||
"useVirtualizer": "@tanstack/react-virtual for virtualized rendering"
|
||
}
|
||
},
|
||
|
||
"total_count_optimization": {
|
||
"description": "total_count is expensive for large tables; use judiciously",
|
||
"strategies": [
|
||
"Default: Do not include total_count (save ~50ms per query)",
|
||
"On explicit request: include_total=true param triggers COUNT(*)",
|
||
"Cached counts: For common queries, cache count and refresh hourly",
|
||
"Estimated counts: Use EXPLAIN or pg_class.reltuples for estimates"
|
||
]
|
||
}
|
||
},
|
||
|
||
"topology_inference_engine": {
|
||
"description": "Advanced algorithmic system for inferring complete mesh topology from partial observations. Combines probabilistic graphical models, signal propagation physics, and graph-theoretic analysis to reconstruct network structure from packet path fragments.",
|
||
|
||
"design_philosophy": {
|
||
"core_principle": "Observation Always Wins Over Theory",
|
||
"rationale": "When high-confidence observed evidence exists (≥80% disambiguation confidence), observations override physics-based models entirely. Real-world evidence takes precedence over theoretical calculations.",
|
||
"approach": "Multi-layer inference combining deterministic observations (zero-hop contacts) as ground truth anchors, probabilistic path disambiguation, and physics-constrained graph completion."
|
||
},
|
||
|
||
"data_sources": {
|
||
"primary_observations": [
|
||
{
|
||
"source": "Zero-hop ADVERT packets",
|
||
"confidence": "1.0 (ground truth)",
|
||
"data": "Direct RF contact confirmed; RSSI, SNR, timestamp",
|
||
"usage": "Anchor points for geographic inference; edge weight calibration"
|
||
},
|
||
{
|
||
"source": "Packet paths (forwarded_path / original_path)",
|
||
"confidence": "0.3-0.95 depending on disambiguation",
|
||
"data": "Sequence of 2-char hex prefixes representing forwarding chain",
|
||
"usage": "Primary topology evidence; edge discovery"
|
||
},
|
||
{
|
||
"source": "RSSI/SNR measurements",
|
||
"confidence": "0.7-0.9",
|
||
"data": "Signal quality at reception",
|
||
"usage": "Distance estimation; link quality modeling"
|
||
},
|
||
{
|
||
"source": "Neighbor table (from API)",
|
||
"confidence": "0.9",
|
||
"data": "Known nodes with hashes, locations, last_seen, contact_type",
|
||
"usage": "Candidate pool for prefix disambiguation"
|
||
}
|
||
],
|
||
"derived_signals": [
|
||
"Packet timing patterns (collision inference)",
|
||
"Route type distribution (FLOOD vs DIRECT)",
|
||
"Airtime measurements (distance proxy)",
|
||
"Noise floor correlation with traffic (interference mapping)"
|
||
]
|
||
},
|
||
|
||
"inference_pipeline": {
|
||
"description": "12-phase pipeline from raw packets to complete topology model",
|
||
|
||
"phase_1_packet_parsing": {
|
||
"name": "Packet Path Extraction",
|
||
"input": "Raw packet with path field",
|
||
"output": "Parsed path segments with metadata",
|
||
"algorithm": "Route-aware parsing: DIRECT routes have path_len semantics different from FLOOD routes",
|
||
"key_logic": {
|
||
"flood_routing": "path contains all forwarders that touched the packet",
|
||
"direct_routing": "path_len indicates intermediary count; path may be pre-computed"
|
||
}
|
||
},
|
||
|
||
"phase_2_prefix_disambiguation": {
|
||
"name": "Four-Factor Prefix Resolution",
|
||
"description": "Resolve 2-char hex prefixes to full node hashes using multi-factor scoring",
|
||
"algorithm": "Weighted combination of position, co-occurrence, geography, and recency",
|
||
"factors": {
|
||
"position_consistency": {
|
||
"weight": 0.15,
|
||
"description": "How consistently does this candidate appear at specific path positions?",
|
||
"calculation": "positionCounts[i] / sum(positionCounts) for typical position i"
|
||
},
|
||
"cooccurrence_frequency": {
|
||
"weight": 0.15,
|
||
"description": "How often does this prefix appear adjacent to known prefixes?",
|
||
"calculation": "adjacentCount[knownPrefix] / totalAdjacentObservations"
|
||
},
|
||
"geographic_plausibility": {
|
||
"weight": 0.35,
|
||
"description": "Is the candidate physically plausible given RF propagation limits?",
|
||
"sub_factors": [
|
||
"Distance to local node (closer = higher score)",
|
||
"Source-geographic correlation (position-1 proximity to packet source)",
|
||
"Previous-hop anchor (proximity to resolved upstream node)",
|
||
"Next-hop anchor (proximity to resolved downstream node)",
|
||
"Zero-hop boost for confirmed direct RF contacts"
|
||
],
|
||
"distance_bands": {
|
||
"very_close": { "range_m": 500, "score": 1.0 },
|
||
"close": { "range_m": 2000, "score": 0.8 },
|
||
"medium": { "range_m": 5000, "score": 0.6 },
|
||
"far": { "range_m": 10000, "score": 0.4 },
|
||
"very_far": { "range_m": 20000, "score": 0.2 }
|
||
}
|
||
},
|
||
"recency_scoring": {
|
||
"weight": 0.35,
|
||
"description": "Exponential decay favoring recently-seen nodes",
|
||
"formula": "e^(-hours_since_seen / 12)",
|
||
"half_life_hours": 12,
|
||
"max_age_hours": 336,
|
||
"note": "Nodes not seen in 14 days are filtered out entirely"
|
||
}
|
||
},
|
||
"confidence_thresholds": {
|
||
"very_high": { "threshold": 0.9, "usage": "Single endpoint sufficient for edge certainty" },
|
||
"high": { "threshold": 0.6, "usage": "Both endpoints required for certain edge" },
|
||
"medium": { "threshold": 0.4, "usage": "Minimum for edge inclusion" },
|
||
"low": { "threshold": 0.0, "usage": "Excluded from topology" }
|
||
},
|
||
"output": "Map<prefix, { bestMatch: hash, confidence: number, candidates: array }>"
|
||
},
|
||
|
||
"phase_3_viterbi_path_decoding": {
|
||
"name": "HMM-Based Optimal Path Recovery",
|
||
"description": "Use Viterbi algorithm to find globally optimal node sequence given all constraints",
|
||
"algorithm": "Hidden Markov Model with custom emission and transition probabilities",
|
||
"mathematical_formulation": {
|
||
"states": "All candidate nodes for each position + optional ghost state",
|
||
"observations": "2-char prefixes at each path position",
|
||
"emission_probability": "P(prefix | node) = 1 if prefix matches, 0 otherwise",
|
||
"transition_probability": "P(node_i+1 | node_i) = physics-based link probability",
|
||
"objective": "argmax_sequence P(observations | sequence) × P(sequence)"
|
||
},
|
||
"state_prior_calculation": {
|
||
"formula": "prior_cost = -ln(recency_score) - confidence_bonus",
|
||
"recency_component": "-ln(max(recency_score, 0.01))",
|
||
"confidence_bonus": "2.0 if disambiguation_confidence >= 0.6"
|
||
},
|
||
"transition_cost_calculation": {
|
||
"physics_cost": "linkCost(lat1, lon1, lat2, lon2) based on distance + earth bulge",
|
||
"observation_override": "If both nodes have confidence >= 0.8, use observed cost (near-zero)",
|
||
"edge_history_bonus": "-0.5 × min(edge_observations / 10, 1.0)"
|
||
},
|
||
"ghost_node_handling": {
|
||
"description": "Virtual state representing unknown/undiscovered nodes",
|
||
"dynamic_cost": {
|
||
"no_candidates": 5.0,
|
||
"low_confidence_candidates": 12.0,
|
||
"medium_confidence_candidates": 15.0,
|
||
"high_confidence_candidates": 20.0
|
||
},
|
||
"suppression_rules": [
|
||
"Candidate has location AND is fresh (recency > 0.35) AND confidence > 0.3",
|
||
"Candidate observed communicating with adjacent nodes >= 5 times"
|
||
]
|
||
},
|
||
"output": "DecodedPath with node sequence, confidences, total cost, ghost flags"
|
||
},
|
||
|
||
"phase_4_edge_construction": {
|
||
"name": "Topology Edge Building",
|
||
"description": "Construct edges from decoded paths with confidence and directionality tracking",
|
||
"edge_attributes": [
|
||
"fromHash, toHash, key (sorted pair)",
|
||
"packetCount, certainCount",
|
||
"avgConfidence, strength, avgRecency",
|
||
"forwardCount, reverseCount, symmetryRatio, dominantDirection",
|
||
"floodCount, directCount, isDirectPathEdge",
|
||
"isZeroHop, avgRssi, avgSnr (for ground truth edges)",
|
||
"hopDistanceFromLocal, isHubConnection, isLoopEdge"
|
||
],
|
||
"certainty_rules": {
|
||
"both_endpoints_high": "confidence >= 0.6 for both → isCertain = true",
|
||
"destination_very_high": "destination confidence >= 0.9 → isCertain = true",
|
||
"destination_is_local": "last hop to local node → isCertain = true"
|
||
},
|
||
"minimum_validations": 5
|
||
},
|
||
|
||
"phase_5_graph_analysis": {
|
||
"name": "Graph-Theoretic Metrics",
|
||
"algorithms": {
|
||
"betweenness_centrality": {
|
||
"description": "Identify backbone edges by traffic flow importance",
|
||
"algorithm": "Brandes algorithm O(VE)",
|
||
"output": "Map<edgeKey, normalizedScore>",
|
||
"usage": "Backbone edge identification, hub detection"
|
||
},
|
||
"connected_components": {
|
||
"description": "Find disconnected subgraphs",
|
||
"algorithm": "Union-Find with path compression",
|
||
"output": "Array of node sets",
|
||
"usage": "Network fragmentation detection"
|
||
},
|
||
"shortest_paths": {
|
||
"description": "All-pairs shortest paths for latency estimation",
|
||
"algorithm": "Floyd-Warshall or Johnson's algorithm",
|
||
"output": "Distance matrix",
|
||
"usage": "Path health scoring, routing optimization"
|
||
}
|
||
}
|
||
},
|
||
|
||
"phase_6_loop_detection": {
|
||
"name": "H₁ Homology Loop Detection",
|
||
"description": "Find redundant paths (cycles) indicating mesh resilience",
|
||
"algorithm": "First Betti number computation via cycle basis",
|
||
"mathematical_basis": {
|
||
"betti_number": "β₁ = |E| - |V| + |connected_components|",
|
||
"interpretation": "Number of independent cycles in the graph",
|
||
"cycle_basis": "Minimal set of cycles that generate all cycles"
|
||
},
|
||
"output": {
|
||
"loops": "Array of NetworkLoop objects",
|
||
"attributes": ["edgeKeys", "nodes", "size", "avgCertainCount", "strength", "includesLocal"]
|
||
},
|
||
"significance": "Loops provide fault tolerance - if one link fails, traffic routes via alternate path"
|
||
},
|
||
|
||
"phase_7_hub_gateway_classification": {
|
||
"name": "Node Role Classification",
|
||
"description": "Classify nodes by traffic percentage (percentage-only, scales with volume)",
|
||
"classification_rules": {
|
||
"hub": {
|
||
"threshold": ">= 10% of last-hop traffic",
|
||
"characteristics": "True network hub, handles significant traffic",
|
||
"typical_count": "1-3 per mesh"
|
||
},
|
||
"gateway": {
|
||
"threshold": "7-10% of last-hop traffic",
|
||
"characteristics": "Significant forwarder, relays substantial traffic"
|
||
},
|
||
"standard": {
|
||
"threshold": "< 7% of last-hop traffic",
|
||
"characteristics": "Normal mesh participant"
|
||
}
|
||
},
|
||
"network_role_for_tx_delay": {
|
||
"backbone": "High centrality + >= 50% symmetric traffic",
|
||
"hub": "High centrality (bridge or gateway role)",
|
||
"relay": ">= 30% symmetric + moderate connectivity",
|
||
"edge": "Low connectivity or asymmetric traffic"
|
||
}
|
||
},
|
||
|
||
"phase_8_mobile_detection": {
|
||
"name": "Mobile/Volatile Node Identification",
|
||
"description": "Detect nodes with unstable network position",
|
||
"metrics": {
|
||
"path_volatility": {
|
||
"description": "How often node appears/disappears from paths",
|
||
"calculation": "stddev(presence_per_window) / mean(presence_per_window)",
|
||
"threshold": "> 0.3 indicates mobile"
|
||
},
|
||
"active_window_ratio": {
|
||
"description": "Fraction of time windows where node was active",
|
||
"calculation": "windows_with_activity / total_windows"
|
||
},
|
||
"neighbor_churn": {
|
||
"description": "Rate of neighbor set changes",
|
||
"calculation": "(neighbors_gained + neighbors_lost) / total_neighbors"
|
||
}
|
||
},
|
||
"output": "NodeMobility { pathVolatility, pathDiversity, isMobile, activeWindowRatio }"
|
||
},
|
||
|
||
"phase_9_ghost_cluster_analysis": {
|
||
"name": "Unknown Node Discovery",
|
||
"description": "Identify and characterize nodes not in neighbor table",
|
||
"detection_method": "Viterbi decoder assigns ghost state when no candidate fits physics",
|
||
"ghost_cluster_attributes": [
|
||
"prefix (2-char)",
|
||
"observation_count",
|
||
"first_seen, last_seen",
|
||
"is_likely_real (vs disambiguation artifact)",
|
||
"estimated_lat, estimated_lon (triangulated)",
|
||
"location_variance",
|
||
"anchor_nodes (adjacent known nodes used for triangulation)"
|
||
],
|
||
"triangulation_method": {
|
||
"description": "Estimate ghost location from known adjacent nodes",
|
||
"algorithm": "Weighted centroid of anchor nodes, weights = 1/link_cost",
|
||
"confidence": "Higher with more anchors and lower variance"
|
||
}
|
||
},
|
||
|
||
"phase_10_physics_model": {
|
||
"name": "RF Propagation Physics",
|
||
"description": "Physics-grounded link feasibility using propagation models",
|
||
"models": {
|
||
"free_space_path_loss": {
|
||
"formula": "FSPL(dB) = 20×log10(d) + 20×log10(f) - 147.55",
|
||
"usage": "Baseline link budget calculation"
|
||
},
|
||
"earth_bulge": {
|
||
"formula": "h = d² / (8 × R_earth)",
|
||
"description": "Height of Earth's curvature at midpoint",
|
||
"usage": "Line-of-sight feasibility for long links"
|
||
},
|
||
"sigmoid_distance_decay": {
|
||
"formula": "P(d) = 1 / (1 + e^(k × (d - d₀)))",
|
||
"parameters": {
|
||
"d0_km": 60,
|
||
"k": 0.15
|
||
},
|
||
"description": "Probability of successful link vs distance"
|
||
},
|
||
"terrain_awareness": {
|
||
"description": "Optional elevation-aware link feasibility",
|
||
"data_source": "SRTM/ASTER DEM tiles",
|
||
"fresnel_zone": "First Fresnel zone clearance check"
|
||
}
|
||
},
|
||
"link_cost_calculation": {
|
||
"formula": "cost = -ln(P_distance × P_bulge × P_terrain)",
|
||
"interpretation": "Negative log-probability; lower = more likely link",
|
||
"hard_cutoff_km": 150
|
||
}
|
||
},
|
||
|
||
"phase_11_temporal_evolution": {
|
||
"name": "Topology Change Tracking",
|
||
"description": "Track how topology evolves over time",
|
||
"metrics": {
|
||
"edge_stability": "How long edges persist without interruption",
|
||
"topology_entropy": "Shannon entropy of edge distribution",
|
||
"graph_edit_distance": "Minimum edits to transform topology T1 → T2"
|
||
},
|
||
"change_events": [
|
||
"edge_appeared (new link discovered)",
|
||
"edge_disappeared (link went dark)",
|
||
"edge_strengthened (more observations)",
|
||
"edge_weakened (fewer recent observations)",
|
||
"hub_promotion / hub_demotion",
|
||
"component_split / component_merge"
|
||
]
|
||
},
|
||
|
||
"phase_12_model_synthesis": {
|
||
"name": "Unified Topology Model",
|
||
"description": "Combine all phases into coherent network model",
|
||
"output_structure": {
|
||
"nodes": "Array of NodeModel with hash, location, role, mobility, confidence",
|
||
"edges": "Array of TopologyEdge with full metadata",
|
||
"loops": "Array of NetworkLoop",
|
||
"ghost_clusters": "Array of GhostCluster",
|
||
"metrics": {
|
||
"total_nodes": "integer",
|
||
"total_edges": "integer",
|
||
"graph_density": "edges / (nodes × (nodes-1) / 2)",
|
||
"avg_degree": "2 × edges / nodes",
|
||
"clustering_coefficient": "local clustering average",
|
||
"diameter": "longest shortest path",
|
||
"betti_1": "number of independent cycles"
|
||
},
|
||
"health": {
|
||
"connectivity_score": "0-1 based on component count and sizes",
|
||
"redundancy_score": "0-1 based on loop coverage",
|
||
"confidence_score": "0-1 based on edge certainty distribution"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"advanced_algorithms": {
|
||
"description": "Cutting-edge mathematical methods for topology inference",
|
||
|
||
"belief_propagation": {
|
||
"name": "Loopy Belief Propagation for Joint Disambiguation",
|
||
"description": "Message-passing algorithm for simultaneous disambiguation of all path positions",
|
||
"rationale": "Standard disambiguation is greedy (position-by-position). BP considers global consistency.",
|
||
"algorithm": {
|
||
"factor_graph": "Nodes = path positions; Factors = pairwise link feasibility + unary priors",
|
||
"messages": "μ_{i→j}(x_j) = Σ_{x_i} ψ(x_i, x_j) × φ(x_i) × Π_{k≠j} μ_{k→i}(x_i)",
|
||
"convergence": "Iterate until message change < ε or max iterations",
|
||
"marginals": "P(x_i) ∝ φ(x_i) × Π_j μ_{j→i}(x_i)"
|
||
},
|
||
"advantages": [
|
||
"Handles multi-hop consistency constraints",
|
||
"Naturally incorporates uncertainty",
|
||
"Can detect inconsistent observations"
|
||
],
|
||
"complexity": "O(n × k² × iterations) where k = avg candidates per position"
|
||
},
|
||
|
||
"graph_neural_network": {
|
||
"name": "GNN-Based Topology Completion",
|
||
"description": "Learn edge prediction function from observed topology",
|
||
"architecture": {
|
||
"encoder": "GraphSAGE or GAT for node embeddings",
|
||
"decoder": "Inner product or MLP for edge probability",
|
||
"loss": "Binary cross-entropy on held-out edges"
|
||
},
|
||
"features": {
|
||
"node_features": ["location", "traffic_volume", "avg_rssi", "contact_type", "recency"],
|
||
"edge_features": ["distance", "observation_count", "symmetry_ratio", "route_type_distribution"]
|
||
},
|
||
"training": {
|
||
"positive_samples": "Observed certain edges",
|
||
"negative_samples": "Random node pairs with no observations",
|
||
"augmentation": "Edge dropout, feature noise"
|
||
},
|
||
"inference": "Predict probability for all unobserved node pairs; threshold for edge creation"
|
||
},
|
||
|
||
"spectral_clustering": {
|
||
"name": "Spectral Analysis for Community Detection",
|
||
"description": "Identify densely connected subgroups using graph Laplacian",
|
||
"algorithm": {
|
||
"laplacian": "L = D - A (unnormalized) or L_sym = I - D^{-1/2} A D^{-1/2}",
|
||
"eigendecomposition": "Compute k smallest eigenvectors of L",
|
||
"embedding": "Each node → k-dimensional vector from eigenvectors",
|
||
"clustering": "K-means on embedded vectors"
|
||
},
|
||
"applications": [
|
||
"Geographic cluster identification",
|
||
"Network partitioning for load analysis",
|
||
"Anomalous subgraph detection"
|
||
]
|
||
},
|
||
|
||
"matrix_factorization": {
|
||
"name": "Low-Rank Matrix Completion",
|
||
"description": "Infer missing edges using matrix factorization",
|
||
"formulation": {
|
||
"adjacency_matrix": "A ∈ R^{n×n}, partially observed",
|
||
"factorization": "A ≈ U × V^T where U, V ∈ R^{n×r}, r << n",
|
||
"objective": "min ||P_Ω(A - UV^T)||² + λ(||U||² + ||V||²)",
|
||
"interpretation": "Each node has r-dimensional latent embedding; edge strength = dot product"
|
||
},
|
||
"advantages": [
|
||
"Naturally handles sparse observations",
|
||
"Provides confidence via reconstruction error",
|
||
"Computationally efficient for large graphs"
|
||
]
|
||
},
|
||
|
||
"kalman_filter_tracking": {
|
||
"name": "Kalman Filter for Mobile Node Tracking",
|
||
"description": "Optimal state estimation for moving nodes",
|
||
"state_vector": "[x, y, vx, vy] (position and velocity)",
|
||
"dynamics_model": "Constant velocity with process noise",
|
||
"observation_model": "Position estimates from RF triangulation",
|
||
"equations": {
|
||
"predict": "x̂_{k|k-1} = F × x̂_{k-1|k-1}",
|
||
"update": "x̂_{k|k} = x̂_{k|k-1} + K × (z_k - H × x̂_{k|k-1})",
|
||
"kalman_gain": "K = P_{k|k-1} × H^T × (H × P_{k|k-1} × H^T + R)^{-1}"
|
||
},
|
||
"output": "Smoothed position trajectory with uncertainty bounds"
|
||
},
|
||
|
||
"gaussian_process_regression": {
|
||
"name": "GP for Signal Propagation Modeling",
|
||
"description": "Non-parametric regression for RSSI prediction",
|
||
"kernel": "RBF + periodic (for daily patterns) + noise",
|
||
"training_data": "(location, time, observed_rssi) tuples",
|
||
"prediction": "RSSI distribution at any location/time with uncertainty",
|
||
"applications": [
|
||
"Coverage map generation",
|
||
"Link quality prediction for unseen pairs",
|
||
"Anomaly detection (observed vs predicted)"
|
||
]
|
||
},
|
||
|
||
"persistent_homology": {
|
||
"name": "Topological Data Analysis",
|
||
"description": "Study topology at multiple scales using persistent homology",
|
||
"algorithm": {
|
||
"filtration": "Build sequence of simplicial complexes by increasing edge threshold",
|
||
"persistence": "Track when topological features (components, loops) appear/disappear",
|
||
"barcode": "Visual representation of feature lifetimes"
|
||
},
|
||
"features": {
|
||
"H0": "Connected components (network fragmentation)",
|
||
"H1": "Loops/cycles (redundant paths)",
|
||
"H2": "Voids (rare in 2D network embeddings)"
|
||
},
|
||
"applications": [
|
||
"Robust topology comparison across time",
|
||
"Identify stable vs transient network features",
|
||
"Network resilience quantification"
|
||
]
|
||
},
|
||
|
||
"monte_carlo_tree_search": {
|
||
"name": "MCTS for Path Exploration",
|
||
"description": "Explore possible path interpretations using MCTS",
|
||
"algorithm": {
|
||
"selection": "UCB1 to balance exploration/exploitation",
|
||
"expansion": "Add child nodes for unexplored candidates",
|
||
"simulation": "Random playout with physics-weighted sampling",
|
||
"backpropagation": "Update ancestor statistics with simulation result"
|
||
},
|
||
"application": "When Viterbi trellis is too large, use MCTS for approximate solution"
|
||
}
|
||
},
|
||
|
||
"incremental_updates": {
|
||
"description": "Efficient algorithms for real-time topology updates",
|
||
|
||
"online_learning": {
|
||
"edge_observation": {
|
||
"algorithm": "Exponential moving average for edge strength",
|
||
"formula": "strength_new = α × observation + (1-α) × strength_old",
|
||
"alpha": 0.1
|
||
},
|
||
"disambiguation_update": {
|
||
"algorithm": "Incremental Bayesian update",
|
||
"formula": "P(node|prefix) ∝ P(observation|node) × P_prior(node)",
|
||
"trigger": "New packet with ambiguous prefix"
|
||
}
|
||
},
|
||
|
||
"lazy_recomputation": {
|
||
"description": "Defer expensive computations until necessary",
|
||
"strategies": [
|
||
"Centrality: Recompute only when edge count changes by >10%",
|
||
"Loops: Recompute only when new edge could create cycle",
|
||
"Ghost clusters: Recompute only when confidence distribution shifts"
|
||
]
|
||
},
|
||
|
||
"change_propagation": {
|
||
"description": "Propagate local changes through dependent computations",
|
||
"dependency_graph": {
|
||
"packet": ["disambiguation", "edge_counts"],
|
||
"disambiguation": ["edge_confidence", "ghost_clusters"],
|
||
"edge_counts": ["centrality", "hub_classification", "loops"],
|
||
"edge_confidence": ["topology_snapshot"],
|
||
"centrality": ["hub_classification", "tx_delay_recommendations"]
|
||
}
|
||
}
|
||
},
|
||
|
||
"api_endpoints": {
|
||
"request_actions": {
|
||
"infer_topology": {
|
||
"description": "Trigger full topology inference pipeline",
|
||
"params": {
|
||
"packet_hours": "integer (default 168 = 1 week)",
|
||
"algorithm": "string (viterbi|belief_propagation|hybrid)",
|
||
"confidence_threshold": "number (default 0.4)",
|
||
"enable_ghost_detection": "boolean (default true)",
|
||
"enable_physics_model": "boolean (default true)"
|
||
},
|
||
"response": {
|
||
"job_id": "string",
|
||
"estimated_duration_ms": "integer"
|
||
}
|
||
},
|
||
|
||
"get_inference_result": {
|
||
"description": "Retrieve completed inference result",
|
||
"params": { "job_id": "string" },
|
||
"response": {
|
||
"status": "pending|computing|complete|failed",
|
||
"progress_percent": "number",
|
||
"topology": "TopologyModel (if complete)",
|
||
"metrics": "InferenceMetrics",
|
||
"warnings": "array of string"
|
||
}
|
||
},
|
||
|
||
"decode_path": {
|
||
"description": "Decode single path using Viterbi",
|
||
"params": {
|
||
"path": "array of 2-char prefixes",
|
||
"src_hash": "string (optional, for source-geo correlation)",
|
||
"enable_ghost": "boolean"
|
||
},
|
||
"response": {
|
||
"nodes": "array of { prefix, hash, confidence, is_ghost, location }",
|
||
"total_cost": "number",
|
||
"path_confidence": "number"
|
||
}
|
||
},
|
||
|
||
"estimate_link_feasibility": {
|
||
"description": "Physics-based link feasibility between two points",
|
||
"params": {
|
||
"lat1": "number",
|
||
"lon1": "number",
|
||
"lat2": "number",
|
||
"lon2": "number",
|
||
"include_terrain": "boolean"
|
||
},
|
||
"response": {
|
||
"distance_km": "number",
|
||
"earth_bulge_m": "number",
|
||
"probability": "number (0-1)",
|
||
"cost": "number (negative log probability)",
|
||
"terrain_clearance_m": "number (if terrain enabled)",
|
||
"feasibility": "certain|likely|possible|unlikely|impossible"
|
||
}
|
||
},
|
||
|
||
"get_graph_metrics": {
|
||
"description": "Compute graph-theoretic metrics on current topology",
|
||
"params": {
|
||
"metrics": "array of string (centrality|clustering|paths|components|loops)"
|
||
},
|
||
"response": {
|
||
"betweenness_centrality": "map<hash, score>",
|
||
"clustering_coefficients": "map<hash, score>",
|
||
"shortest_paths": "matrix or sparse map",
|
||
"connected_components": "array of node arrays",
|
||
"loops": "array of NetworkLoop",
|
||
"summary": {
|
||
"diameter": "integer",
|
||
"avg_path_length": "number",
|
||
"density": "number",
|
||
"betti_1": "integer"
|
||
}
|
||
}
|
||
},
|
||
|
||
"triangulate_ghost": {
|
||
"description": "Estimate location of ghost node from anchor observations",
|
||
"params": {
|
||
"prefix": "string",
|
||
"anchor_hashes": "array of string (optional, auto-detect if omitted)"
|
||
},
|
||
"response": {
|
||
"estimated_lat": "number",
|
||
"estimated_lon": "number",
|
||
"uncertainty_m": "number (radius of 95% confidence)",
|
||
"anchor_count": "integer",
|
||
"method": "centroid|trilateration|least_squares"
|
||
}
|
||
}
|
||
},
|
||
|
||
"websocket_events": {
|
||
"topology_model_updated": {
|
||
"description": "Full topology model recomputed",
|
||
"payload": {
|
||
"nodes_added": "array of hash",
|
||
"nodes_removed": "array of hash",
|
||
"edges_added": "array of edge_key",
|
||
"edges_removed": "array of edge_key",
|
||
"metrics_changed": "object with changed metrics"
|
||
}
|
||
},
|
||
"inference_progress": {
|
||
"description": "Progress update for long-running inference",
|
||
"payload": {
|
||
"job_id": "string",
|
||
"phase": "string (current phase name)",
|
||
"progress_percent": "number",
|
||
"packets_processed": "integer",
|
||
"edges_discovered": "integer"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
"database_schema": {
|
||
"topology_inference_jobs": {
|
||
"columns": {
|
||
"job_id": "TEXT PRIMARY KEY",
|
||
"status": "TEXT (pending|computing|complete|failed)",
|
||
"algorithm": "TEXT",
|
||
"params_json": "TEXT",
|
||
"started_at": "INTEGER",
|
||
"completed_at": "INTEGER",
|
||
"packets_analyzed": "INTEGER",
|
||
"result_json": "TEXT (compressed)",
|
||
"error_message": "TEXT"
|
||
}
|
||
},
|
||
"disambiguation_evidence": {
|
||
"description": "Accumulated evidence for prefix disambiguation",
|
||
"columns": {
|
||
"prefix": "TEXT NOT NULL",
|
||
"candidate_hash": "TEXT NOT NULL",
|
||
"position_counts_json": "TEXT",
|
||
"adjacent_prefixes_json": "TEXT",
|
||
"geo_evidence_score": "REAL",
|
||
"last_seen": "INTEGER",
|
||
"total_observations": "INTEGER"
|
||
},
|
||
"primary_key": "(prefix, candidate_hash)"
|
||
},
|
||
"viterbi_cache": {
|
||
"description": "Cache decoded paths to avoid recomputation",
|
||
"columns": {
|
||
"path_signature": "TEXT PRIMARY KEY (hash of input path)",
|
||
"decoded_json": "TEXT",
|
||
"total_cost": "REAL",
|
||
"computed_at": "INTEGER",
|
||
"hit_count": "INTEGER"
|
||
},
|
||
"eviction": "LRU with 10K entry limit"
|
||
}
|
||
},
|
||
|
||
"performance_characteristics": {
|
||
"viterbi_decoding": {
|
||
"complexity": "O(T × S²) where T = path length, S = avg candidates per position",
|
||
"typical_time": "1-5ms per path",
|
||
"parallelization": "Paths are independent; parallelize across packets"
|
||
},
|
||
"full_topology_inference": {
|
||
"complexity": "O(P × T × S²) + O(E × V) for centrality",
|
||
"typical_time": "5-30 seconds for 50K packets",
|
||
"memory": "~100MB for topology state"
|
||
},
|
||
"incremental_update": {
|
||
"complexity": "O(T × S²) for single packet",
|
||
"typical_time": "< 10ms",
|
||
"trigger": "Every packet or batched every 100ms"
|
||
}
|
||
}
|
||
},
|
||
|
||
"future_proofing": {
|
||
"description": "Design decisions for future extensibility",
|
||
|
||
"versioning": {
|
||
"strategy": "URL path versioning for REST, subprotocol versioning for WebSocket",
|
||
"rest_example": "/api/v2/packets",
|
||
"ws_example": "Sec-WebSocket-Protocol: pymc-console-v2",
|
||
"deprecation_policy": "Support N-1 version for 6 months after new version release"
|
||
},
|
||
|
||
"extensible_events": {
|
||
"description": "All events include a 'version' field for schema evolution",
|
||
"example": {
|
||
"type": "event",
|
||
"channel": "packets",
|
||
"event_type": "packet_received",
|
||
"version": 1,
|
||
"payload": "..."
|
||
},
|
||
"migration": "Clients should ignore unknown fields; server can add fields without breaking clients"
|
||
},
|
||
|
||
"plugin_channels": {
|
||
"description": "Reserved namespace for future plugin/extension channels",
|
||
"reserved_prefixes": ["plugin.", "ext.", "custom."],
|
||
"example": "plugin.wardriving",
|
||
"note": "Plugins can register custom channels without modifying core protocol"
|
||
},
|
||
|
||
"batch_operations": {
|
||
"description": "Future support for bulk operations",
|
||
"planned_actions": [
|
||
"batch_hide_neighbors - Hide multiple neighbors at once",
|
||
"batch_export - Export multiple data types in one request",
|
||
"batch_ping - Ping multiple neighbors in parallel"
|
||
]
|
||
},
|
||
|
||
"streaming_export": {
|
||
"description": "Future support for large data exports without timeout",
|
||
"approach": "Server-Sent Events (SSE) or chunked transfer for exports > 10MB",
|
||
"planned_endpoint": "GET /api/export/stream?format=csv&start_time=X&end_time=Y"
|
||
},
|
||
|
||
"federation_hooks": {
|
||
"description": "Hooks for future multi-node federation",
|
||
"planned_features": [
|
||
"Subscribe to remote node's WebSocket via proxy",
|
||
"Aggregate stats from multiple repeaters",
|
||
"Cross-node topology view"
|
||
],
|
||
"note": "Not implemented in v2.0, but API designed to not preclude it"
|
||
},
|
||
|
||
"machine_learning_hooks": {
|
||
"description": "Data formats compatible with ML pipeline ingestion",
|
||
"features": [
|
||
"Time series export in ML-friendly formats (Parquet, Arrow)",
|
||
"Feature vectors for anomaly detection training",
|
||
"Labeled data export for supervised learning"
|
||
]
|
||
},
|
||
|
||
"reserved_channels": [
|
||
"alerts - Future alerting system",
|
||
"automation - Future automation triggers",
|
||
"federation - Multi-node federation",
|
||
"ml - Machine learning insights"
|
||
]
|
||
},
|
||
|
||
"implementation_strategy": {
|
||
"description": "Phased implementation approach for WebSocket architecture",
|
||
|
||
"phases": [
|
||
{
|
||
"phase": 1,
|
||
"name": "Core WebSocket Infrastructure",
|
||
"description": "Implement WebSocket server, message routing, subscription management, and heartbeat",
|
||
"deliverables": [
|
||
"WebSocket endpoint at /ws",
|
||
"Message envelope parsing/serialization",
|
||
"Subscription state management",
|
||
"Ping/pong heartbeat",
|
||
"Connection lifecycle handling"
|
||
],
|
||
"risk": "Medium",
|
||
"effort": "High"
|
||
},
|
||
{
|
||
"phase": 2,
|
||
"name": "Packets Channel",
|
||
"description": "Real-time packet streaming with filtering",
|
||
"deliverables": [
|
||
"Packet event emission on RX/TX",
|
||
"Filter matching for subscriptions",
|
||
"Initial state hydration",
|
||
"Duplicate detection events"
|
||
],
|
||
"risk": "Low",
|
||
"effort": "Medium"
|
||
},
|
||
{
|
||
"phase": 3,
|
||
"name": "Stats Channel",
|
||
"description": "Real-time statistics updates",
|
||
"deliverables": [
|
||
"Count/rate updates",
|
||
"Bucket aggregation",
|
||
"Noise floor and duty cycle events",
|
||
"Hourly pre-aggregation"
|
||
],
|
||
"risk": "Low",
|
||
"effort": "Medium"
|
||
},
|
||
{
|
||
"phase": 4,
|
||
"name": "Topology Channel",
|
||
"description": "Server-side topology computation and streaming",
|
||
"deliverables": [
|
||
"Incremental edge tracking",
|
||
"Hub/gateway detection",
|
||
"Mobile node detection",
|
||
"Ghost cluster tracking",
|
||
"Prefix disambiguation"
|
||
],
|
||
"risk": "High",
|
||
"effort": "High"
|
||
},
|
||
{
|
||
"phase": 5,
|
||
"name": "Neighbors & LBT Channels",
|
||
"description": "Contact management and LBT statistics",
|
||
"deliverables": [
|
||
"Zero-hop neighbor detection",
|
||
"Neighbor status tracking",
|
||
"LBT metric aggregation",
|
||
"Collision risk calculation"
|
||
],
|
||
"risk": "Low",
|
||
"effort": "Medium"
|
||
},
|
||
{
|
||
"phase": 6,
|
||
"name": "Session Management & Replay",
|
||
"description": "Reconnection handling and message replay",
|
||
"deliverables": [
|
||
"Session persistence",
|
||
"Event log storage",
|
||
"Replay on reconnect",
|
||
"Sequence gap detection"
|
||
],
|
||
"risk": "Medium",
|
||
"effort": "Medium"
|
||
}
|
||
]
|
||
},
|
||
|
||
"performance_targets": {
|
||
"description": "Performance targets for WebSocket implementation",
|
||
|
||
"latency": {
|
||
"packet_event_latency": "< 50ms from radio RX to client WebSocket",
|
||
"stats_update_latency": "< 100ms from packet to stats event",
|
||
"topology_update_latency": "< 200ms from packet to topology event"
|
||
},
|
||
|
||
"throughput": {
|
||
"max_packets_per_second": "100 packets/sec sustained",
|
||
"max_concurrent_connections": "10 WebSocket clients",
|
||
"max_subscriptions_per_client": "All channels simultaneously"
|
||
},
|
||
|
||
"memory": {
|
||
"event_log_size": "< 50MB (10K events × 5KB avg)",
|
||
"topology_cache": "< 10MB",
|
||
"subscription_state": "< 1MB per client"
|
||
},
|
||
|
||
"reconnection": {
|
||
"session_timeout": "5 minutes (session valid for replay)",
|
||
"max_replay_events": "1000 events per channel on reconnect"
|
||
}
|
||
},
|
||
|
||
"frontend_integration": {
|
||
"description": "Frontend changes required to consume WebSocket API",
|
||
|
||
"new_services": [
|
||
"WebSocketService - Connection management, reconnection, message routing",
|
||
"SubscriptionManager - Channel subscription state",
|
||
"EventBuffer - Buffer events during brief disconnects"
|
||
],
|
||
|
||
"zustand_store_changes": [
|
||
"Replace polling intervals with WebSocket subscriptions",
|
||
"Add connection state (connected/disconnected/reconnecting)",
|
||
"Process delta updates instead of full refreshes",
|
||
"Handle sequence gaps and replay"
|
||
],
|
||
|
||
"removed_dependencies": [
|
||
"Polling intervals in useStore.ts (3-second stats/packets polling)",
|
||
"topology.worker.ts - Server computes topology",
|
||
"sparkline.worker.ts - Server computes sparklines",
|
||
"LBTDataContext client-side computation",
|
||
"packetCache client-side memory management"
|
||
],
|
||
|
||
"backward_compatibility": {
|
||
"description": "Frontend should detect WebSocket availability and fall back to REST polling if unavailable",
|
||
"detection": "Attempt WebSocket connection; if fails or server returns 404, fall back to REST",
|
||
"fallback_behavior": "Use existing polling architecture with REST endpoints"
|
||
}
|
||
},
|
||
|
||
"error_handling": {
|
||
"error_codes": {
|
||
"1000": "Normal closure",
|
||
"1001": "Going away (server shutdown)",
|
||
"1002": "Protocol error",
|
||
"1003": "Unsupported data",
|
||
"4000": "Invalid message format",
|
||
"4001": "Unknown channel",
|
||
"4002": "Invalid subscription options",
|
||
"4003": "Rate limited",
|
||
"4004": "Authentication required",
|
||
"4005": "Session expired",
|
||
"4006": "Action failed"
|
||
},
|
||
|
||
"reconnection_strategy": {
|
||
"initial_delay_ms": 1000,
|
||
"max_delay_ms": 30000,
|
||
"backoff_multiplier": 2,
|
||
"max_attempts": 10,
|
||
"jitter": true
|
||
}
|
||
}
|
||
}
|