add region filtering to stats

This commit is contained in:
ajvpot
2025-08-19 06:48:41 +02:00
parent 3aec06782d
commit 881174dce7
9 changed files with 147 additions and 26 deletions
+9 -1
View File
@@ -1,12 +1,20 @@
import { NextResponse } from "next/server";
import { clickhouse } from "@/lib/clickhouse/clickhouse";
import { generateRegionWhereClause } from "@/lib/regionFilters";
export async function GET() {
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const region = searchParams.get("region") || undefined;
const regionFilter = generateRegionWhereClause(region);
const regionWhereClause = regionFilter.whereClause ? `WHERE ${regionFilter.whereClause}` : '';
const query = `
WITH all_nodes AS (
SELECT toDate(ingest_timestamp) AS day, public_key, latitude, longitude
FROM meshcore_adverts
${regionWhereClause}
),
all_days AS (
SELECT DISTINCT day FROM all_nodes
+9 -1
View File
@@ -1,11 +1,19 @@
import { NextResponse } from "next/server";
import { clickhouse } from "@/lib/clickhouse/clickhouse";
import { generateRegionWhereClauseFromArray } from "@/lib/regionFilters";
export async function GET() {
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const region = searchParams.get("region") || undefined;
const regionFilter = generateRegionWhereClauseFromArray(region);
const whereClause = regionFilter.whereClause ? `WHERE ${regionFilter.whereClause}` : '';
const query = `
SELECT channel_hash, count() AS message_count
FROM meshcore_public_channel_messages
${whereClause}
GROUP BY channel_hash
ORDER BY message_count DESC
LIMIT 10
+9 -1
View File
@@ -1,8 +1,15 @@
import { NextResponse } from "next/server";
import { clickhouse } from "@/lib/clickhouse/clickhouse";
import { generateRegionWhereClause } from "@/lib/regionFilters";
export async function GET() {
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const region = searchParams.get("region") || undefined;
const regionFilter = generateRegionWhereClause(region);
const regionWhereClause = regionFilter.whereClause ? `AND ${regionFilter.whereClause}` : '';
const query = `
SELECT
substring(public_key, 1, 2) as prefix,
@@ -11,6 +18,7 @@ export async function GET() {
FROM meshcore_adverts_latest
WHERE is_repeater = 1
AND last_seen >= now() - INTERVAL 7 DAY
${regionWhereClause}
GROUP BY prefix
ORDER BY node_count DESC, prefix ASC
`;
+10 -2
View File
@@ -1,9 +1,17 @@
import { NextResponse } from "next/server";
import { clickhouse } from "@/lib/clickhouse/clickhouse";
import { generateRegionWhereClause } from "@/lib/regionFilters";
export async function GET() {
export async function GET(req: Request) {
try {
const query = `SELECT count() AS total_nodes FROM meshcore_adverts_latest`;
const { searchParams } = new URL(req.url);
const region = searchParams.get("region") || undefined;
const regionFilter = generateRegionWhereClause(region);
const whereClause = regionFilter.whereClause ? `WHERE ${regionFilter.whereClause}` : '';
const query = `SELECT count() AS total_nodes FROM meshcore_adverts_latest ${whereClause}`;
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;
+25 -6
View File
@@ -2,8 +2,11 @@
import { useEffect, useState } from "react";
import { buildApiUrl } from "@/lib/api";
import { useConfig } from "@/components/ConfigContext";
import { getRegionConfig } from "@/lib/regions";
export default function StatsPage() {
const { config } = useConfig();
const [totalNodes, setTotalNodes] = useState<number | null>(null);
const [nodesOverTime, setNodesOverTime] = useState<any[]>([]);
const [popularChannels, setPopularChannels] = useState<any[]>([]);
@@ -13,11 +16,15 @@ export default function StatsPage() {
useEffect(() => {
async function fetchStats() {
setLoading(true);
// Build API URLs with region parameter if selected
const regionParam = config?.selectedRegion ? `?region=${encodeURIComponent(config.selectedRegion)}` : '';
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()),
fetch(buildApiUrl(`/api/stats/total-nodes${regionParam}`)).then(r => r.json()),
fetch(buildApiUrl(`/api/stats/nodes-over-time${regionParam}`)).then(r => r.json()),
fetch(buildApiUrl(`/api/stats/popular-channels${regionParam}`)).then(r => r.json()),
fetch(buildApiUrl(`/api/stats/repeater-prefixes${regionParam}`)).then(r => r.json()),
]);
setTotalNodes(totalNodesRes.total_nodes ?? null);
setNodesOverTime(nodesOverTimeRes.data ?? []);
@@ -26,11 +33,23 @@ export default function StatsPage() {
setLoading(false);
}
fetchStats();
}, []);
}, [config?.selectedRegion]);
// Get the friendly name for the selected region
const regionFriendlyName = config?.selectedRegion
? getRegionConfig(config.selectedRegion)?.friendlyName || config.selectedRegion
: null;
return (
<div className="max-w-2xl w-full mx-auto my-4 py-2 px-4 text-gray-800 dark:text-gray-200 bg-white dark:bg-neutral-900 rounded-lg shadow-lg">
<h1 className="text-2xl font-bold mb-6">MeshCore Network Stats</h1>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">MeshCore Network Stats</h1>
{regionFriendlyName && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{regionFriendlyName}
</div>
)}
</div>
{loading ? (
<div>Loading...</div>
) : (
+5 -5
View File
@@ -161,7 +161,7 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
: "w-80 h-96 px-4 py-4"
}`}>
<div className={`flex items-center justify-between ${startExpanded ? "px-4 py-2 border-b border-gray-200 dark:border-neutral-800" : ""}`} style={startExpanded ? {} : { minHeight: minimized ? '2rem' : '2rem' }}>
<span className="font-semibold text-gray-800 dark:text-gray-100">MeshCore Chat</span>
<span className="font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap">MeshCore Chat</span>
{!startExpanded && (
<button
className="p-1 rounded text-gray-800 dark:text-gray-100 hover:bg-neutral-100 dark:hover:bg-neutral-800"
@@ -205,13 +205,13 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
}`}
>
<div className={`flex items-center justify-between ${startExpanded ? "px-4 py-2 border-b border-gray-200 dark:border-neutral-800" : ""}`} style={startExpanded ? {} : { minHeight: minimized ? '2rem' : '2rem' }}>
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-800 dark:text-gray-100">MeshCore Chat</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap flex-shrink-0">MeshCore Chat</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate" title={getRegionConfig(config.selectedRegion!)?.friendlyName || config.selectedRegion}>
{getRegionConfig(config.selectedRegion!)?.friendlyName || config.selectedRegion}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-shrink-0">
{(!minimized) && (
<RefreshButton
onClick={handleRefresh}
+1 -1
View File
@@ -257,7 +257,7 @@ function ConfigPopover({ config, setConfig, onClose, anchorRef, onOpenKeyModal }
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Select a region to filter chat messages by broker and topic
Select a region to filter chat messages
</p>
</div>
</div>
+4 -9
View File
@@ -1,5 +1,6 @@
"use server";
import { clickhouse } from "./clickhouse";
import { generateRegionWhereClauseFromArray } from "../regionFilters";
export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen }: { minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[], lastSeen?: string | null } = {}) {
try {
@@ -69,15 +70,9 @@ export async function getLatestChatMessages({ limit = 20, before, after, channel
}
// Add region filtering if specified
// todo: generate these from the regions.ts file
if (region) {
if (region === 'seattle') {
where.push("arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND (x.2 = 'meshcore' OR x.2 = 'meshcore/salish'), topic_broker_array)");
} else if (region === 'portland') {
where.push("arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND x.2 = 'meshcore/pdx', topic_broker_array)");
} else if (region === 'boston') {
where.push("arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND x.2 = 'meshcore/bos', topic_broker_array)");
}
const regionFilter = generateRegionWhereClauseFromArray(region);
if (regionFilter.whereClause) {
where.push(regionFilter.whereClause);
}
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
+75
View File
@@ -0,0 +1,75 @@
import { getRegionConfig } from "./regions";
/**
* Generates a ClickHouse WHERE clause for filtering by region using broker and topic fields
* @param region The region name to filter by
* @param tableAlias Optional table alias for the query
* @returns Object containing the where clause and parameters
*/
export function generateRegionWhereClause(region?: string, tableAlias: string = '') {
if (!region) {
return { whereClause: '', params: {} };
}
const regionConfig = getRegionConfig(region);
if (!regionConfig) {
return { whereClause: '', params: {} };
}
const alias = tableAlias ? `${tableAlias}.` : '';
if (region === 'seattle') {
return {
whereClause: `${alias}broker = 'tcp://mqtt.davekeogh.com:1883' AND (${alias}topic = 'meshcore' OR ${alias}topic = 'meshcore/salish')`,
params: {}
};
} else if (region === 'portland') {
return {
whereClause: `${alias}broker = 'tcp://mqtt.davekeogh.com:1883' AND ${alias}topic = 'meshcore/pdx'`,
params: {}
};
} else if (region === 'boston') {
return {
whereClause: `${alias}broker = 'tcp://mqtt.davekeogh.com:1883' AND ${alias}topic = 'meshcore/bos'`,
params: {}
};
}
return { whereClause: '', params: {} };
}
/**
* Generates a ClickHouse WHERE clause for filtering by region using topic_broker_array
* This is for views that already have the topic_broker_array field
* @param region The region name to filter by
* @returns Object containing the where clause and parameters
*/
export function generateRegionWhereClauseFromArray(region?: string) {
if (!region) {
return { whereClause: '', params: {} };
}
const regionConfig = getRegionConfig(region);
if (!regionConfig) {
return { whereClause: '', params: {} };
}
if (region === 'seattle') {
return {
whereClause: "arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND (x.2 = 'meshcore' OR x.2 = 'meshcore/salish'), topic_broker_array)",
params: {}
};
} else if (region === 'portland') {
return {
whereClause: "arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND x.2 = 'meshcore/pdx', topic_broker_array)",
params: {}
};
} else if (region === 'boston') {
return {
whereClause: "arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND x.2 = 'meshcore/bos', topic_broker_array)",
params: {}
};
}
return { whereClause: '', params: {} };
}