mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-08 01:22:43 +02:00
repeater prefixes
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user