From 8416910fc90c8ef74d2744d33ea6cf9a19742bec Mon Sep 17 00:00:00 2001 From: Alex Vanderpot Date: Sun, 14 Jun 2026 17:49:18 -0400 Subject: [PATCH] Migrate the /api/regions endpoint to ConnectRPC main added a dynamically-discovered regions endpoint (region IATA system). Port it to a RegionsService.GetRegions RPC: new regions.proto (Region / RegionGroup messages), a handler reusing getAvailableRegions()/getRegionGroups(), and useRegions/useRegionGroups switched to connect-query against the generated types. The REST /api/regions route stays for coexistence. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../proto/meshexplorer/v1/regions.proto | 31 ++++++++++++++++ meshexplorer/src/hooks/useRegions.ts | 35 ++++--------------- meshexplorer/src/server/connect/regions.ts | 23 ++++++++++++ meshexplorer/src/server/connect/routes.ts | 3 ++ 4 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 meshexplorer/proto/meshexplorer/v1/regions.proto create mode 100644 meshexplorer/src/server/connect/regions.ts diff --git a/meshexplorer/proto/meshexplorer/v1/regions.proto b/meshexplorer/proto/meshexplorer/v1/regions.proto new file mode 100644 index 0000000..77adc2f --- /dev/null +++ b/meshexplorer/proto/meshexplorer/v1/regions.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package meshexplorer.v1; + +// A dynamically-discovered region (meshcore_regions MV). +message Region { + // Canonical IATA region code, e.g. "SEA". + string name = 1; + string friendly_name = 2; + // Distinct nodes seen in this region (last 30 days). + int32 node_count = 3; + string last_seen = 4; +} + +// A named set of IATA region codes (region_groups table). +message RegionGroup { + string code = 1; + string name = 2; + repeated string members = 3; +} + +message GetRegionsRequest {} + +message GetRegionsResponse { + repeated Region regions = 1; + repeated RegionGroup groups = 2; +} + +service RegionsService { + rpc GetRegions(GetRegionsRequest) returns (GetRegionsResponse); +} diff --git a/meshexplorer/src/hooks/useRegions.ts b/meshexplorer/src/hooks/useRegions.ts index 30aa441..9e5e3c0 100644 --- a/meshexplorer/src/hooks/useRegions.ts +++ b/meshexplorer/src/hooks/useRegions.ts @@ -1,37 +1,16 @@ -import { useQuery } from "@tanstack/react-query"; -import { buildApiUrl } from "@/lib/api"; -import type { RegionGroup } from "@/lib/regionGroups"; - -export interface RegionOption { - name: string; - friendlyName: string; - nodeCount?: number; - lastSeen?: string; -} - -interface RegionsResponse { - regions: RegionOption[]; - groups: RegionGroup[]; -} +import { useQuery } from "@connectrpc/connect-query"; +import { RegionsService } from "@/gen/meshexplorer/v1/regions_pb"; const STALE_TIME = 60 * 60 * 1000; // 1h — the region set is slow-moving / MV-backed const GC_TIME = 2 * 60 * 60 * 1000; // Single shared query (one network request) feeding both useRegions() and useRegionGroups(). function useRegionsQuery() { - return useQuery({ - queryKey: ["regions"], - queryFn: async ({ signal }) => { - const response = await fetch(buildApiUrl("/api/regions"), { signal }); - if (!response.ok) { - throw new Error(`Failed to fetch regions: ${response.statusText}`); - } - return response.json(); - }, - staleTime: STALE_TIME, - gcTime: GC_TIME, - retry: 2, - }); + return useQuery( + RegionsService.method.getRegions, + {}, + { staleTime: STALE_TIME, gcTime: GC_TIME, retry: 2 }, + ); } export function useRegions() { diff --git a/meshexplorer/src/server/connect/regions.ts b/meshexplorer/src/server/connect/regions.ts new file mode 100644 index 0000000..915607d --- /dev/null +++ b/meshexplorer/src/server/connect/regions.ts @@ -0,0 +1,23 @@ +import type { ServiceImpl } from "@connectrpc/connect"; +import { RegionsService } from "@/gen/meshexplorer/v1/regions_pb"; +import { getAvailableRegions, getRegionGroups } from "@/lib/clickhouse/regions"; +import { num } from "./mappers"; + +export const regionsServiceImpl: ServiceImpl = { + async getRegions() { + const [regions, groups] = await Promise.all([getAvailableRegions(), getRegionGroups()]); + return { + regions: regions.map((r) => ({ + name: r.name, + friendlyName: r.friendlyName, + nodeCount: num(r.nodeCount), + lastSeen: r.lastSeen, + })), + groups: groups.map((g) => ({ + code: g.code, + name: g.name, + members: g.members, + })), + }; + }, +}; diff --git a/meshexplorer/src/server/connect/routes.ts b/meshexplorer/src/server/connect/routes.ts index 58c61c5..b0ebde5 100644 --- a/meshexplorer/src/server/connect/routes.ts +++ b/meshexplorer/src/server/connect/routes.ts @@ -5,12 +5,14 @@ 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 { RegionsService } from "@/gen/meshexplorer/v1/regions_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"; +import { regionsServiceImpl } from "./regions"; export default function routes(router: ConnectRouter) { router.service(MapService, mapServiceImpl); @@ -19,4 +21,5 @@ export default function routes(router: ConnectRouter) { router.service(StatsService, statsServiceImpl); router.service(ChatService, chatServiceImpl); router.service(PacketsService, packetsServiceImpl); + router.service(RegionsService, regionsServiceImpl); }