{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "pyMC Console API Specification", "version": "3.0.0", "description": "Simplified API specification for pyMC_Repeater backend. WebSocket for radio events, REST for everything else.", "meta": { "author": "pyMC Console Team", "created": "2026-01-16", "target_backend": "pyMC_Repeater (CherryPy + SQLite)", "philosophy": "KISS - Keep It Simple Stupid" }, "architecture": { "overview": "Two communication channels with clear separation of concerns.", "websocket": { "purpose": "Real-time radio hardware events only", "endpoint": "ws://host:8000/radio", "when_to_use": "Streaming RX packets, TX confirmations, noise floor samples, radio errors" }, "rest": { "purpose": "Everything else - queries, configuration, historical data, actions", "base_url": "http://host:8000/api", "when_to_use": "Initial data load, pagination, config changes, time series queries" }, "mental_model": "WebSocket = live radio wire. REST = database/config access." }, "sx1262_capabilities": { "description": "What the SX1262 radio hardware can actually report. These are the source data for WebSocket events.", "on_packet_receive": { "rssi_dbm": "Received signal strength (register / -2)", "snr_db": "Signal-to-noise ratio", "signal_rssi_dbm": "Alternative RSSI measurement", "payload": "Raw packet bytes", "crc_valid": "Boolean - CRC check passed", "header_valid": "Boolean - header decoded successfully" }, "on_transmit_complete": { "airtime_ms": "Actual transmission duration in milliseconds", "lbt_attempts": "Number of CAD (Listen Before Talk) retries", "lbt_backoff_delays_ms": "Array of backoff durations if channel was busy", "channel_busy": "Boolean - whether LBT detected activity" }, "continuous_sampling": { "noise_floor_dbm": "Instantaneous RSSI when radio is idle", "sampling_interval": "Background task samples every ~10ms during quiet periods", "averaging": "20 samples averaged, reported every 5 seconds" }, "radio_state": { "frequency_hz": "Current operating frequency", "tx_power_dbm": "Current TX power", "spreading_factor": "Current SF (5-12)", "bandwidth_hz": "Current bandwidth", "coding_rate": "Current coding rate (5-8)", "initialized": "Boolean - radio ready" }, "irq_events": { "description": "Hardware interrupts that can be monitored", "IRQ_TX_DONE": "Transmission complete", "IRQ_RX_DONE": "Packet received", "IRQ_CRC_ERR": "CRC error on receive", "IRQ_TIMEOUT": "RX or TX timeout", "IRQ_PREAMBLE_DETECTED": "Preamble detected (packet incoming)", "IRQ_HEADER_VALID": "Header successfully decoded", "IRQ_HEADER_ERR": "Header decode error", "IRQ_CAD_DONE": "Channel activity detection complete", "IRQ_CAD_DETECTED": "Activity detected during CAD" } }, "websocket": { "endpoint": "/radio", "description": "Single WebSocket endpoint for all radio events. No subscriptions, no channels - just connect and receive.", "message_format": { "type": "string - event type identifier", "ts": "integer - Unix timestamp in milliseconds", "data": "object - event-specific payload" }, "server_events": { "rx": { "description": "Packet received from radio", "trigger": "IRQ_RX_DONE with valid CRC", "data": { "packet_hash": "string - unique packet identifier", "rssi": "integer - RSSI in dBm", "snr": "number - SNR in dB", "signal_rssi": "integer - signal RSSI in dBm", "payload_len": "integer - payload size in bytes", "crc_ok": "boolean", "type": "integer - MeshCore payload type (0-15)", "route": "integer - MeshCore route type (0-3)", "src_hash": "string - source node hash", "path": "array of strings - 2-char hop prefixes" }, "example": { "type": "rx", "ts": 1736934849000, "data": { "packet_hash": "a1b2c3d4", "rssi": -95, "snr": 7.5, "signal_rssi": -97, "payload_len": 48, "crc_ok": true, "type": 4, "route": 1, "src_hash": "0x42AB", "path": ["FA", "79", "24"] } } }, "tx": { "description": "Packet transmitted by this node", "trigger": "IRQ_TX_DONE after send_packet()", "data": { "packet_hash": "string - unique packet identifier", "airtime_ms": "number - actual TX duration", "lbt_attempts": "integer - CAD retry count (0 = clear on first try)", "lbt_backoff_ms": "array of numbers - backoff delays if retried", "channel_busy": "boolean - true if any CAD detected activity" }, "example": { "type": "tx", "ts": 1736934849000, "data": { "packet_hash": "d4c3b2a1", "airtime_ms": 145.2, "lbt_attempts": 2, "lbt_backoff_ms": [85, 170], "channel_busy": true } } }, "noise": { "description": "Periodic noise floor measurement", "trigger": "Background sampling during idle periods", "frequency": "Every 5 seconds when radio is idle", "data": { "noise_floor_dbm": "number - averaged noise floor reading" }, "example": { "type": "noise", "ts": 1736934849000, "data": { "noise_floor_dbm": -107.5 } } }, "cad": { "description": "Channel activity detection result", "trigger": "CAD operation completed (during LBT or explicit check)", "data": { "detected": "boolean - activity detected", "det_peak": "integer - detection peak threshold used", "det_min": "integer - detection minimum threshold used" }, "example": { "type": "cad", "ts": 1736934849000, "data": { "detected": true, "det_peak": 24, "det_min": 12 } } }, "error": { "description": "Radio error event", "trigger": "CRC error, header error, or timeout", "data": { "code": "string - error type: CRC_ERR, HEADER_ERR, TIMEOUT", "rssi": "integer - RSSI if available (may help diagnose)", "snr": "number - SNR if available" }, "example": { "type": "error", "ts": 1736934849000, "data": { "code": "CRC_ERR", "rssi": -98, "snr": -2.5 } } }, "hardware": { "description": "Periodic system hardware metrics", "trigger": "Background sampling every 10 seconds", "frequency": "Every 10 seconds", "data": { "cpu_percent": "number - CPU usage percentage", "memory_percent": "number - RAM usage percentage", "disk_percent": "number - Disk usage percentage", "temperature_c": "number or null - CPU temperature in Celsius (null if unavailable)" }, "example": { "type": "hardware", "ts": 1736934849000, "data": { "cpu_percent": 23.5, "memory_percent": 45.2, "disk_percent": 67.8, "temperature_c": 52.3 } } } }, "client_commands": { "description": "Commands client can send to control radio. Keep these minimal.", "send_advert": { "description": "Trigger ADVERT broadcast", "request": {"cmd": "send_advert"}, "response": {"ok": true} }, "set_tx_power": { "description": "Change TX power (live, no restart needed)", "request": {"cmd": "set_tx_power", "dbm": 22}, "response": {"ok": true, "applied": 22}, "constraints": "-17 to 22 dBm for SX1262" } }, "connection_lifecycle": { "1_connect": "Client opens WebSocket to ws://host:8000/radio", "2_receive": "Server immediately starts pushing events", "3_command": "Client sends commands as needed (optional)", "4_disconnect": "On disconnect, client simply reconnects", "notes": [ "No authentication on WebSocket (same as REST API currently)", "No session state - each connection is independent", "No replay of missed events - use REST for historical data", "No heartbeat needed - WebSocket protocol handles keepalive" ] }, "implementation_notes": { "library": "Use ws4py or websockets with CherryPy", "broadcast": "Maintain list of connected clients, broadcast to all", "threading": "Events come from async radio callbacks - use queue or call_soon_threadsafe", "estimated_code": "~100 lines for WebSocket handler" } }, "rest_api": { "description": "RESTful endpoints organized by domain. Build in phases.", "phase_1_core": { "description": "Foundation endpoints - these exist or need minor updates", "GET /api/stats": { "description": "Current system state snapshot", "response": { "node": { "name": "string", "hash": "string - e.g. 0x42AB1234", "uptime_seconds": "integer" }, "radio": { "frequency_hz": "integer", "tx_power_dbm": "integer", "spreading_factor": "integer", "bandwidth_hz": "integer", "coding_rate": "integer", "noise_floor_dbm": "number" }, "counts": { "rx_total": "integer", "tx_total": "integer", "forwarded_total": "integer", "dropped_total": "integer" }, "mode": "string - forward|monitor", "duty_cycle_enabled": "boolean" }, "status": "EXISTS - may need field additions" }, "GET /api/config": { "description": "Full configuration dump", "status": "EXISTS" }, "POST /api/config": { "description": "Update configuration (persists to disk)", "status": "EXISTS as update_radio_config - consider consolidating" }, "GET /api/logs": { "description": "Recent system log entries", "params": { "limit": {"type": "integer", "default": 100, "max": 1000}, "level": {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERROR"], "description": "Filter by minimum log level"} }, "response": { "logs": [{ "timestamp": "integer - Unix timestamp", "level": "string - DEBUG|INFO|WARNING|ERROR", "module": "string - source module name", "message": "string - log message content" }] }, "status": "EXISTS" } }, "phase_2_packets": { "description": "Packet storage and retrieval with proper pagination", "GET /api/packets": { "description": "Paginated packet list with filtering", "params": { "limit": {"type": "integer", "default": 100, "max": 1000}, "offset": {"type": "integer", "default": 0, "description": "Skip N packets for pagination"}, "before": {"type": "integer", "description": "Unix timestamp - get packets older than this"}, "after": {"type": "integer", "description": "Unix timestamp - get packets newer than this"}, "type": {"type": "integer", "description": "Filter by payload type (0-15)"}, "route": {"type": "integer", "description": "Filter by route type (0-3)"}, "transmitted": {"type": "boolean", "description": "Filter to TX only"}, "received": {"type": "boolean", "description": "Filter to RX only"}, "src_hash": {"type": "string", "description": "Filter by source node hash"}, "dst_hash": {"type": "string", "description": "Filter by destination node hash"}, "path_contains": {"type": "string", "description": "Filter packets where path contains this 2-char prefix"} }, "response": { "packets": "array of Packet objects", "pagination": { "has_more": "boolean", "next_cursor": "string - timestamp for next page", "count": "integer - packets in this response" } }, "status": "EXISTS as recent_packets - needs pagination upgrade" }, "GET /api/packets/{hash}": { "description": "Single packet by hash", "response": "Packet object or 404", "status": "EXISTS as packet_by_hash" } }, "phase_3_neighbors": { "description": "Contact/neighbor management with zero-hop detection", "GET /api/neighbors": { "description": "List all known neighbors with optional filtering", "params": { "type": {"type": "string", "enum": ["repeater", "companion", "room_server", "client"], "description": "Filter by contact type"}, "status": {"type": "string", "enum": ["active", "stale", "expired"]}, "zero_hop": {"type": "boolean", "description": "Only direct RF contacts"} }, "response": { "neighbors": [{ "hash": "string", "name": "string", "contact_type": "string", "last_seen": "integer - Unix timestamp", "advert_count": "integer", "is_zero_hop": "boolean", "avg_rssi": "integer or null", "avg_snr": "number or null", "location": {"lat": "number", "lon": "number"} }], "counts": { "total": "integer", "repeaters": "integer", "companions": "integer", "room_servers": "integer", "zero_hop": "integer" } }, "status": "PARTIAL - need to add zero_hop tracking", "zero_hop_logic": "A neighbor is zero-hop if we received ADVERT packets from them with path_len=0 (DIRECT route) or path_len<=1 (FLOOD route)" }, "DELETE /api/neighbors/{hash}": { "description": "Hide neighbor from display", "status": "EXISTS as DELETE /api/advert/:id" }, "POST /api/neighbors/{hash}/ping": { "description": "Ping specific neighbor", "response": { "success": "boolean", "rtt_ms": "number", "rssi": "integer", "snr": "number" }, "status": "EXISTS as ping_neighbor" } }, "phase_4_timeseries": { "description": "Historical data for charts - single flexible endpoint", "GET /api/timeseries": { "description": "Time-bucketed metrics for charting", "params": { "metric": { "type": "string", "required": true, "enum": ["rx", "tx", "forwarded", "dropped", "noise_floor"], "description": "Which metric to retrieve" }, "start": {"type": "integer", "required": true, "description": "Unix timestamp"}, "end": {"type": "integer", "required": true, "description": "Unix timestamp"}, "resolution": { "type": "string", "default": "auto", "enum": ["auto", "1m", "5m", "1h", "1d"], "description": "Bucket size" } }, "response": { "metric": "string", "resolution": "string - actual resolution used", "buckets": [{"ts": "integer", "value": "number"}], "summary": { "total": "number", "avg": "number", "min": "number", "max": "number" } }, "auto_resolution_logic": { "under_1_hour": "1m buckets", "under_24_hours": "5m buckets", "under_7_days": "1h buckets", "over_7_days": "1d buckets" }, "status": "NEW - replaces multiple chart endpoints" }, "GET /api/stats/bucketed": { "description": "Pre-bucketed packet statistics for dashboard charts", "params": { "hours": {"type": "integer", "default": 24, "max": 168, "description": "Hours of history to return"}, "bucket_minutes": {"type": "integer", "default": 60, "enum": [5, 15, 60], "description": "Bucket size in minutes"} }, "response": { "buckets": [{ "timestamp": "integer - bucket start Unix timestamp", "rx_count": "integer", "tx_count": "integer", "forwarded_count": "integer", "dropped_count": "integer" }], "totals": { "rx": "integer", "tx": "integer", "forwarded": "integer", "dropped": "integer" } }, "status": "NEW - for dashboard packet type charts", "note": "Alternative to generic /api/timeseries for simpler dashboard use case" } }, "phase_5_actions": { "description": "Operational commands", "POST /api/mode": { "description": "Set operating mode", "request": {"mode": "forward|monitor"}, "status": "EXISTS as set_mode" }, "POST /api/duty_cycle": { "description": "Enable/disable duty cycle", "request": {"enabled": "boolean"}, "status": "EXISTS as set_duty_cycle" }, "POST /api/radio": { "description": "Update radio configuration", "request": { "frequency_hz": "integer (optional)", "tx_power_dbm": "integer (optional)", "spreading_factor": "integer (optional)", "bandwidth_hz": "integer (optional)" }, "response": { "applied": "array - fields applied immediately", "pending_restart": "array - fields requiring restart", "warnings": "array of strings" }, "status": "EXISTS as update_radio_config" }, "POST /api/restart": { "description": "Restart the repeater service", "status": "EXISTS as restart_service" }, "GET /api/radio_presets": { "description": "Available radio configuration presets", "response": { "presets": [{ "name": "string - preset identifier", "description": "string - human readable description", "frequency_hz": "integer", "tx_power_dbm": "integer", "spreading_factor": "integer", "bandwidth_hz": "integer", "coding_rate": "integer" }] }, "status": "EXISTS" }, "POST /api/log_level": { "description": "Set system logging level", "request": {"level": "string - DEBUG|INFO|WARNING|ERROR"}, "response": {"success": "boolean", "level": "string - applied level"}, "status": "EXISTS as set_log_level" }, "POST /api/flood_policy": { "description": "Set global flood routing policy", "request": {"allow_all": "boolean - if true, forward all floods; if false, apply filtering rules"}, "response": {"success": "boolean"}, "status": "EXISTS as global_flood_policy" }, "GET /api/duty_cycle": { "description": "Get current duty cycle configuration", "response": { "enabled": "boolean", "max_tx_percent": "number - maximum TX duty cycle percentage", "window_seconds": "integer - measurement window duration", "current_usage_percent": "number - current TX usage in window" }, "status": "NEW - extends existing set_duty_cycle" }, "POST /api/duty_cycle": { "description": "Update duty cycle configuration", "request": { "enabled": {"type": "boolean", "description": "Enable/disable duty cycle limiting"}, "max_tx_percent": {"type": "number", "description": "Maximum TX percentage (optional)"}, "window_seconds": {"type": "integer", "description": "Measurement window (optional)"} }, "response": {"success": "boolean"}, "status": "EXISTS as set_duty_cycle - expanded request body" } }, "phase_6_identity": { "description": "Multi-identity and ACL management", "status": "EXISTS - no changes needed", "endpoints": [ "GET /api/identities", "POST /api/identities", "GET /api/identities/{name}", "PUT /api/identities/{name}", "DELETE /api/identities/{name}", "GET /api/acl", "GET /api/acl/clients", "GET /api/acl/stats", "DELETE /api/acl/clients/{public_key}" ], "POST /api/identities/{name}/advert": { "description": "Send ADVERT for a specific identity (e.g., room server)", "response": {"success": "boolean"}, "status": "EXISTS as send_room_server_advert - generalized" } }, "phase_6b_transport_keys": { "description": "Transport key management for encrypted mesh communication", "status": "EXISTS - no changes needed", "GET /api/transport_keys": { "description": "List all transport keys", "response": { "keys": [{ "id": "string - unique key identifier", "name": "string - human readable name", "created": "integer - Unix timestamp", "last_used": "integer or null - Unix timestamp" }] } }, "POST /api/transport_keys": { "description": "Create a new transport key", "request": { "name": "string - human readable name" }, "response": { "id": "string - generated key ID", "key": "string - the actual key (only returned on creation)" } }, "GET /api/transport_keys/{id}": { "description": "Get transport key details (not the key itself)", "response": { "id": "string", "name": "string", "created": "integer", "last_used": "integer or null" } }, "PUT /api/transport_keys/{id}": { "description": "Update transport key metadata", "request": {"name": "string"}, "response": {"success": "boolean"} }, "DELETE /api/transport_keys/{id}": { "description": "Delete a transport key", "response": {"success": "boolean"} } }, "phase_6c_api_tokens": { "description": "API token management for authentication", "status": "EXISTS - no changes needed", "GET /api/tokens": { "description": "List all API tokens", "response": { "tokens": [{ "id": "string - token identifier", "name": "string - human readable name", "created": "integer - Unix timestamp", "last_used": "integer or null", "expires": "integer or null - Unix timestamp if expiring" }] } }, "POST /api/tokens": { "description": "Create a new API token", "request": { "name": "string - human readable name", "expires_days": "integer or null - days until expiration (null = never)" }, "response": { "id": "string", "token": "string - the actual token (only returned on creation)" } }, "DELETE /api/tokens/{id}": { "description": "Revoke an API token", "response": {"success": "boolean"} } }, "phase_7_rooms": { "description": "Room server message management", "status": "EXISTS - no changes needed", "endpoints": [ "GET /api/rooms", "GET /api/rooms/{name}/messages", "POST /api/rooms/{name}/messages", "DELETE /api/rooms/{name}/messages/{id}", "DELETE /api/rooms/{name}/messages" ], "GET /api/rooms/{name}/stats": { "description": "Room statistics", "response": { "message_count": "integer", "client_count": "integer", "last_activity": "integer - Unix timestamp" }, "status": "EXISTS as room_stats" }, "GET /api/rooms/{name}/clients": { "description": "List clients synced to a room", "response": { "clients": [{ "hash": "string", "name": "string", "last_sync": "integer - Unix timestamp" }] }, "status": "EXISTS as room_clients" } }, "phase_8_frontend": { "description": "Frontend/web UI configuration - pymc_console specific", "status": "EXISTS - pymc_console overlay features", "GET /api/web_config": { "description": "Get current web UI configuration", "response": { "active_frontend": "string - 'pymc_console' or 'default'", "pymc_console_installed": "boolean", "default_frontend_installed": "boolean", "web_path": "string - current static file path" } }, "POST /api/web_config": { "description": "Switch active frontend", "request": { "frontend": "string - 'pymc_console' or 'default'" }, "response": { "success": "boolean", "restart_required": "boolean" }, "status": "EXISTS as update_web_config" }, "GET /api/web_config/check": { "description": "Check frontend installation status", "response": { "pymc_console": {"installed": "boolean", "version": "string or null"}, "default": {"installed": "boolean"} }, "status": "EXISTS as check_pymc_console + check_default_frontend" } } }, "database_schema": { "description": "Minimal schema additions. Most tables already exist.", "existing_tables": { "packets": "Already exists - stores all packet data", "adverts": "Already exists - stores neighbor/contact info" }, "new_tables": { "zero_hop_contacts": { "description": "Track direct RF contacts for zero-hop detection", "columns": { "node_hash": "TEXT PRIMARY KEY", "packet_count": "INTEGER - number of zero-hop packets received", "rssi_sum": "INTEGER - for calculating average", "snr_sum": "REAL - for calculating average", "last_seen": "INTEGER - Unix timestamp" }, "sql": "CREATE TABLE IF NOT EXISTS zero_hop_contacts (node_hash TEXT PRIMARY KEY, packet_count INTEGER DEFAULT 0, rssi_sum INTEGER DEFAULT 0, snr_sum REAL DEFAULT 0, last_seen INTEGER);" }, "stats_buckets": { "description": "Pre-aggregated time buckets for efficient chart queries", "columns": { "bucket_ts": "INTEGER - bucket start timestamp", "resolution": "TEXT - '1m', '5m', '1h', '1d'", "rx_count": "INTEGER", "tx_count": "INTEGER", "forwarded_count": "INTEGER", "dropped_count": "INTEGER", "noise_floor_avg": "REAL", "noise_floor_min": "REAL", "noise_floor_max": "REAL" }, "sql": "CREATE TABLE IF NOT EXISTS stats_buckets (bucket_ts INTEGER, resolution TEXT, rx_count INTEGER DEFAULT 0, tx_count INTEGER DEFAULT 0, forwarded_count INTEGER DEFAULT 0, dropped_count INTEGER DEFAULT 0, noise_floor_avg REAL, noise_floor_min REAL, noise_floor_max REAL, PRIMARY KEY (bucket_ts, resolution));", "index": "CREATE INDEX IF NOT EXISTS idx_stats_resolution ON stats_buckets(resolution, bucket_ts DESC);" }, "noise_floor_samples": { "description": "Raw noise floor readings for detailed analysis", "columns": { "id": "INTEGER PRIMARY KEY AUTOINCREMENT", "timestamp": "INTEGER", "value_dbm": "REAL" }, "sql": "CREATE TABLE IF NOT EXISTS noise_floor_samples (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, value_dbm REAL);", "index": "CREATE INDEX IF NOT EXISTS idx_noise_ts ON noise_floor_samples(timestamp DESC);", "retention": "Keep 24 hours, prune older" } } }, "background_tasks": { "description": "Periodic tasks needed for the system", "stats_aggregation": { "description": "Aggregate packet counts into time buckets", "frequency": "Every 60 seconds", "logic": [ "1. Count packets in last minute, upsert into stats_buckets with resolution='1m'", "2. Every 5 minutes: roll up 1m buckets into 5m bucket", "3. Every hour: roll up 5m buckets into 1h bucket", "4. Every day at midnight: roll up 1h buckets into 1d bucket", "5. Prune: delete 1m buckets older than 24h, 5m older than 7d" ], "estimated_code": "~50 lines" }, "zero_hop_detection": { "description": "Update zero-hop contact tracking on packet receive", "trigger": "On each ADVERT packet received", "logic": [ "1. Check if packet is zero-hop (DIRECT route: path_len=0, FLOOD route: path_len<=1)", "2. If zero-hop, upsert into zero_hop_contacts table", "3. Increment packet_count, add to rssi_sum/snr_sum, update last_seen" ], "estimated_code": "~20 lines" }, "noise_floor_sampling": { "description": "Already implemented in SX1262 wrapper", "location": "sx1262_wrapper.py _sample_noise_floor()", "change_needed": "Add insert into noise_floor_samples table, broadcast to WebSocket" } }, "implementation_priority": { "sprint_1": { "name": "WebSocket Radio Events + Hardware", "effort": "1-2 days", "tasks": [ "Add /radio WebSocket endpoint to CherryPy", "Create broadcast function for connected clients", "Hook SX1262 RX callback to push 'rx' events", "Hook TX completion to push 'tx' events", "Hook noise floor sampling to push 'noise' events", "Add 'hardware' event with CPU/memory/disk/temp (every 10s)", "Add 'send_advert' command handler" ] }, "sprint_2": { "name": "REST Core Cleanup", "effort": "1-2 days", "tasks": [ "Ensure /api/stats returns all current state fields", "Add /api/logs endpoint with level filter", "Add proper before/after/offset pagination to /api/packets", "Add src_hash, dst_hash, path_contains filters to /api/packets", "Add /api/neighbors endpoint with type and zero_hop filters" ] }, "sprint_3": { "name": "Time Series + Actions", "effort": "2-3 days", "tasks": [ "Create stats_buckets and noise_floor_samples tables", "Implement aggregation background task", "Implement /api/timeseries endpoint", "Implement /api/stats/bucketed endpoint", "Add /api/radio_presets endpoint", "Add /api/log_level POST endpoint", "Add /api/flood_policy POST endpoint", "Expand /api/duty_cycle with GET and full config POST" ] }, "sprint_4": { "name": "Identity + Keys + Tokens", "effort": "1-2 days", "tasks": [ "Add POST /api/identities/{name}/advert for room server adverts", "Verify transport key CRUD endpoints", "Verify API token CRUD endpoints" ], "note": "Most of these already exist - just verify and document" }, "sprint_5": { "name": "Rooms + Frontend Config", "effort": "1 day", "tasks": [ "Verify room stats and clients endpoints", "Add /api/web_config GET/POST endpoints", "Add /api/web_config/check endpoint" ], "note": "Frontend config is pymc_console overlay specific" } }, "what_we_are_not_doing": { "description": "Complexity removed from previous design to keep it simple", "removed": [ "Subscription channels - WebSocket just streams everything", "Message sequencing/replay - miss events? use REST for history", "Server-side topology computation - keep client-side, it works", "Server-side prefix disambiguation - same, keep client-side", "Sparkline pre-computation - client computes from time series", "Encoded pagination cursors - simple timestamps work fine", "Prometheus metrics export - add later if needed", "Federation/multi-node support - premature", "Machine learning hooks - premature" ] }, "packet_schema": { "description": "Reference: MeshCore packet structure for context", "fields": { "packet_hash": "string - unique identifier", "timestamp": "integer - Unix timestamp", "type": "integer - payload type (0-15)", "route": "integer - route type (0-3)", "transmitted": "boolean - true if TX, false if RX", "src_hash": "string - source node hash", "dst_hash": "string - destination node hash (if direct)", "rssi": "integer - RSSI in dBm (RX only)", "snr": "number - SNR in dB (RX only)", "path": "array of strings - hop prefixes", "payload": "bytes - raw payload" }, "payload_types": { "0": "REQ", "1": "RESPONSE", "2": "TXT_MSG", "3": "ACK", "4": "ADVERT", "5": "GRP_TXT", "6": "GRP_DATA", "7": "ANON_REQ", "8": "PATH", "9": "TRACE", "10": "MULTIPART", "15": "RAW_CUSTOM" }, "route_types": { "0": "T_FLOOD (transport flood)", "1": "FLOOD", "2": "DIRECT", "3": "T_DIRECT (transport direct)" } } }