From 3e6126e108f6ad2bd3d9450254cb66096fedddec Mon Sep 17 00:00:00 2001 From: ajvpot <553597+ajvpot@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:00:00 +0000 Subject: [PATCH] stats --- src/app/api/stats/nodes-over-time/route.ts | 44 +++++++++++ src/app/api/stats/popular-channels/route.ts | 19 +++++ src/app/api/stats/total-nodes/route.ts | 14 ++++ src/app/messages/page.tsx | 2 +- src/app/stats/page.tsx | 86 +++++++++++++++++++++ src/components/ChatBox.tsx | 4 +- src/components/Header.tsx | 1 + 7 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/app/api/stats/nodes-over-time/route.ts create mode 100644 src/app/api/stats/popular-channels/route.ts create mode 100644 src/app/api/stats/total-nodes/route.ts create mode 100644 src/app/stats/page.tsx diff --git a/src/app/api/stats/nodes-over-time/route.ts b/src/app/api/stats/nodes-over-time/route.ts new file mode 100644 index 0000000..955ad52 --- /dev/null +++ b/src/app/api/stats/nodes-over-time/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import { clickhouse } from "@/lib/clickhouse/clickhouse"; + +export async function GET() { + try { + const query = ` + WITH days AS ( + SELECT arrayJoin(range(0, 30)) AS offset, toDate(subtractDays(today(), 29 - offset)) AS day + ), + all_nodes AS ( + SELECT toDate(ingest_timestamp) AS day, public_key, latitude, longitude + FROM meshcore_adverts + WHERE ingest_timestamp >= subtractDays(today(), 30) + ), + node_days AS ( + SELECT public_key, min(day) AS first_seen, any(latitude) AS latitude, any(longitude) AS longitude + FROM all_nodes + GROUP BY public_key + ), + expanded AS ( + SELECT d.day, nd.public_key, nd.latitude, nd.longitude + FROM days d + INNER JOIN node_days nd ON nd.first_seen <= 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 THEN public_key END) AS nodes_with_location, + count(DISTINCT CASE WHEN latitude IS NULL OR longitude IS NULL THEN public_key END) AS nodes_without_location + FROM expanded + 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 + }>; + return NextResponse.json({ data: rows }); + } catch (error) { + return NextResponse.json({ error: "Failed to fetch nodes over time" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/stats/popular-channels/route.ts b/src/app/api/stats/popular-channels/route.ts new file mode 100644 index 0000000..7670b2c --- /dev/null +++ b/src/app/api/stats/popular-channels/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { clickhouse } from "@/lib/clickhouse/clickhouse"; + +export async function GET() { + try { + const query = ` + SELECT channel_hash, count() AS message_count + FROM meshcore_public_channel_messages + 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 NextResponse.json({ data: rows }); + } catch (error) { + return NextResponse.json({ error: "Failed to fetch popular channels" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/stats/total-nodes/route.ts b/src/app/api/stats/total-nodes/route.ts new file mode 100644 index 0000000..652f250 --- /dev/null +++ b/src/app/api/stats/total-nodes/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { clickhouse } from "@/lib/clickhouse/clickhouse"; + +export async function GET() { + try { + const query = `SELECT count() AS total_nodes FROM meshcore_adverts_latest`; + const resultSet = await clickhouse.query({ query, format: 'JSONEachRow' }); + const rows = await resultSet.json() as Array<{ total_nodes: number }>; + const total = rows.length > 0 ? Number(rows[0].total_nodes) : 0; + return NextResponse.json({ total_nodes: total }); + } catch (error) { + return NextResponse.json({ error: "Failed to fetch total nodes" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/messages/page.tsx b/src/app/messages/page.tsx index 30d2533..c1687bd 100644 --- a/src/app/messages/page.tsx +++ b/src/app/messages/page.tsx @@ -110,7 +110,7 @@ export default function MessagesPage() {
{/* Header row with title and refresh button */}
-

All Messages

+

MeshCore Messages

fetchMessages(undefined, true)} loading={loading} diff --git a/src/app/stats/page.tsx b/src/app/stats/page.tsx new file mode 100644 index 0000000..e14b106 --- /dev/null +++ b/src/app/stats/page.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function StatsPage() { + const [totalNodes, setTotalNodes] = useState(null); + const [nodesOverTime, setNodesOverTime] = useState([]); + const [popularChannels, setPopularChannels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchStats() { + setLoading(true); + const [totalNodesRes, nodesOverTimeRes, popularChannelsRes] = await Promise.all([ + fetch("/api/stats/total-nodes").then(r => r.json()), + fetch("/api/stats/nodes-over-time").then(r => r.json()), + fetch("/api/stats/popular-channels").then(r => r.json()), + ]); + setTotalNodes(totalNodesRes.total_nodes ?? null); + setNodesOverTime(nodesOverTimeRes.data ?? []); + setPopularChannels(popularChannelsRes.data ?? []); + setLoading(false); + } + fetchStats(); + }, []); + + return ( +
+

MeshCore Network Stats

+ {loading ? ( +
Loading...
+ ) : ( + <> +
+

Total Unique Nodes

+
{totalNodes}
+
+ +
+

Nodes Heard Over Time

+ + + + + + + + + + + {nodesOverTime.map((row, i) => ( + + + + + + + ))} + +
DayTotal NodesWith LocationWithout Location
{row.day}{row.cumulative_unique_nodes}{row.nodes_with_location}{row.nodes_without_location}
+
+ +
+

Most Popular Channels

+ + + + + + + + + {popularChannels.map((row, i) => ( + + + + + ))} + +
Channel HashMessage Count
{row.channel_hash}{row.message_count}
+
+ + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index 68cc91e..4f44340 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -84,11 +84,11 @@ export default function ChatBox() { }`} >
- Chat + MeshCore Chat