mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-07-06 01:30:58 +02:00
add region filtering to stats
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ')}` : '';
|
||||
|
||||
@@ -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: {} };
|
||||
}
|
||||
Reference in New Issue
Block a user