Files
pymc_console-dist/docs/api-design.json
T
GitHub Actions Bot b55320b83d Sync build v0.9.136
Automated sync from private repository.
Commit: 183deb7a619e896fa8d09edc117486562118dfcd
2026-01-16 00:20:07 +00:00

2724 lines
99 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"$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 ×× 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
}
}
}