From 68470767178f700b20c3f0be4721e83f4d91460f Mon Sep 17 00:00:00 2001 From: ajvpot <553597+ajvpot@users.noreply.github.com> Date: Mon, 4 Aug 2025 05:50:57 +0200 Subject: [PATCH] repeater prefixes --- src/app/api/stats/repeater-prefixes/route.ts | 23 +++++++++++ src/app/stats/page.tsx | 40 +++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/app/api/stats/repeater-prefixes/route.ts diff --git a/src/app/api/stats/repeater-prefixes/route.ts b/src/app/api/stats/repeater-prefixes/route.ts new file mode 100644 index 0000000..3afb8f1 --- /dev/null +++ b/src/app/api/stats/repeater-prefixes/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { clickhouse } from "@/lib/clickhouse/clickhouse"; + +export async function GET() { + try { + 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 7 DAY + 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 NextResponse.json({ data: rows }); + } catch (error) { + return NextResponse.json({ error: "Failed to fetch repeater prefixes" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/stats/page.tsx b/src/app/stats/page.tsx index f159c2b..f388b06 100644 --- a/src/app/stats/page.tsx +++ b/src/app/stats/page.tsx @@ -7,19 +7,22 @@ export default function StatsPage() { const [totalNodes, setTotalNodes] = useState(null); const [nodesOverTime, setNodesOverTime] = useState([]); const [popularChannels, setPopularChannels] = useState([]); + const [repeaterPrefixes, setRepeaterPrefixes] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchStats() { setLoading(true); - const [totalNodesRes, nodesOverTimeRes, popularChannelsRes] = await Promise.all([ + const [totalNodesRes, nodesOverTimeRes, popularChannelsRes, repeaterPrefixesRes] = await Promise.all([ fetch(buildApiUrl("/api/stats/total-nodes")).then(r => r.json()), fetch(buildApiUrl("/api/stats/nodes-over-time")).then(r => r.json()), fetch(buildApiUrl("/api/stats/popular-channels")).then(r => r.json()), + fetch(buildApiUrl("/api/stats/repeater-prefixes")).then(r => r.json()), ]); setTotalNodes(totalNodesRes.total_nodes ?? null); setNodesOverTime(nodesOverTimeRes.data ?? []); setPopularChannels(popularChannelsRes.data ?? []); + setRepeaterPrefixes(repeaterPrefixesRes.data ?? []); setLoading(false); } fetchStats(); @@ -83,6 +86,41 @@ export default function StatsPage() { + +
+

Used Repeater Prefixes

+

+ Shows repeater nodes grouped by the first byte of their public key, seen within the last 7 days. +

+ + + + + + + + + {repeaterPrefixes.map((row, i) => ( + + + + + ))} + +
PrefixNode Names
{row.prefix} + {row.node_names && row.node_names.length > 0 ? ( +
+ {row.node_names.map((name: string, j: number) => ( +
+ {name || 'Unnamed Node'} +
+ ))} +
+ ) : ( + No named nodes + )} +
+
)}