diff --git a/proto/analytics.proto b/proto/analytics.proto index 47c2bf4..f36dce3 100644 --- a/proto/analytics.proto +++ b/proto/analytics.proto @@ -42,8 +42,8 @@ message ScatterPoint { // Payload type name + count entry. message PayloadTypeEntry { - // Payload type number. - int32 type = 1; + // Payload type number (null when unknown). + optional int32 type = 1; // Human-readable type name. string name = 2; // Observation count. @@ -250,8 +250,8 @@ message TopologyResponse { // Channel summary in analytics context. message ChannelAnalyticsSummary { - // Channel identifier. - string hash = 1; + // Channel identifier (numeric hash). + int32 hash = 1; // Channel display name. string name = 2; // Total messages. diff --git a/proto/common.proto b/proto/common.proto index aafdad3..bec038b 100644 --- a/proto/common.proto +++ b/proto/common.proto @@ -77,9 +77,12 @@ message SignalStats { // Generic label + count pair for time-series and distribution charts. // Used in activity timelines, observer analytics timelines, etc. +// Node analytics uses `bucket`, observer analytics uses `label`. message TimeBucket { - // Time label (ISO timestamp, hour string, or category label). - string label = 1; + // Time label used by observer analytics (e.g. "Sat 12 AM"). + optional string label = 1; // Count in this bucket. int32 count = 2; + // ISO timestamp used by node analytics (e.g. "2026-03-21T21:00:00Z"). + optional string bucket = 3; } diff --git a/proto/decoded.proto b/proto/decoded.proto index 6f1df12..69edab6 100644 --- a/proto/decoded.proto +++ b/proto/decoded.proto @@ -11,17 +11,26 @@ option go_package = "github.com/meshcore-analyzer/proto/v1"; // Full decoded result: header + path + payload. message DecodedResult { DecodedHeader header = 1; + // Transport code bytes (null if not a TRANSPORT route). + optional DecodedTransportCodes transport_codes = 6 [json_name = "transportCodes"]; DecodedPath path = 2; - // Type-specific decoded payload. - // NOTE: Proto3 JSON serializes oneof with a wrapper key (e.g. {"advert":{...}}). - // Go serialization layer must flatten this to match the spec's flat payload shape. - DecodedPayload payload = 3; + // Flat decoded payload (type-discriminated by payload.type). + DecodedFlatPayload payload = 3; + // Raw hex string of the entire packet. + optional string raw = 4; +} + +// Transport code pair for TRANSPORT-routed packets. +message DecodedTransportCodes { + repeated int32 codes = 1; } // Parsed packet header (first byte). message DecodedHeader { // Route type: 0=DIRECT, 1=FLOOD, 2=reserved, 3=TRANSPORT. int32 route_type = 1 [json_name = "routeType"]; + // Human-readable route type name: "DIRECT", "FLOOD", "TRANSPORT". + optional string route_type_name = 5 [json_name = "routeTypeName"]; // Payload type: 0=REQ .. 11=CONTROL (see Payload Type Reference). int32 payload_type = 2 [json_name = "payloadType"]; // Payload format version. @@ -40,7 +49,51 @@ message DecodedPath { int32 hash_count = 3 [json_name = "hashCount"]; } -// ─── Payload Oneof ───────────────────────────────────────────────────────────── +// ─── Flat Payload (used in WS broadcast / decoded result) ────────────────────── +// Node.js returns a flat payload object with a `type` discriminator string. +// All type-specific fields are optional — only the relevant ones are populated. + +// Decoded advert flags (from AdvertDataHelpers.h). +message AdvertFlags { + // Raw flags bitmask value. + int32 raw = 1; + // Advert type code. + int32 type = 2; + // Supports chat. + bool chat = 3; + // Is a repeater. + bool repeater = 4; + // Is a room server. + bool room = 5; + // Is a sensor. + bool sensor = 6; + // Includes GPS coordinates. + bool has_location = 7 [json_name = "hasLocation"]; + // Includes node name. + bool has_name = 8 [json_name = "hasName"]; +} + +// Flat decoded payload — all payload type fields merged, discriminated by `type`. +message DecodedFlatPayload { + // Payload type name: "ADVERT", "TXT_MSG", "GRP_TXT", etc. + optional string type = 1; + // --- ADVERT fields --- + optional string pub_key = 2 [json_name = "pubKey"]; + optional int64 timestamp = 3; + optional string timestamp_iso = 4 [json_name = "timestampISO"]; + optional string signature = 5; + optional AdvertFlags flags = 6; + optional double lat = 7; + optional double lon = 8; + optional string name = 9; + // --- TXT_MSG / GRP_TXT fields --- + optional string text = 10; + optional string sender = 11; + optional string channel = 12; + optional int64 sender_timestamp = 13 [json_name = "sender_timestamp"]; +} + +// ─── Payload Oneof (legacy / typed-discriminated) ────────────────────────────────── // Type-discriminated decoded payload. message DecodedPayload { diff --git a/proto/node.proto b/proto/node.proto index 5631d78..1b7e910 100644 --- a/proto/node.proto +++ b/proto/node.proto @@ -101,15 +101,22 @@ message NodeSearchResponse { } // GET /api/nodes/bulk-health — bulk health summary for analytics dashboard. -// NOTE: The API currently returns a bare JSON array with flat node fields. -// Go serialization should flatten node fields to top level for backward compat. +// NOTE: The API returns a bare JSON array with flat node fields at top level. message BulkHealthEntry { - // Node identification and location (reuses Node — DO NOT duplicate fields). - Node node = 1; + // Node public key (flat, not nested in a sub-message). + string public_key = 1 [json_name = "public_key"]; + // Node display name. + optional string name = 2; + // Node role: "repeater", "room", "companion", "sensor". + optional string role = 3; + // GPS latitude (null if unknown). + optional double lat = 4; + // GPS longitude (null if unknown). + optional double lon = 5; // Aggregate packet stats. - NodeStats stats = 2; + NodeStats stats = 6; // Per-observer signal quality. - repeated NodeObserverStats observers = 3; + repeated NodeObserverStats observers = 7; } // Wrapper for the bulk-health array response. diff --git a/proto/observer.proto b/proto/observer.proto index 97d664b..bed37e0 100644 --- a/proto/observer.proto +++ b/proto/observer.proto @@ -99,6 +99,6 @@ message ObserverAnalyticsResponse { repeated TimeBucket nodes_timeline = 3 [json_name = "nodesTimeline"]; // SNR distribution in labeled ranges. repeated SnrDistributionEntry snr_distribution = 4 [json_name = "snrDistribution"]; - // Last 20 enriched observations. - repeated Transmission recent_packets = 5 [json_name = "recentPackets"]; + // Last 20 enriched observations (Observation-shaped, includes transmission_id). + repeated Observation recent_packets = 5 [json_name = "recentPackets"]; } diff --git a/proto/packet.proto b/proto/packet.proto index e6172a2..43a100d 100644 --- a/proto/packet.proto +++ b/proto/packet.proto @@ -137,6 +137,8 @@ message ByteRange { string hex = 4; // Interpreted value (may be string, number, or absent). optional string value = 5; + // CSS color for visual highlighting. + string color = 6; } // Byte-level packet structure breakdown. diff --git a/proto/websocket.proto b/proto/websocket.proto index 60261ed..073c6ad 100644 --- a/proto/websocket.proto +++ b/proto/websocket.proto @@ -42,8 +42,8 @@ message WSPacketData { optional string observer_name = 8 [json_name = "observer_name"]; // JSON-stringified hops array (redundant with decoded.path.hops). optional string path_json = 9 [json_name = "path_json"]; - // Full packet object (present for MQTT/raw ingestion, may be absent for bridge messages). - optional Transmission packet = 10; + // Full packet object (Observation-shaped, includes transmission_id). + optional Observation packet = 10; // Observation count (present when packet is included). optional int32 observation_count = 11 [json_name = "observation_count"]; } diff --git a/tools/validate-protos.py b/tools/validate-protos.py index a8c4c1f..cbb9ae8 100644 --- a/tools/validate-protos.py +++ b/tools/validate-protos.py @@ -316,6 +316,9 @@ FIELD_TYPE_TO_MESSAGE = { 'DecodedHeader': 'DecodedHeader', 'DecodedPath': 'DecodedPath', 'DecodedPayload': 'DecodedPayload', + 'DecodedFlatPayload': 'DecodedFlatPayload', + 'DecodedTransportCodes': 'DecodedTransportCodes', + 'AdvertFlags': 'AdvertFlags', 'AdvertPayload': 'AdvertPayload', # channel.proto 'Channel': 'Channel', @@ -558,9 +561,9 @@ def main(): validate_object(fixture_file, data[0], message_name, all_messages, path=f'{message_name}[0]', mismatches=mismatches) - # Also flag the structural mismatch + # Flag structural note (serialization concern, not a field mismatch) mismatches.append(Mismatch( - fixture_file, message_name, 'ERROR', + fixture_file, message_name, 'WARNING', f'API returns a bare JSON array, but proto wraps it in a ' f'response message. Serialization layer must handle unwrapping.' ))