repeater prefixes

This commit is contained in:
ajvpot
2025-08-04 05:50:57 +02:00
parent 80f283a495
commit 6847076717
2 changed files with 62 additions and 1 deletions
@@ -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 });
}
}
+39 -1
View File
@@ -7,19 +7,22 @@ export default function StatsPage() {
const [totalNodes, setTotalNodes] = useState<number | null>(null);
const [nodesOverTime, setNodesOverTime] = useState<any[]>([]);
const [popularChannels, setPopularChannels] = useState<any[]>([]);
const [repeaterPrefixes, setRepeaterPrefixes] = useState<any[]>([]);
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() {
</tbody>
</table>
</div>
<div className="mb-6">
<h2 className="text-lg font-semibold mb-2">Used Repeater Prefixes</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
Shows repeater nodes grouped by the first byte of their public key, seen within the last 7 days.
</p>
<table className="w-full text-sm border">
<thead>
<tr>
<th className="border px-2 py-1">Prefix</th>
<th className="border px-2 py-1">Node Names</th>
</tr>
</thead>
<tbody>
{repeaterPrefixes.map((row, i) => (
<tr key={i}>
<td className="border px-2 py-1 font-mono">{row.prefix}</td>
<td className="border px-2 py-1">
{row.node_names && row.node_names.length > 0 ? (
<div className="space-y-1">
{row.node_names.map((name: string, j: number) => (
<div key={j} className="text-xs">
{name || 'Unnamed Node'}
</div>
))}
</div>
) : (
<span className="text-gray-500">No named nodes</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>