mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-08 17:42:46 +02:00
Use generated ConnectRPC types in the web UI; satisfy buf lint
Remove the snake_case mirror interfaces and mapper functions that translated generated protobuf-es messages for the components, and have the UI consume the generated types directly (single source of truth = the proto schema). - Hooks (useStats/useNeighbors/useNodeData/useMeshcoreSearch/useChatMessages) return generated message types; delete toNodeData/toChatMessage/toSearchResult and the snake_case select/.map adapters. Delete the dead useAllNeighbors hook and src/types/map.ts; components import NodePosition/Neighbor/NeighborEdge/ Advert/SearchResult/ChatMessage from src/gen. - Components read camelCase fields (node page, AdvertDetails, MapView, MapIcons, stats page, search results, chat). useNodeData surfaces ConnectError with a nodeErrorCode() helper for the error UI. Drop the always-empty Alt row in the map popup (generated NodePosition has no altitude). - buf lint (Option A): give each StatsService RPC its own request message, and wrap the server-streaming responses (StreamChatResponse / StreamPacketsResponse). Server stream handlers + the chat stream consumer updated accordingly. - Add a buf CI workflow (lint/format/breaking on proto changes). tsc, next build, and buf lint are clean; verified end-to-end in Docker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 }}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-800 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@@ -196,7 +197,7 @@ export default function MeshcoreNodePage() {
|
||||
const { node, recentAdverts, locationHistory, mqtt, region } = nodeData;
|
||||
|
||||
return (
|
||||
<RegionProvider region={region}>
|
||||
<RegionProvider region={region ?? null}>
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-800 py-8">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
@@ -205,15 +206,15 @@ export default function MeshcoreNodePage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{node.has_name ? getNameIconLabel(node.node_name) : "Unknown Node"}
|
||||
{node.hasName ? getNameIconLabel(node.nodeName) : "Unknown Node"}
|
||||
</h1>
|
||||
{node.has_name && (
|
||||
{node.hasName && (
|
||||
<p className="text-lg text-gray-700 dark:text-gray-300 mb-2">
|
||||
{node.node_name}
|
||||
{node.nodeName}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-gray-600 dark:text-gray-300 font-mono text-sm">
|
||||
{formatPublicKey(node.public_key)}
|
||||
{formatPublicKey(node.publicKey)}
|
||||
</p>
|
||||
{region && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
@@ -221,22 +222,22 @@ export default function MeshcoreNodePage() {
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{node.is_repeater && (
|
||||
{node.isRepeater && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
Repeater
|
||||
</span>
|
||||
) || null}
|
||||
{node.is_chat_node && (
|
||||
{node.isChatNode && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Companion
|
||||
</span>
|
||||
) || null}
|
||||
{node.is_room_server && (
|
||||
{node.isRoomServer && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
||||
Room Server
|
||||
</span>
|
||||
) || null}
|
||||
{!node.is_repeater && !node.is_chat_node && !node.is_room_server && (
|
||||
{!node.isRepeater && !node.isChatNode && !node.isRoomServer && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Unknown
|
||||
</span>
|
||||
@@ -245,8 +246,8 @@ export default function MeshcoreNodePage() {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<ContactQRCode
|
||||
name={node.has_name ? node.node_name : "Unknown Node"}
|
||||
publicKey={node.public_key}
|
||||
name={node.hasName ? node.nodeName : "Unknown Node"}
|
||||
publicKey={node.publicKey}
|
||||
type={getNodeType(node)}
|
||||
size={150}
|
||||
/>
|
||||
@@ -265,7 +266,7 @@ export default function MeshcoreNodePage() {
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Public Key</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
|
||||
{node.public_key}
|
||||
{node.publicKey}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
@@ -273,10 +274,10 @@ export default function MeshcoreNodePage() {
|
||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
{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
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">
|
||||
{moment.utc(node.first_seen).local().fromNow()}
|
||||
{moment.utc(node.firstSeen).local().fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
@@ -286,10 +287,10 @@ export default function MeshcoreNodePage() {
|
||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
{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
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">
|
||||
{moment.utc(node.last_seen).local().fromNow()}
|
||||
{moment.utc(node.lastSeen).local().fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
@@ -297,7 +298,7 @@ export default function MeshcoreNodePage() {
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Current Location</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
{node.has_location && node.latitude && node.longitude ? (
|
||||
{node.hasLocation && node.latitude && node.longitude ? (
|
||||
<span>
|
||||
{node.latitude.toFixed(6)}, {node.longitude.toFixed(6)}
|
||||
</span>
|
||||
@@ -311,11 +312,11 @@ export default function MeshcoreNodePage() {
|
||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
mqtt.is_uplinked
|
||||
mqtt.isUplinked
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
}`}>
|
||||
{mqtt.is_uplinked ? 'Connected' : 'Not Connected'}
|
||||
{mqtt.isUplinked ? 'Connected' : 'Not Connected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -331,10 +332,10 @@ export default function MeshcoreNodePage() {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-gray-900 dark:text-gray-100">
|
||||
{moment.utc(topic.last_packet_time).format('MM-DD HH:mm')}
|
||||
{moment.utc(topic.lastPacketTime).format('MM-DD HH:mm')}
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">
|
||||
{moment.utc(topic.last_packet_time).local().fromNow()}
|
||||
{moment.utc(topic.lastPacketTime).local().fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,7 +363,7 @@ export default function MeshcoreNodePage() {
|
||||
</div>
|
||||
) : (
|
||||
recentAdverts.map((advert) => (
|
||||
<AdvertDetails key={advert.group_id} advert={advert} initiatingNodeKey={node.public_key} />
|
||||
<AdvertDetails key={advert.packetHash} advert={advert} initiatingNodeKey={node.publicKey} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
@@ -387,7 +388,7 @@ export default function MeshcoreNodePage() {
|
||||
{locationHistory.map((location, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{moment.utc(location.mesh_timestamp).format('MM-DD HH:mm:ss')}
|
||||
{moment.utc(location.meshTimestamp).format('MM-DD HH:mm:ss')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{location.latitude.toFixed(6)}
|
||||
@@ -434,23 +435,23 @@ export default function MeshcoreNodePage() {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{neighbors.map((neighbor) => (
|
||||
<div key={neighbor.public_key} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors">
|
||||
<div key={neighbor.publicKey} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{neighbor.has_name ? getNameIconLabel(neighbor.node_name) : "Unknown Node"}
|
||||
{neighbor.hasName ? getNameIconLabel(neighbor.nodeName) : "Unknown Node"}
|
||||
</h3>
|
||||
{neighbor.has_name && (
|
||||
{neighbor.hasName && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 truncate">
|
||||
{neighbor.node_name}
|
||||
{neighbor.nodeName}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 font-mono truncate">
|
||||
{formatPublicKey(neighbor.public_key)}
|
||||
{formatPublicKey(neighbor.publicKey)}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={`/meshcore/node/${neighbor.public_key}`}
|
||||
href={`/meshcore/node/${neighbor.publicKey}`}
|
||||
className="ml-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-xs font-medium"
|
||||
>
|
||||
View →
|
||||
@@ -458,17 +459,17 @@ export default function MeshcoreNodePage() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{neighbor.is_repeater && (
|
||||
{neighbor.isRepeater && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
Repeater
|
||||
</span>
|
||||
) || null}
|
||||
{neighbor.is_chat_node && (
|
||||
{neighbor.isChatNode && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Companion
|
||||
</span>
|
||||
) || null}
|
||||
{neighbor.is_room_server && (
|
||||
{neighbor.isRoomServer && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
||||
Room
|
||||
</span>
|
||||
@@ -476,7 +477,7 @@ export default function MeshcoreNodePage() {
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||
{neighbor.has_location && neighbor.latitude && neighbor.longitude && (
|
||||
{neighbor.hasLocation && neighbor.latitude && neighbor.longitude && (
|
||||
<div>
|
||||
Location: {neighbor.latitude.toFixed(4)}, {neighbor.longitude.toFixed(4)}
|
||||
</div>
|
||||
|
||||
@@ -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) => (
|
||||
<tr key={i} className="border-t">
|
||||
<td className="border px-3 py-2 text-center min-w-[120px]">{row.day}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.cumulative_unique_nodes}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.nodes_with_location}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.nodes_without_location}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.cumulativeUniqueNodes}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.nodesWithLocation}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.nodesWithoutLocation}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.repeaters}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.room_servers}</td>
|
||||
<td className="border px-3 py-2 text-center">{row.roomServers}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -158,8 +158,8 @@ export default function StatsPage() {
|
||||
<tbody>
|
||||
{popularChannels.map((row, i) => (
|
||||
<tr key={i}>
|
||||
<td className="border px-2 py-1">{row.channel_hash}</td>
|
||||
<td className="border px-2 py-1">{row.message_count}</td>
|
||||
<td className="border px-2 py-1">{row.channelHash}</td>
|
||||
<td className="border px-2 py-1">{row.messageCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -186,9 +186,9 @@ export default function StatsPage() {
|
||||
<tr key={i}>
|
||||
<td className="border px-2 py-1 font-mono">{row.prefix}</td>
|
||||
<td className="border px-2 py-1">
|
||||
{row.node_names && row.node_names.length > 0 ? (
|
||||
{row.nodeNames && row.nodeNames.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{row.node_names.map((name: string, j: number) => (
|
||||
{row.nodeNames.map((name: string, j: number) => (
|
||||
<div key={j} className="text-xs">
|
||||
{name || 'Unnamed Node'}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
@@ -39,28 +27,28 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{moment.utc(advert.earliest_timestamp).format('MM-DD HH:mm:ss')}
|
||||
{moment.utc(advert.earliestTimestamp).format('MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{timeRange}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Heard {advert.advert_count} time{advert.advert_count !== 1 ? 's' : ''}
|
||||
Heard {advert.advertCount} time{advert.advertCount !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex space-x-1">
|
||||
{advert.is_repeater && (
|
||||
{advert.isRepeater && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
R
|
||||
</span>
|
||||
) || null}
|
||||
{advert.is_chat_node && (
|
||||
{advert.isChatNode && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
C
|
||||
</span>
|
||||
) || null}
|
||||
{advert.is_room_server && (
|
||||
{advert.isRoomServer && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
||||
S
|
||||
</span>
|
||||
@@ -92,19 +80,19 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai
|
||||
{/* Path details */}
|
||||
<div>
|
||||
<PathVisualization
|
||||
paths={advert.origin_path_pubkey_tuples.map(([origin, path, origin_pubkey], index) => ({
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location details */}
|
||||
{advert.has_location && advert.latitude && advert.longitude && (
|
||||
{advert.hasLocation && advert.latitude && advert.longitude && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
Location
|
||||
@@ -122,14 +110,14 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai
|
||||
</h4>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">Earliest:</span> {moment.utc(advert.earliest_timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC
|
||||
<span className="font-medium">Earliest:</span> {moment.utc(advert.earliestTimestamp).format('YYYY-MM-DD HH:mm:ss')} UTC
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Latest:</span> {moment.utc(advert.latest_timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC
|
||||
<span className="font-medium">Latest:</span> {moment.utc(advert.latestTimestamp).format('YYYY-MM-DD HH:mm:ss')} UTC
|
||||
</div>
|
||||
{advert.earliest_timestamp !== advert.latest_timestamp && (
|
||||
{advert.earliestTimestamp !== advert.latestTimestamp && (
|
||||
<div>
|
||||
<span className="font-medium">Duration:</span> {moment.utc(advert.latest_timestamp).diff(moment.utc(advert.earliest_timestamp), 'seconds')} seconds
|
||||
<span className="font-medium">Duration:</span> {moment.utc(advert.latestTimestamp).diff(moment.utc(advert.earliestTimestamp), 'seconds')} seconds
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -141,22 +129,22 @@ export default function AdvertDetails({ advert, initiatingNodeKey }: AdvertDetai
|
||||
Node Capabilities
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{advert.is_repeater && (
|
||||
{advert.isRepeater && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
Repeater
|
||||
</span>
|
||||
) || null}
|
||||
{advert.is_chat_node && (
|
||||
{advert.isChatNode && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Companion
|
||||
</span>
|
||||
) || null}
|
||||
{advert.is_room_server && (
|
||||
{advert.isRoomServer && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
||||
Room Server
|
||||
</span>
|
||||
) || null}
|
||||
{!advert.is_repeater && !advert.is_chat_node && !advert.is_room_server && (
|
||||
{!advert.isRepeater && !advert.isChatNode && !advert.isRoomServer && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Unknown
|
||||
</span>
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function ChatBox({
|
||||
{/* Messages */}
|
||||
{(startExpanded ? messages : messages.toReversed()).map((msg, i) => (
|
||||
<ChatMessageItem
|
||||
key={`${msg.message_id}-${msg.origin_path_info?.length || 0}`}
|
||||
key={`${msg.messageId}-${msg.originPathInfo?.length || 0}`}
|
||||
msg={msg}
|
||||
showErrorRow={selectedKey.isAllMessages}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
<div className="text-xs text-gray-400 flex items-center gap-2">
|
||||
{formatLocalTime(new Date(parsed.timestamp * 1000).toISOString())}
|
||||
<span className="text-xs text-gray-500">type: {parsed.msgType}</span>
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channel_hash}</span>
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channelHash}</span>
|
||||
</div>
|
||||
<div className="break-words whitespace-pre-wrap">
|
||||
{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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -174,8 +162,8 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow
|
||||
return (
|
||||
<div className="border-b border-red-200 dark:border-red-800 pb-2 mb-2 bg-red-50 dark:bg-red-900/30">
|
||||
<div className="text-xs text-gray-400 flex items-center gap-2">
|
||||
{formatLocalTime(msg.ingest_timestamp)}
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channel_hash}</span>
|
||||
{formatLocalTime(msg.ingestTimestamp)}
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channelHash}</span>
|
||||
</div>
|
||||
<div className="text-xs text-red-600 dark:text-red-300">
|
||||
{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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -197,15 +185,15 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow
|
||||
return (
|
||||
<div className="border-b border-gray-200 dark:border-neutral-800 pb-2 mb-2">
|
||||
<div className="text-xs text-gray-400 flex items-center gap-2">
|
||||
{formatLocalTime(msg.ingest_timestamp)}
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channel_hash}</span>
|
||||
{formatLocalTime(msg.ingestTimestamp)}
|
||||
<span className="text-xs text-gray-500 ml-2">channel: {msg.channelHash}</span>
|
||||
</div>
|
||||
<div className="w-full h-5 bg-gray-200 dark:bg-neutral-800 rounded animate-pulse my-2" />
|
||||
<PathVisualization
|
||||
paths={pathData}
|
||||
title={`Heard ${pathData.length} repeat${pathData.length !== 1 ? 's' : ''}`}
|
||||
className="text-xs"
|
||||
packetHash={msg.message_id}
|
||||
packetHash={msg.messageId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
@@ -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 (
|
||||
<div className="custom-node-marker-container">
|
||||
{showNodeNames && node.short_name && (
|
||||
{showNodeNames && node.shortName && (
|
||||
<div className="custom-node-label">
|
||||
{getNameIconLabel(node.name || node.short_name)}
|
||||
{getNameIconLabel(node.name || node.shortName)}
|
||||
</div>
|
||||
)}
|
||||
<div className={getMarkerClass()}></div>
|
||||
@@ -101,32 +101,31 @@ export function ClusterMarker({ children }: ClusterMarkerProps) {
|
||||
export function PopupContent({ node, target = '_self' }: PopupContentProps) {
|
||||
return (
|
||||
<div>
|
||||
<div><b>ID:</b> {formatPublicKey(node.node_id)}</div>
|
||||
<div><b>ID:</b> {formatPublicKey(node.nodeId)}</div>
|
||||
<div><b>Full Name:</b> {node.name ?? "-"}</div>
|
||||
<div><b>Short Name:</b> {node.short_name ? getNameIconLabel(node.name || node.short_name) : "-"}</div>
|
||||
<div><b>Short Name:</b> {node.shortName ? getNameIconLabel(node.name || node.shortName) : "-"}</div>
|
||||
<div><b>Type:</b> {node.type ?? "-"}</div>
|
||||
<div><b>Lat:</b> {node.latitude}</div>
|
||||
<div><b>Lng:</b> {node.longitude}</div>
|
||||
<div><b>Alt:</b> {node.altitude !== undefined ? node.altitude : "-"}</div>
|
||||
{node.last_seen ? (
|
||||
{node.lastSeen ? (
|
||||
<div>
|
||||
<b>Last seen:</b> {moment.utc(node.last_seen).format('YYYY-MM-DD HH:mm:ss')} <span style={{color: '#888'}}>(UTC)</span><br/>
|
||||
<span style={{color: '#888'}}>{moment.utc(node.last_seen).local().fromNow()}</span>
|
||||
<b>Last seen:</b> {moment.utc(node.lastSeen).format('YYYY-MM-DD HH:mm:ss')} <span style={{color: '#888'}}>(UTC)</span><br/>
|
||||
<span style={{color: '#888'}}>{moment.utc(node.lastSeen).local().fromNow()}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div><b>Last seen:</b> -</div>
|
||||
)}
|
||||
{node.first_seen ? (
|
||||
{node.firstSeen ? (
|
||||
<div>
|
||||
<b>First seen:</b> {moment.utc(node.first_seen).format('YYYY-MM-DD HH:mm:ss')} <span style={{color: '#888'}}>(UTC)</span><br/>
|
||||
<span style={{color: '#888'}}>{moment.utc(node.first_seen).local().fromNow()}</span>
|
||||
<b>First seen:</b> {moment.utc(node.firstSeen).format('YYYY-MM-DD HH:mm:ss')} <span style={{color: '#888'}}>(UTC)</span><br/>
|
||||
<span style={{color: '#888'}}>{moment.utc(node.firstSeen).local().fromNow()}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div><b>First seen:</b> -</div>
|
||||
)}
|
||||
<div style={{marginTop: '8px', paddingTop: '8px', borderTop: '1px solid #e5e7eb'}}>
|
||||
<a
|
||||
href={`/meshcore/node/${node.node_id}`}
|
||||
href={`/meshcore/node/${node.nodeId}`}
|
||||
target={target}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
|
||||
@@ -15,9 +15,10 @@ import { NodeMarker, ClusterMarker, PopupContent } from "./MapIcons";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { Code, ConnectError } from "@connectrpc/connect";
|
||||
import { mapClient } from "@/lib/connect/client";
|
||||
import { NodePosition } from "@/types/map";
|
||||
import { useNeighbors, type Neighbor } from "@/hooks/useNeighbors";
|
||||
import { type AllNeighborsConnection } from "@/hooks/useAllNeighbors";
|
||||
import type { NodePosition } from "@/gen/meshexplorer/v1/map_pb";
|
||||
import type { Neighbor } from "@/gen/meshexplorer/v1/node_pb";
|
||||
import type { NeighborEdge } from "@/gen/meshexplorer/v1/common_pb";
|
||||
import { useNeighbors } from "@/hooks/useNeighbors";
|
||||
import { useQueryParams } from "@/hooks/useQueryParams";
|
||||
import { useMapPosition } from "@/hooks/useMapPosition";
|
||||
|
||||
@@ -67,7 +68,7 @@ const IndividualMarker = React.memo(function IndividualMarker({
|
||||
useEffect(() => {
|
||||
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) => (
|
||||
<IndividualMarker
|
||||
key={node.node_id}
|
||||
key={node.nodeId}
|
||||
node={node}
|
||||
showNodeNames={showNodeNames}
|
||||
selectedNodeId={selectedNodeId}
|
||||
@@ -312,15 +313,15 @@ function NeighborLines({
|
||||
if (!selectedNodeId || neighbors.length === 0) return null;
|
||||
|
||||
// Find the selected node's position
|
||||
const selectedNode = nodes.find(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 (
|
||||
<Polyline
|
||||
key={`${selectedNodeId}-${neighbor.public_key}`}
|
||||
key={`${selectedNodeId}-${neighbor.publicKey}`}
|
||||
positions={positions}
|
||||
pathOptions={{
|
||||
color: lineColor,
|
||||
@@ -370,7 +371,7 @@ function AllNeighborLines({
|
||||
minPacketCount = 1,
|
||||
strokeWidth = 2
|
||||
}: {
|
||||
connections: AllNeighborsConnection[];
|
||||
connections: NeighborEdge[];
|
||||
nodes: NodePosition[];
|
||||
useColors?: boolean;
|
||||
minPacketCount?: number;
|
||||
@@ -379,19 +380,19 @@ function AllNeighborLines({
|
||||
if (connections.length === 0) return null;
|
||||
|
||||
// Create a set of visible node IDs for quick lookup
|
||||
const visibleNodeIds = new Set(nodes.map(node => 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 (
|
||||
<Polyline
|
||||
key={`${connection.source_node}-${connection.target_node}-${connection.connection_type}`}
|
||||
key={`${connection.sourceNode}-${connection.targetNode}-${connection.connectionType}`}
|
||||
positions={positions}
|
||||
pathOptions={{
|
||||
color: lineColor,
|
||||
@@ -518,7 +519,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
// Neighbor-related state
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
const [showAllNeighbors, setShowAllNeighbors] = useState<boolean>(false);
|
||||
const [allNeighborConnections, setAllNeighborConnections] = useState<AllNeighborsConnection[]>([]);
|
||||
const [allNeighborConnections, setAllNeighborConnections] = useState<NeighborEdge[]>([]);
|
||||
const [allNeighborsLoading, setAllNeighborsLoading] = useState<boolean>(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];
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className="flex flex-col w-screen overflow-hidden"
|
||||
|
||||
@@ -4,44 +4,30 @@ import { MapPinIcon, WifiIcon, ChatBubbleLeftRightIcon, ServerIcon } from '@hero
|
||||
import Link from 'next/link';
|
||||
import moment from 'moment';
|
||||
import { formatPublicKey } from '@/lib/meshcore';
|
||||
import { MeshcoreSearchResult } from '@/hooks/useMeshcoreSearch';
|
||||
|
||||
export interface NodeCardData {
|
||||
public_key: string;
|
||||
node_name: string | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
has_location: number;
|
||||
is_repeater: number;
|
||||
is_chat_node: number;
|
||||
is_room_server: number;
|
||||
last_seen: string;
|
||||
topic?: string;
|
||||
broker?: string;
|
||||
}
|
||||
import type { SearchResult } from '@/gen/meshexplorer/v1/node_pb';
|
||||
|
||||
interface NodeCardProps {
|
||||
node: NodeCardData | MeshcoreSearchResult;
|
||||
node: SearchResult;
|
||||
className?: string;
|
||||
showTopicInfo?: boolean;
|
||||
}
|
||||
|
||||
export default function NodeCard({ node, className = "", showTopicInfo = true }: NodeCardProps) {
|
||||
const hasLocation = node.has_location === 1;
|
||||
const isRepeater = node.is_repeater === 1;
|
||||
const isChatNode = node.is_chat_node === 1;
|
||||
const isRoomServer = node.is_room_server === 1;
|
||||
const hasLocation = node.hasLocation === 1;
|
||||
const isRepeater = node.isRepeater === 1;
|
||||
const isChatNode = node.isChatNode === 1;
|
||||
const isRoomServer = node.isRoomServer === 1;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/meshcore/node/${node.public_key}`}
|
||||
href={`/meshcore/node/${node.publicKey}`}
|
||||
className={`block bg-white dark:bg-neutral-800 rounded-lg border border-gray-200 dark:border-neutral-700 p-4 hover:shadow-md dark:hover:shadow-lg transition-shadow ${className}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{node.node_name || 'Unnamed Node'}
|
||||
{node.nodeName || 'Unnamed Node'}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1">
|
||||
{isRepeater && (
|
||||
@@ -76,9 +62,9 @@ export default function NodeCard({ node, className = "", showTopicInfo = true }:
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span>Last seen: {moment.utc(node.last_seen).local().fromNow()}</span>
|
||||
<span>Last seen: {moment.utc(node.lastSeen).local().fromNow()}</span>
|
||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-500">
|
||||
{formatPublicKey(node.public_key)}
|
||||
{formatPublicKey(node.publicKey)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
<div className="space-y-3">
|
||||
{results.map((node) => (
|
||||
<NodeCard key={node.public_key} node={node} />
|
||||
<NodeCard key={node.publicKey} node={node} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<NodeData, NodeError & { status: number }>({
|
||||
queryKey: ['node-data', publicKey, limit],
|
||||
queryFn: async (): Promise<NodeData> => {
|
||||
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;
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -122,7 +122,7 @@ export const chatServiceImpl: ServiceImpl<typeof ChatService> = {
|
||||
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) };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,18 +43,20 @@ export const packetsServiceImpl: ServiceImpl<typeof PacketsService> = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user