diff --git a/.github/workflows/buf.yaml b/.github/workflows/buf.yaml new file mode 100644 index 0000000..1196b30 --- /dev/null +++ b/.github/workflows/buf.yaml @@ -0,0 +1,36 @@ +name: buf + +on: + pull_request: + paths: + - "meshexplorer/proto/**" + - "meshexplorer/buf.yaml" + - "meshexplorer/buf.gen.yaml" + - "meshexplorer/buf.lock" + - ".github/workflows/buf.yaml" + push: + branches: + - main + paths: + - "meshexplorer/proto/**" + - "meshexplorer/buf.yaml" + - "meshexplorer/buf.gen.yaml" + - "meshexplorer/buf.lock" + +permissions: + contents: read + pull-requests: write # lets buf-action post check summaries as PR comments + +jobs: + buf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-action@v1 + with: + input: meshexplorer # buf.yaml lives in this subdir + lint: true + format: true + breaking: true + push: false # no BSR publishing / token + github_token: ${{ github.token }} diff --git a/meshexplorer/proto/meshexplorer/v1/chat.proto b/meshexplorer/proto/meshexplorer/v1/chat.proto index 8d69789..08aeb9f 100644 --- a/meshexplorer/proto/meshexplorer/v1/chat.proto +++ b/meshexplorer/proto/meshexplorer/v1/chat.proto @@ -37,7 +37,10 @@ message ChatMessage { } message GetChatRequest { - optional int32 limit = 1 [(buf.validate.field).int32 = {gte: 1, lte: 1000}]; + optional int32 limit = 1 [(buf.validate.field).int32 = { + gte: 1 + lte: 1000 + }]; optional string before = 2; optional string after = 3; optional string channel_id = 4 [(buf.validate.field).string.pattern = "^[0-9A-Fa-f]+$"]; @@ -56,13 +59,23 @@ message StreamChatRequest { bool decrypt = 3; repeated string private_keys = 4; // Poll interval in ms (clamped 100..10000, default 1000). - optional int32 poll_interval = 5 [(buf.validate.field).int32 = {gte: 100, lte: 10000}]; + optional int32 poll_interval = 5 [(buf.validate.field).int32 = { + gte: 100 + lte: 10000 + }]; // Max rows per poll (clamped 10..1000, default 500). - optional int32 max_rows = 6 [(buf.validate.field).int32 = {gte: 10, lte: 1000}]; + optional int32 max_rows = 6 [(buf.validate.field).int32 = { + gte: 10 + lte: 1000 + }]; bool skip_initial_messages = 7; } +message StreamChatResponse { + ChatMessage message = 1; +} + service ChatService { rpc GetChat(GetChatRequest) returns (GetChatResponse); - rpc StreamChat(StreamChatRequest) returns (stream ChatMessage); + rpc StreamChat(StreamChatRequest) returns (stream StreamChatResponse); } diff --git a/meshexplorer/proto/meshexplorer/v1/map.proto b/meshexplorer/proto/meshexplorer/v1/map.proto index 4246841..39a24a2 100644 --- a/meshexplorer/proto/meshexplorer/v1/map.proto +++ b/meshexplorer/proto/meshexplorer/v1/map.proto @@ -20,10 +20,22 @@ message NodePosition { message GetMapRequest { // Bounding box (decimal degrees). Unset fields mean "unbounded" on that edge. - optional double min_lat = 1 [(buf.validate.field).double = {gte: -90, lte: 90}]; - optional double max_lat = 2 [(buf.validate.field).double = {gte: -90, lte: 90}]; - optional double min_lng = 3 [(buf.validate.field).double = {gte: -180, lte: 180}]; - optional double max_lng = 4 [(buf.validate.field).double = {gte: -180, lte: 180}]; + optional double min_lat = 1 [(buf.validate.field).double = { + gte: -90 + lte: 90 + }]; + optional double max_lat = 2 [(buf.validate.field).double = { + gte: -90 + lte: 90 + }]; + optional double min_lng = 3 [(buf.validate.field).double = { + gte: -180 + lte: 180 + }]; + optional double max_lng = 4 [(buf.validate.field).double = { + gte: -180 + lte: 180 + }]; repeated string node_types = 5; // Only include nodes seen within this many seconds. optional int32 last_seen = 6 [(buf.validate.field).int32.gte = 0]; diff --git a/meshexplorer/proto/meshexplorer/v1/neighbors.proto b/meshexplorer/proto/meshexplorer/v1/neighbors.proto index b2fbb12..fb6e189 100644 --- a/meshexplorer/proto/meshexplorer/v1/neighbors.proto +++ b/meshexplorer/proto/meshexplorer/v1/neighbors.proto @@ -6,10 +6,22 @@ import "buf/validate/validate.proto"; import "meshexplorer/v1/common.proto"; message GetAllNeighborsRequest { - optional double min_lat = 1 [(buf.validate.field).double = {gte: -90, lte: 90}]; - optional double max_lat = 2 [(buf.validate.field).double = {gte: -90, lte: 90}]; - optional double min_lng = 3 [(buf.validate.field).double = {gte: -180, lte: 180}]; - optional double max_lng = 4 [(buf.validate.field).double = {gte: -180, lte: 180}]; + optional double min_lat = 1 [(buf.validate.field).double = { + gte: -90 + lte: 90 + }]; + optional double max_lat = 2 [(buf.validate.field).double = { + gte: -90 + lte: 90 + }]; + optional double min_lng = 3 [(buf.validate.field).double = { + gte: -180 + lte: 180 + }]; + optional double max_lng = 4 [(buf.validate.field).double = { + gte: -180 + lte: 180 + }]; repeated string node_types = 5; optional int32 last_seen = 6 [(buf.validate.field).int32.gte = 0]; optional string region = 7; diff --git a/meshexplorer/proto/meshexplorer/v1/node.proto b/meshexplorer/proto/meshexplorer/v1/node.proto index 86b7c0d..8b85e94 100644 --- a/meshexplorer/proto/meshexplorer/v1/node.proto +++ b/meshexplorer/proto/meshexplorer/v1/node.proto @@ -67,7 +67,10 @@ message MqttInfo { message GetNodeRequest { string public_key = 1 [(buf.validate.field).string.min_len = 10]; // Max number of recent adverts to return (default 50). - optional int32 limit = 2 [(buf.validate.field).int32 = {gte: 1, lte: 1000}]; + optional int32 limit = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 1000 + }]; } message GetNodeResponse { @@ -106,7 +109,10 @@ message SearchQuery { optional string query = 1 [(buf.validate.field).string.max_len = 100]; optional string region = 2; optional int32 last_seen = 3 [(buf.validate.field).int32.gte = 0]; - optional int32 limit = 4 [(buf.validate.field).int32 = {gte: 1, lte: 200}]; + optional int32 limit = 4 [(buf.validate.field).int32 = { + gte: 1 + lte: 200 + }]; optional bool exact = 5; optional bool is_repeater = 6; } diff --git a/meshexplorer/proto/meshexplorer/v1/packets.proto b/meshexplorer/proto/meshexplorer/v1/packets.proto index 846ebe3..407642e 100644 --- a/meshexplorer/proto/meshexplorer/v1/packets.proto +++ b/meshexplorer/proto/meshexplorer/v1/packets.proto @@ -23,16 +23,32 @@ message Packet { message StreamPacketsRequest { optional string region = 1; // Payload type filter (0..15). - optional int32 payload_type = 2 [(buf.validate.field).int32 = {gte: 0, lte: 15}]; + optional int32 payload_type = 2 [(buf.validate.field).int32 = { + gte: 0 + lte: 15 + }]; // Route type filter (0..3). - optional int32 route_type = 3 [(buf.validate.field).int32 = {gte: 0, lte: 3}]; + optional int32 route_type = 3 [(buf.validate.field).int32 = { + gte: 0 + lte: 3 + }]; optional string origin_pubkey = 4 [(buf.validate.field).string.pattern = "^[0-9A-Fa-f]+$"]; // Poll interval in ms (clamped 100..10000, default 500). - optional int32 poll_interval = 5 [(buf.validate.field).int32 = {gte: 100, lte: 10000}]; + optional int32 poll_interval = 5 [(buf.validate.field).int32 = { + gte: 100 + lte: 10000 + }]; // Max rows per poll (clamped 10..10000, default 10). - optional int32 max_rows = 6 [(buf.validate.field).int32 = {gte: 10, lte: 10000}]; + optional int32 max_rows = 6 [(buf.validate.field).int32 = { + gte: 10 + lte: 10000 + }]; +} + +message StreamPacketsResponse { + Packet packet = 1; } service PacketsService { - rpc StreamPackets(StreamPacketsRequest) returns (stream Packet); + rpc StreamPackets(StreamPacketsRequest) returns (stream StreamPacketsResponse); } diff --git a/meshexplorer/proto/meshexplorer/v1/stats.proto b/meshexplorer/proto/meshexplorer/v1/stats.proto index 6b5ae28..abe7096 100644 --- a/meshexplorer/proto/meshexplorer/v1/stats.proto +++ b/meshexplorer/proto/meshexplorer/v1/stats.proto @@ -2,7 +2,19 @@ syntax = "proto3"; package meshexplorer.v1; -message StatsRequest { +message GetTotalNodesRequest { + optional string region = 1; +} + +message GetNodesOverTimeRequest { + optional string region = 1; +} + +message GetPopularChannelsRequest { + optional string region = 1; +} + +message GetRepeaterPrefixesRequest { optional string region = 1; } @@ -42,8 +54,8 @@ message GetRepeaterPrefixesResponse { } service StatsService { - rpc GetTotalNodes(StatsRequest) returns (GetTotalNodesResponse); - rpc GetNodesOverTime(StatsRequest) returns (GetNodesOverTimeResponse); - rpc GetPopularChannels(StatsRequest) returns (GetPopularChannelsResponse); - rpc GetRepeaterPrefixes(StatsRequest) returns (GetRepeaterPrefixesResponse); + rpc GetTotalNodes(GetTotalNodesRequest) returns (GetTotalNodesResponse); + rpc GetNodesOverTime(GetNodesOverTimeRequest) returns (GetNodesOverTimeResponse); + rpc GetPopularChannels(GetPopularChannelsRequest) returns (GetPopularChannelsResponse); + rpc GetRepeaterPrefixes(GetRepeaterPrefixesRequest) returns (GetRepeaterPrefixesResponse); } diff --git a/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx b/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx index d63e1aa..5491acf 100644 --- a/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx +++ b/meshexplorer/src/app/(app)/meshcore/node/[publicKey]/page.tsx @@ -8,8 +8,9 @@ import { getNameIconLabel } from "@/lib/meshcore-map-nodeutils"; import AdvertDetails from "@/components/AdvertDetails"; import ContactQRCode from "@/components/ContactQRCode"; import { useConfig, LAST_SEEN_OPTIONS } from "@/components/ConfigContext"; -import { useNeighbors, type Neighbor } from "@/hooks/useNeighbors"; -import { useNodeData, type NodeData, type NodeInfo, type Advert, type LocationHistory, type MqttInfo, type NodeError } from "@/hooks/useNodeData"; +import { useNeighbors } from "@/hooks/useNeighbors"; +import { useNodeData, nodeErrorCode } from "@/hooks/useNodeData"; +import type { NodeInfo } from "@/gen/meshexplorer/v1/node_pb"; import { ArrowRightEndOnRectangleIcon, ArrowRightStartOnRectangleIcon } from "@heroicons/react/24/outline"; import { RegionProvider } from "@/contexts/RegionContext"; @@ -17,9 +18,9 @@ import { RegionProvider } from "@/contexts/RegionContext"; // Function to determine node type based on capabilities function getNodeType(node: NodeInfo): number { - if (node.is_chat_node) return 1; // companion - if (node.is_repeater) return 2; // repeater - if (node.is_room_server) return 3; // room + if (node.isChatNode) return 1; // companion + if (node.isRepeater) return 2; // repeater + if (node.isRoomServer) return 3; // room return 4; // sensor (default for standard nodes) } @@ -48,9 +49,9 @@ export default function MeshcoreNodePage() { enabled: !!publicKey }); - // Extract error information from TanStack Query error - const error = queryError?.error || null; - const errorCode = queryError?.code || null; + // Extract error information from the ConnectError + const error = queryError ? (queryError.rawMessage || queryError.message) : null; + const errorCode = nodeErrorCode(queryError); if (loading) { @@ -183,7 +184,7 @@ export default function MeshcoreNodePage() { ); } - if (!nodeData) { + if (!nodeData || !nodeData.node || !nodeData.mqtt) { return (
@@ -196,7 +197,7 @@ export default function MeshcoreNodePage() { const { node, recentAdverts, locationHistory, mqtt, region } = nodeData; return ( - +
{/* Header */} @@ -205,15 +206,15 @@ export default function MeshcoreNodePage() {

- {node.has_name ? getNameIconLabel(node.node_name) : "Unknown Node"} + {node.hasName ? getNameIconLabel(node.nodeName) : "Unknown Node"}

- {node.has_name && ( + {node.hasName && (

- {node.node_name} + {node.nodeName}

)}

- {formatPublicKey(node.public_key)} + {formatPublicKey(node.publicKey)}

{region && (

@@ -221,22 +222,22 @@ export default function MeshcoreNodePage() {

)}
- {node.is_repeater && ( + {node.isRepeater && ( Repeater ) || null} - {node.is_chat_node && ( + {node.isChatNode && ( Companion ) || null} - {node.is_room_server && ( + {node.isRoomServer && ( Room Server ) || null} - {!node.is_repeater && !node.is_chat_node && !node.is_room_server && ( + {!node.isRepeater && !node.isChatNode && !node.isRoomServer && ( Unknown @@ -245,8 +246,8 @@ export default function MeshcoreNodePage() {
@@ -265,7 +266,7 @@ export default function MeshcoreNodePage() {
Public Key
- {node.public_key} + {node.publicKey}
@@ -273,10 +274,10 @@ export default function MeshcoreNodePage() {
- {moment.utc(node.first_seen).format('YYYY-MM-DD HH:mm:ss')} UTC + {moment.utc(node.firstSeen).format('YYYY-MM-DD HH:mm:ss')} UTC
- {moment.utc(node.first_seen).local().fromNow()} + {moment.utc(node.firstSeen).local().fromNow()}
@@ -286,10 +287,10 @@ export default function MeshcoreNodePage() {
- {moment.utc(node.last_seen).format('YYYY-MM-DD HH:mm:ss')} UTC + {moment.utc(node.lastSeen).format('YYYY-MM-DD HH:mm:ss')} UTC
- {moment.utc(node.last_seen).local().fromNow()} + {moment.utc(node.lastSeen).local().fromNow()}
@@ -297,7 +298,7 @@ export default function MeshcoreNodePage() {
Current Location
- {node.has_location && node.latitude && node.longitude ? ( + {node.hasLocation && node.latitude && node.longitude ? ( {node.latitude.toFixed(6)}, {node.longitude.toFixed(6)} @@ -311,11 +312,11 @@ export default function MeshcoreNodePage() {
- {mqtt.is_uplinked ? 'Connected' : 'Not Connected'} + {mqtt.isUplinked ? 'Connected' : 'Not Connected'}
@@ -331,10 +332,10 @@ export default function MeshcoreNodePage() {
- {moment.utc(topic.last_packet_time).format('MM-DD HH:mm')} + {moment.utc(topic.lastPacketTime).format('MM-DD HH:mm')}
- {moment.utc(topic.last_packet_time).local().fromNow()} + {moment.utc(topic.lastPacketTime).local().fromNow()}
@@ -362,7 +363,7 @@ export default function MeshcoreNodePage() {
) : ( recentAdverts.map((advert) => ( - + )) )}
@@ -387,7 +388,7 @@ export default function MeshcoreNodePage() { {locationHistory.map((location, index) => ( - {moment.utc(location.mesh_timestamp).format('MM-DD HH:mm:ss')} + {moment.utc(location.meshTimestamp).format('MM-DD HH:mm:ss')} {location.latitude.toFixed(6)} @@ -434,23 +435,23 @@ export default function MeshcoreNodePage() { ) : (
{neighbors.map((neighbor) => ( -
+

- {neighbor.has_name ? getNameIconLabel(neighbor.node_name) : "Unknown Node"} + {neighbor.hasName ? getNameIconLabel(neighbor.nodeName) : "Unknown Node"}

- {neighbor.has_name && ( + {neighbor.hasName && (

- {neighbor.node_name} + {neighbor.nodeName}

)}

- {formatPublicKey(neighbor.public_key)} + {formatPublicKey(neighbor.publicKey)}

View → @@ -458,17 +459,17 @@ export default function MeshcoreNodePage() {
- {neighbor.is_repeater && ( + {neighbor.isRepeater && ( Repeater ) || null} - {neighbor.is_chat_node && ( + {neighbor.isChatNode && ( Companion ) || null} - {neighbor.is_room_server && ( + {neighbor.isRoomServer && ( Room @@ -476,7 +477,7 @@ export default function MeshcoreNodePage() {
- {neighbor.has_location && neighbor.latitude && neighbor.longitude && ( + {neighbor.hasLocation && neighbor.latitude && neighbor.longitude && (
Location: {neighbor.latitude.toFixed(4)}, {neighbor.longitude.toFixed(4)}
diff --git a/meshexplorer/src/app/(app)/stats/page.tsx b/meshexplorer/src/app/(app)/stats/page.tsx index 2ed22c2..756a44e 100644 --- a/meshexplorer/src/app/(app)/stats/page.tsx +++ b/meshexplorer/src/app/(app)/stats/page.tsx @@ -49,7 +49,7 @@ export default function StatsPage() { repeaterPrefixesQuery.error; // Extract data with fallbacks - const totalNodes = totalNodesQuery.data?.total_nodes ?? null; + const totalNodes = totalNodesQuery.data?.totalNodes ?? null; const nodesOverTime = nodesOverTimeQuery.data?.data ?? []; const popularChannels = popularChannelsQuery.data?.data ?? []; const repeaterPrefixes = repeaterPrefixesQuery.data?.data ?? []; @@ -131,11 +131,11 @@ export default function StatsPage() { {nodesOverTime.map((row, i) => ( {row.day} - {row.cumulative_unique_nodes} - {row.nodes_with_location} - {row.nodes_without_location} + {row.cumulativeUniqueNodes} + {row.nodesWithLocation} + {row.nodesWithoutLocation} {row.repeaters} - {row.room_servers} + {row.roomServers} ))} @@ -158,8 +158,8 @@ export default function StatsPage() { {popularChannels.map((row, i) => ( - {row.channel_hash} - {row.message_count} + {row.channelHash} + {row.messageCount} ))} @@ -186,9 +186,9 @@ export default function StatsPage() { {row.prefix} - {row.node_names && row.node_names.length > 0 ? ( + {row.nodeNames && row.nodeNames.length > 0 ? (
- {row.node_names.map((name: string, j: number) => ( + {row.nodeNames.map((name: string, j: number) => (
{name || 'Unnamed Node'}
diff --git a/meshexplorer/src/components/AdvertDetails.tsx b/meshexplorer/src/components/AdvertDetails.tsx index 52edea6..9c4d446 100644 --- a/meshexplorer/src/components/AdvertDetails.tsx +++ b/meshexplorer/src/components/AdvertDetails.tsx @@ -4,30 +4,18 @@ import { useState } from "react"; import moment from "moment"; import PathVisualization from "./PathVisualization"; import { PathData } from "@/lib/pathUtils"; +import type { Advert } from "@/gen/meshexplorer/v1/node_pb"; interface AdvertDetailsProps { - advert: { - group_id: number; - origin_path_pubkey_tuples: Array<[string, string, string]>; - advert_count: number; - earliest_timestamp: string; - latest_timestamp: string; - latitude: number | null; - longitude: number | null; - is_repeater: number; - is_chat_node: number; - is_room_server: number; - has_location: number; - packet_hash: string; - }; + advert: Advert; initiatingNodeKey?: string; } export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetailsProps) { const [isExpanded, setIsExpanded] = useState(false); - const timeRange = advert.earliest_timestamp !== advert.latest_timestamp - ? `to ${moment.utc(advert.latest_timestamp).format('HH:mm:ss')}` : ''; + const timeRange = advert.earliestTimestamp !== advert.latestTimestamp + ? `to ${moment.utc(advert.latestTimestamp).format('HH:mm:ss')}` : ''; return (
@@ -39,28 +27,28 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai
- {moment.utc(advert.earliest_timestamp).format('MM-DD HH:mm:ss')} + {moment.utc(advert.earliestTimestamp).format('MM-DD HH:mm:ss')}
{timeRange}
- Heard {advert.advert_count} time{advert.advert_count !== 1 ? 's' : ''} + Heard {advert.advertCount} time{advert.advertCount !== 1 ? 's' : ''}
- {advert.is_repeater && ( + {advert.isRepeater && ( R ) || null} - {advert.is_chat_node && ( + {advert.isChatNode && ( C ) || null} - {advert.is_room_server && ( + {advert.isRoomServer && ( S @@ -92,19 +80,19 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai {/* Path details */}
({ - origin: origin || origin_pubkey.substring(0, 8), // Use origin name if available, fallback to pubkey - pubkey: origin_pubkey, - path: path + paths={advert.originPathPubkeyTuples.map((t) => ({ + origin: t.origin || t.originPubkey.substring(0, 8), // Use origin name if available, fallback to pubkey + pubkey: t.originPubkey, + path: t.path }))} className="text-sm" initiatingNodeKey={initiatingNodeKey} - packetHash={advert.packet_hash} + packetHash={advert.packetHash} />
{/* Location details */} - {advert.has_location && advert.latitude && advert.longitude && ( + {advert.hasLocation && advert.latitude && advert.longitude && (

Location @@ -122,14 +110,14 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai

- Earliest: {moment.utc(advert.earliest_timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC + Earliest: {moment.utc(advert.earliestTimestamp).format('YYYY-MM-DD HH:mm:ss')} UTC
- Latest: {moment.utc(advert.latest_timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC + Latest: {moment.utc(advert.latestTimestamp).format('YYYY-MM-DD HH:mm:ss')} UTC
- {advert.earliest_timestamp !== advert.latest_timestamp && ( + {advert.earliestTimestamp !== advert.latestTimestamp && (
- Duration: {moment.utc(advert.latest_timestamp).diff(moment.utc(advert.earliest_timestamp), 'seconds')} seconds + Duration: {moment.utc(advert.latestTimestamp).diff(moment.utc(advert.earliestTimestamp), 'seconds')} seconds
)}
@@ -141,22 +129,22 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai Node Capabilities
- {advert.is_repeater && ( + {advert.isRepeater && ( Repeater ) || null} - {advert.is_chat_node && ( + {advert.isChatNode && ( Companion ) || null} - {advert.is_room_server && ( + {advert.isRoomServer && ( Room Server ) || null} - {!advert.is_repeater && !advert.is_chat_node && !advert.is_room_server && ( + {!advert.isRepeater && !advert.isChatNode && !advert.isRoomServer && ( Unknown diff --git a/meshexplorer/src/components/ChatBox.tsx b/meshexplorer/src/components/ChatBox.tsx index bd3f094..8f516a2 100644 --- a/meshexplorer/src/components/ChatBox.tsx +++ b/meshexplorer/src/components/ChatBox.tsx @@ -197,7 +197,7 @@ export default function ChatBox({ {/* Messages */} {(startExpanded ? messages : messages.toReversed()).map((msg, i) => ( diff --git a/meshexplorer/src/components/ChatMessageItem.tsx b/meshexplorer/src/components/ChatMessageItem.tsx index 01ed42e..5073b09 100644 --- a/meshexplorer/src/components/ChatMessageItem.tsx +++ b/meshexplorer/src/components/ChatMessageItem.tsx @@ -6,19 +6,7 @@ import PathVisualization from "./PathVisualization"; import { PathData } from "@/lib/pathUtils"; import NodeLinkWithHover from "./NodeLinkWithHover"; import { findNodeMentions } from "@/lib/node-utils"; - -export interface ChatMessage { - message_id: string; - ingest_timestamp: string; - origins: string[]; - mesh_timestamp: string; - path_len: number; - channel_hash: string; - mac: string; - encrypted_message: string; - message_count: number; - origin_path_info: Array<[string, string, string, string, string]>; // Array of [origin, origin_pubkey, path, broker, topic] tuples -} +import type { ChatMessage } from "@/gen/meshexplorer/v1/chat_pb"; function formatHex(hex: string): string { @@ -113,9 +101,9 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow ], [config?.meshcoreKeys]); const { data: decryptionResult, isLoading } = useMessageDecryption({ - encrypted_message: msg.encrypted_message, + encrypted_message: msg.encryptedMessage, mac: msg.mac, - channel_hash: msg.channel_hash, + channel_hash: msg.channelHash, knownKeys, parse: true, }); @@ -123,17 +111,17 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow const parsed = decryptionResult?.decrypted || null; const error = decryptionResult?.error || null; - const originPathInfo = useMemo(() => - msg.origin_path_info && msg.origin_path_info.length > 0 ? msg.origin_path_info : [], - [msg.origin_path_info] + const originPathInfo = useMemo(() => + msg.originPathInfo && msg.originPathInfo.length > 0 ? msg.originPathInfo : [], + [msg.originPathInfo] ); // Convert to PathData format for the new component - const pathData: PathData[] = useMemo(() => - originPathInfo.map(([origin, origin_pubkey, path, broker, topic]) => ({ - origin, - pubkey: origin_pubkey, - path + const pathData: PathData[] = useMemo(() => + originPathInfo.map((o) => ({ + origin: o.origin, + pubkey: o.originPubkey, + path: o.path })), [originPathInfo] ); @@ -145,7 +133,7 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow
{formatLocalTime(new Date(parsed.timestamp * 1000).toISOString())} type: {parsed.msgType} - channel: {msg.channel_hash} + channel: {msg.channelHash}
{parsed.sender ? ( @@ -163,7 +151,7 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow paths={pathData} title={`Heard ${pathData.length} repeat${pathData.length !== 1 ? 's' : ''}`} className="text-xs" - packetHash={msg.message_id} + packetHash={msg.messageId} />
); @@ -174,8 +162,8 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow return (
- {formatLocalTime(msg.ingest_timestamp)} - channel: {msg.channel_hash} + {formatLocalTime(msg.ingestTimestamp)} + channel: {msg.channelHash}
{error} @@ -184,7 +172,7 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow paths={pathData} title={`Heard ${pathData.length} repeat${pathData.length !== 1 ? 's' : ''}`} className="text-xs" - packetHash={msg.message_id} + packetHash={msg.messageId} />
); @@ -197,15 +185,15 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow return (
- {formatLocalTime(msg.ingest_timestamp)} - channel: {msg.channel_hash} + {formatLocalTime(msg.ingestTimestamp)} + channel: {msg.channelHash}
); @@ -217,12 +205,12 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow export default React.memo(ChatMessageItem, (prevProps, nextProps) => { // Only re-render if these key properties change return ( - prevProps.msg.message_id === nextProps.msg.message_id && - prevProps.msg.ingest_timestamp === nextProps.msg.ingest_timestamp && - prevProps.msg.encrypted_message === nextProps.msg.encrypted_message && + prevProps.msg.messageId === nextProps.msg.messageId && + prevProps.msg.ingestTimestamp === nextProps.msg.ingestTimestamp && + prevProps.msg.encryptedMessage === nextProps.msg.encryptedMessage && prevProps.msg.mac === nextProps.msg.mac && - prevProps.msg.channel_hash === nextProps.msg.channel_hash && - prevProps.msg.origin_path_info?.length === nextProps.msg.origin_path_info?.length && + prevProps.msg.channelHash === nextProps.msg.channelHash && + prevProps.msg.originPathInfo?.length === nextProps.msg.originPathInfo?.length && prevProps.showErrorRow === nextProps.showErrorRow ); }); \ No newline at end of file diff --git a/meshexplorer/src/components/MapIcons.tsx b/meshexplorer/src/components/MapIcons.tsx index 60a7644..630ccfb 100644 --- a/meshexplorer/src/components/MapIcons.tsx +++ b/meshexplorer/src/components/MapIcons.tsx @@ -2,7 +2,7 @@ import React from 'react'; import moment from "moment"; import { formatPublicKey } from '@/lib/meshcore'; import { getNameIconLabel } from '@/lib/meshcore-map-nodeutils'; -import { NodePosition } from '@/types/map'; +import type { NodePosition } from '@/gen/meshexplorer/v1/map_pb'; interface NodeMarkerProps { node: NodePosition; @@ -35,9 +35,9 @@ export function NodeMarker({ node, showNodeNames = true, isSelected = false, isL return (
- {showNodeNames && node.short_name && ( + {showNodeNames && node.shortName && (
- {getNameIconLabel(node.name || node.short_name)} + {getNameIconLabel(node.name || node.shortName)}
)}
@@ -101,32 +101,31 @@ export function ClusterMarker({ children }: ClusterMarkerProps) { export function PopupContent({ node, target = '_self' }: PopupContentProps) { return (
-
ID: {formatPublicKey(node.node_id)}
+
ID: {formatPublicKey(node.nodeId)}
Full Name: {node.name ?? "-"}
-
Short Name: {node.short_name ? getNameIconLabel(node.name || node.short_name) : "-"}
+
Short Name: {node.shortName ? getNameIconLabel(node.name || node.shortName) : "-"}
Type: {node.type ?? "-"}
Lat: {node.latitude}
Lng: {node.longitude}
-
Alt: {node.altitude !== undefined ? node.altitude : "-"}
- {node.last_seen ? ( + {node.lastSeen ? (
- Last seen: {moment.utc(node.last_seen).format('YYYY-MM-DD HH:mm:ss')} (UTC)
- {moment.utc(node.last_seen).local().fromNow()} + Last seen: {moment.utc(node.lastSeen).format('YYYY-MM-DD HH:mm:ss')} (UTC)
+ {moment.utc(node.lastSeen).local().fromNow()}
) : (
Last seen: -
)} - {node.first_seen ? ( + {node.firstSeen ? (
- First seen: {moment.utc(node.first_seen).format('YYYY-MM-DD HH:mm:ss')} (UTC)
- {moment.utc(node.first_seen).local().fromNow()} + First seen: {moment.utc(node.firstSeen).format('YYYY-MM-DD HH:mm:ss')} (UTC)
+ {moment.utc(node.firstSeen).local().fromNow()}
) : (
First seen: -
)}
{ if (!map) return; - const isSelected = selectedNodeId === node.node_id; + const isSelected = selectedNodeId === node.nodeId; const icon = L.divIcon({ className: 'custom-node-marker-container', iconSize: [12, 24], @@ -89,7 +90,7 @@ const IndividualMarker = React.memo(function IndividualMarker({ // Add hover handler for meshcore nodes if (node.type === "meshcore") { marker.on('mouseover', () => { - onNodeClickRef.current(node.node_id); + onNodeClickRef.current(node.nodeId); }); } @@ -102,13 +103,13 @@ const IndividualMarker = React.memo(function IndividualMarker({ } }; // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally omitting selectedNodeId, showNodeNames, isLoadingNeighbors to prevent marker recreation - }, [map, node.node_id, node.latitude, node.longitude, node.type, target]); + }, [map, node.nodeId, node.latitude, node.longitude, node.type, target]); // Update marker when visual properties change (but don't recreate marker) useEffect(() => { if (markerRef.current) { // Update icon and popup content only - const isSelected = selectedNodeId === node.node_id; + const isSelected = selectedNodeId === node.nodeId; const icon = L.divIcon({ className: 'custom-node-marker-container', iconSize: [12, 24], @@ -185,7 +186,7 @@ const ClusteredMarkersGroup = React.memo(function ClusteredMarkersGroup({ }); nodes.forEach((node: NodePosition) => { - const isSelected = selectedNodeId === node.node_id; + const isSelected = selectedNodeId === node.nodeId; const icon = L.divIcon({ className: 'custom-node-marker-container', iconSize: [12, 24], @@ -206,7 +207,7 @@ const ClusteredMarkersGroup = React.memo(function ClusteredMarkersGroup({ // Add hover handler for meshcore nodes if (node.type === "meshcore") { marker.on('mouseover', () => { - onNodeClickRef.current(node.node_id); + onNodeClickRef.current(node.nodeId); }); } @@ -232,7 +233,7 @@ const ClusteredMarkersGroup = React.memo(function ClusteredMarkersGroup({ clusterGroupRef.current.eachLayer((marker: any) => { const nodeData = marker.options.nodeData; if (nodeData) { - const isSelected = selectedNodeId === nodeData.node_id; + const isSelected = selectedNodeId === nodeData.nodeId; const icon = L.divIcon({ className: 'custom-node-marker-container', iconSize: [16, 32], @@ -271,7 +272,7 @@ const ClusteredMarkers = React.memo(function ClusteredMarkers({ <> {nodes.map((node) => ( node.node_id === selectedNodeId); + const selectedNode = nodes.find(node => node.nodeId === selectedNodeId); if (!selectedNode) return null; // Create lines to neighbors that have location data and are visible on the map const lines = neighbors - .filter(neighbor => neighbor.has_location && neighbor.latitude && neighbor.longitude) + .filter(neighbor => neighbor.hasLocation && neighbor.latitude && neighbor.longitude) .map(neighbor => { // Check if the neighbor is also visible on the map - const neighborOnMap = nodes.find(node => node.node_id === neighbor.public_key); + const neighborOnMap = nodes.find(node => node.nodeId === neighbor.publicKey); const hasIncoming = neighbor.directions?.includes('incoming') || false; const hasOutgoing = neighbor.directions?.includes('outgoing') || false; @@ -347,7 +348,7 @@ function NeighborLines({ return ( node.node_id)); + const visibleNodeIds = new Set(nodes.map(node => node.nodeId)); // Filter connections to only show lines between nodes that are visible on the map // and meet the minimum packet count threshold const visibleConnections = connections.filter(connection => - visibleNodeIds.has(connection.source_node) && - visibleNodeIds.has(connection.target_node) && - connection.packet_count >= minPacketCount + visibleNodeIds.has(connection.sourceNode) && + visibleNodeIds.has(connection.targetNode) && + connection.packetCount >= minPacketCount ); // Calculate logarithmic thresholds based on packet counts for path connections - const pathConnections = visibleConnections.filter(conn => conn.connection_type === 'path'); - const packetCounts = pathConnections.map(conn => conn.packet_count).sort((a, b) => a - b); + const pathConnections = visibleConnections.filter(conn => conn.connectionType === 'path'); + const packetCounts = pathConnections.map(conn => conn.packetCount).sort((a, b) => a - b); const getLogThresholds = (counts: number[]) => { if (counts.length === 0) return { min: 1, t1: 1, t2: 1, t3: 1, t4: 1, max: 1 }; @@ -429,8 +430,8 @@ function AllNeighborLines({ <> {visibleConnections.map((connection) => { const positions: [number, number][] = [ - [connection.source_latitude, connection.source_longitude], - [connection.target_latitude, connection.target_longitude] + [connection.sourceLatitude, connection.sourceLongitude], + [connection.targetLatitude, connection.targetLongitude] ]; // Different colors based on connection type and logarithmic packet count @@ -453,14 +454,14 @@ function AllNeighborLines({ return '#6b7280'; // Gray for minimum traffic }; - const lineColor = getConnectionColor(connection.connection_type, connection.packet_count); + const lineColor = getConnectionColor(connection.connectionType, connection.packetCount); // Use strokeWidth setting for line weight - const lineWeight = connection.connection_type === 'direct' ? strokeWidth : Math.max(1, strokeWidth - 1); + const lineWeight = connection.connectionType === 'direct' ? strokeWidth : Math.max(1, strokeWidth - 1); return ( (null); const [showAllNeighbors, setShowAllNeighbors] = useState(false); - const [allNeighborConnections, setAllNeighborConnections] = useState([]); + const [allNeighborConnections, setAllNeighborConnections] = useState([]); const [allNeighborsLoading, setAllNeighborsLoading] = useState(false); // Update showAllNeighbors when mapLayerSettings changes @@ -598,39 +599,9 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) { mapClient .getMap(request, { signal: controller.signal }) .then((res) => { - setNodePositions( - res.nodes.map((n) => ({ - node_id: n.nodeId, - latitude: n.latitude, - longitude: n.longitude, - last_seen: n.lastSeen, - first_seen: n.firstSeen, - type: n.type, - short_name: n.shortName, - name: n.name ?? null, - })), - ); + setNodePositions(res.nodes); setLastResultCount(res.nodes.length); - if (includeNeighbors) { - setAllNeighborConnections( - res.neighbors.map((e) => ({ - source_node: e.sourceNode, - target_node: e.targetNode, - connection_type: e.connectionType, - packet_count: e.packetCount, - source_name: e.sourceName, - source_latitude: e.sourceLatitude, - source_longitude: e.sourceLongitude, - source_has_location: e.sourceHasLocation, - target_name: e.targetName, - target_latitude: e.targetLatitude, - target_longitude: e.targetLongitude, - target_has_location: e.targetHasLocation, - })), - ); - } else { - setAllNeighborConnections([]); - } + setAllNeighborConnections(includeNeighbors ? res.neighbors : []); if (fetchController.current === controller) { setLoading(false); @@ -856,8 +827,8 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) { {/* Traffic Legend */} {showAllNeighbors && mapLayerSettings.useColors && allNeighborConnections.length > 0 && (() => { // Calculate logarithmic thresholds for legend display - const pathConnections = allNeighborConnections.filter(conn => conn.connection_type === 'path'); - const packetCounts = pathConnections.map(conn => conn.packet_count).sort((a, b) => a - b); + const pathConnections = allNeighborConnections.filter(conn => conn.connectionType === 'path'); + const packetCounts = pathConnections.map(conn => conn.packetCount).sort((a, b) => a - b); const legendThresholds = packetCounts.length > 0 ? (() => { const min = Math.max(1, packetCounts[0]); const max = packetCounts[packetCounts.length - 1]; diff --git a/meshexplorer/src/components/MapWithChatClient.tsx b/meshexplorer/src/components/MapWithChatClient.tsx index 0785854..0b4a4b2 100644 --- a/meshexplorer/src/components/MapWithChatClient.tsx +++ b/meshexplorer/src/components/MapWithChatClient.tsx @@ -1,25 +1,13 @@ "use client"; import dynamic from "next/dynamic"; -type NodePosition = { - from_node_id: string; - latitude: number; - longitude: number; - altitude?: number; - last_seen?: string; -}; - -interface MapWithChatProps { - nodePositions?: NodePosition[]; -} - const MapView = dynamic( () => import("./MapView"), { ssr: false } ); const ChatBox = dynamic(() => import("./ChatBox"), { ssr: false }); -export default function MapWithChat({ nodePositions }: MapWithChatProps) { +export default function MapWithChat() { return (

- {node.node_name || 'Unnamed Node'} + {node.nodeName || 'Unnamed Node'}

{isRepeater && ( @@ -76,9 +62,9 @@ export default function NodeCard({ node, className = "", showTopicInfo = true }: )}
- Last seen: {moment.utc(node.last_seen).local().fromNow()} + Last seen: {moment.utc(node.lastSeen).local().fromNow()} - {formatPublicKey(node.public_key)} + {formatPublicKey(node.publicKey)}
diff --git a/meshexplorer/src/components/NodeLinkWithHover.tsx b/meshexplorer/src/components/NodeLinkWithHover.tsx index b13034e..6b6af58 100644 --- a/meshexplorer/src/components/NodeLinkWithHover.tsx +++ b/meshexplorer/src/components/NodeLinkWithHover.tsx @@ -51,7 +51,7 @@ export default function NodeLinkWithHover({ if (isSearchLoading) return "#"; // If exactly one result found, link directly to node - if (foundNode) return `/meshcore/node/${foundNode.public_key}`; + if (foundNode) return `/meshcore/node/${foundNode.publicKey}`; // If no results or multiple results, link to search page const searchUrl = `/search?q=${encodeURIComponent(nodeName)}`; @@ -83,7 +83,7 @@ export default function NodeLinkWithHover({ // Calculate navigation URL directly here since linkHref might still be "#" const navigationUrl = foundNode - ? `/meshcore/node/${foundNode.public_key}` + ? `/meshcore/node/${foundNode.publicKey}` : (() => { const searchUrl = `/search?q=${encodeURIComponent(nodeName)}`; const params = []; diff --git a/meshexplorer/src/components/PathVisualization.tsx b/meshexplorer/src/components/PathVisualization.tsx index e0a1b97..0a6be5a 100644 --- a/meshexplorer/src/components/PathVisualization.tsx +++ b/meshexplorer/src/components/PathVisualization.tsx @@ -8,7 +8,6 @@ import { ArrowsPointingOutIcon, ArrowsPointingInIcon } from "@heroicons/react/24 import { ExternalLink } from "lucide-react"; import NodeLinkWithHover from "./NodeLinkWithHover"; import { useMeshcoreSearches } from "@/hooks/useMeshcoreSearch"; -import type { MeshcoreSearchResult } from "@/hooks/useMeshcoreSearch"; import { useConfigWithRegion } from "@/hooks/useConfigWithRegion"; import { PathData, @@ -91,10 +90,10 @@ export default function PathVisualization({ const searchResult = searchResults[index]; if (searchResult?.data?.results) { const matchingNodes = searchResult.data.results - .filter(result => result.public_key.toLowerCase().startsWith(prefix.toLowerCase()) && result.node_name) + .filter(result => result.publicKey.toLowerCase().startsWith(prefix.toLowerCase()) && result.nodeName) .map(result => ({ - name: result.node_name, - publicKey: result.public_key + name: result.nodeName, + publicKey: result.publicKey })) .filter(node => node.name.length > 0); diff --git a/meshexplorer/src/components/SearchResults.tsx b/meshexplorer/src/components/SearchResults.tsx index 69ae5e2..1fb7e2b 100644 --- a/meshexplorer/src/components/SearchResults.tsx +++ b/meshexplorer/src/components/SearchResults.tsx @@ -1,11 +1,11 @@ "use client"; -import { MeshcoreSearchResult } from '@/hooks/useMeshcoreSearch'; +import type { SearchResult } from '@/gen/meshexplorer/v1/node_pb'; import { WifiIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import NodeCard from '@/components/NodeCard'; interface SearchResultsProps { - results: MeshcoreSearchResult[]; + results: SearchResult[]; isLoading: boolean; error: Error | null; query: string; @@ -84,7 +84,7 @@ export default function SearchResults({ results, isLoading, error, query, total
{results.map((node) => ( - + ))}
diff --git a/meshexplorer/src/hooks/useAllNeighbors.ts b/meshexplorer/src/hooks/useAllNeighbors.ts deleted file mode 100644 index eaafaf5..0000000 --- a/meshexplorer/src/hooks/useAllNeighbors.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useQuery } from '@connectrpc/connect-query'; -import { NeighborsService } from '@/gen/meshexplorer/v1/neighbors_pb'; - -export interface AllNeighborsConnection { - source_node: string; - target_node: string; - connection_type: string; - packet_count: number; - source_name: string; - source_latitude: number; - source_longitude: number; - source_has_location: number; - target_name: string; - target_latitude: number; - target_longitude: number; - target_has_location: number; -} - -interface UseAllNeighborsParams { - minLat?: number | null; - maxLat?: number | null; - minLng?: number | null; - maxLng?: number | null; - nodeTypes?: string[]; - lastSeen?: number | null; - region?: string; - enabled?: boolean; -} - -export function useAllNeighbors({ - minLat, - maxLat, - minLng, - maxLng, - nodeTypes, - lastSeen, - region, - enabled = true, -}: UseAllNeighborsParams) { - return useQuery( - NeighborsService.method.getAllNeighbors, - { - minLat: minLat ?? undefined, - maxLat: maxLat ?? undefined, - minLng: minLng ?? undefined, - maxLng: maxLng ?? undefined, - nodeTypes: nodeTypes ?? [], - lastSeen: lastSeen ?? undefined, - region, - }, - { - select: (res): AllNeighborsConnection[] => - res.neighbors.map((n) => ({ - source_node: n.sourceNode, - target_node: n.targetNode, - connection_type: n.connectionType, - packet_count: n.packetCount, - source_name: n.sourceName, - source_latitude: n.sourceLatitude, - source_longitude: n.sourceLongitude, - source_has_location: n.sourceHasLocation, - target_name: n.targetName, - target_latitude: n.targetLatitude, - target_longitude: n.targetLongitude, - target_has_location: n.targetHasLocation, - })), - enabled, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 15 * 60 * 1000, // 15 minutes - }, - ); -} diff --git a/meshexplorer/src/hooks/useChatMessages.ts b/meshexplorer/src/hooks/useChatMessages.ts index a539c1a..28e0068 100644 --- a/meshexplorer/src/hooks/useChatMessages.ts +++ b/meshexplorer/src/hooks/useChatMessages.ts @@ -4,8 +4,7 @@ import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useMemo } from 'react'; import { Code, ConnectError } from '@connectrpc/connect'; import { chatClient } from '@/lib/connect/client'; -import type { ChatMessage as GenChatMessage } from '@/gen/meshexplorer/v1/chat_pb'; -import { ChatMessage } from '@/components/ChatMessageItem'; +import type { ChatMessage } from '@/gen/meshexplorer/v1/chat_pb'; interface ChatMessagesParams { channelId?: string; @@ -22,38 +21,13 @@ interface ChatMessagesPage { const PAGE_SIZE = 20; -// Maps the generated (camelCase) ChatMessage to the snake_case shape the chat -// components consume. `origins`/`path_len` were never populated by the REST API -// and are unused by the renderer, so they're intentionally omitted. -function toChatMessage(m: GenChatMessage): ChatMessage { - return { - message_id: m.messageId, - ingest_timestamp: m.ingestTimestamp, - mesh_timestamp: m.meshTimestamp, - channel_hash: m.channelHash, - mac: m.mac, - encrypted_message: m.encryptedMessage, - message_count: m.messageCount, - origin_path_info: m.originPathInfo.map( - (o) => - [o.origin, o.originPubkey, o.path, o.broker, o.topic] as [ - string, - string, - string, - string, - string, - ], - ), - } as ChatMessage; -} - // Inserts a single streamed message into the infinite-query cache, de-duping by -// message_id, keeping newest-first order, and re-paginating into PAGE_SIZE pages. +// messageId, keeping newest-first order, and re-paginating into PAGE_SIZE pages. function mergeStreamedMessage(oldData: any, newMessage: ChatMessage) { if (!oldData?.pages?.[0]) return oldData; const all = oldData.pages.flatMap((p: ChatMessagesPage) => p.messages) as ChatMessage[]; - const existingIndex = all.findIndex((m) => m.message_id === newMessage.message_id); + const existingIndex = all.findIndex((m) => m.messageId === newMessage.messageId); let merged: ChatMessage[]; if (existingIndex !== -1) { @@ -64,7 +38,7 @@ function mergeStreamedMessage(oldData: any, newMessage: ChatMessage) { } merged.sort( - (a, b) => new Date(b.ingest_timestamp).getTime() - new Date(a.ingest_timestamp).getTime(), + (a, b) => new Date(b.ingestTimestamp).getTime() - new Date(a.ingestTimestamp).getTime(), ); const pages = []; @@ -111,13 +85,13 @@ export function useChatMessages({ { signal }, ); - const messages = res.messages.map(toChatMessage); + const messages = res.messages; return { messages, hasMore: messages.length === PAGE_SIZE, oldestTimestamp: - messages.length > 0 ? messages[messages.length - 1].ingest_timestamp : undefined, + messages.length > 0 ? messages[messages.length - 1].ingestTimestamp : undefined, }; }, getNextPageParam: (lastPage) => { @@ -152,9 +126,10 @@ export function useChatMessages({ { signal: controller.signal }, ); - for await (const genMsg of stream) { + for await (const resp of stream) { if (cancelled) break; - const msg = toChatMessage(genMsg); + if (!resp.message) continue; + const msg = resp.message; queryClient.setQueryData(baseQueryKey, (oldData: any) => mergeStreamedMessage(oldData, msg), ); diff --git a/meshexplorer/src/hooks/useMeshcoreSearch.ts b/meshexplorer/src/hooks/useMeshcoreSearch.ts index df7f1b0..7477bc5 100644 --- a/meshexplorer/src/hooks/useMeshcoreSearch.ts +++ b/meshexplorer/src/hooks/useMeshcoreSearch.ts @@ -4,43 +4,8 @@ import { nodeClient } from '@/lib/connect/client'; import type { SearchResult } from '@/gen/meshexplorer/v1/node_pb'; import { useMemo } from 'react'; -export interface MeshcoreSearchResult { - public_key: string; - node_name: string; - latitude: number | null; - longitude: number | null; - has_location: number; - is_repeater: number; - is_chat_node: number; - is_room_server: number; - has_name: number; - first_heard: string; - last_seen: string; - broker: string; - topic: string; -} - -// Maps a generated (camelCase) SearchResult to the snake_case shape consumers use. -function toSearchResult(r: SearchResult): MeshcoreSearchResult { - return { - public_key: r.publicKey, - node_name: r.nodeName, - latitude: r.latitude ?? null, - longitude: r.longitude ?? null, - has_location: r.hasLocation, - is_repeater: r.isRepeater, - is_chat_node: r.isChatNode, - is_room_server: r.isRoomServer, - has_name: r.hasName, - first_heard: r.firstHeard, - last_seen: r.lastSeen, - broker: r.broker, - topic: r.topic, - }; -} - export interface MeshcoreSearchResponse { - results: MeshcoreSearchResult[]; + results: SearchResult[]; total: number; } @@ -88,12 +53,12 @@ const searchBatcher = create({ // Return results with batch context for resolver (array-of-arrays, one per query) return { - results: response.results.map((list) => list.results.map(toSearchResult)), + results: response.results.map((list) => list.results), queries: queries }; }, - - resolver: (batchData: {results: MeshcoreSearchResult[][], queries: SearchQuery[]}, query: SearchQuery) => { + + resolver: (batchData: {results: SearchResult[][], queries: SearchQuery[]}, query: SearchQuery) => { const index = batchData.queries.findIndex(q => JSON.stringify(q) === JSON.stringify(query)); return batchData.results[index] || []; }, @@ -146,7 +111,7 @@ export function useMeshcoreSearch({ signal?.addEventListener('abort', handleAbort); try { - const queryResults = await searchBatcher.fetch(searchQuery) as MeshcoreSearchResult[] || []; + const queryResults = await searchBatcher.fetch(searchQuery) as SearchResult[] || []; return { results: queryResults, total: queryResults.length @@ -214,7 +179,7 @@ export function useMeshcoreSearches({ searches }: UseMeshcoreSearchesParams) { signal?.addEventListener('abort', handleAbort); try { - const queryResults = await searchBatcher.fetch(searchQuery) as MeshcoreSearchResult[] || []; + const queryResults = await searchBatcher.fetch(searchQuery) as SearchResult[] || []; return { results: queryResults, total: queryResults.length diff --git a/meshexplorer/src/hooks/useNeighbors.ts b/meshexplorer/src/hooks/useNeighbors.ts index cedb46d..1612c4e 100644 --- a/meshexplorer/src/hooks/useNeighbors.ts +++ b/meshexplorer/src/hooks/useNeighbors.ts @@ -1,19 +1,6 @@ import { useQuery } from '@connectrpc/connect-query'; import { NodeService } from '@/gen/meshexplorer/v1/node_pb'; -export interface Neighbor { - public_key: string; - node_name: string; - latitude: number | null; - longitude: number | null; - has_location: number; - is_repeater: number; - is_chat_node: number; - is_room_server: number; - has_name: number; - directions: string[]; -} - interface UseNeighborsParams { nodeId: string | null; lastSeen?: number | null; @@ -28,19 +15,8 @@ export function useNeighbors({ nodeId, lastSeen, enabled = true }: UseNeighborsP lastSeen: lastSeen ?? undefined, }, { - select: (res): Neighbor[] => - res.neighbors.map((n) => ({ - public_key: n.publicKey, - node_name: n.nodeName, - latitude: n.latitude ?? null, - longitude: n.longitude ?? null, - has_location: n.hasLocation, - is_repeater: n.isRepeater, - is_chat_node: n.isChatNode, - is_room_server: n.isRoomServer, - has_name: n.hasName, - directions: n.directions, - })), + // Unwrap the response to the generated Neighbor[] (no field mapping). + select: (res) => res.neighbors, enabled: enabled && !!nodeId, staleTime: 15 * 60 * 1000, // 15 minutes gcTime: 15 * 60 * 1000, // 15 minutes diff --git a/meshexplorer/src/hooks/useNodeData.ts b/meshexplorer/src/hooks/useNodeData.ts index c714afc..6d3dc60 100644 --- a/meshexplorer/src/hooks/useNodeData.ts +++ b/meshexplorer/src/hooks/useNodeData.ts @@ -1,71 +1,6 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery } from '@connectrpc/connect-query'; import { Code, ConnectError } from '@connectrpc/connect'; -import { nodeClient } from '@/lib/connect/client'; -import type { GetNodeResponse } from '@/gen/meshexplorer/v1/node_pb'; - -export interface NodeInfo { - public_key: string; - node_name: string; - latitude: number | null; - longitude: number | null; - has_location: number; - is_repeater: number; - is_chat_node: number; - is_room_server: number; - has_name: number; - broker: string | null; - topic: string | null; - first_seen: string; - last_seen: string; -} - -export interface Advert { - group_id: number; - origin_path_pubkey_tuples: Array<[string, string, string]>; // Array of [origin, path, origin_pubkey] tuples - advert_count: number; - earliest_timestamp: string; - latest_timestamp: string; - latitude: number | null; - longitude: number | null; - is_repeater: number; - is_chat_node: number; - is_room_server: number; - has_location: number; - packet_hash: string; -} - -export interface LocationHistory { - mesh_timestamp: string; - latitude: number; - longitude: number; -} - -export interface MqttTopic { - topic: string; - broker: string; - last_packet_time: string; - is_recent: boolean; -} - -export interface MqttInfo { - is_uplinked: boolean; - has_packets: boolean; - topics: MqttTopic[]; -} - -export interface NodeData { - node: NodeInfo; - recentAdverts: Advert[]; - locationHistory: LocationHistory[]; - mqtt: MqttInfo; - region: string | null; -} - -export interface NodeError { - error: string; - code: string; - publicKey?: string; -} +import { NodeService } from '@/gen/meshexplorer/v1/node_pb'; interface UseNodeDataParams { publicKey: string | null; @@ -73,107 +8,37 @@ interface UseNodeDataParams { enabled?: boolean; } -// Maps the generated (camelCase) GetNodeResponse to the snake_case NodeData -// shape the page components already consume. -function toNodeData(res: GetNodeResponse): NodeData { - const node = res.node!; - return { - node: { - public_key: node.publicKey, - node_name: node.nodeName, - latitude: node.latitude ?? null, - longitude: node.longitude ?? null, - has_location: node.hasLocation, - is_repeater: node.isRepeater, - is_chat_node: node.isChatNode, - is_room_server: node.isRoomServer, - has_name: node.hasName, - broker: node.broker ?? null, - topic: node.topic ?? null, - first_seen: node.firstSeen, - last_seen: node.lastSeen, - }, - recentAdverts: res.recentAdverts.map((a, index) => ({ - group_id: index, - origin_path_pubkey_tuples: a.originPathPubkeyTuples.map( - (t) => [t.origin, t.path, t.originPubkey] as [string, string, string], - ), - advert_count: a.advertCount, - earliest_timestamp: a.earliestTimestamp, - latest_timestamp: a.latestTimestamp, - latitude: a.latitude ?? null, - longitude: a.longitude ?? null, - is_repeater: a.isRepeater, - is_chat_node: a.isChatNode, - is_room_server: a.isRoomServer, - has_location: a.hasLocation, - packet_hash: a.packetHash, - })), - locationHistory: res.locationHistory.map((l) => ({ - mesh_timestamp: l.meshTimestamp, - latitude: l.latitude, - longitude: l.longitude, - })), - mqtt: { - is_uplinked: res.mqtt?.isUplinked ?? false, - has_packets: res.mqtt?.hasPackets ?? false, - topics: (res.mqtt?.topics ?? []).map((t) => ({ - topic: t.topic, - broker: t.broker, - last_packet_time: t.lastPacketTime, - is_recent: t.isRecent, - })), - }, - region: res.region ?? null, - }; -} - -// Maps a ConnectError to the NodeError shape (with HTTP-like status) the -// node page uses to pick an error icon/title and drive retry behavior. -function toNodeError(err: unknown): NodeError & { status: number } { - if (err instanceof ConnectError) { - switch (err.code) { - case Code.NotFound: - return { error: err.message, code: 'NODE_NOT_FOUND', status: 404 }; - case Code.InvalidArgument: - return { error: err.message, code: 'INVALID_PUBLIC_KEY', status: 400 }; - case Code.Unavailable: - return { error: err.message, code: 'DATABASE_ERROR', status: 503 }; - default: - return { error: err.message, code: 'INTERNAL_ERROR', status: 500 }; - } +// Maps a ConnectError to the string code the node page uses to pick an +// error icon/title/description. (Error-code mapping, not a proto-type mirror.) +export function nodeErrorCode(err: ConnectError | null): string | null { + if (!err) return null; + switch (err.code) { + case Code.NotFound: + return 'NODE_NOT_FOUND'; + case Code.InvalidArgument: + return 'INVALID_PUBLIC_KEY'; + case Code.Unavailable: + return 'DATABASE_ERROR'; + default: + return 'INTERNAL_ERROR'; } - return { error: 'An unexpected error occurred', code: 'UNKNOWN_ERROR', status: 500 }; } export function useNodeData({ publicKey, limit = 50, enabled = true }: UseNodeDataParams) { - return useQuery({ - queryKey: ['node-data', publicKey, limit], - queryFn: async (): Promise => { - if (!publicKey) { - throw { error: 'Public key is required', code: 'MISSING_PUBLIC_KEY', status: 400 } as NodeError & { - status: number; - }; - } - - try { - const res = await nodeClient.getNode({ publicKey, limit }); - return toNodeData(res); - } catch (err) { - throw toNodeError(err); - } + return useQuery( + NodeService.method.getNode, + { publicKey: publicKey ?? '', limit }, + { + enabled: enabled && !!publicKey, + staleTime: 15 * 60 * 1000, // 15 minutes + gcTime: 15 * 60 * 1000, // 15 minutes + retry: (failureCount, error) => { + // Don't retry client errors (bad key / not found). + if (error.code === Code.NotFound || error.code === Code.InvalidArgument) { + return false; + } + return failureCount < 1; + }, }, - enabled: enabled && !!publicKey, - staleTime: 15 * 60 * 1000, // 15 minutes - gcTime: 15 * 60 * 1000, // 15 minutes - retry: (failureCount, error) => { - // Don't retry for client errors (4xx) - const status = error?.status; - if (status && status >= 400 && status < 500) { - return false; - } - // Retry up to 1 time for server errors - return failureCount < 1; - }, - }); + ); } diff --git a/meshexplorer/src/hooks/useStats.ts b/meshexplorer/src/hooks/useStats.ts index a05f5de..6dd8759 100644 --- a/meshexplorer/src/hooks/useStats.ts +++ b/meshexplorer/src/hooks/useStats.ts @@ -2,41 +2,6 @@ import React from "react"; import { useQuery } from "@connectrpc/connect-query"; import { StatsService } from "@/gen/meshexplorer/v1/stats_pb"; -interface TotalNodesResponse { - total_nodes: number; -} - -interface NodesOverTimeRow { - day: string; - cumulative_unique_nodes: number; - nodes_with_location: number; - nodes_without_location: number; - repeaters: number; - room_servers: number; -} - -interface NodesOverTimeResponse { - data: NodesOverTimeRow[]; -} - -interface PopularChannelRow { - channel_hash: string; - message_count: number; -} - -interface PopularChannelsResponse { - data: PopularChannelRow[]; -} - -interface RepeaterPrefixRow { - prefix: string; - node_names: string[]; -} - -interface RepeaterPrefixesResponse { - data: RepeaterPrefixRow[]; -} - const STALE_TIME = 5 * 60 * 1000; // 5 minutes const GC_TIME = 10 * 60 * 1000; // 10 minutes @@ -44,12 +9,7 @@ export function useTotalNodes(region?: string) { return useQuery( StatsService.method.getTotalNodes, { region }, - { - select: (res): TotalNodesResponse => ({ total_nodes: res.totalNodes }), - staleTime: STALE_TIME, - gcTime: GC_TIME, - retry: 2, - }, + { staleTime: STALE_TIME, gcTime: GC_TIME, retry: 2 }, ); } @@ -57,21 +17,7 @@ export function useNodesOverTime(region?: string) { return useQuery( StatsService.method.getNodesOverTime, { region }, - { - select: (res): NodesOverTimeResponse => ({ - data: res.data.map((r) => ({ - day: r.day, - cumulative_unique_nodes: r.cumulativeUniqueNodes, - nodes_with_location: r.nodesWithLocation, - nodes_without_location: r.nodesWithoutLocation, - repeaters: r.repeaters, - room_servers: r.roomServers, - })), - }), - staleTime: STALE_TIME, - gcTime: GC_TIME, - retry: 2, - }, + { staleTime: STALE_TIME, gcTime: GC_TIME, retry: 2 }, ); } @@ -79,17 +25,7 @@ export function usePopularChannels(region?: string) { return useQuery( StatsService.method.getPopularChannels, { region }, - { - select: (res): PopularChannelsResponse => ({ - data: res.data.map((r) => ({ - channel_hash: r.channelHash, - message_count: r.messageCount, - })), - }), - staleTime: STALE_TIME, - gcTime: GC_TIME, - retry: 2, - }, + { staleTime: STALE_TIME, gcTime: GC_TIME, retry: 2 }, ); } @@ -97,17 +33,7 @@ export function useRepeaterPrefixes(region?: string) { return useQuery( StatsService.method.getRepeaterPrefixes, { region }, - { - select: (res): RepeaterPrefixesResponse => ({ - data: res.data.map((r) => ({ - prefix: r.prefix, - node_names: r.nodeNames, - })), - }), - staleTime: STALE_TIME, - gcTime: GC_TIME, - retry: 2, - }, + { staleTime: STALE_TIME, gcTime: GC_TIME, retry: 2 }, ); } @@ -124,10 +50,10 @@ export function useUnusedPrefixes(region?: string) { } // Get used prefixes from the API response - const usedPrefixes = new Set(repeaterPrefixesData.data.map(row => row.prefix)); + const usedPrefixes = new Set(repeaterPrefixesData.data.map((row) => row.prefix)); // Find unused prefixes - return allPrefixes.filter(prefix => !usedPrefixes.has(prefix)); + return allPrefixes.filter((prefix) => !usedPrefixes.has(prefix)); }, [repeaterPrefixesData?.data]); return { diff --git a/meshexplorer/src/server/connect/chat.ts b/meshexplorer/src/server/connect/chat.ts index c4f96aa..09ac1f3 100644 --- a/meshexplorer/src/server/connect/chat.ts +++ b/meshexplorer/src/server/connect/chat.ts @@ -122,7 +122,7 @@ export const chatServiceImpl: ServiceImpl = { for await (const result of streamer(params)) { const row = result.row; const decrypted = req.decrypt ? await decryptRow(row, keys) : null; - yield toChatMessage(row, decrypted); + yield { message: toChatMessage(row, decrypted) }; } }, }; diff --git a/meshexplorer/src/server/connect/packets.ts b/meshexplorer/src/server/connect/packets.ts index 0bccde7..bc1af84 100644 --- a/meshexplorer/src/server/connect/packets.ts +++ b/meshexplorer/src/server/connect/packets.ts @@ -43,18 +43,20 @@ export const packetsServiceImpl: ServiceImpl = { for await (const result of streamer(params)) { const row = result.row; yield { - ingestTimestamp: row.ingest_timestamp, - meshTimestamp: row.mesh_timestamp, - broker: row.broker, - topic: row.topic, - packet: row.packet, - pathLen: num(row.path_len), - path: row.path, - routeType: num(row.route_type), - payloadType: num(row.payload_type), - payloadVersion: num(row.payload_version), - header: num(row.header), - originPubkey: row.origin_pubkey, + packet: { + ingestTimestamp: row.ingest_timestamp, + meshTimestamp: row.mesh_timestamp, + broker: row.broker, + topic: row.topic, + packet: row.packet, + pathLen: num(row.path_len), + path: row.path, + routeType: num(row.route_type), + payloadType: num(row.payload_type), + payloadVersion: num(row.payload_version), + header: num(row.header), + originPubkey: row.origin_pubkey, + }, }; } }, diff --git a/meshexplorer/src/types/map.ts b/meshexplorer/src/types/map.ts deleted file mode 100644 index 2ac685d..0000000 --- a/meshexplorer/src/types/map.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type NodePosition = { - node_id: string; - latitude: number; - longitude: number; - altitude?: number; - last_seen?: string; - first_seen?: string; - type?: string; - short_name?: string; - name?: string | null; -};