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
+
+
+
+ | Day |
+ Total Nodes |
+ With Location |
+ Without Location |
+
+
+
+ {nodesOverTime.map((row, i) => (
+
+ | {row.day} |
+ {row.cumulative_unique_nodes} |
+ {row.nodes_with_location} |
+ {row.nodes_without_location} |
+
+ ))}
+
+
+
+
+
+
Most Popular Channels
+
+
+
+ | Channel Hash |
+ Message Count |
+
+
+
+ {popularChannels.map((row, i) => (
+
+ | {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