mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-06-27 13:20:58 +02:00
Migrate API to ConnectRPC with protovalidate
Put a schema-first ConnectRPC contract in front of the API, served alongside the existing REST routes (kept live for a gradual cutover). - Proto: meshexplorer.v1 services (Map, Node, Neighbors, Stats, Chat, Packets) in proto/, generated to src/gen via buf + protoc-gen-es. Request messages carry buf.validate rules. - Server: per-service handlers in src/server/connect/ reuse the existing ClickHouse actions/streamers; mounted via @connectrpc/connect-next at src/pages/api/[[...connect]].ts. A protovalidate interceptor enforces the buf.validate rules on every request before handlers run. - Client: connect-web transport + connect-query TransportProvider. Simple hooks (stats, neighbors, all-neighbors) use connect-query; node/search/chat and the map's imperative fetch use generated promise clients. Hooks map the generated camelCase messages back to the existing snake_case shapes so components are unchanged. - Chat: live updates now come from the StreamChat server-streaming RPC (history still paged via GetChat), replacing the 5s polling query. - Two SSE endpoints become Connect server-streaming (StreamChat, StreamPackets). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
version: v2
|
||||
clean: true
|
||||
plugins:
|
||||
- local: protoc-gen-es
|
||||
out: src/gen
|
||||
opt:
|
||||
- target=ts
|
||||
- import_extension=none
|
||||
@@ -0,0 +1,6 @@
|
||||
# Generated by buf. DO NOT EDIT.
|
||||
version: v2
|
||||
deps:
|
||||
- name: buf.build/bufbuild/protovalidate
|
||||
commit: 50325440f8f24053b047484a6bf60b76
|
||||
digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31
|
||||
@@ -0,0 +1,11 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: proto
|
||||
deps:
|
||||
- buf.build/bufbuild/protovalidate
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
Generated
+334
-3
@@ -8,7 +8,13 @@
|
||||
"name": "meshexplorer",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.9.0",
|
||||
"@bufbuild/protovalidate": "^1.2.0",
|
||||
"@clickhouse/client": "^1.11.2",
|
||||
"@connectrpc/connect": "^2.1.0",
|
||||
"@connectrpc/connect-next": "^2.1.0",
|
||||
"@connectrpc/connect-query": "^2.1.0",
|
||||
"@connectrpc/connect-web": "^2.1.0",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@tanstack/react-query": "^5.87.1",
|
||||
@@ -35,6 +41,8 @@
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.50.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.9.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/aes-js": "^3.1.4",
|
||||
@@ -99,6 +107,240 @@
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.70.0.tgz",
|
||||
"integrity": "sha512-oJWGqltlu8F7VVNHLoJ3pFXhjfiGpbh7+/mXW0y+VMPWFGxc9YDv4de1UcX7zhhjV6MbE4SiEGo5Gs5jhpVg5A==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"buf": "bin/buf",
|
||||
"protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking",
|
||||
"protoc-gen-buf-lint": "bin/protoc-gen-buf-lint"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@bufbuild/buf-darwin-arm64": "1.70.0",
|
||||
"@bufbuild/buf-darwin-x64": "1.70.0",
|
||||
"@bufbuild/buf-linux-aarch64": "1.70.0",
|
||||
"@bufbuild/buf-linux-armv7": "1.70.0",
|
||||
"@bufbuild/buf-linux-x64": "1.70.0",
|
||||
"@bufbuild/buf-win32-arm64": "1.70.0",
|
||||
"@bufbuild/buf-win32-x64": "1.70.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-darwin-arm64": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.70.0.tgz",
|
||||
"integrity": "sha512-c7owUswBbMmwfHPH9JRBEJu09mrXYGC33V2JQCgraWCBm74Z95AOkhDua50qiBrQnysvJkJ0p/z4MWxJqcpnIA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-darwin-x64": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.70.0.tgz",
|
||||
"integrity": "sha512-sucV3lQXVuOqYs3+ToulkUh2tZuMnl286DKb44imp3PnexVhAVOP7d3ybYe98HNGwysEdjNP2WIOGb0uKuRCIQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-linux-aarch64": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.70.0.tgz",
|
||||
"integrity": "sha512-4viSYqbhIusd6LR+JayDex8S1rLUL+hTUMYUgSPl75EC93FpJM4vkk2RhoAhyjQqWF/JQLcyWV8kjRRiIwygdg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-linux-armv7": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.70.0.tgz",
|
||||
"integrity": "sha512-GqujpTX4MXtYiUkxd6oI1g0JaCX3L6koT16Gl0D0HIQ/V2mptH7x4UW8nK3tAURMjrHsEEhcSJRtmfINTTKnsg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-linux-x64": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.70.0.tgz",
|
||||
"integrity": "sha512-5WHGUIb5iLFXcnqV33TDejqaPgx0CWFaYW7b4wh12wT0w3DR+ghFq6S6RmYyZLbTuhS4ZFsf+xyk5m+HViKxrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-win32-arm64": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.70.0.tgz",
|
||||
"integrity": "sha512-dU1qh7iD08/1avCHwIOoGsatQctE6uGwgOue9GOaThi8/Rdy1x9CC/eFdyFSeCMwbUg9ABQvMGUocylfUe6xDw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-win32-x64": {
|
||||
"version": "1.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.70.0.tgz",
|
||||
"integrity": "sha512-iKYbjTbEk0ppkv2SrsPFhYus93kj/aMN8aRsrpuo91ZVqXg8JcH4XXbgFpwiFAsiABjqKICFfnDomrFvv49UOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/cel": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/cel/-/cel-0.4.0.tgz",
|
||||
"integrity": "sha512-CdW/JgiTJCYXqnwuaJRo7NcoYhR37AaF58MMiog0/t8nudn86ZyLXYaA1f2yGhm2U17h8pKGZNksTHVSTcpmAw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/cel-spec": "0.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/cel-spec": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/cel-spec/-/cel-spec-0.4.0.tgz",
|
||||
"integrity": "sha512-dUS6f2fNt6KEumsYGE7YFxERZE5ZuyME1hQmGjtO8tkZhR6ow6/ne3v4Gik9cfdb9lSLK3AJ+vDxCdGWmDbWvA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz",
|
||||
"integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@bufbuild/protoc-gen-es": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.12.0.tgz",
|
||||
"integrity": "sha512-d9htF6jEkSwPbp9d/vSmZOBF7eeG18AvTMKmVg4I23afnrQOxL2w3WOXa9TaufMCyu24QakEUb4vux8apI5e7A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "2.12.0",
|
||||
"@bufbuild/protoplugin": "2.12.0"
|
||||
},
|
||||
"bin": {
|
||||
"protoc-gen-es": "bin/protoc-gen-es"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "2.12.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@bufbuild/protobuf": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.12.0.tgz",
|
||||
"integrity": "sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "2.12.0",
|
||||
"@typescript/vfs": "^1.6.2",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin/node_modules/typescript": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protovalidate": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protovalidate/-/protovalidate-1.2.0.tgz",
|
||||
"integrity": "sha512-tD08DwGrHIV88khLz1Kdz9DYUwEo1TsoepaIg7/B/zd//AymaTx67kvCS1jlkcG/+vgQaSOl6f0HCE3sL8vI7w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/cel": "0.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clickhouse/client": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.11.2.tgz",
|
||||
@@ -115,6 +357,81 @@
|
||||
"resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.11.2.tgz",
|
||||
"integrity": "sha512-H4ECHqaipzMgiZKqpb1Z4N3Ofq+lVTCn8I59XsSynqrsfR4jWZD3PipXVvIzMpDmTMvrlJWrOwAdm0DMNiMQbA=="
|
||||
},
|
||||
"node_modules/@connectrpc/connect": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.1.tgz",
|
||||
"integrity": "sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-next": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-next/-/connect-next-2.1.1.tgz",
|
||||
"integrity": "sha512-Ch3CmGas7QngT5P2DzS/8xUolZPaN2/EHdfl8F+Bsf6KVi9TZ3wIIVw2879e669xEB3ck7SYv6zVLZ9fkUOa0Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.7.0",
|
||||
"@connectrpc/connect": "2.1.1",
|
||||
"@connectrpc/connect-node": "2.1.1",
|
||||
"next": "^13.2.4 || ^14.2.5 || ^15.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-node": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-2.1.1.tgz",
|
||||
"integrity": "sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.7.0",
|
||||
"@connectrpc/connect": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-query": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-query/-/connect-query-2.2.0.tgz",
|
||||
"integrity": "sha512-oQ+coXOwBmfl4/t6EOrTfzW0zdoGDe3kvUYqZHrbzORkRFd693Cz8PxuDBjRCjmBPhDRCQMxFhR1jn2+SbgEKQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@connectrpc/connect-query-core": "^2.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "2.x",
|
||||
"@connectrpc/connect": "^2.0.1",
|
||||
"@tanstack/react-query": ">=5.62.0",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-query-core": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-query-core/-/connect-query-core-2.2.0.tgz",
|
||||
"integrity": "sha512-t/CuxW/vP84y2iyS+PnbAnBwgOTYMzHXTSoBUKC1vIn706aNiZP40Y6mGJybglyH63RhAPcOdUgzG7DjzaAHCw==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "2.x",
|
||||
"@connectrpc/connect": "^2.0.1",
|
||||
"@tanstack/query-core": ">=5.62.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-web": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-2.1.1.tgz",
|
||||
"integrity": "sha512-J8317Q2MaFRCT1jzVR1o06bZhDIBmU0UAzWx6xOIXzOq8+k71/+k7MUF7AwcBUX+34WIvbm5syRgC5HXQA8fOg==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.7.0",
|
||||
"@connectrpc/connect": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
@@ -2266,6 +2583,19 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/vfs": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz",
|
||||
"integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.10.1.tgz",
|
||||
@@ -3325,10 +3655,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
||||
@@ -7,11 +7,18 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"generate": "buf generate --include-imports",
|
||||
"discord-bot": "tsx scripts/discord-bot.ts",
|
||||
"discord-bot:dev": "tsx watch scripts/discord-bot.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.9.0",
|
||||
"@bufbuild/protovalidate": "^1.2.0",
|
||||
"@clickhouse/client": "^1.11.2",
|
||||
"@connectrpc/connect": "^2.1.0",
|
||||
"@connectrpc/connect-next": "^2.1.0",
|
||||
"@connectrpc/connect-query": "^2.1.0",
|
||||
"@connectrpc/connect-web": "^2.1.0",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@tanstack/react-query": "^5.87.1",
|
||||
@@ -38,6 +45,8 @@
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/buf": "^1.50.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.9.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/aes-js": "^3.1.4",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
|
||||
// One (origin, origin_pubkey, path, broker, topic) reception tuple.
|
||||
message OriginPathInfo {
|
||||
string origin = 1;
|
||||
string origin_pubkey = 2;
|
||||
string path = 3;
|
||||
string broker = 4;
|
||||
string topic = 5;
|
||||
}
|
||||
|
||||
// A parsed/decrypted public-channel message (parseMeshcoreGroupMessage).
|
||||
message DecryptedChat {
|
||||
double timestamp = 1;
|
||||
int32 msg_type = 2;
|
||||
string sender = 3;
|
||||
string text = 4;
|
||||
string raw_text = 5;
|
||||
}
|
||||
|
||||
message ChatMessage {
|
||||
string ingest_timestamp = 1;
|
||||
string mesh_timestamp = 2;
|
||||
string channel_hash = 3;
|
||||
string mac = 4;
|
||||
// Hex-encoded ciphertext.
|
||||
string encrypted_message = 5;
|
||||
int32 message_count = 6;
|
||||
repeated OriginPathInfo origin_path_info = 7;
|
||||
string message_id = 8;
|
||||
// Present only when decrypt was requested and decryption succeeded.
|
||||
optional DecryptedChat decrypted = 9;
|
||||
}
|
||||
|
||||
message GetChatRequest {
|
||||
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]+$"];
|
||||
optional string region = 5;
|
||||
bool decrypt = 6;
|
||||
repeated string private_keys = 7;
|
||||
}
|
||||
|
||||
message GetChatResponse {
|
||||
repeated ChatMessage messages = 1;
|
||||
}
|
||||
|
||||
message StreamChatRequest {
|
||||
optional string channel_id = 1 [(buf.validate.field).string.pattern = "^[0-9A-Fa-f]+$"];
|
||||
optional string region = 2;
|
||||
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}];
|
||||
// Max rows per poll (clamped 10..1000, default 500).
|
||||
optional int32 max_rows = 6 [(buf.validate.field).int32 = {gte: 10, lte: 1000}];
|
||||
bool skip_initial_messages = 7;
|
||||
}
|
||||
|
||||
service ChatService {
|
||||
rpc GetChat(GetChatRequest) returns (GetChatResponse);
|
||||
rpc StreamChat(StreamChatRequest) returns (stream ChatMessage);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
// A directed edge in the neighbor graph between two located nodes.
|
||||
// Shared by MapService.GetMap (when includeNeighbors=true) and
|
||||
// NeighborsService.GetAllNeighbors. Mirrors meshcore_all_neighbor_edges.
|
||||
message NeighborEdge {
|
||||
string source_node = 1;
|
||||
string target_node = 2;
|
||||
string connection_type = 3;
|
||||
int32 packet_count = 4;
|
||||
string source_name = 5;
|
||||
double source_latitude = 6;
|
||||
double source_longitude = 7;
|
||||
int32 source_has_location = 8;
|
||||
string target_name = 9;
|
||||
double target_latitude = 10;
|
||||
double target_longitude = 11;
|
||||
int32 target_has_location = 12;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
import "meshexplorer/v1/common.proto";
|
||||
|
||||
// Latest known position of a node. Mirrors unified_latest_nodeinfo rows
|
||||
// returned by getNodePositions().
|
||||
message NodePosition {
|
||||
string node_id = 1;
|
||||
optional string name = 2;
|
||||
optional string short_name = 3;
|
||||
double latitude = 4;
|
||||
double longitude = 5;
|
||||
string last_seen = 6;
|
||||
optional string first_seen = 7;
|
||||
string type = 8;
|
||||
}
|
||||
|
||||
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}];
|
||||
repeated string node_types = 5;
|
||||
// Only include nodes seen within this many seconds.
|
||||
optional int32 last_seen = 6 [(buf.validate.field).int32.gte = 0];
|
||||
optional string region = 7;
|
||||
// When true, also compute and return the neighbor edge graph.
|
||||
bool include_neighbors = 8;
|
||||
}
|
||||
|
||||
message GetMapResponse {
|
||||
repeated NodePosition nodes = 1;
|
||||
// Populated only when include_neighbors was set on the request.
|
||||
repeated NeighborEdge neighbors = 2;
|
||||
}
|
||||
|
||||
service MapService {
|
||||
rpc GetMap(GetMapRequest) returns (GetMapResponse);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
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}];
|
||||
repeated string node_types = 5;
|
||||
optional int32 last_seen = 6 [(buf.validate.field).int32.gte = 0];
|
||||
optional string region = 7;
|
||||
}
|
||||
|
||||
message GetAllNeighborsResponse {
|
||||
repeated NeighborEdge neighbors = 1;
|
||||
}
|
||||
|
||||
service NeighborsService {
|
||||
rpc GetAllNeighbors(GetAllNeighborsRequest) returns (GetAllNeighborsResponse);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
|
||||
// Basic node identity/capabilities from the latest advert (getMeshcoreNodeInfo).
|
||||
message NodeInfo {
|
||||
string public_key = 1;
|
||||
string node_name = 2;
|
||||
optional double latitude = 3;
|
||||
optional double longitude = 4;
|
||||
int32 has_location = 5;
|
||||
int32 is_repeater = 6;
|
||||
int32 is_chat_node = 7;
|
||||
int32 is_room_server = 8;
|
||||
int32 has_name = 9;
|
||||
optional string broker = 10;
|
||||
optional string topic = 11;
|
||||
string first_seen = 12;
|
||||
string last_seen = 13;
|
||||
}
|
||||
|
||||
// One (origin, path, origin_pubkey) tuple from an advert's reception paths.
|
||||
message OriginPathPubkeyTuple {
|
||||
string origin = 1;
|
||||
string path = 2;
|
||||
string origin_pubkey = 3;
|
||||
}
|
||||
|
||||
// An advert grouped by packet_hash, with all the paths it was heard over.
|
||||
message Advert {
|
||||
string adv_timestamp = 1;
|
||||
repeated OriginPathPubkeyTuple origin_path_pubkey_tuples = 2;
|
||||
int32 advert_count = 3;
|
||||
string earliest_timestamp = 4;
|
||||
string latest_timestamp = 5;
|
||||
optional double latitude = 6;
|
||||
optional double longitude = 7;
|
||||
int32 is_repeater = 8;
|
||||
int32 is_chat_node = 9;
|
||||
int32 is_room_server = 10;
|
||||
int32 has_location = 11;
|
||||
string packet_hash = 12;
|
||||
}
|
||||
|
||||
message LocationHistory {
|
||||
string mesh_timestamp = 1;
|
||||
double latitude = 2;
|
||||
double longitude = 3;
|
||||
}
|
||||
|
||||
message MqttTopic {
|
||||
string topic = 1;
|
||||
string broker = 2;
|
||||
string last_packet_time = 3;
|
||||
bool is_recent = 4;
|
||||
}
|
||||
|
||||
message MqttInfo {
|
||||
bool is_uplinked = 1;
|
||||
bool has_packets = 2;
|
||||
repeated MqttTopic topics = 3;
|
||||
}
|
||||
|
||||
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}];
|
||||
}
|
||||
|
||||
message GetNodeResponse {
|
||||
NodeInfo node = 1;
|
||||
repeated Advert recent_adverts = 2;
|
||||
repeated LocationHistory location_history = 3;
|
||||
MqttInfo mqtt = 4;
|
||||
optional string region = 5;
|
||||
}
|
||||
|
||||
// Direct neighbor of a node (meshcore_node_direct_neighbors).
|
||||
message Neighbor {
|
||||
string public_key = 1;
|
||||
string node_name = 2;
|
||||
optional double latitude = 3;
|
||||
optional double longitude = 4;
|
||||
int32 has_location = 5;
|
||||
int32 is_repeater = 6;
|
||||
int32 is_chat_node = 7;
|
||||
int32 is_room_server = 8;
|
||||
int32 has_name = 9;
|
||||
repeated string directions = 10;
|
||||
}
|
||||
|
||||
message GetNodeNeighborsRequest {
|
||||
string public_key = 1 [(buf.validate.field).string.min_len = 10];
|
||||
optional int32 last_seen = 2 [(buf.validate.field).int32.gte = 0];
|
||||
}
|
||||
|
||||
message GetNodeNeighborsResponse {
|
||||
repeated Neighbor neighbors = 1;
|
||||
}
|
||||
|
||||
// One search request within a (possibly batched) SearchNodes call.
|
||||
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 bool exact = 5;
|
||||
optional bool is_repeater = 6;
|
||||
}
|
||||
|
||||
message SearchResult {
|
||||
string public_key = 1;
|
||||
string node_name = 2;
|
||||
optional double latitude = 3;
|
||||
optional double longitude = 4;
|
||||
int32 has_location = 5;
|
||||
int32 is_repeater = 6;
|
||||
int32 is_chat_node = 7;
|
||||
int32 is_room_server = 8;
|
||||
int32 has_name = 9;
|
||||
string first_heard = 10;
|
||||
string last_seen = 11;
|
||||
string broker = 12;
|
||||
string topic = 13;
|
||||
}
|
||||
|
||||
// Results for a single query in the batch (preserves per-query grouping).
|
||||
message SearchResultList {
|
||||
repeated SearchResult results = 1;
|
||||
}
|
||||
|
||||
message SearchNodesRequest {
|
||||
repeated SearchQuery queries = 1 [(buf.validate.field).repeated.max_items = 500];
|
||||
}
|
||||
|
||||
message SearchNodesResponse {
|
||||
// One entry per input query, in the same order.
|
||||
repeated SearchResultList results = 1;
|
||||
}
|
||||
|
||||
service NodeService {
|
||||
rpc GetNode(GetNodeRequest) returns (GetNodeResponse);
|
||||
rpc GetNodeNeighbors(GetNodeNeighborsRequest) returns (GetNodeNeighborsResponse);
|
||||
rpc SearchNodes(SearchNodesRequest) returns (SearchNodesResponse);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
import "buf/validate/validate.proto";
|
||||
|
||||
// A raw mesh packet (meshcore_packets), hex fields kept as hex strings.
|
||||
message Packet {
|
||||
string ingest_timestamp = 1;
|
||||
string mesh_timestamp = 2;
|
||||
string broker = 3;
|
||||
string topic = 4;
|
||||
string packet = 5;
|
||||
int32 path_len = 6;
|
||||
string path = 7;
|
||||
int32 route_type = 8;
|
||||
int32 payload_type = 9;
|
||||
int32 payload_version = 10;
|
||||
int32 header = 11;
|
||||
string origin_pubkey = 12;
|
||||
}
|
||||
|
||||
message StreamPacketsRequest {
|
||||
optional string region = 1;
|
||||
// Payload type filter (0..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 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}];
|
||||
// Max rows per poll (clamped 10..10000, default 10).
|
||||
optional int32 max_rows = 6 [(buf.validate.field).int32 = {gte: 10, lte: 10000}];
|
||||
}
|
||||
|
||||
service PacketsService {
|
||||
rpc StreamPackets(StreamPacketsRequest) returns (stream Packet);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package meshexplorer.v1;
|
||||
|
||||
message StatsRequest {
|
||||
optional string region = 1;
|
||||
}
|
||||
|
||||
message GetTotalNodesResponse {
|
||||
int32 total_nodes = 1;
|
||||
}
|
||||
|
||||
message NodesOverTimeRow {
|
||||
string day = 1;
|
||||
int32 cumulative_unique_nodes = 2;
|
||||
int32 nodes_with_location = 3;
|
||||
int32 nodes_without_location = 4;
|
||||
int32 repeaters = 5;
|
||||
int32 room_servers = 6;
|
||||
}
|
||||
|
||||
message GetNodesOverTimeResponse {
|
||||
repeated NodesOverTimeRow data = 1;
|
||||
}
|
||||
|
||||
message PopularChannelRow {
|
||||
string channel_hash = 1;
|
||||
int32 message_count = 2;
|
||||
}
|
||||
|
||||
message GetPopularChannelsResponse {
|
||||
repeated PopularChannelRow data = 1;
|
||||
}
|
||||
|
||||
message RepeaterPrefixRow {
|
||||
string prefix = 1;
|
||||
repeated string node_names = 2;
|
||||
}
|
||||
|
||||
message GetRepeaterPrefixesResponse {
|
||||
repeated RepeaterPrefixRow data = 1;
|
||||
}
|
||||
|
||||
service StatsService {
|
||||
rpc GetTotalNodes(StatsRequest) returns (GetTotalNodesResponse);
|
||||
rpc GetNodesOverTime(StatsRequest) returns (GetNodesOverTimeResponse);
|
||||
rpc GetPopularChannels(StatsRequest) returns (GetPopularChannelsResponse);
|
||||
rpc GetRepeaterPrefixes(StatsRequest) returns (GetRepeaterPrefixesResponse);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ function getNodeType(node: NodeInfo): number {
|
||||
|
||||
export default function MeshcoreNodePage() {
|
||||
const params = useParams();
|
||||
const publicKey = params.publicKey as string;
|
||||
const publicKey = params?.publicKey as string;
|
||||
const { config } = useConfig();
|
||||
|
||||
// Use TanStack Query for node data
|
||||
|
||||
@@ -13,7 +13,8 @@ import MapLayerSettingsComponent from "@/components/MapLayerSettings";
|
||||
import { type MapLayerSettings } from "@/hooks/useMapLayerSettings";
|
||||
import { NodeMarker, ClusterMarker, PopupContent } from "./MapIcons";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { buildApiUrl } from "@/lib/api";
|
||||
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";
|
||||
@@ -572,65 +573,75 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
setAllNeighborsLoading(true);
|
||||
}
|
||||
|
||||
let url = "/api/map";
|
||||
const params = [];
|
||||
if (bounds) {
|
||||
const [[minLat, minLng], [maxLat, maxLng]] = bounds;
|
||||
params.push(`minLat=${minLat}`);
|
||||
params.push(`maxLat=${maxLat}`);
|
||||
params.push(`minLng=${minLng}`);
|
||||
params.push(`maxLng=${maxLng}`);
|
||||
}
|
||||
if (mapLayerSettings.nodeTypes && mapLayerSettings.nodeTypes.length > 0) {
|
||||
for (const type of mapLayerSettings.nodeTypes) {
|
||||
params.push(`nodeTypes=${encodeURIComponent(type)}`);
|
||||
}
|
||||
}
|
||||
if (config?.lastSeen !== null && config?.lastSeen !== undefined) {
|
||||
params.push(`lastSeen=${config.lastSeen}`);
|
||||
}
|
||||
if (config?.selectedRegion) {
|
||||
params.push(`region=${encodeURIComponent(config.selectedRegion)}`);
|
||||
}
|
||||
if (includeNeighbors) {
|
||||
params.push('includeNeighbors=true');
|
||||
}
|
||||
if (params.length > 0) {
|
||||
url += `?${params.join("&")}`;
|
||||
}
|
||||
|
||||
fetch(buildApiUrl(url), { signal: controller.signal })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
// Backward compatibility: just nodes array
|
||||
setNodePositions(data);
|
||||
setLastResultCount(data.length);
|
||||
if (includeNeighbors) {
|
||||
// If we expected neighbors but got just nodes, clear neighbors
|
||||
setAllNeighborConnections([]);
|
||||
}
|
||||
} else if (data && data.nodes && Array.isArray(data.nodes)) {
|
||||
// New format: object with nodes and neighbors
|
||||
setNodePositions(data.nodes);
|
||||
setLastResultCount(data.nodes.length);
|
||||
if (data.neighbors && Array.isArray(data.neighbors)) {
|
||||
setAllNeighborConnections(data.neighbors);
|
||||
} else {
|
||||
setAllNeighborConnections([]);
|
||||
}
|
||||
// Clamp to valid lat/lng ranges so the protovalidate bounds rules accept the
|
||||
// (buffered) viewport; node coordinates never fall outside these ranges.
|
||||
const clampLat = (v: number) => Math.max(-90, Math.min(90, v));
|
||||
const clampLng = (v: number) => Math.max(-180, Math.min(180, v));
|
||||
|
||||
const request = {
|
||||
minLat: bounds ? clampLat(bounds[0][0]) : undefined,
|
||||
minLng: bounds ? clampLng(bounds[0][1]) : undefined,
|
||||
maxLat: bounds ? clampLat(bounds[1][0]) : undefined,
|
||||
maxLng: bounds ? clampLng(bounds[1][1]) : undefined,
|
||||
nodeTypes:
|
||||
mapLayerSettings.nodeTypes && mapLayerSettings.nodeTypes.length > 0
|
||||
? mapLayerSettings.nodeTypes
|
||||
: [],
|
||||
lastSeen:
|
||||
config?.lastSeen !== null && config?.lastSeen !== undefined
|
||||
? config.lastSeen
|
||||
: undefined,
|
||||
region: config?.selectedRegion || undefined,
|
||||
includeNeighbors,
|
||||
};
|
||||
|
||||
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,
|
||||
})),
|
||||
);
|
||||
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 {
|
||||
setNodePositions([]);
|
||||
setAllNeighborConnections([]);
|
||||
}
|
||||
|
||||
|
||||
if (fetchController.current === controller) {
|
||||
setLoading(false);
|
||||
setAllNeighborsLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name !== "AbortError") {
|
||||
const canceled =
|
||||
controller.signal.aborted ||
|
||||
(err instanceof ConnectError && err.code === Code.Canceled);
|
||||
if (!canceled) {
|
||||
setNodePositions([]);
|
||||
setAllNeighborConnections([]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { TransportProvider } from '@connectrpc/connect-query';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { transport } from '@/lib/connect/transport';
|
||||
|
||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
@@ -16,8 +18,10 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
||||
}));
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
<TransportProvider transport={transport}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</TransportProvider>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,292 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/chat.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/chat.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_chat: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChptZXNoZXhwbG9yZXIvdjEvY2hhdC5wcm90bxIPbWVzaGV4cGxvcmVyLnYxImQKDk9yaWdpblBhdGhJbmZvEg4KBm9yaWdpbhgBIAEoCRIVCg1vcmlnaW5fcHVia2V5GAIgASgJEgwKBHBhdGgYAyABKAkSDgoGYnJva2VyGAQgASgJEg0KBXRvcGljGAUgASgJImQKDURlY3J5cHRlZENoYXQSEQoJdGltZXN0YW1wGAEgASgBEhAKCG1zZ190eXBlGAIgASgFEg4KBnNlbmRlchgDIAEoCRIMCgR0ZXh0GAQgASgJEhAKCHJhd190ZXh0GAUgASgJIqkCCgtDaGF0TWVzc2FnZRIYChBpbmdlc3RfdGltZXN0YW1wGAEgASgJEhYKDm1lc2hfdGltZXN0YW1wGAIgASgJEhQKDGNoYW5uZWxfaGFzaBgDIAEoCRILCgNtYWMYBCABKAkSGQoRZW5jcnlwdGVkX21lc3NhZ2UYBSABKAkSFQoNbWVzc2FnZV9jb3VudBgGIAEoBRI5ChBvcmlnaW5fcGF0aF9pbmZvGAcgAygLMh8ubWVzaGV4cGxvcmVyLnYxLk9yaWdpblBhdGhJbmZvEhIKCm1lc3NhZ2VfaWQYCCABKAkSNgoJZGVjcnlwdGVkGAkgASgLMh4ubWVzaGV4cGxvcmVyLnYxLkRlY3J5cHRlZENoYXRIAIgBAUIMCgpfZGVjcnlwdGVkIv4BCg5HZXRDaGF0UmVxdWVzdBIeCgVsaW1pdBgBIAEoBUIKukgHGgUY6AcoAUgAiAEBEhMKBmJlZm9yZRgCIAEoCUgBiAEBEhIKBWFmdGVyGAMgASgJSAKIAQESLgoKY2hhbm5lbF9pZBgEIAEoCUIVukgSchAyDl5bMC05QS1GYS1mXSskSAOIAQESEwoGcmVnaW9uGAUgASgJSASIAQESDwoHZGVjcnlwdBgGIAEoCBIUCgxwcml2YXRlX2tleXMYByADKAlCCAoGX2xpbWl0QgkKB19iZWZvcmVCCAoGX2FmdGVyQg0KC19jaGFubmVsX2lkQgkKB19yZWdpb24iQQoPR2V0Q2hhdFJlc3BvbnNlEi4KCG1lc3NhZ2VzGAEgAygLMhwubWVzaGV4cGxvcmVyLnYxLkNoYXRNZXNzYWdlIqICChFTdHJlYW1DaGF0UmVxdWVzdBIuCgpjaGFubmVsX2lkGAEgASgJQhW6SBJyEDIOXlswLTlBLUZhLWZdKyRIAIgBARITCgZyZWdpb24YAiABKAlIAYgBARIPCgdkZWNyeXB0GAMgASgIEhQKDHByaXZhdGVfa2V5cxgEIAMoCRImCg1wb2xsX2ludGVydmFsGAUgASgFQgq6SAcaBRiQTihkSAKIAQESIQoIbWF4X3Jvd3MYBiABKAVCCrpIBxoFGOgHKApIA4gBARIdChVza2lwX2luaXRpYWxfbWVzc2FnZXMYByABKAhCDQoLX2NoYW5uZWxfaWRCCQoHX3JlZ2lvbkIQCg5fcG9sbF9pbnRlcnZhbEILCglfbWF4X3Jvd3MyrQEKC0NoYXRTZXJ2aWNlEkwKB0dldENoYXQSHy5tZXNoZXhwbG9yZXIudjEuR2V0Q2hhdFJlcXVlc3QaIC5tZXNoZXhwbG9yZXIudjEuR2V0Q2hhdFJlc3BvbnNlElAKClN0cmVhbUNoYXQSIi5tZXNoZXhwbG9yZXIudjEuU3RyZWFtQ2hhdFJlcXVlc3QaHC5tZXNoZXhwbG9yZXIudjEuQ2hhdE1lc3NhZ2UwAWIGcHJvdG8z", [file_buf_validate_validate]);
|
||||
|
||||
/**
|
||||
* One (origin, origin_pubkey, path, broker, topic) reception tuple.
|
||||
*
|
||||
* @generated from message meshexplorer.v1.OriginPathInfo
|
||||
*/
|
||||
export type OriginPathInfo = Message<"meshexplorer.v1.OriginPathInfo"> & {
|
||||
/**
|
||||
* @generated from field: string origin = 1;
|
||||
*/
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string origin_pubkey = 2;
|
||||
*/
|
||||
originPubkey: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string path = 3;
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string broker = 4;
|
||||
*/
|
||||
broker: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string topic = 5;
|
||||
*/
|
||||
topic: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.OriginPathInfo.
|
||||
* Use `create(OriginPathInfoSchema)` to create a new message.
|
||||
*/
|
||||
export const OriginPathInfoSchema: GenMessage<OriginPathInfo> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_chat, 0);
|
||||
|
||||
/**
|
||||
* A parsed/decrypted public-channel message (parseMeshcoreGroupMessage).
|
||||
*
|
||||
* @generated from message meshexplorer.v1.DecryptedChat
|
||||
*/
|
||||
export type DecryptedChat = Message<"meshexplorer.v1.DecryptedChat"> & {
|
||||
/**
|
||||
* @generated from field: double timestamp = 1;
|
||||
*/
|
||||
timestamp: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 msg_type = 2;
|
||||
*/
|
||||
msgType: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string sender = 3;
|
||||
*/
|
||||
sender: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string text = 4;
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string raw_text = 5;
|
||||
*/
|
||||
rawText: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.DecryptedChat.
|
||||
* Use `create(DecryptedChatSchema)` to create a new message.
|
||||
*/
|
||||
export const DecryptedChatSchema: GenMessage<DecryptedChat> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_chat, 1);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.ChatMessage
|
||||
*/
|
||||
export type ChatMessage = Message<"meshexplorer.v1.ChatMessage"> & {
|
||||
/**
|
||||
* @generated from field: string ingest_timestamp = 1;
|
||||
*/
|
||||
ingestTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string mesh_timestamp = 2;
|
||||
*/
|
||||
meshTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string channel_hash = 3;
|
||||
*/
|
||||
channelHash: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string mac = 4;
|
||||
*/
|
||||
mac: string;
|
||||
|
||||
/**
|
||||
* Hex-encoded ciphertext.
|
||||
*
|
||||
* @generated from field: string encrypted_message = 5;
|
||||
*/
|
||||
encryptedMessage: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 message_count = 6;
|
||||
*/
|
||||
messageCount: number;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.OriginPathInfo origin_path_info = 7;
|
||||
*/
|
||||
originPathInfo: OriginPathInfo[];
|
||||
|
||||
/**
|
||||
* @generated from field: string message_id = 8;
|
||||
*/
|
||||
messageId: string;
|
||||
|
||||
/**
|
||||
* Present only when decrypt was requested and decryption succeeded.
|
||||
*
|
||||
* @generated from field: optional meshexplorer.v1.DecryptedChat decrypted = 9;
|
||||
*/
|
||||
decrypted?: DecryptedChat | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.ChatMessage.
|
||||
* Use `create(ChatMessageSchema)` to create a new message.
|
||||
*/
|
||||
export const ChatMessageSchema: GenMessage<ChatMessage> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_chat, 2);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetChatRequest
|
||||
*/
|
||||
export type GetChatRequest = Message<"meshexplorer.v1.GetChatRequest"> & {
|
||||
/**
|
||||
* @generated from field: optional int32 limit = 1;
|
||||
*/
|
||||
limit?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string before = 2;
|
||||
*/
|
||||
before?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string after = 3;
|
||||
*/
|
||||
after?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string channel_id = 4;
|
||||
*/
|
||||
channelId?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string region = 5;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: bool decrypt = 6;
|
||||
*/
|
||||
decrypt: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string private_keys = 7;
|
||||
*/
|
||||
privateKeys: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetChatRequest.
|
||||
* Use `create(GetChatRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetChatRequestSchema: GenMessage<GetChatRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_chat, 3);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetChatResponse
|
||||
*/
|
||||
export type GetChatResponse = Message<"meshexplorer.v1.GetChatResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.ChatMessage messages = 1;
|
||||
*/
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetChatResponse.
|
||||
* Use `create(GetChatResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetChatResponseSchema: GenMessage<GetChatResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_chat, 4);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.StreamChatRequest
|
||||
*/
|
||||
export type StreamChatRequest = Message<"meshexplorer.v1.StreamChatRequest"> & {
|
||||
/**
|
||||
* @generated from field: optional string channel_id = 1;
|
||||
*/
|
||||
channelId?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string region = 2;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: bool decrypt = 3;
|
||||
*/
|
||||
decrypt: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string private_keys = 4;
|
||||
*/
|
||||
privateKeys: string[];
|
||||
|
||||
/**
|
||||
* Poll interval in ms (clamped 100..10000, default 1000).
|
||||
*
|
||||
* @generated from field: optional int32 poll_interval = 5;
|
||||
*/
|
||||
pollInterval?: number | undefined;
|
||||
|
||||
/**
|
||||
* Max rows per poll (clamped 10..1000, default 500).
|
||||
*
|
||||
* @generated from field: optional int32 max_rows = 6;
|
||||
*/
|
||||
maxRows?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: bool skip_initial_messages = 7;
|
||||
*/
|
||||
skipInitialMessages: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.StreamChatRequest.
|
||||
* Use `create(StreamChatRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const StreamChatRequestSchema: GenMessage<StreamChatRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_chat, 5);
|
||||
|
||||
/**
|
||||
* @generated from service meshexplorer.v1.ChatService
|
||||
*/
|
||||
export const ChatService: GenService<{
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.ChatService.GetChat
|
||||
*/
|
||||
getChat: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetChatRequestSchema;
|
||||
output: typeof GetChatResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.ChatService.StreamChat
|
||||
*/
|
||||
streamChat: {
|
||||
methodKind: "server_streaming";
|
||||
input: typeof StreamChatRequestSchema;
|
||||
output: typeof ChatMessageSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_meshexplorer_v1_chat, 0);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/common.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/common.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_common: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChxtZXNoZXhwbG9yZXIvdjEvY29tbW9uLnByb3RvEg9tZXNoZXhwbG9yZXIudjEisQIKDE5laWdoYm9yRWRnZRITCgtzb3VyY2Vfbm9kZRgBIAEoCRITCgt0YXJnZXRfbm9kZRgCIAEoCRIXCg9jb25uZWN0aW9uX3R5cGUYAyABKAkSFAoMcGFja2V0X2NvdW50GAQgASgFEhMKC3NvdXJjZV9uYW1lGAUgASgJEhcKD3NvdXJjZV9sYXRpdHVkZRgGIAEoARIYChBzb3VyY2VfbG9uZ2l0dWRlGAcgASgBEhsKE3NvdXJjZV9oYXNfbG9jYXRpb24YCCABKAUSEwoLdGFyZ2V0X25hbWUYCSABKAkSFwoPdGFyZ2V0X2xhdGl0dWRlGAogASgBEhgKEHRhcmdldF9sb25naXR1ZGUYCyABKAESGwoTdGFyZ2V0X2hhc19sb2NhdGlvbhgMIAEoBWIGcHJvdG8z");
|
||||
|
||||
/**
|
||||
* A directed edge in the neighbor graph between two located nodes.
|
||||
* Shared by MapService.GetMap (when includeNeighbors=true) and
|
||||
* NeighborsService.GetAllNeighbors. Mirrors meshcore_all_neighbor_edges.
|
||||
*
|
||||
* @generated from message meshexplorer.v1.NeighborEdge
|
||||
*/
|
||||
export type NeighborEdge = Message<"meshexplorer.v1.NeighborEdge"> & {
|
||||
/**
|
||||
* @generated from field: string source_node = 1;
|
||||
*/
|
||||
sourceNode: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string target_node = 2;
|
||||
*/
|
||||
targetNode: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string connection_type = 3;
|
||||
*/
|
||||
connectionType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 packet_count = 4;
|
||||
*/
|
||||
packetCount: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string source_name = 5;
|
||||
*/
|
||||
sourceName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: double source_latitude = 6;
|
||||
*/
|
||||
sourceLatitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: double source_longitude = 7;
|
||||
*/
|
||||
sourceLongitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 source_has_location = 8;
|
||||
*/
|
||||
sourceHasLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string target_name = 9;
|
||||
*/
|
||||
targetName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: double target_latitude = 10;
|
||||
*/
|
||||
targetLatitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: double target_longitude = 11;
|
||||
*/
|
||||
targetLongitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 target_has_location = 12;
|
||||
*/
|
||||
targetHasLocation: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.NeighborEdge.
|
||||
* Use `create(NeighborEdgeSchema)` to create a new message.
|
||||
*/
|
||||
export const NeighborEdgeSchema: GenMessage<NeighborEdge> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_common, 0);
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/map.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { NeighborEdge } from "./common_pb";
|
||||
import { file_meshexplorer_v1_common } from "./common_pb";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/map.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_map: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChltZXNoZXhwbG9yZXIvdjEvbWFwLnByb3RvEg9tZXNoZXhwbG9yZXIudjEi0QEKDE5vZGVQb3NpdGlvbhIPCgdub2RlX2lkGAEgASgJEhEKBG5hbWUYAiABKAlIAIgBARIXCgpzaG9ydF9uYW1lGAMgASgJSAGIAQESEAoIbGF0aXR1ZGUYBCABKAESEQoJbG9uZ2l0dWRlGAUgASgBEhEKCWxhc3Rfc2VlbhgGIAEoCRIXCgpmaXJzdF9zZWVuGAcgASgJSAKIAQESDAoEdHlwZRgIIAEoCUIHCgVfbmFtZUINCgtfc2hvcnRfbmFtZUINCgtfZmlyc3Rfc2VlbiL5AgoNR2V0TWFwUmVxdWVzdBItCgdtaW5fbGF0GAEgASgBQhe6SBQSEhkAAAAAAIBWQCkAAAAAAIBWwEgAiAEBEi0KB21heF9sYXQYAiABKAFCF7pIFBISGQAAAAAAgFZAKQAAAAAAgFbASAGIAQESLQoHbWluX2xuZxgDIAEoAUIXukgUEhIZAAAAAACAZkApAAAAAACAZsBIAogBARItCgdtYXhfbG5nGAQgASgBQhe6SBQSEhkAAAAAAIBmQCkAAAAAAIBmwEgDiAEBEhIKCm5vZGVfdHlwZXMYBSADKAkSHwoJbGFzdF9zZWVuGAYgASgFQge6SAQaAigASASIAQESEwoGcmVnaW9uGAcgASgJSAWIAQESGQoRaW5jbHVkZV9uZWlnaGJvcnMYCCABKAhCCgoIX21pbl9sYXRCCgoIX21heF9sYXRCCgoIX21pbl9sbmdCCgoIX21heF9sbmdCDAoKX2xhc3Rfc2VlbkIJCgdfcmVnaW9uInAKDkdldE1hcFJlc3BvbnNlEiwKBW5vZGVzGAEgAygLMh0ubWVzaGV4cGxvcmVyLnYxLk5vZGVQb3NpdGlvbhIwCgluZWlnaGJvcnMYAiADKAsyHS5tZXNoZXhwbG9yZXIudjEuTmVpZ2hib3JFZGdlMlcKCk1hcFNlcnZpY2USSQoGR2V0TWFwEh4ubWVzaGV4cGxvcmVyLnYxLkdldE1hcFJlcXVlc3QaHy5tZXNoZXhwbG9yZXIudjEuR2V0TWFwUmVzcG9uc2ViBnByb3RvMw", [file_buf_validate_validate, file_meshexplorer_v1_common]);
|
||||
|
||||
/**
|
||||
* Latest known position of a node. Mirrors unified_latest_nodeinfo rows
|
||||
* returned by getNodePositions().
|
||||
*
|
||||
* @generated from message meshexplorer.v1.NodePosition
|
||||
*/
|
||||
export type NodePosition = Message<"meshexplorer.v1.NodePosition"> & {
|
||||
/**
|
||||
* @generated from field: string node_id = 1;
|
||||
*/
|
||||
nodeId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string name = 2;
|
||||
*/
|
||||
name?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string short_name = 3;
|
||||
*/
|
||||
shortName?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: double latitude = 4;
|
||||
*/
|
||||
latitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: double longitude = 5;
|
||||
*/
|
||||
longitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string last_seen = 6;
|
||||
*/
|
||||
lastSeen: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string first_seen = 7;
|
||||
*/
|
||||
firstSeen?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string type = 8;
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.NodePosition.
|
||||
* Use `create(NodePositionSchema)` to create a new message.
|
||||
*/
|
||||
export const NodePositionSchema: GenMessage<NodePosition> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_map, 0);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetMapRequest
|
||||
*/
|
||||
export type GetMapRequest = Message<"meshexplorer.v1.GetMapRequest"> & {
|
||||
/**
|
||||
* Bounding box (decimal degrees). Unset fields mean "unbounded" on that edge.
|
||||
*
|
||||
* @generated from field: optional double min_lat = 1;
|
||||
*/
|
||||
minLat?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double max_lat = 2;
|
||||
*/
|
||||
maxLat?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double min_lng = 3;
|
||||
*/
|
||||
minLng?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double max_lng = 4;
|
||||
*/
|
||||
maxLng?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string node_types = 5;
|
||||
*/
|
||||
nodeTypes: string[];
|
||||
|
||||
/**
|
||||
* Only include nodes seen within this many seconds.
|
||||
*
|
||||
* @generated from field: optional int32 last_seen = 6;
|
||||
*/
|
||||
lastSeen?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string region = 7;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
|
||||
/**
|
||||
* When true, also compute and return the neighbor edge graph.
|
||||
*
|
||||
* @generated from field: bool include_neighbors = 8;
|
||||
*/
|
||||
includeNeighbors: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetMapRequest.
|
||||
* Use `create(GetMapRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetMapRequestSchema: GenMessage<GetMapRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_map, 1);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetMapResponse
|
||||
*/
|
||||
export type GetMapResponse = Message<"meshexplorer.v1.GetMapResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.NodePosition nodes = 1;
|
||||
*/
|
||||
nodes: NodePosition[];
|
||||
|
||||
/**
|
||||
* Populated only when include_neighbors was set on the request.
|
||||
*
|
||||
* @generated from field: repeated meshexplorer.v1.NeighborEdge neighbors = 2;
|
||||
*/
|
||||
neighbors: NeighborEdge[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetMapResponse.
|
||||
* Use `create(GetMapResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetMapResponseSchema: GenMessage<GetMapResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_map, 2);
|
||||
|
||||
/**
|
||||
* @generated from service meshexplorer.v1.MapService
|
||||
*/
|
||||
export const MapService: GenService<{
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.MapService.GetMap
|
||||
*/
|
||||
getMap: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetMapRequestSchema;
|
||||
output: typeof GetMapResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_meshexplorer_v1_map, 0);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/neighbors.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { NeighborEdge } from "./common_pb";
|
||||
import { file_meshexplorer_v1_common } from "./common_pb";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/neighbors.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_neighbors: GenFile = /*@__PURE__*/
|
||||
fileDesc("Ch9tZXNoZXhwbG9yZXIvdjEvbmVpZ2hib3JzLnByb3RvEg9tZXNoZXhwbG9yZXIudjEi5wIKFkdldEFsbE5laWdoYm9yc1JlcXVlc3QSLQoHbWluX2xhdBgBIAEoAUIXukgUEhIZAAAAAACAVkApAAAAAACAVsBIAIgBARItCgdtYXhfbGF0GAIgASgBQhe6SBQSEhkAAAAAAIBWQCkAAAAAAIBWwEgBiAEBEi0KB21pbl9sbmcYAyABKAFCF7pIFBISGQAAAAAAgGZAKQAAAAAAgGbASAKIAQESLQoHbWF4X2xuZxgEIAEoAUIXukgUEhIZAAAAAACAZkApAAAAAACAZsBIA4gBARISCgpub2RlX3R5cGVzGAUgAygJEh8KCWxhc3Rfc2VlbhgGIAEoBUIHukgEGgIoAEgEiAEBEhMKBnJlZ2lvbhgHIAEoCUgFiAEBQgoKCF9taW5fbGF0QgoKCF9tYXhfbGF0QgoKCF9taW5fbG5nQgoKCF9tYXhfbG5nQgwKCl9sYXN0X3NlZW5CCQoHX3JlZ2lvbiJLChdHZXRBbGxOZWlnaGJvcnNSZXNwb25zZRIwCgluZWlnaGJvcnMYASADKAsyHS5tZXNoZXhwbG9yZXIudjEuTmVpZ2hib3JFZGdlMngKEE5laWdoYm9yc1NlcnZpY2USZAoPR2V0QWxsTmVpZ2hib3JzEicubWVzaGV4cGxvcmVyLnYxLkdldEFsbE5laWdoYm9yc1JlcXVlc3QaKC5tZXNoZXhwbG9yZXIudjEuR2V0QWxsTmVpZ2hib3JzUmVzcG9uc2ViBnByb3RvMw", [file_buf_validate_validate, file_meshexplorer_v1_common]);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetAllNeighborsRequest
|
||||
*/
|
||||
export type GetAllNeighborsRequest = Message<"meshexplorer.v1.GetAllNeighborsRequest"> & {
|
||||
/**
|
||||
* @generated from field: optional double min_lat = 1;
|
||||
*/
|
||||
minLat?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double max_lat = 2;
|
||||
*/
|
||||
maxLat?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double min_lng = 3;
|
||||
*/
|
||||
minLng?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double max_lng = 4;
|
||||
*/
|
||||
maxLng?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string node_types = 5;
|
||||
*/
|
||||
nodeTypes: string[];
|
||||
|
||||
/**
|
||||
* @generated from field: optional int32 last_seen = 6;
|
||||
*/
|
||||
lastSeen?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string region = 7;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetAllNeighborsRequest.
|
||||
* Use `create(GetAllNeighborsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetAllNeighborsRequestSchema: GenMessage<GetAllNeighborsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_neighbors, 0);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetAllNeighborsResponse
|
||||
*/
|
||||
export type GetAllNeighborsResponse = Message<"meshexplorer.v1.GetAllNeighborsResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.NeighborEdge neighbors = 1;
|
||||
*/
|
||||
neighbors: NeighborEdge[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetAllNeighborsResponse.
|
||||
* Use `create(GetAllNeighborsResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetAllNeighborsResponseSchema: GenMessage<GetAllNeighborsResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_neighbors, 1);
|
||||
|
||||
/**
|
||||
* @generated from service meshexplorer.v1.NeighborsService
|
||||
*/
|
||||
export const NeighborsService: GenService<{
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.NeighborsService.GetAllNeighbors
|
||||
*/
|
||||
getAllNeighbors: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetAllNeighborsRequestSchema;
|
||||
output: typeof GetAllNeighborsResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_meshexplorer_v1_neighbors, 0);
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/node.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/node.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_node: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChptZXNoZXhwbG9yZXIvdjEvbm9kZS5wcm90bxIPbWVzaGV4cGxvcmVyLnYxIssCCghOb2RlSW5mbxISCgpwdWJsaWNfa2V5GAEgASgJEhEKCW5vZGVfbmFtZRgCIAEoCRIVCghsYXRpdHVkZRgDIAEoAUgAiAEBEhYKCWxvbmdpdHVkZRgEIAEoAUgBiAEBEhQKDGhhc19sb2NhdGlvbhgFIAEoBRITCgtpc19yZXBlYXRlchgGIAEoBRIUCgxpc19jaGF0X25vZGUYByABKAUSFgoOaXNfcm9vbV9zZXJ2ZXIYCCABKAUSEAoIaGFzX25hbWUYCSABKAUSEwoGYnJva2VyGAogASgJSAKIAQESEgoFdG9waWMYCyABKAlIA4gBARISCgpmaXJzdF9zZWVuGAwgASgJEhEKCWxhc3Rfc2VlbhgNIAEoCUILCglfbGF0aXR1ZGVCDAoKX2xvbmdpdHVkZUIJCgdfYnJva2VyQggKBl90b3BpYyJMChVPcmlnaW5QYXRoUHVia2V5VHVwbGUSDgoGb3JpZ2luGAEgASgJEgwKBHBhdGgYAiABKAkSFQoNb3JpZ2luX3B1YmtleRgDIAEoCSLuAgoGQWR2ZXJ0EhUKDWFkdl90aW1lc3RhbXAYASABKAkSSQoZb3JpZ2luX3BhdGhfcHVia2V5X3R1cGxlcxgCIAMoCzImLm1lc2hleHBsb3Jlci52MS5PcmlnaW5QYXRoUHVia2V5VHVwbGUSFAoMYWR2ZXJ0X2NvdW50GAMgASgFEhoKEmVhcmxpZXN0X3RpbWVzdGFtcBgEIAEoCRIYChBsYXRlc3RfdGltZXN0YW1wGAUgASgJEhUKCGxhdGl0dWRlGAYgASgBSACIAQESFgoJbG9uZ2l0dWRlGAcgASgBSAGIAQESEwoLaXNfcmVwZWF0ZXIYCCABKAUSFAoMaXNfY2hhdF9ub2RlGAkgASgFEhYKDmlzX3Jvb21fc2VydmVyGAogASgFEhQKDGhhc19sb2NhdGlvbhgLIAEoBRITCgtwYWNrZXRfaGFzaBgMIAEoCUILCglfbGF0aXR1ZGVCDAoKX2xvbmdpdHVkZSJOCg9Mb2NhdGlvbkhpc3RvcnkSFgoObWVzaF90aW1lc3RhbXAYASABKAkSEAoIbGF0aXR1ZGUYAiABKAESEQoJbG9uZ2l0dWRlGAMgASgBIlcKCU1xdHRUb3BpYxINCgV0b3BpYxgBIAEoCRIOCgZicm9rZXIYAiABKAkSGAoQbGFzdF9wYWNrZXRfdGltZRgDIAEoCRIRCglpc19yZWNlbnQYBCABKAgiYAoITXF0dEluZm8SEwoLaXNfdXBsaW5rZWQYASABKAgSEwoLaGFzX3BhY2tldHMYAiABKAgSKgoGdG9waWNzGAMgAygLMhoubWVzaGV4cGxvcmVyLnYxLk1xdHRUb3BpYyJXCg5HZXROb2RlUmVxdWVzdBIbCgpwdWJsaWNfa2V5GAEgASgJQge6SARyAhAKEh4KBWxpbWl0GAIgASgFQgq6SAcaBRjoBygBSACIAQFCCAoGX2xpbWl0IvABCg9HZXROb2RlUmVzcG9uc2USJwoEbm9kZRgBIAEoCzIZLm1lc2hleHBsb3Jlci52MS5Ob2RlSW5mbxIvCg5yZWNlbnRfYWR2ZXJ0cxgCIAMoCzIXLm1lc2hleHBsb3Jlci52MS5BZHZlcnQSOgoQbG9jYXRpb25faGlzdG9yeRgDIAMoCzIgLm1lc2hleHBsb3Jlci52MS5Mb2NhdGlvbkhpc3RvcnkSJwoEbXF0dBgEIAEoCzIZLm1lc2hleHBsb3Jlci52MS5NcXR0SW5mbxITCgZyZWdpb24YBSABKAlIAIgBAUIJCgdfcmVnaW9uIvoBCghOZWlnaGJvchISCgpwdWJsaWNfa2V5GAEgASgJEhEKCW5vZGVfbmFtZRgCIAEoCRIVCghsYXRpdHVkZRgDIAEoAUgAiAEBEhYKCWxvbmdpdHVkZRgEIAEoAUgBiAEBEhQKDGhhc19sb2NhdGlvbhgFIAEoBRITCgtpc19yZXBlYXRlchgGIAEoBRIUCgxpc19jaGF0X25vZGUYByABKAUSFgoOaXNfcm9vbV9zZXJ2ZXIYCCABKAUSEAoIaGFzX25hbWUYCSABKAUSEgoKZGlyZWN0aW9ucxgKIAMoCUILCglfbGF0aXR1ZGVCDAoKX2xvbmdpdHVkZSJlChdHZXROb2RlTmVpZ2hib3JzUmVxdWVzdBIbCgpwdWJsaWNfa2V5GAEgASgJQge6SARyAhAKEh8KCWxhc3Rfc2VlbhgCIAEoBUIHukgEGgIoAEgAiAEBQgwKCl9sYXN0X3NlZW4iSAoYR2V0Tm9kZU5laWdoYm9yc1Jlc3BvbnNlEiwKCW5laWdoYm9ycxgBIAMoCzIZLm1lc2hleHBsb3Jlci52MS5OZWlnaGJvciL1AQoLU2VhcmNoUXVlcnkSGwoFcXVlcnkYASABKAlCB7pIBHICGGRIAIgBARITCgZyZWdpb24YAiABKAlIAYgBARIfCglsYXN0X3NlZW4YAyABKAVCB7pIBBoCKABIAogBARIeCgVsaW1pdBgEIAEoBUIKukgHGgUYyAEoAUgDiAEBEhIKBWV4YWN0GAUgASgISASIAQESGAoLaXNfcmVwZWF0ZXIYBiABKAhIBYgBAUIICgZfcXVlcnlCCQoHX3JlZ2lvbkIMCgpfbGFzdF9zZWVuQggKBl9saW1pdEIICgZfZXhhY3RCDgoMX2lzX3JlcGVhdGVyIrECCgxTZWFyY2hSZXN1bHQSEgoKcHVibGljX2tleRgBIAEoCRIRCglub2RlX25hbWUYAiABKAkSFQoIbGF0aXR1ZGUYAyABKAFIAIgBARIWCglsb25naXR1ZGUYBCABKAFIAYgBARIUCgxoYXNfbG9jYXRpb24YBSABKAUSEwoLaXNfcmVwZWF0ZXIYBiABKAUSFAoMaXNfY2hhdF9ub2RlGAcgASgFEhYKDmlzX3Jvb21fc2VydmVyGAggASgFEhAKCGhhc19uYW1lGAkgASgFEhMKC2ZpcnN0X2hlYXJkGAogASgJEhEKCWxhc3Rfc2VlbhgLIAEoCRIOCgZicm9rZXIYDCABKAkSDQoFdG9waWMYDSABKAlCCwoJX2xhdGl0dWRlQgwKCl9sb25naXR1ZGUiQgoQU2VhcmNoUmVzdWx0TGlzdBIuCgdyZXN1bHRzGAEgAygLMh0ubWVzaGV4cGxvcmVyLnYxLlNlYXJjaFJlc3VsdCJOChJTZWFyY2hOb2Rlc1JlcXVlc3QSOAoHcXVlcmllcxgBIAMoCzIcLm1lc2hleHBsb3Jlci52MS5TZWFyY2hRdWVyeUIJukgGkgEDEPQDIkkKE1NlYXJjaE5vZGVzUmVzcG9uc2USMgoHcmVzdWx0cxgBIAMoCzIhLm1lc2hleHBsb3Jlci52MS5TZWFyY2hSZXN1bHRMaXN0Mp4CCgtOb2RlU2VydmljZRJMCgdHZXROb2RlEh8ubWVzaGV4cGxvcmVyLnYxLkdldE5vZGVSZXF1ZXN0GiAubWVzaGV4cGxvcmVyLnYxLkdldE5vZGVSZXNwb25zZRJnChBHZXROb2RlTmVpZ2hib3JzEigubWVzaGV4cGxvcmVyLnYxLkdldE5vZGVOZWlnaGJvcnNSZXF1ZXN0GikubWVzaGV4cGxvcmVyLnYxLkdldE5vZGVOZWlnaGJvcnNSZXNwb25zZRJYCgtTZWFyY2hOb2RlcxIjLm1lc2hleHBsb3Jlci52MS5TZWFyY2hOb2Rlc1JlcXVlc3QaJC5tZXNoZXhwbG9yZXIudjEuU2VhcmNoTm9kZXNSZXNwb25zZWIGcHJvdG8z", [file_buf_validate_validate]);
|
||||
|
||||
/**
|
||||
* Basic node identity/capabilities from the latest advert (getMeshcoreNodeInfo).
|
||||
*
|
||||
* @generated from message meshexplorer.v1.NodeInfo
|
||||
*/
|
||||
export type NodeInfo = Message<"meshexplorer.v1.NodeInfo"> & {
|
||||
/**
|
||||
* @generated from field: string public_key = 1;
|
||||
*/
|
||||
publicKey: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string node_name = 2;
|
||||
*/
|
||||
nodeName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double latitude = 3;
|
||||
*/
|
||||
latitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double longitude = 4;
|
||||
*/
|
||||
longitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_location = 5;
|
||||
*/
|
||||
hasLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_repeater = 6;
|
||||
*/
|
||||
isRepeater: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_chat_node = 7;
|
||||
*/
|
||||
isChatNode: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_room_server = 8;
|
||||
*/
|
||||
isRoomServer: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_name = 9;
|
||||
*/
|
||||
hasName: number;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string broker = 10;
|
||||
*/
|
||||
broker?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string topic = 11;
|
||||
*/
|
||||
topic?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: string first_seen = 12;
|
||||
*/
|
||||
firstSeen: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string last_seen = 13;
|
||||
*/
|
||||
lastSeen: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.NodeInfo.
|
||||
* Use `create(NodeInfoSchema)` to create a new message.
|
||||
*/
|
||||
export const NodeInfoSchema: GenMessage<NodeInfo> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 0);
|
||||
|
||||
/**
|
||||
* One (origin, path, origin_pubkey) tuple from an advert's reception paths.
|
||||
*
|
||||
* @generated from message meshexplorer.v1.OriginPathPubkeyTuple
|
||||
*/
|
||||
export type OriginPathPubkeyTuple = Message<"meshexplorer.v1.OriginPathPubkeyTuple"> & {
|
||||
/**
|
||||
* @generated from field: string origin = 1;
|
||||
*/
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string path = 2;
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string origin_pubkey = 3;
|
||||
*/
|
||||
originPubkey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.OriginPathPubkeyTuple.
|
||||
* Use `create(OriginPathPubkeyTupleSchema)` to create a new message.
|
||||
*/
|
||||
export const OriginPathPubkeyTupleSchema: GenMessage<OriginPathPubkeyTuple> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 1);
|
||||
|
||||
/**
|
||||
* An advert grouped by packet_hash, with all the paths it was heard over.
|
||||
*
|
||||
* @generated from message meshexplorer.v1.Advert
|
||||
*/
|
||||
export type Advert = Message<"meshexplorer.v1.Advert"> & {
|
||||
/**
|
||||
* @generated from field: string adv_timestamp = 1;
|
||||
*/
|
||||
advTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.OriginPathPubkeyTuple origin_path_pubkey_tuples = 2;
|
||||
*/
|
||||
originPathPubkeyTuples: OriginPathPubkeyTuple[];
|
||||
|
||||
/**
|
||||
* @generated from field: int32 advert_count = 3;
|
||||
*/
|
||||
advertCount: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string earliest_timestamp = 4;
|
||||
*/
|
||||
earliestTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string latest_timestamp = 5;
|
||||
*/
|
||||
latestTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double latitude = 6;
|
||||
*/
|
||||
latitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double longitude = 7;
|
||||
*/
|
||||
longitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_repeater = 8;
|
||||
*/
|
||||
isRepeater: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_chat_node = 9;
|
||||
*/
|
||||
isChatNode: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_room_server = 10;
|
||||
*/
|
||||
isRoomServer: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_location = 11;
|
||||
*/
|
||||
hasLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string packet_hash = 12;
|
||||
*/
|
||||
packetHash: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.Advert.
|
||||
* Use `create(AdvertSchema)` to create a new message.
|
||||
*/
|
||||
export const AdvertSchema: GenMessage<Advert> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 2);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.LocationHistory
|
||||
*/
|
||||
export type LocationHistory = Message<"meshexplorer.v1.LocationHistory"> & {
|
||||
/**
|
||||
* @generated from field: string mesh_timestamp = 1;
|
||||
*/
|
||||
meshTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: double latitude = 2;
|
||||
*/
|
||||
latitude: number;
|
||||
|
||||
/**
|
||||
* @generated from field: double longitude = 3;
|
||||
*/
|
||||
longitude: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.LocationHistory.
|
||||
* Use `create(LocationHistorySchema)` to create a new message.
|
||||
*/
|
||||
export const LocationHistorySchema: GenMessage<LocationHistory> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 3);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.MqttTopic
|
||||
*/
|
||||
export type MqttTopic = Message<"meshexplorer.v1.MqttTopic"> & {
|
||||
/**
|
||||
* @generated from field: string topic = 1;
|
||||
*/
|
||||
topic: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string broker = 2;
|
||||
*/
|
||||
broker: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string last_packet_time = 3;
|
||||
*/
|
||||
lastPacketTime: string;
|
||||
|
||||
/**
|
||||
* @generated from field: bool is_recent = 4;
|
||||
*/
|
||||
isRecent: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.MqttTopic.
|
||||
* Use `create(MqttTopicSchema)` to create a new message.
|
||||
*/
|
||||
export const MqttTopicSchema: GenMessage<MqttTopic> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 4);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.MqttInfo
|
||||
*/
|
||||
export type MqttInfo = Message<"meshexplorer.v1.MqttInfo"> & {
|
||||
/**
|
||||
* @generated from field: bool is_uplinked = 1;
|
||||
*/
|
||||
isUplinked: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: bool has_packets = 2;
|
||||
*/
|
||||
hasPackets: boolean;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.MqttTopic topics = 3;
|
||||
*/
|
||||
topics: MqttTopic[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.MqttInfo.
|
||||
* Use `create(MqttInfoSchema)` to create a new message.
|
||||
*/
|
||||
export const MqttInfoSchema: GenMessage<MqttInfo> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 5);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetNodeRequest
|
||||
*/
|
||||
export type GetNodeRequest = Message<"meshexplorer.v1.GetNodeRequest"> & {
|
||||
/**
|
||||
* @generated from field: string public_key = 1;
|
||||
*/
|
||||
publicKey: string;
|
||||
|
||||
/**
|
||||
* Max number of recent adverts to return (default 50).
|
||||
*
|
||||
* @generated from field: optional int32 limit = 2;
|
||||
*/
|
||||
limit?: number | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetNodeRequest.
|
||||
* Use `create(GetNodeRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetNodeRequestSchema: GenMessage<GetNodeRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 6);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetNodeResponse
|
||||
*/
|
||||
export type GetNodeResponse = Message<"meshexplorer.v1.GetNodeResponse"> & {
|
||||
/**
|
||||
* @generated from field: meshexplorer.v1.NodeInfo node = 1;
|
||||
*/
|
||||
node?: NodeInfo | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.Advert recent_adverts = 2;
|
||||
*/
|
||||
recentAdverts: Advert[];
|
||||
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.LocationHistory location_history = 3;
|
||||
*/
|
||||
locationHistory: LocationHistory[];
|
||||
|
||||
/**
|
||||
* @generated from field: meshexplorer.v1.MqttInfo mqtt = 4;
|
||||
*/
|
||||
mqtt?: MqttInfo | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string region = 5;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetNodeResponse.
|
||||
* Use `create(GetNodeResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetNodeResponseSchema: GenMessage<GetNodeResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 7);
|
||||
|
||||
/**
|
||||
* Direct neighbor of a node (meshcore_node_direct_neighbors).
|
||||
*
|
||||
* @generated from message meshexplorer.v1.Neighbor
|
||||
*/
|
||||
export type Neighbor = Message<"meshexplorer.v1.Neighbor"> & {
|
||||
/**
|
||||
* @generated from field: string public_key = 1;
|
||||
*/
|
||||
publicKey: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string node_name = 2;
|
||||
*/
|
||||
nodeName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double latitude = 3;
|
||||
*/
|
||||
latitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double longitude = 4;
|
||||
*/
|
||||
longitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_location = 5;
|
||||
*/
|
||||
hasLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_repeater = 6;
|
||||
*/
|
||||
isRepeater: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_chat_node = 7;
|
||||
*/
|
||||
isChatNode: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_room_server = 8;
|
||||
*/
|
||||
isRoomServer: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_name = 9;
|
||||
*/
|
||||
hasName: number;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string directions = 10;
|
||||
*/
|
||||
directions: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.Neighbor.
|
||||
* Use `create(NeighborSchema)` to create a new message.
|
||||
*/
|
||||
export const NeighborSchema: GenMessage<Neighbor> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 8);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetNodeNeighborsRequest
|
||||
*/
|
||||
export type GetNodeNeighborsRequest = Message<"meshexplorer.v1.GetNodeNeighborsRequest"> & {
|
||||
/**
|
||||
* @generated from field: string public_key = 1;
|
||||
*/
|
||||
publicKey: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional int32 last_seen = 2;
|
||||
*/
|
||||
lastSeen?: number | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetNodeNeighborsRequest.
|
||||
* Use `create(GetNodeNeighborsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetNodeNeighborsRequestSchema: GenMessage<GetNodeNeighborsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 9);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetNodeNeighborsResponse
|
||||
*/
|
||||
export type GetNodeNeighborsResponse = Message<"meshexplorer.v1.GetNodeNeighborsResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.Neighbor neighbors = 1;
|
||||
*/
|
||||
neighbors: Neighbor[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetNodeNeighborsResponse.
|
||||
* Use `create(GetNodeNeighborsResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetNodeNeighborsResponseSchema: GenMessage<GetNodeNeighborsResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 10);
|
||||
|
||||
/**
|
||||
* One search request within a (possibly batched) SearchNodes call.
|
||||
*
|
||||
* @generated from message meshexplorer.v1.SearchQuery
|
||||
*/
|
||||
export type SearchQuery = Message<"meshexplorer.v1.SearchQuery"> & {
|
||||
/**
|
||||
* @generated from field: optional string query = 1;
|
||||
*/
|
||||
query?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string region = 2;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional int32 last_seen = 3;
|
||||
*/
|
||||
lastSeen?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional int32 limit = 4;
|
||||
*/
|
||||
limit?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool exact = 5;
|
||||
*/
|
||||
exact?: boolean | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional bool is_repeater = 6;
|
||||
*/
|
||||
isRepeater?: boolean | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.SearchQuery.
|
||||
* Use `create(SearchQuerySchema)` to create a new message.
|
||||
*/
|
||||
export const SearchQuerySchema: GenMessage<SearchQuery> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 11);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.SearchResult
|
||||
*/
|
||||
export type SearchResult = Message<"meshexplorer.v1.SearchResult"> & {
|
||||
/**
|
||||
* @generated from field: string public_key = 1;
|
||||
*/
|
||||
publicKey: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string node_name = 2;
|
||||
*/
|
||||
nodeName: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double latitude = 3;
|
||||
*/
|
||||
latitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional double longitude = 4;
|
||||
*/
|
||||
longitude?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_location = 5;
|
||||
*/
|
||||
hasLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_repeater = 6;
|
||||
*/
|
||||
isRepeater: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_chat_node = 7;
|
||||
*/
|
||||
isChatNode: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 is_room_server = 8;
|
||||
*/
|
||||
isRoomServer: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 has_name = 9;
|
||||
*/
|
||||
hasName: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string first_heard = 10;
|
||||
*/
|
||||
firstHeard: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string last_seen = 11;
|
||||
*/
|
||||
lastSeen: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string broker = 12;
|
||||
*/
|
||||
broker: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string topic = 13;
|
||||
*/
|
||||
topic: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.SearchResult.
|
||||
* Use `create(SearchResultSchema)` to create a new message.
|
||||
*/
|
||||
export const SearchResultSchema: GenMessage<SearchResult> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 12);
|
||||
|
||||
/**
|
||||
* Results for a single query in the batch (preserves per-query grouping).
|
||||
*
|
||||
* @generated from message meshexplorer.v1.SearchResultList
|
||||
*/
|
||||
export type SearchResultList = Message<"meshexplorer.v1.SearchResultList"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.SearchResult results = 1;
|
||||
*/
|
||||
results: SearchResult[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.SearchResultList.
|
||||
* Use `create(SearchResultListSchema)` to create a new message.
|
||||
*/
|
||||
export const SearchResultListSchema: GenMessage<SearchResultList> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 13);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.SearchNodesRequest
|
||||
*/
|
||||
export type SearchNodesRequest = Message<"meshexplorer.v1.SearchNodesRequest"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.SearchQuery queries = 1;
|
||||
*/
|
||||
queries: SearchQuery[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.SearchNodesRequest.
|
||||
* Use `create(SearchNodesRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const SearchNodesRequestSchema: GenMessage<SearchNodesRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 14);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.SearchNodesResponse
|
||||
*/
|
||||
export type SearchNodesResponse = Message<"meshexplorer.v1.SearchNodesResponse"> & {
|
||||
/**
|
||||
* One entry per input query, in the same order.
|
||||
*
|
||||
* @generated from field: repeated meshexplorer.v1.SearchResultList results = 1;
|
||||
*/
|
||||
results: SearchResultList[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.SearchNodesResponse.
|
||||
* Use `create(SearchNodesResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const SearchNodesResponseSchema: GenMessage<SearchNodesResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_node, 15);
|
||||
|
||||
/**
|
||||
* @generated from service meshexplorer.v1.NodeService
|
||||
*/
|
||||
export const NodeService: GenService<{
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.NodeService.GetNode
|
||||
*/
|
||||
getNode: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetNodeRequestSchema;
|
||||
output: typeof GetNodeResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.NodeService.GetNodeNeighbors
|
||||
*/
|
||||
getNodeNeighbors: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetNodeNeighborsRequestSchema;
|
||||
output: typeof GetNodeNeighborsResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.NodeService.SearchNodes
|
||||
*/
|
||||
searchNodes: {
|
||||
methodKind: "unary";
|
||||
input: typeof SearchNodesRequestSchema;
|
||||
output: typeof SearchNodesResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_meshexplorer_v1_node, 0);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/packets.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/packets.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_packets: GenFile = /*@__PURE__*/
|
||||
fileDesc("Ch1tZXNoZXhwbG9yZXIvdjEvcGFja2V0cy5wcm90bxIPbWVzaGV4cGxvcmVyLnYxIvMBCgZQYWNrZXQSGAoQaW5nZXN0X3RpbWVzdGFtcBgBIAEoCRIWCg5tZXNoX3RpbWVzdGFtcBgCIAEoCRIOCgZicm9rZXIYAyABKAkSDQoFdG9waWMYBCABKAkSDgoGcGFja2V0GAUgASgJEhAKCHBhdGhfbGVuGAYgASgFEgwKBHBhdGgYByABKAkSEgoKcm91dGVfdHlwZRgIIAEoBRIUCgxwYXlsb2FkX3R5cGUYCSABKAUSFwoPcGF5bG9hZF92ZXJzaW9uGAogASgFEg4KBmhlYWRlchgLIAEoBRIVCg1vcmlnaW5fcHVia2V5GAwgASgJIs8CChRTdHJlYW1QYWNrZXRzUmVxdWVzdBITCgZyZWdpb24YASABKAlIAIgBARIkCgxwYXlsb2FkX3R5cGUYAiABKAVCCbpIBhoEGA8oAEgBiAEBEiIKCnJvdXRlX3R5cGUYAyABKAVCCbpIBhoEGAMoAEgCiAEBEjEKDW9yaWdpbl9wdWJrZXkYBCABKAlCFbpIEnIQMg5eWzAtOUEtRmEtZl0rJEgDiAEBEiYKDXBvbGxfaW50ZXJ2YWwYBSABKAVCCrpIBxoFGJBOKGRIBIgBARIhCghtYXhfcm93cxgGIAEoBUIKukgHGgUYkE4oCkgFiAEBQgkKB19yZWdpb25CDwoNX3BheWxvYWRfdHlwZUINCgtfcm91dGVfdHlwZUIQCg5fb3JpZ2luX3B1YmtleUIQCg5fcG9sbF9pbnRlcnZhbEILCglfbWF4X3Jvd3MyYwoOUGFja2V0c1NlcnZpY2USUQoNU3RyZWFtUGFja2V0cxIlLm1lc2hleHBsb3Jlci52MS5TdHJlYW1QYWNrZXRzUmVxdWVzdBoXLm1lc2hleHBsb3Jlci52MS5QYWNrZXQwAWIGcHJvdG8z", [file_buf_validate_validate]);
|
||||
|
||||
/**
|
||||
* A raw mesh packet (meshcore_packets), hex fields kept as hex strings.
|
||||
*
|
||||
* @generated from message meshexplorer.v1.Packet
|
||||
*/
|
||||
export type Packet = Message<"meshexplorer.v1.Packet"> & {
|
||||
/**
|
||||
* @generated from field: string ingest_timestamp = 1;
|
||||
*/
|
||||
ingestTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string mesh_timestamp = 2;
|
||||
*/
|
||||
meshTimestamp: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string broker = 3;
|
||||
*/
|
||||
broker: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string topic = 4;
|
||||
*/
|
||||
topic: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string packet = 5;
|
||||
*/
|
||||
packet: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 path_len = 6;
|
||||
*/
|
||||
pathLen: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string path = 7;
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 route_type = 8;
|
||||
*/
|
||||
routeType: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 payload_type = 9;
|
||||
*/
|
||||
payloadType: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 payload_version = 10;
|
||||
*/
|
||||
payloadVersion: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 header = 11;
|
||||
*/
|
||||
header: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string origin_pubkey = 12;
|
||||
*/
|
||||
originPubkey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.Packet.
|
||||
* Use `create(PacketSchema)` to create a new message.
|
||||
*/
|
||||
export const PacketSchema: GenMessage<Packet> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_packets, 0);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.StreamPacketsRequest
|
||||
*/
|
||||
export type StreamPacketsRequest = Message<"meshexplorer.v1.StreamPacketsRequest"> & {
|
||||
/**
|
||||
* @generated from field: optional string region = 1;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
|
||||
/**
|
||||
* Payload type filter (0..15).
|
||||
*
|
||||
* @generated from field: optional int32 payload_type = 2;
|
||||
*/
|
||||
payloadType?: number | undefined;
|
||||
|
||||
/**
|
||||
* Route type filter (0..3).
|
||||
*
|
||||
* @generated from field: optional int32 route_type = 3;
|
||||
*/
|
||||
routeType?: number | undefined;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string origin_pubkey = 4;
|
||||
*/
|
||||
originPubkey?: string | undefined;
|
||||
|
||||
/**
|
||||
* Poll interval in ms (clamped 100..10000, default 500).
|
||||
*
|
||||
* @generated from field: optional int32 poll_interval = 5;
|
||||
*/
|
||||
pollInterval?: number | undefined;
|
||||
|
||||
/**
|
||||
* Max rows per poll (clamped 10..10000, default 10).
|
||||
*
|
||||
* @generated from field: optional int32 max_rows = 6;
|
||||
*/
|
||||
maxRows?: number | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.StreamPacketsRequest.
|
||||
* Use `create(StreamPacketsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const StreamPacketsRequestSchema: GenMessage<StreamPacketsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_packets, 1);
|
||||
|
||||
/**
|
||||
* @generated from service meshexplorer.v1.PacketsService
|
||||
*/
|
||||
export const PacketsService: GenService<{
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.PacketsService.StreamPackets
|
||||
*/
|
||||
streamPackets: {
|
||||
methodKind: "server_streaming";
|
||||
input: typeof StreamPacketsRequestSchema;
|
||||
output: typeof PacketSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_meshexplorer_v1_packets, 0);
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts,import_extension=none"
|
||||
// @generated from file meshexplorer/v1/stats.proto (package meshexplorer.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file meshexplorer/v1/stats.proto.
|
||||
*/
|
||||
export const file_meshexplorer_v1_stats: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChttZXNoZXhwbG9yZXIvdjEvc3RhdHMucHJvdG8SD21lc2hleHBsb3Jlci52MSIuCgxTdGF0c1JlcXVlc3QSEwoGcmVnaW9uGAEgASgJSACIAQFCCQoHX3JlZ2lvbiIsChVHZXRUb3RhbE5vZGVzUmVzcG9uc2USEwoLdG90YWxfbm9kZXMYASABKAUipgEKEE5vZGVzT3ZlclRpbWVSb3cSCwoDZGF5GAEgASgJEh8KF2N1bXVsYXRpdmVfdW5pcXVlX25vZGVzGAIgASgFEhsKE25vZGVzX3dpdGhfbG9jYXRpb24YAyABKAUSHgoWbm9kZXNfd2l0aG91dF9sb2NhdGlvbhgEIAEoBRIRCglyZXBlYXRlcnMYBSABKAUSFAoMcm9vbV9zZXJ2ZXJzGAYgASgFIksKGEdldE5vZGVzT3ZlclRpbWVSZXNwb25zZRIvCgRkYXRhGAEgAygLMiEubWVzaGV4cGxvcmVyLnYxLk5vZGVzT3ZlclRpbWVSb3ciQAoRUG9wdWxhckNoYW5uZWxSb3cSFAoMY2hhbm5lbF9oYXNoGAEgASgJEhUKDW1lc3NhZ2VfY291bnQYAiABKAUiTgoaR2V0UG9wdWxhckNoYW5uZWxzUmVzcG9uc2USMAoEZGF0YRgBIAMoCzIiLm1lc2hleHBsb3Jlci52MS5Qb3B1bGFyQ2hhbm5lbFJvdyI3ChFSZXBlYXRlclByZWZpeFJvdxIOCgZwcmVmaXgYASABKAkSEgoKbm9kZV9uYW1lcxgCIAMoCSJPChtHZXRSZXBlYXRlclByZWZpeGVzUmVzcG9uc2USMAoEZGF0YRgBIAMoCzIiLm1lc2hleHBsb3Jlci52MS5SZXBlYXRlclByZWZpeFJvdzKKAwoMU3RhdHNTZXJ2aWNlElYKDUdldFRvdGFsTm9kZXMSHS5tZXNoZXhwbG9yZXIudjEuU3RhdHNSZXF1ZXN0GiYubWVzaGV4cGxvcmVyLnYxLkdldFRvdGFsTm9kZXNSZXNwb25zZRJcChBHZXROb2Rlc092ZXJUaW1lEh0ubWVzaGV4cGxvcmVyLnYxLlN0YXRzUmVxdWVzdBopLm1lc2hleHBsb3Jlci52MS5HZXROb2Rlc092ZXJUaW1lUmVzcG9uc2USYAoSR2V0UG9wdWxhckNoYW5uZWxzEh0ubWVzaGV4cGxvcmVyLnYxLlN0YXRzUmVxdWVzdBorLm1lc2hleHBsb3Jlci52MS5HZXRQb3B1bGFyQ2hhbm5lbHNSZXNwb25zZRJiChNHZXRSZXBlYXRlclByZWZpeGVzEh0ubWVzaGV4cGxvcmVyLnYxLlN0YXRzUmVxdWVzdBosLm1lc2hleHBsb3Jlci52MS5HZXRSZXBlYXRlclByZWZpeGVzUmVzcG9uc2ViBnByb3RvMw");
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.StatsRequest
|
||||
*/
|
||||
export type StatsRequest = Message<"meshexplorer.v1.StatsRequest"> & {
|
||||
/**
|
||||
* @generated from field: optional string region = 1;
|
||||
*/
|
||||
region?: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.StatsRequest.
|
||||
* Use `create(StatsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const StatsRequestSchema: GenMessage<StatsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 0);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetTotalNodesResponse
|
||||
*/
|
||||
export type GetTotalNodesResponse = Message<"meshexplorer.v1.GetTotalNodesResponse"> & {
|
||||
/**
|
||||
* @generated from field: int32 total_nodes = 1;
|
||||
*/
|
||||
totalNodes: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetTotalNodesResponse.
|
||||
* Use `create(GetTotalNodesResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetTotalNodesResponseSchema: GenMessage<GetTotalNodesResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 1);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.NodesOverTimeRow
|
||||
*/
|
||||
export type NodesOverTimeRow = Message<"meshexplorer.v1.NodesOverTimeRow"> & {
|
||||
/**
|
||||
* @generated from field: string day = 1;
|
||||
*/
|
||||
day: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 cumulative_unique_nodes = 2;
|
||||
*/
|
||||
cumulativeUniqueNodes: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 nodes_with_location = 3;
|
||||
*/
|
||||
nodesWithLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 nodes_without_location = 4;
|
||||
*/
|
||||
nodesWithoutLocation: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 repeaters = 5;
|
||||
*/
|
||||
repeaters: number;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 room_servers = 6;
|
||||
*/
|
||||
roomServers: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.NodesOverTimeRow.
|
||||
* Use `create(NodesOverTimeRowSchema)` to create a new message.
|
||||
*/
|
||||
export const NodesOverTimeRowSchema: GenMessage<NodesOverTimeRow> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 2);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetNodesOverTimeResponse
|
||||
*/
|
||||
export type GetNodesOverTimeResponse = Message<"meshexplorer.v1.GetNodesOverTimeResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.NodesOverTimeRow data = 1;
|
||||
*/
|
||||
data: NodesOverTimeRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetNodesOverTimeResponse.
|
||||
* Use `create(GetNodesOverTimeResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetNodesOverTimeResponseSchema: GenMessage<GetNodesOverTimeResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 3);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.PopularChannelRow
|
||||
*/
|
||||
export type PopularChannelRow = Message<"meshexplorer.v1.PopularChannelRow"> & {
|
||||
/**
|
||||
* @generated from field: string channel_hash = 1;
|
||||
*/
|
||||
channelHash: string;
|
||||
|
||||
/**
|
||||
* @generated from field: int32 message_count = 2;
|
||||
*/
|
||||
messageCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.PopularChannelRow.
|
||||
* Use `create(PopularChannelRowSchema)` to create a new message.
|
||||
*/
|
||||
export const PopularChannelRowSchema: GenMessage<PopularChannelRow> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 4);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetPopularChannelsResponse
|
||||
*/
|
||||
export type GetPopularChannelsResponse = Message<"meshexplorer.v1.GetPopularChannelsResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.PopularChannelRow data = 1;
|
||||
*/
|
||||
data: PopularChannelRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetPopularChannelsResponse.
|
||||
* Use `create(GetPopularChannelsResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetPopularChannelsResponseSchema: GenMessage<GetPopularChannelsResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 5);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.RepeaterPrefixRow
|
||||
*/
|
||||
export type RepeaterPrefixRow = Message<"meshexplorer.v1.RepeaterPrefixRow"> & {
|
||||
/**
|
||||
* @generated from field: string prefix = 1;
|
||||
*/
|
||||
prefix: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string node_names = 2;
|
||||
*/
|
||||
nodeNames: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.RepeaterPrefixRow.
|
||||
* Use `create(RepeaterPrefixRowSchema)` to create a new message.
|
||||
*/
|
||||
export const RepeaterPrefixRowSchema: GenMessage<RepeaterPrefixRow> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 6);
|
||||
|
||||
/**
|
||||
* @generated from message meshexplorer.v1.GetRepeaterPrefixesResponse
|
||||
*/
|
||||
export type GetRepeaterPrefixesResponse = Message<"meshexplorer.v1.GetRepeaterPrefixesResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated meshexplorer.v1.RepeaterPrefixRow data = 1;
|
||||
*/
|
||||
data: RepeaterPrefixRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message meshexplorer.v1.GetRepeaterPrefixesResponse.
|
||||
* Use `create(GetRepeaterPrefixesResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetRepeaterPrefixesResponseSchema: GenMessage<GetRepeaterPrefixesResponse> = /*@__PURE__*/
|
||||
messageDesc(file_meshexplorer_v1_stats, 7);
|
||||
|
||||
/**
|
||||
* @generated from service meshexplorer.v1.StatsService
|
||||
*/
|
||||
export const StatsService: GenService<{
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.StatsService.GetTotalNodes
|
||||
*/
|
||||
getTotalNodes: {
|
||||
methodKind: "unary";
|
||||
input: typeof StatsRequestSchema;
|
||||
output: typeof GetTotalNodesResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.StatsService.GetNodesOverTime
|
||||
*/
|
||||
getNodesOverTime: {
|
||||
methodKind: "unary";
|
||||
input: typeof StatsRequestSchema;
|
||||
output: typeof GetNodesOverTimeResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.StatsService.GetPopularChannels
|
||||
*/
|
||||
getPopularChannels: {
|
||||
methodKind: "unary";
|
||||
input: typeof StatsRequestSchema;
|
||||
output: typeof GetPopularChannelsResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc meshexplorer.v1.StatsService.GetRepeaterPrefixes
|
||||
*/
|
||||
getRepeaterPrefixes: {
|
||||
methodKind: "unary";
|
||||
input: typeof StatsRequestSchema;
|
||||
output: typeof GetRepeaterPrefixesResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_meshexplorer_v1_stats, 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { buildApiUrl } from '@/lib/api';
|
||||
import { useQuery } from '@connectrpc/connect-query';
|
||||
import { NeighborsService } from '@/gen/meshexplorer/v1/neighbors_pb';
|
||||
|
||||
export interface AllNeighborsConnection {
|
||||
source_node: string;
|
||||
@@ -27,55 +27,46 @@ interface UseAllNeighborsParams {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function useAllNeighbors({
|
||||
minLat,
|
||||
maxLat,
|
||||
minLng,
|
||||
maxLng,
|
||||
nodeTypes,
|
||||
lastSeen,
|
||||
export function useAllNeighbors({
|
||||
minLat,
|
||||
maxLat,
|
||||
minLng,
|
||||
maxLng,
|
||||
nodeTypes,
|
||||
lastSeen,
|
||||
region,
|
||||
enabled = true
|
||||
enabled = true,
|
||||
}: UseAllNeighborsParams) {
|
||||
return useQuery({
|
||||
queryKey: ['allNeighbors', minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen, region],
|
||||
queryFn: async (): Promise<AllNeighborsConnection[]> => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (minLat !== null && minLat !== undefined) {
|
||||
params.append('minLat', minLat.toString());
|
||||
}
|
||||
if (maxLat !== null && maxLat !== undefined) {
|
||||
params.append('maxLat', maxLat.toString());
|
||||
}
|
||||
if (minLng !== null && minLng !== undefined) {
|
||||
params.append('minLng', minLng.toString());
|
||||
}
|
||||
if (maxLng !== null && maxLng !== undefined) {
|
||||
params.append('maxLng', maxLng.toString());
|
||||
}
|
||||
if (nodeTypes && nodeTypes.length > 0) {
|
||||
nodeTypes.forEach(type => params.append('nodeTypes', type));
|
||||
}
|
||||
if (lastSeen !== null && lastSeen !== undefined) {
|
||||
params.append('lastSeen', lastSeen.toString());
|
||||
}
|
||||
if (region) {
|
||||
params.append('region', region);
|
||||
}
|
||||
|
||||
const url = `/api/neighbors/all${params.toString() ? `?${params.toString()}` : ''}`;
|
||||
|
||||
const response = await fetch(buildApiUrl(url));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch all neighbors: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return useQuery(
|
||||
NeighborsService.method.getAllNeighbors,
|
||||
{
|
||||
minLat: minLat ?? undefined,
|
||||
maxLat: maxLat ?? undefined,
|
||||
minLng: minLng ?? undefined,
|
||||
maxLng: maxLng ?? undefined,
|
||||
nodeTypes: nodeTypes ?? [],
|
||||
lastSeen: lastSeen ?? undefined,
|
||||
region,
|
||||
},
|
||||
enabled: enabled,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes (shorter than individual neighbors since this is more expensive)
|
||||
gcTime: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
{
|
||||
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
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { buildApiUrl } from '@/lib/api';
|
||||
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';
|
||||
|
||||
interface ChatMessagesParams {
|
||||
@@ -20,6 +22,63 @@ 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.
|
||||
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);
|
||||
|
||||
let merged: ChatMessage[];
|
||||
if (existingIndex !== -1) {
|
||||
merged = [...all];
|
||||
merged[existingIndex] = newMessage;
|
||||
} else {
|
||||
merged = [newMessage, ...all];
|
||||
}
|
||||
|
||||
merged.sort(
|
||||
(a, b) => new Date(b.ingest_timestamp).getTime() - new Date(a.ingest_timestamp).getTime(),
|
||||
);
|
||||
|
||||
const pages = [];
|
||||
for (let i = 0; i < merged.length; i += PAGE_SIZE) {
|
||||
const pageIndex = Math.floor(i / PAGE_SIZE);
|
||||
pages.push({
|
||||
...(oldData.pages[pageIndex] || { hasMore: false }),
|
||||
messages: merged.slice(i, i + PAGE_SIZE),
|
||||
});
|
||||
}
|
||||
|
||||
return { ...oldData, pages };
|
||||
}
|
||||
|
||||
export function useChatMessages({
|
||||
channelId,
|
||||
region,
|
||||
@@ -29,12 +88,12 @@ export function useChatMessages({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Build base query key
|
||||
const baseQueryKey = useMemo(() =>
|
||||
['chat-messages', channelId, region] as const,
|
||||
[channelId, region]
|
||||
const baseQueryKey = useMemo(
|
||||
() => ['chat-messages', channelId, region] as const,
|
||||
[channelId, region],
|
||||
);
|
||||
|
||||
// Main infinite query for loading messages with pagination
|
||||
// Infinite query loads message history (older pages) via the unary GetChat RPC.
|
||||
const messagesQuery = useInfiniteQuery({
|
||||
queryKey: baseQueryKey,
|
||||
queryFn: async ({ pageParam, signal }): Promise<ChatMessagesPage> => {
|
||||
@@ -42,28 +101,23 @@ export function useChatMessages({
|
||||
throw new Error('Region is required');
|
||||
}
|
||||
|
||||
let url = `/api/chat?limit=${PAGE_SIZE}®ion=${encodeURIComponent(region)}`;
|
||||
if (channelId) {
|
||||
url += `&channel_id=${channelId}`;
|
||||
}
|
||||
|
||||
if (pageParam) {
|
||||
url += `&before=${encodeURIComponent(pageParam)}`;
|
||||
}
|
||||
const res = await chatClient.getChat(
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
region,
|
||||
channelId: channelId || undefined,
|
||||
before: pageParam || undefined,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
const messages = res.messages.map(toChatMessage);
|
||||
|
||||
const response = await fetch(buildApiUrl(url), { signal });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chat messages: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const messages = Array.isArray(data) ? data : [];
|
||||
|
||||
return {
|
||||
messages,
|
||||
hasMore: messages.length === PAGE_SIZE,
|
||||
oldestTimestamp: messages.length > 0 ? messages[messages.length - 1].ingest_timestamp : undefined,
|
||||
oldestTimestamp:
|
||||
messages.length > 0 ? messages[messages.length - 1].ingest_timestamp : undefined,
|
||||
};
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
@@ -76,106 +130,57 @@ export function useChatMessages({
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
// Auto-refresh query to get newer messages
|
||||
const latestTimestamp = messagesQuery.data?.pages[0]?.messages[0]?.ingest_timestamp;
|
||||
|
||||
const autoRefreshQuery = useQuery({
|
||||
queryKey: [...baseQueryKey, 'auto-refresh', latestTimestamp],
|
||||
queryFn: async ({ signal }): Promise<ChatMessage[]> => {
|
||||
if (!region || !latestTimestamp) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let url = `/api/chat?limit=${PAGE_SIZE}®ion=${encodeURIComponent(region)}`;
|
||||
if (channelId) {
|
||||
url += `&channel_id=${channelId}`;
|
||||
}
|
||||
url += `&after=${encodeURIComponent(latestTimestamp)}`;
|
||||
|
||||
const response = await fetch(buildApiUrl(url), { signal });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch new chat messages: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
},
|
||||
enabled: enabled && autoRefreshEnabled && !!region && !!latestTimestamp,
|
||||
refetchInterval: 5000, // 5 seconds
|
||||
staleTime: 0, // Always fresh for auto-refresh
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
// When auto-refresh finds new messages, update the main query
|
||||
// Live updates: subscribe to the StreamChat server-streaming RPC and merge new
|
||||
// messages into the cache as they arrive (replaces the old 5s polling query).
|
||||
useEffect(() => {
|
||||
if (autoRefreshQuery.data && autoRefreshQuery.data.length > 0) {
|
||||
queryClient.setQueryData(baseQueryKey, (oldData: any) => {
|
||||
if (!oldData?.pages?.[0]) return oldData;
|
||||
|
||||
const newMessages = autoRefreshQuery.data;
|
||||
|
||||
// Get all existing messages from all pages
|
||||
const allExistingMessages = oldData.pages.flatMap((page: any) => page.messages);
|
||||
|
||||
// Process new messages: replace duplicates and collect truly new ones
|
||||
const trulyNewMessages: ChatMessage[] = [];
|
||||
const updatedExistingMessages = [...allExistingMessages];
|
||||
|
||||
for (const newMessage of newMessages) {
|
||||
const existingIndex = updatedExistingMessages.findIndex(
|
||||
(msg: ChatMessage) => msg.message_id === newMessage.message_id
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Replace existing message with the new one
|
||||
updatedExistingMessages[existingIndex] = newMessage;
|
||||
} else {
|
||||
// This is a truly new message
|
||||
trulyNewMessages.push(newMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine truly new messages with updated existing messages
|
||||
// Sort by ingest_timestamp to maintain order
|
||||
const allMessages = [...trulyNewMessages, ...updatedExistingMessages]
|
||||
.sort((a, b) => new Date(b.ingest_timestamp).getTime() - new Date(a.ingest_timestamp).getTime());
|
||||
|
||||
// Redistribute messages back into pages
|
||||
const updatedPages = [];
|
||||
let currentPageMessages = [];
|
||||
|
||||
for (let i = 0; i < allMessages.length; i++) {
|
||||
currentPageMessages.push(allMessages[i]);
|
||||
|
||||
if (currentPageMessages.length === PAGE_SIZE || i === allMessages.length - 1) {
|
||||
updatedPages.push({
|
||||
...oldData.pages[Math.floor(i / PAGE_SIZE)] || { hasMore: false },
|
||||
messages: currentPageMessages,
|
||||
});
|
||||
currentPageMessages = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
pages: updatedPages,
|
||||
};
|
||||
});
|
||||
if (!enabled || !autoRefreshEnabled || !region) {
|
||||
return;
|
||||
}
|
||||
}, [autoRefreshQuery.data, queryClient, baseQueryKey]);
|
||||
|
||||
const controller = new AbortController();
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const stream = chatClient.streamChat(
|
||||
{
|
||||
channelId: channelId || undefined,
|
||||
region,
|
||||
// History is loaded by GetChat; only stream messages from now on.
|
||||
skipInitialMessages: true,
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
for await (const genMsg of stream) {
|
||||
if (cancelled) break;
|
||||
const msg = toChatMessage(genMsg);
|
||||
queryClient.setQueryData(baseQueryKey, (oldData: any) =>
|
||||
mergeStreamedMessage(oldData, msg),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (controller.signal.aborted) return;
|
||||
if (err instanceof ConnectError && err.code === Code.Canceled) return;
|
||||
// A dropped stream shouldn't crash the chat UI; history is still usable.
|
||||
console.warn('Chat stream error:', err);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [enabled, autoRefreshEnabled, region, channelId, queryClient, baseQueryKey]);
|
||||
|
||||
// Flatten all messages from all pages
|
||||
const allMessages = messagesQuery.data?.pages.flatMap(page => page.messages) ?? [];
|
||||
|
||||
// Check if there are more pages to load
|
||||
const hasNextPage = messagesQuery.hasNextPage;
|
||||
|
||||
const allMessages = messagesQuery.data?.pages.flatMap((page) => page.messages) ?? [];
|
||||
|
||||
return {
|
||||
messages: allMessages,
|
||||
loading: messagesQuery.isLoading,
|
||||
error: messagesQuery.error || autoRefreshQuery.error,
|
||||
hasMore: hasNextPage,
|
||||
error: messagesQuery.error,
|
||||
hasMore: messagesQuery.hasNextPage,
|
||||
loadMore: messagesQuery.fetchNextPage,
|
||||
isLoadingMore: messagesQuery.isFetchingNextPage,
|
||||
refresh: () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||
import { create, windowScheduler, indexedResolver } from '@yornaath/batshit';
|
||||
import { buildApiUrl } from '@/lib/api';
|
||||
import { nodeClient } from '@/lib/connect/client';
|
||||
import type { SearchResult } from '@/gen/meshexplorer/v1/node_pb';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface MeshcoreSearchResult {
|
||||
@@ -19,6 +20,25 @@ export interface MeshcoreSearchResult {
|
||||
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[];
|
||||
total: number;
|
||||
@@ -48,26 +68,27 @@ const searchBatcher = create({
|
||||
|
||||
// Create AbortController for this batch
|
||||
const abortController = new AbortController();
|
||||
|
||||
|
||||
// Store the abort controller so individual queries can cancel the batch
|
||||
(searchBatcher as any)._currentAbortController = abortController;
|
||||
|
||||
const response = await fetch(buildApiUrl('/api/meshcore/search'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ queries: normalizedQueries }),
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to execute batch search: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const batchResponse = await response.json();
|
||||
|
||||
// Return results with batch context for resolver
|
||||
const response = await nodeClient.searchNodes(
|
||||
{
|
||||
queries: normalizedQueries.map((q) => ({
|
||||
query: q.query,
|
||||
region: q.region,
|
||||
lastSeen: q.lastSeen,
|
||||
limit: q.limit,
|
||||
exact: q.exact,
|
||||
isRepeater: q.is_repeater,
|
||||
})),
|
||||
},
|
||||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
// Return results with batch context for resolver (array-of-arrays, one per query)
|
||||
return {
|
||||
results: batchResponse.results || [],
|
||||
results: response.results.map((list) => list.results.map(toSearchResult)),
|
||||
queries: queries
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { buildApiUrl } from '@/lib/api';
|
||||
import { useQuery } from '@connectrpc/connect-query';
|
||||
import { NodeService } from '@/gen/meshexplorer/v1/node_pb';
|
||||
|
||||
export interface Neighbor {
|
||||
public_key: string;
|
||||
@@ -21,26 +21,29 @@ interface UseNeighborsParams {
|
||||
}
|
||||
|
||||
export function useNeighbors({ nodeId, lastSeen, enabled = true }: UseNeighborsParams) {
|
||||
return useQuery({
|
||||
queryKey: ['neighbors', nodeId, lastSeen],
|
||||
queryFn: async (): Promise<Neighbor[]> => {
|
||||
if (!nodeId) return [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (lastSeen !== null && lastSeen !== undefined) {
|
||||
params.append('lastSeen', lastSeen.toString());
|
||||
}
|
||||
const url = `/api/meshcore/node/${nodeId}/neighbors${params.toString() ? `?${params.toString()}` : ''}`;
|
||||
|
||||
const response = await fetch(buildApiUrl(url));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch neighbors: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return useQuery(
|
||||
NodeService.method.getNodeNeighbors,
|
||||
{
|
||||
publicKey: nodeId ?? '',
|
||||
lastSeen: lastSeen ?? undefined,
|
||||
},
|
||||
enabled: enabled && !!nodeId,
|
||||
staleTime: 15 * 60 * 1000, // 15 minutes
|
||||
gcTime: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
{
|
||||
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,
|
||||
})),
|
||||
enabled: enabled && !!nodeId,
|
||||
staleTime: 15 * 60 * 1000, // 15 minutes
|
||||
gcTime: 15 * 60 * 1000, // 15 minutes
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { buildApiUrl } from '@/lib/api';
|
||||
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;
|
||||
@@ -71,50 +73,102 @@ 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 };
|
||||
}
|
||||
}
|
||||
return { error: 'An unexpected error occurred', code: 'UNKNOWN_ERROR', status: 500 };
|
||||
}
|
||||
|
||||
export function useNodeData({ publicKey, limit = 50, enabled = true }: UseNodeDataParams) {
|
||||
return useQuery<NodeData, NodeError>({
|
||||
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" } as NodeError;
|
||||
throw { error: 'Public key is required', code: 'MISSING_PUBLIC_KEY', status: 400 } as NodeError & {
|
||||
status: number;
|
||||
};
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (limit !== 50) {
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
try {
|
||||
const res = await nodeClient.getNode({ publicKey, limit });
|
||||
return toNodeData(res);
|
||||
} catch (err) {
|
||||
throw toNodeError(err);
|
||||
}
|
||||
|
||||
const url = `/api/meshcore/node/${publicKey}${params.toString() ? `?${params.toString()}` : ''}`;
|
||||
|
||||
const response = await fetch(buildApiUrl(url));
|
||||
|
||||
// Handle specific error responses
|
||||
if (!response.ok) {
|
||||
let errorData: NodeError;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
errorData = {
|
||||
error: `HTTP ${response.status}: ${response.statusText}`,
|
||||
code: 'UNKNOWN_ERROR'
|
||||
};
|
||||
}
|
||||
|
||||
// Add status information to error for better handling
|
||||
throw {
|
||||
...errorData,
|
||||
status: response.status
|
||||
} as NodeError & { status: number };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
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 as NodeError & { status?: number })?.status;
|
||||
const status = error?.status;
|
||||
if (status && status >= 400 && status < 500) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ export function useQueryParams<T extends Record<string, any>>(defaultValues: T =
|
||||
// (not from our own updates)
|
||||
useEffect(() => {
|
||||
const newState = { ...defaultValues };
|
||||
|
||||
searchParams.forEach((value, key) => {
|
||||
|
||||
searchParams?.forEach((value, key) => {
|
||||
// Don't auto-convert 'q' (query) parameter to number since it should always be a string
|
||||
if (key !== 'q' && !isNaN(Number(value)) && value !== '') {
|
||||
newState[key as keyof T] = Number(value) as T[keyof T];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { buildApiUrl } from "@/lib/api";
|
||||
import { useQuery } from "@connectrpc/connect-query";
|
||||
import { StatsService } from "@/gen/meshexplorer/v1/stats_pb";
|
||||
|
||||
interface TotalNodesResponse {
|
||||
total_nodes: number;
|
||||
@@ -41,108 +41,95 @@ const STALE_TIME = 5 * 60 * 1000; // 5 minutes
|
||||
const GC_TIME = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
export function useTotalNodes(region?: string) {
|
||||
return useQuery<TotalNodesResponse>({
|
||||
queryKey: ['stats', 'total-nodes', region],
|
||||
queryFn: async ({ signal }) => {
|
||||
const regionParam = region ? `?region=${encodeURIComponent(region)}` : '';
|
||||
const response = await fetch(buildApiUrl(`/api/stats/total-nodes${regionParam}`), {
|
||||
signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch total nodes: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
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,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function useNodesOverTime(region?: string) {
|
||||
return useQuery<NodesOverTimeResponse>({
|
||||
queryKey: ['stats', 'nodes-over-time', region],
|
||||
queryFn: async ({ signal }) => {
|
||||
const regionParam = region ? `?region=${encodeURIComponent(region)}` : '';
|
||||
const response = await fetch(buildApiUrl(`/api/stats/nodes-over-time${regionParam}`), {
|
||||
signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch nodes over time: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
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,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function usePopularChannels(region?: string) {
|
||||
return useQuery<PopularChannelsResponse>({
|
||||
queryKey: ['stats', 'popular-channels', region],
|
||||
queryFn: async ({ signal }) => {
|
||||
const regionParam = region ? `?region=${encodeURIComponent(region)}` : '';
|
||||
const response = await fetch(buildApiUrl(`/api/stats/popular-channels${regionParam}`), {
|
||||
signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch popular channels: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
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,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function useRepeaterPrefixes(region?: string) {
|
||||
return useQuery<RepeaterPrefixesResponse>({
|
||||
queryKey: ['stats', 'repeater-prefixes', region],
|
||||
queryFn: async ({ signal }) => {
|
||||
const regionParam = region ? `?region=${encodeURIComponent(region)}` : '';
|
||||
const response = await fetch(buildApiUrl(`/api/stats/repeater-prefixes${regionParam}`), {
|
||||
signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch repeater prefixes: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
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,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function useUnusedPrefixes(region?: string) {
|
||||
const { data: repeaterPrefixesData, isLoading, error } = useRepeaterPrefixes(region);
|
||||
|
||||
|
||||
const unusedPrefixes = React.useMemo(() => {
|
||||
if (!repeaterPrefixesData?.data) return [];
|
||||
|
||||
|
||||
// Generate all possible 2-character hex prefixes (01-FE, excluding 00 and FF)
|
||||
const allPrefixes = [];
|
||||
for (let i = 1; i < 255; i++) {
|
||||
allPrefixes.push(i.toString(16).padStart(2, '0').toUpperCase());
|
||||
}
|
||||
|
||||
|
||||
// Get used prefixes from the API response
|
||||
const usedPrefixes = new Set(repeaterPrefixesData.data.map(row => row.prefix));
|
||||
|
||||
|
||||
// Find unused prefixes
|
||||
return allPrefixes.filter(prefix => !usedPrefixes.has(prefix));
|
||||
}, [repeaterPrefixesData?.data]);
|
||||
|
||||
|
||||
return {
|
||||
data: unusedPrefixes,
|
||||
isLoading,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createClient } from "@connectrpc/connect";
|
||||
import { transport } from "./transport";
|
||||
import { MapService } from "@/gen/meshexplorer/v1/map_pb";
|
||||
import { NodeService } from "@/gen/meshexplorer/v1/node_pb";
|
||||
import { ChatService } from "@/gen/meshexplorer/v1/chat_pb";
|
||||
|
||||
// Promise-based clients for the imperative call sites that don't fit the
|
||||
// connect-query hook shape (the map's manual fetch, batched search, and the
|
||||
// infinite-scroll chat query). The simple hooks use connect-query directly.
|
||||
export const mapClient = createClient(MapService, transport);
|
||||
export const nodeClient = createClient(NodeService, transport);
|
||||
export const chatClient = createClient(ChatService, transport);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createConnectTransport } from "@connectrpc/connect-web";
|
||||
import { getApiBaseUrl } from "@/lib/api";
|
||||
|
||||
// ConnectRPC services are served under the `/api` prefix (see
|
||||
// src/pages/api/[[...connect]].ts), alongside the legacy REST routes.
|
||||
// Honors NEXT_PUBLIC_API_URL the same way buildApiUrl() does.
|
||||
export const transport = createConnectTransport({
|
||||
baseUrl: `${getApiBaseUrl()}/api`,
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { nextJsApiRouter } from "@connectrpc/connect-next";
|
||||
import routes from "@/server/connect/routes";
|
||||
import { validationInterceptor } from "@/server/connect/validation";
|
||||
|
||||
// Serves every ConnectRPC service under /api/<package>.<Service>/<Method>.
|
||||
// Next only treats files under pages/api/** as API routes, so the catch-all
|
||||
// lives here (the default /api prefix matches the file location). protovalidate
|
||||
// runs on every request via the interceptor before handlers see the message.
|
||||
const { handler, config } = nextJsApiRouter({
|
||||
routes,
|
||||
interceptors: [validationInterceptor],
|
||||
});
|
||||
|
||||
export { handler as default, config };
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { MessageInitShape } from "@bufbuild/protobuf";
|
||||
import type { ServiceImpl } from "@connectrpc/connect";
|
||||
import { ChatService } from "@/gen/meshexplorer/v1/chat_pb";
|
||||
import type { ChatMessageSchema } from "@/gen/meshexplorer/v1/chat_pb";
|
||||
import { getLatestChatMessages } from "@/lib/clickhouse/actions";
|
||||
import {
|
||||
createClickHouseStreamer,
|
||||
createChatMessagesStreamerConfig,
|
||||
} from "@/lib/clickhouse/streaming";
|
||||
import { decryptMeshcoreGroupMessage } from "@/lib/meshcore";
|
||||
|
||||
const PUBLIC_MESHCORE_KEY = "izOH6cXN6mrJ5e26oRXNcg==";
|
||||
|
||||
interface ChatRow {
|
||||
ingest_timestamp: string;
|
||||
mesh_timestamp: string;
|
||||
channel_hash: string;
|
||||
mac: string;
|
||||
encrypted_message: string;
|
||||
message_count: number;
|
||||
origin_path_info: Array<[string, string, string, string, string]>;
|
||||
message_id: string;
|
||||
}
|
||||
|
||||
type ParsedMessage = {
|
||||
timestamp: number;
|
||||
msgType: number;
|
||||
sender: string;
|
||||
text: string;
|
||||
rawText: string;
|
||||
};
|
||||
|
||||
function toChatMessage(
|
||||
row: ChatRow,
|
||||
decrypted: ParsedMessage | null,
|
||||
): MessageInitShape<typeof ChatMessageSchema> {
|
||||
return {
|
||||
ingestTimestamp: row.ingest_timestamp,
|
||||
meshTimestamp: row.mesh_timestamp,
|
||||
channelHash: row.channel_hash,
|
||||
mac: row.mac,
|
||||
encryptedMessage: row.encrypted_message,
|
||||
messageCount: row.message_count,
|
||||
originPathInfo: (row.origin_path_info ?? []).map(
|
||||
([origin, originPubkey, path, broker, topic]) => ({
|
||||
origin,
|
||||
originPubkey,
|
||||
path,
|
||||
broker,
|
||||
topic,
|
||||
}),
|
||||
),
|
||||
messageId: row.message_id,
|
||||
decrypted: decrypted
|
||||
? {
|
||||
timestamp: decrypted.timestamp,
|
||||
msgType: decrypted.msgType,
|
||||
sender: decrypted.sender,
|
||||
text: decrypted.text,
|
||||
rawText: decrypted.rawText,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function decryptRow(row: ChatRow, keys: string[]): Promise<ParsedMessage | null> {
|
||||
try {
|
||||
const decrypted = (await decryptMeshcoreGroupMessage({
|
||||
encrypted_message: row.encrypted_message,
|
||||
mac: row.mac,
|
||||
channel_hash: row.channel_hash,
|
||||
knownKeys: keys,
|
||||
parse: true,
|
||||
})) as ParsedMessage | null;
|
||||
return decrypted ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const chatServiceImpl: ServiceImpl<typeof ChatService> = {
|
||||
async getChat(req) {
|
||||
const messages = (await getLatestChatMessages({
|
||||
limit: req.limit ?? 20,
|
||||
before: req.before,
|
||||
after: req.after,
|
||||
channelId: req.channelId,
|
||||
region: req.region,
|
||||
})) as ChatRow[];
|
||||
|
||||
const keys = req.decrypt ? [PUBLIC_MESHCORE_KEY, ...req.privateKeys] : [];
|
||||
|
||||
const out: MessageInitShape<typeof ChatMessageSchema>[] = [];
|
||||
for (const row of messages) {
|
||||
if (req.decrypt) {
|
||||
const decrypted = await decryptRow(row, keys);
|
||||
// Match the REST endpoint: only emit messages that decrypted.
|
||||
if (decrypted) {
|
||||
out.push(toChatMessage(row, decrypted));
|
||||
}
|
||||
} else {
|
||||
out.push(toChatMessage(row, null));
|
||||
}
|
||||
}
|
||||
return { messages: out };
|
||||
},
|
||||
|
||||
async *streamChat(req) {
|
||||
const channelId = req.channelId ? req.channelId.toLowerCase() : undefined;
|
||||
const config = createChatMessagesStreamerConfig(channelId, req.region);
|
||||
config.pollInterval = req.pollInterval ?? 1000;
|
||||
config.maxRowsPerPoll = req.maxRows ?? 500;
|
||||
config.skipInitialMessages = req.skipInitialMessages;
|
||||
|
||||
const streamer = createClickHouseStreamer<ChatRow>(config);
|
||||
const keys = req.decrypt ? [PUBLIC_MESHCORE_KEY, ...req.privateKeys] : [];
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
if (channelId) params.channelId = channelId;
|
||||
|
||||
for await (const result of streamer(params)) {
|
||||
const row = result.row;
|
||||
const decrypted = req.decrypt ? await decryptRow(row, keys) : null;
|
||||
yield toChatMessage(row, decrypted);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ServiceImpl } from "@connectrpc/connect";
|
||||
import { MapService } from "@/gen/meshexplorer/v1/map_pb";
|
||||
import { getNodePositions, getAllNodeNeighbors } from "@/lib/clickhouse/actions";
|
||||
import { numToParam, toNeighborEdge, type NeighborEdgeRow } from "./mappers";
|
||||
|
||||
export const mapServiceImpl: ServiceImpl<typeof MapService> = {
|
||||
async getMap(req) {
|
||||
const minLat = numToParam(req.minLat);
|
||||
const maxLat = numToParam(req.maxLat);
|
||||
const minLng = numToParam(req.minLng);
|
||||
const maxLng = numToParam(req.maxLng);
|
||||
const lastSeen = numToParam(req.lastSeen);
|
||||
|
||||
const positions = await getNodePositions({
|
||||
minLat,
|
||||
maxLat,
|
||||
minLng,
|
||||
maxLng,
|
||||
nodeTypes: req.nodeTypes,
|
||||
lastSeen,
|
||||
});
|
||||
|
||||
let neighbors: NeighborEdgeRow[] = [];
|
||||
if (req.includeNeighbors) {
|
||||
neighbors = await getAllNodeNeighbors(
|
||||
lastSeen,
|
||||
minLat,
|
||||
maxLat,
|
||||
minLng,
|
||||
maxLng,
|
||||
req.nodeTypes,
|
||||
req.region,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: positions.map((p) => ({
|
||||
nodeId: p.node_id,
|
||||
name: p.name ?? undefined,
|
||||
shortName: p.short_name ?? undefined,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
lastSeen: p.last_seen,
|
||||
firstSeen: p.first_seen ?? undefined,
|
||||
type: p.type,
|
||||
})),
|
||||
neighbors: neighbors.map(toNeighborEdge),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { MessageInitShape } from "@bufbuild/protobuf";
|
||||
import type { NeighborEdgeSchema } from "@/gen/meshexplorer/v1/common_pb";
|
||||
|
||||
// Row shape returned by getAllNodeNeighbors() / getNodePositions() neighbor data.
|
||||
export interface NeighborEdgeRow {
|
||||
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;
|
||||
}
|
||||
|
||||
/** Maps a ClickHouse neighbor-edge row to the NeighborEdge proto init shape. */
|
||||
export function toNeighborEdge(row: NeighborEdgeRow): MessageInitShape<typeof NeighborEdgeSchema> {
|
||||
return {
|
||||
sourceNode: row.source_node,
|
||||
targetNode: row.target_node,
|
||||
connectionType: row.connection_type,
|
||||
packetCount: row.packet_count,
|
||||
sourceName: row.source_name,
|
||||
sourceLatitude: row.source_latitude,
|
||||
sourceLongitude: row.source_longitude,
|
||||
sourceHasLocation: row.source_has_location,
|
||||
targetName: row.target_name,
|
||||
targetLatitude: row.target_latitude,
|
||||
targetLongitude: row.target_longitude,
|
||||
targetHasLocation: row.target_has_location,
|
||||
};
|
||||
}
|
||||
|
||||
/** Converts an optional numeric request field to the `string | null` the actions expect. */
|
||||
export function numToParam(n: number | undefined): string | null {
|
||||
return n === undefined ? null : String(n);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ServiceImpl } from "@connectrpc/connect";
|
||||
import { NeighborsService } from "@/gen/meshexplorer/v1/neighbors_pb";
|
||||
import { getAllNodeNeighbors } from "@/lib/clickhouse/actions";
|
||||
import { numToParam, toNeighborEdge } from "./mappers";
|
||||
|
||||
export const neighborsServiceImpl: ServiceImpl<typeof NeighborsService> = {
|
||||
async getAllNeighbors(req) {
|
||||
const neighbors = await getAllNodeNeighbors(
|
||||
numToParam(req.lastSeen),
|
||||
numToParam(req.minLat),
|
||||
numToParam(req.maxLat),
|
||||
numToParam(req.minLng),
|
||||
numToParam(req.maxLng),
|
||||
req.nodeTypes,
|
||||
req.region,
|
||||
);
|
||||
return { neighbors: neighbors.map(toNeighborEdge) };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { MessageInitShape } from "@bufbuild/protobuf";
|
||||
import { Code, ConnectError, type ServiceImpl } from "@connectrpc/connect";
|
||||
import { NodeService } from "@/gen/meshexplorer/v1/node_pb";
|
||||
import type { SearchResultSchema } from "@/gen/meshexplorer/v1/node_pb";
|
||||
import {
|
||||
getMeshcoreNodeInfo,
|
||||
getMeshcoreNodeNeighbors,
|
||||
searchMeshcoreNodes,
|
||||
} from "@/lib/clickhouse/actions";
|
||||
|
||||
interface SearchResultRow {
|
||||
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;
|
||||
}
|
||||
|
||||
function toSearchResult(row: SearchResultRow): MessageInitShape<typeof SearchResultSchema> {
|
||||
return {
|
||||
publicKey: row.public_key,
|
||||
nodeName: row.node_name,
|
||||
latitude: row.latitude ?? undefined,
|
||||
longitude: row.longitude ?? undefined,
|
||||
hasLocation: row.has_location,
|
||||
isRepeater: row.is_repeater,
|
||||
isChatNode: row.is_chat_node,
|
||||
isRoomServer: row.is_room_server,
|
||||
hasName: row.has_name,
|
||||
firstHeard: row.first_heard,
|
||||
lastSeen: row.last_seen,
|
||||
broker: row.broker,
|
||||
topic: row.topic,
|
||||
};
|
||||
}
|
||||
|
||||
export const nodeServiceImpl: ServiceImpl<typeof NodeService> = {
|
||||
async getNode(req) {
|
||||
const publicKey = req.publicKey.toUpperCase();
|
||||
const nodeInfo = await getMeshcoreNodeInfo(publicKey, req.limit ?? 50);
|
||||
|
||||
if (!nodeInfo) {
|
||||
throw new ConnectError(`node not found: ${publicKey}`, Code.NotFound);
|
||||
}
|
||||
|
||||
const node = nodeInfo.node;
|
||||
// recentAdverts / locationHistory come back as untyped JSON rows.
|
||||
const adverts = nodeInfo.recentAdverts as Array<{
|
||||
adv_timestamp: string;
|
||||
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;
|
||||
}>;
|
||||
const locationHistory = nodeInfo.locationHistory as Array<{
|
||||
mesh_timestamp: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}>;
|
||||
|
||||
return {
|
||||
node: {
|
||||
publicKey: node.public_key,
|
||||
nodeName: node.node_name,
|
||||
latitude: node.latitude ?? undefined,
|
||||
longitude: node.longitude ?? undefined,
|
||||
hasLocation: node.has_location,
|
||||
isRepeater: node.is_repeater,
|
||||
isChatNode: node.is_chat_node,
|
||||
isRoomServer: node.is_room_server,
|
||||
hasName: node.has_name,
|
||||
broker: node.broker ?? undefined,
|
||||
topic: node.topic ?? undefined,
|
||||
firstSeen: node.first_seen,
|
||||
lastSeen: node.last_seen,
|
||||
},
|
||||
recentAdverts: adverts.map((a) => ({
|
||||
advTimestamp: a.adv_timestamp,
|
||||
originPathPubkeyTuples: (a.origin_path_pubkey_tuples ?? []).map(
|
||||
([origin, path, originPubkey]) => ({ origin, path, originPubkey }),
|
||||
),
|
||||
advertCount: a.advert_count,
|
||||
earliestTimestamp: a.earliest_timestamp,
|
||||
latestTimestamp: a.latest_timestamp,
|
||||
latitude: a.latitude ?? undefined,
|
||||
longitude: a.longitude ?? undefined,
|
||||
isRepeater: a.is_repeater,
|
||||
isChatNode: a.is_chat_node,
|
||||
isRoomServer: a.is_room_server,
|
||||
hasLocation: a.has_location,
|
||||
packetHash: a.packet_hash,
|
||||
})),
|
||||
locationHistory: locationHistory.map((l) => ({
|
||||
meshTimestamp: l.mesh_timestamp,
|
||||
latitude: l.latitude,
|
||||
longitude: l.longitude,
|
||||
})),
|
||||
mqtt: {
|
||||
isUplinked: nodeInfo.mqtt.is_uplinked,
|
||||
hasPackets: nodeInfo.mqtt.has_packets,
|
||||
topics: nodeInfo.mqtt.topics.map((t) => ({
|
||||
topic: t.topic,
|
||||
broker: t.broker,
|
||||
lastPacketTime: t.last_packet_time,
|
||||
isRecent: t.is_recent,
|
||||
})),
|
||||
},
|
||||
region: nodeInfo.region ?? undefined,
|
||||
};
|
||||
},
|
||||
|
||||
async getNodeNeighbors(req) {
|
||||
const neighbors = await getMeshcoreNodeNeighbors(
|
||||
req.publicKey.toUpperCase(),
|
||||
req.lastSeen === undefined ? null : String(req.lastSeen),
|
||||
);
|
||||
return {
|
||||
neighbors: neighbors.map((n) => ({
|
||||
publicKey: n.public_key,
|
||||
nodeName: n.node_name,
|
||||
latitude: n.latitude ?? undefined,
|
||||
longitude: n.longitude ?? undefined,
|
||||
hasLocation: n.has_location,
|
||||
isRepeater: n.is_repeater,
|
||||
isChatNode: n.is_chat_node,
|
||||
isRoomServer: n.is_room_server,
|
||||
hasName: n.has_name,
|
||||
directions: n.directions,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async searchNodes(req) {
|
||||
if (req.queries.length === 0) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
const queries = req.queries.map((q) => ({
|
||||
query: q.query,
|
||||
region: q.region,
|
||||
lastSeen: q.lastSeen === undefined ? null : String(q.lastSeen),
|
||||
limit: q.limit ?? 50,
|
||||
exact: q.exact ?? false,
|
||||
is_repeater: q.isRepeater,
|
||||
}));
|
||||
|
||||
const grouped = (await searchMeshcoreNodes(queries)) as SearchResultRow[][];
|
||||
|
||||
return {
|
||||
results: grouped.map((group) => ({
|
||||
results: group.map(toSearchResult),
|
||||
})),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ServiceImpl } from "@connectrpc/connect";
|
||||
import { PacketsService } from "@/gen/meshexplorer/v1/packets_pb";
|
||||
import {
|
||||
createClickHouseStreamer,
|
||||
createMeshcorePacketsStreamerConfig,
|
||||
} from "@/lib/clickhouse/streaming";
|
||||
|
||||
interface PacketRow {
|
||||
ingest_timestamp: string;
|
||||
mesh_timestamp: string;
|
||||
broker: string;
|
||||
topic: string;
|
||||
packet: string;
|
||||
path_len: number;
|
||||
path: string;
|
||||
route_type: number;
|
||||
payload_type: number;
|
||||
payload_version: number;
|
||||
header: number;
|
||||
origin_pubkey: string;
|
||||
}
|
||||
|
||||
export const packetsServiceImpl: ServiceImpl<typeof PacketsService> = {
|
||||
async *streamPackets(req) {
|
||||
const originPubkey = req.originPubkey ? req.originPubkey.toUpperCase() : undefined;
|
||||
const config = createMeshcorePacketsStreamerConfig(
|
||||
req.region,
|
||||
req.payloadType,
|
||||
req.routeType,
|
||||
originPubkey,
|
||||
);
|
||||
config.pollInterval = req.pollInterval ?? 500;
|
||||
config.maxRowsPerPoll = req.maxRows ?? 10;
|
||||
|
||||
const streamer = createClickHouseStreamer<PacketRow>(config);
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
if (req.payloadType !== undefined) params.payloadType = req.payloadType;
|
||||
if (req.routeType !== undefined) params.routeType = req.routeType;
|
||||
if (originPubkey) params.originPubkey = originPubkey;
|
||||
|
||||
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: row.path_len,
|
||||
path: row.path,
|
||||
routeType: row.route_type,
|
||||
payloadType: row.payload_type,
|
||||
payloadVersion: row.payload_version,
|
||||
header: row.header,
|
||||
originPubkey: row.origin_pubkey,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { ConnectRouter } from "@connectrpc/connect";
|
||||
import { MapService } from "@/gen/meshexplorer/v1/map_pb";
|
||||
import { NeighborsService } from "@/gen/meshexplorer/v1/neighbors_pb";
|
||||
import { NodeService } from "@/gen/meshexplorer/v1/node_pb";
|
||||
import { StatsService } from "@/gen/meshexplorer/v1/stats_pb";
|
||||
import { ChatService } from "@/gen/meshexplorer/v1/chat_pb";
|
||||
import { PacketsService } from "@/gen/meshexplorer/v1/packets_pb";
|
||||
import { mapServiceImpl } from "./map";
|
||||
import { neighborsServiceImpl } from "./neighbors";
|
||||
import { nodeServiceImpl } from "./node";
|
||||
import { statsServiceImpl } from "./stats";
|
||||
import { chatServiceImpl } from "./chat";
|
||||
import { packetsServiceImpl } from "./packets";
|
||||
|
||||
export default function routes(router: ConnectRouter) {
|
||||
router.service(MapService, mapServiceImpl);
|
||||
router.service(NeighborsService, neighborsServiceImpl);
|
||||
router.service(NodeService, nodeServiceImpl);
|
||||
router.service(StatsService, statsServiceImpl);
|
||||
router.service(ChatService, chatServiceImpl);
|
||||
router.service(PacketsService, packetsServiceImpl);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { ServiceImpl } from "@connectrpc/connect";
|
||||
import { StatsService } from "@/gen/meshexplorer/v1/stats_pb";
|
||||
import { clickhouse } from "@/lib/clickhouse/clickhouse";
|
||||
import {
|
||||
generateRegionWhereClause,
|
||||
generateRegionWhereClauseFromArray,
|
||||
} from "@/lib/regionFilters";
|
||||
|
||||
export const statsServiceImpl: ServiceImpl<typeof StatsService> = {
|
||||
async getTotalNodes(req) {
|
||||
const regionFilter = generateRegionWhereClause(req.region);
|
||||
const whereClause = regionFilter.whereClause ? `WHERE ${regionFilter.whereClause}` : "";
|
||||
const query = `SELECT count(DISTINCT public_key) AS total_nodes FROM meshcore_adverts ${whereClause}`;
|
||||
const resultSet = await clickhouse.query({ query, format: "JSONEachRow" });
|
||||
const rows = (await resultSet.json()) as Array<{ total_nodes: number }>;
|
||||
return { totalNodes: rows.length > 0 ? Number(rows[0].total_nodes) : 0 };
|
||||
},
|
||||
|
||||
async getNodesOverTime(req) {
|
||||
const regionFilter = generateRegionWhereClause(req.region);
|
||||
const regionWhereClause = regionFilter.whereClause ? `WHERE ${regionFilter.whereClause}` : "";
|
||||
const query = `
|
||||
WITH all_nodes AS (
|
||||
SELECT toDate(ingest_timestamp) AS day, public_key, latitude, longitude, is_repeater, is_room_server
|
||||
FROM meshcore_adverts
|
||||
${regionWhereClause}
|
||||
),
|
||||
all_days AS (
|
||||
SELECT DISTINCT day FROM all_nodes
|
||||
ORDER BY day ASC
|
||||
),
|
||||
rolling_window AS (
|
||||
SELECT
|
||||
d.day,
|
||||
n.public_key,
|
||||
n.latitude,
|
||||
n.longitude,
|
||||
n.is_repeater,
|
||||
n.is_room_server
|
||||
FROM all_days d
|
||||
INNER JOIN all_nodes n ON n.day BETWEEN (d.day - INTERVAL 6 DAY) AND d.day
|
||||
)
|
||||
SELECT day,
|
||||
count(DISTINCT public_key) AS cumulative_unique_nodes,
|
||||
count(DISTINCT CASE WHEN latitude IS NOT NULL AND longitude IS NOT NULL AND latitude != 0 AND longitude != 0 THEN public_key END) AS nodes_with_location,
|
||||
count(DISTINCT CASE WHEN latitude IS NULL OR longitude IS NULL OR latitude = 0 OR longitude = 0 THEN public_key END) AS nodes_without_location,
|
||||
count(DISTINCT CASE WHEN is_repeater = 1 THEN public_key END) AS repeaters,
|
||||
count(DISTINCT CASE WHEN is_room_server = 1 THEN public_key END) AS room_servers
|
||||
FROM rolling_window
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`;
|
||||
const resultSet = await clickhouse.query({ query, format: "JSONEachRow" });
|
||||
const rows = (await resultSet.json()) as Array<{
|
||||
day: string;
|
||||
cumulative_unique_nodes: number;
|
||||
nodes_with_location: number;
|
||||
nodes_without_location: number;
|
||||
repeaters: number;
|
||||
room_servers: number;
|
||||
}>;
|
||||
return {
|
||||
data: rows.map((r) => ({
|
||||
day: r.day,
|
||||
cumulativeUniqueNodes: Number(r.cumulative_unique_nodes),
|
||||
nodesWithLocation: Number(r.nodes_with_location),
|
||||
nodesWithoutLocation: Number(r.nodes_without_location),
|
||||
repeaters: Number(r.repeaters),
|
||||
roomServers: Number(r.room_servers),
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async getPopularChannels(req) {
|
||||
const regionFilter = generateRegionWhereClauseFromArray(req.region);
|
||||
const whereClause = regionFilter.whereClause ? `WHERE ${regionFilter.whereClause}` : "";
|
||||
const query = `
|
||||
SELECT channel_hash, count() AS message_count
|
||||
FROM meshcore_public_channel_messages
|
||||
${whereClause}
|
||||
GROUP BY channel_hash
|
||||
ORDER BY message_count DESC
|
||||
LIMIT 10
|
||||
`;
|
||||
const resultSet = await clickhouse.query({ query, format: "JSONEachRow" });
|
||||
const rows = (await resultSet.json()) as Array<{ channel_hash: string; message_count: number }>;
|
||||
return {
|
||||
data: rows.map((r) => ({
|
||||
channelHash: r.channel_hash,
|
||||
messageCount: Number(r.message_count),
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async getRepeaterPrefixes(req) {
|
||||
const regionFilter = generateRegionWhereClause(req.region);
|
||||
const regionWhereClause = regionFilter.whereClause ? `AND ${regionFilter.whereClause}` : "";
|
||||
const query = `
|
||||
SELECT
|
||||
substring(public_key, 1, 2) as prefix,
|
||||
count() as node_count,
|
||||
groupArray(node_name) as node_names
|
||||
FROM meshcore_adverts_latest
|
||||
WHERE is_repeater = 1
|
||||
AND last_seen >= now() - INTERVAL 2 DAY
|
||||
${regionWhereClause}
|
||||
GROUP BY prefix
|
||||
ORDER BY node_count DESC, prefix ASC
|
||||
`;
|
||||
const resultSet = await clickhouse.query({ query, format: "JSONEachRow" });
|
||||
const rows = (await resultSet.json()) as Array<{
|
||||
prefix: string;
|
||||
node_count: number;
|
||||
node_names: string[];
|
||||
}>;
|
||||
return {
|
||||
data: rows.map((r) => ({
|
||||
prefix: r.prefix,
|
||||
nodeNames: r.node_names,
|
||||
})),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
|
||||
import { createValidator } from "@bufbuild/protovalidate";
|
||||
import { Code, ConnectError, type Interceptor } from "@connectrpc/connect";
|
||||
|
||||
// A single validator instance compiles and caches CEL programs per message
|
||||
// type, so reuse it across all requests.
|
||||
const validator = createValidator();
|
||||
|
||||
function assertValid<Desc extends DescMessage>(schema: Desc, message: MessageShape<Desc>): void {
|
||||
const result = validator.validate(schema, message);
|
||||
if (result.kind === "invalid") {
|
||||
// Surface the human-readable rule violations to the caller.
|
||||
throw new ConnectError(result.error.message, Code.InvalidArgument);
|
||||
}
|
||||
if (result.kind === "error") {
|
||||
// A rule failed to compile/evaluate — that's a server-side bug, not bad input.
|
||||
throw new ConnectError(`request validation failed: ${result.error.message}`, Code.Internal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server interceptor that enforces protovalidate (buf.validate) rules on every
|
||||
* inbound request message before it reaches a handler. Works for unary calls
|
||||
* and for the single request message of server-streaming calls.
|
||||
*/
|
||||
export const validationInterceptor: Interceptor = (next) => async (req) => {
|
||||
if (req.stream) {
|
||||
const source = req.message;
|
||||
const validated = (async function* () {
|
||||
for await (const message of source) {
|
||||
assertValid(req.method.input, message);
|
||||
yield message;
|
||||
}
|
||||
})();
|
||||
return next({ ...req, message: validated });
|
||||
}
|
||||
assertValid(req.method.input, req.message);
|
||||
return next(req);
|
||||
};
|
||||
Reference in New Issue
Block a user