"use client";
import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";
import { motion } from "framer-motion";
import { format, isToday, isYesterday } from "date-fns";
import Image from "next/image";
import {
Activity,
Users,
Network,
Database,
Clock,
AlertTriangle,
RotateCw,
Search,
ChevronLeft,
ChevronRight,
Globe,
Info,
Wallet,
Terminal,
} from "lucide-react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import Head from "next/head";
// Initialize Supabase client
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const supabase = createClient(
supabaseUrl || '',
supabaseAnonKey || ''
);
// Add error handling for missing environment variables
if (!supabaseUrl || !supabaseAnonKey) {
console.error('Missing environment variables for Supabase');
}
const ITEMS_PER_PAGE = 5;
// Add Discord icon component
const DiscordIcon = ({ className }) => (
);
export default function Dashboard() {
const [metrics, setMetrics] = useState([]);
const [activeNodes, setActiveNodes] = useState([]);
const [timeframe, setTimeframe] = useState("7d");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [componentLoading, setComponentLoading] = useState({
metrics: true,
nodes: true,
versions: true,
peers: true,
});
// Pagination and Search states
const [versionPage, setVersionPage] = useState(1);
const [peerPage, setPeerPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
useEffect(() => {
fetchData();
}, [timeframe]);
// Update search results when query changes
useEffect(() => {
if (searchQuery.trim()) {
setIsSearching(true);
const results = activeNodeIds.filter(nodeId =>
nodeId.toLowerCase().includes(searchQuery.toLowerCase())
);
setSearchResults(results);
setPeerPage(1);
setIsSearching(false);
} else {
setSearchResults([]);
}
}, [searchQuery]);
const fetchData = async () => {
setLoading(true);
setError(null);
setComponentLoading({
metrics: true,
nodes: true,
versions: true,
peers: true,
});
try {
// Calculate date range based on timeframe
const now = new Date();
const startDate = new Date();
if (timeframe === "7d") {
startDate.setDate(now.getDate() - 7);
} else if (timeframe === "30d") {
startDate.setDate(now.getDate() - 30);
} else if (timeframe === "1y") {
startDate.setDate(now.getDate() - 365);
}
// Fetch metrics
const { data: metricsData, error: metricsError } = await supabase
.from("metrics")
.select("*")
.gte('date', startDate.toISOString().split('T')[0])
.lte('date', now.toISOString().split('T')[0])
.order("date", { ascending: true });
if (metricsError) throw metricsError;
setMetrics(metricsData || []);
setComponentLoading(prev => ({ ...prev, metrics: false }));
// Fetch nodes
const { data: nodesData, error: nodesError } = await supabase
.from("node_records")
.select("*")
.order("timestamp", { ascending: false });
if (nodesError) throw nodesError;
setActiveNodes(nodesData || []);
setComponentLoading(prev => ({ ...prev, nodes: false, versions: false, peers: false }));
if (!metricsData?.length && !nodesData?.length) {
throw new Error("No data available");
}
} catch (error) {
console.error("Error fetching data:", error);
setError(error.message);
} finally {
setLoading(false);
}
};
// Pagination helpers
const getPaginatedData = (data, page, itemsPerPage = ITEMS_PER_PAGE) => {
const startIndex = (page - 1) * itemsPerPage;
return data.slice(startIndex, startIndex + itemsPerPage);
};
const getPageCount = (totalItems, itemsPerPage = ITEMS_PER_PAGE) => {
return Math.ceil(totalItems / itemsPerPage);
};
const PaginationControls = ({ currentPage, totalPages, onPageChange, className = "" }) => (
Page {currentPage} of {totalPages}
);
// Component loading skeleton
const ComponentSkeleton = ({ className = "" }) => (
);
const formatLastUpdated = (timestamp) => {
if (!timestamp) return "N/A";
const date = new Date(timestamp);
const time = format(date, "HH:mm");
let dateText;
if (isToday(date)) {
dateText = "Today";
} else if (isYesterday(date)) {
dateText = "Yesterday";
} else {
dateText = format(date, "dd.MM.yyyy");
}
return { time, dateText };
};
// Calculate statistics
const totalUniqueNodes = [...new Set(activeNodes.map(node => node.node_id))].length;
const averagePeerCount = activeNodes.length
? (activeNodes.reduce((acc, node) => acc + node.peer_count, 0) / activeNodes.length).toFixed(1)
: 0;
const activePeerIds = [...new Set(activeNodes.map((node) => node.peer_id))];
// Calculate today's active nodes
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const todayActiveNodes = [...new Set(
activeNodes
.filter(node => new Date(node.timestamp) >= todayStart)
.map(node => node.node_id)
)].length;
const versionDistribution = activeNodes.reduce((acc, node) => {
acc[node.version] = (acc[node.version] || 0) + 1;
return acc;
}, {});
const lastUpdated = formatLastUpdated(activeNodes[0]?.timestamp);
// Get paginated data
const versionEntries = Object.entries(versionDistribution);
const paginatedVersions = getPaginatedData(versionEntries, versionPage);
const displayPeerIds = searchQuery ? searchResults : activePeerIds;
const paginatedPeerIds = getPaginatedData(displayPeerIds, peerPage);
// Calculate total pages
const totalVersionPages = getPageCount(versionEntries.length);
const totalPeerPages = getPageCount(displayPeerIds.length);
// Prepare chart data
const chartData = metrics.map((day) => ({
date: new Date(day.date),
"Active Nodes": day.active_nodes_count
}));
// Get unique node IDs and their latest records
const nodeRecords = activeNodes.reduce((acc, node) => {
if (!acc[node.node_id] || new Date(acc[node.node_id].timestamp) < new Date(node.timestamp)) {
acc[node.node_id] = node;
}
return acc;
}, {});
const activeNodeIds = Object.keys(nodeRecords);
const displayNodeIds = searchQuery ? searchResults : activeNodeIds;
const paginatedNodeIds = getPaginatedData(displayNodeIds, peerPage);
const totalNodePages = getPageCount(displayNodeIds.length);
// Node details dialog state
const [selectedNode, setSelectedNode] = useState(null);
// Format timestamp for node details
const formatNodeTimestamp = (timestamp) => {
if (!timestamp) return "N/A";
const date = new Date(timestamp);
if (isToday(date)) {
return `Today at ${format(date, "HH:mm")}`;
} else if (isYesterday(date)) {
return `Yesterday at ${format(date, "HH:mm")}`;
}
return format(date, "MMM d, yyyy 'at' HH:mm");
};
return (
<>
Codex Metrics | Testnet Network Statistics
{/* Open Graph / Facebook */}
{/* Twitter */}
{/* Additional SEO */}
{/* Rotating Background Image */}
{/* Header */}
Metrics
Testnet
Run a testnet node
{error ? (
) : (
{/* Top Section: Stats + Graph */}
{/* Left Column - Stats Cards */}
{[
{
title: "Total Unique Nodes",
value: totalUniqueNodes,
Icon: Users,
delay: 0,
isLoading: componentLoading.nodes,
},
{
title: "Average Peer Count",
value: averagePeerCount,
Icon: Network,
delay: 0.1,
isLoading: componentLoading.nodes,
},
{
title: "Active Today",
value: todayActiveNodes,
Icon: Database,
delay: 0.2,
isLoading: componentLoading.nodes,
},
{
title: "Last Updated",
value: lastUpdated,
Icon: Clock,
delay: 0.3,
isLoading: componentLoading.nodes,
},
].map((stat) => (
{stat.title}
{stat.isLoading ? (
) : stat.title === "Last Updated" ? (
{lastUpdated.time}
{lastUpdated.dateText}
) : (
{stat.value}
)}
))}
{/* Right Column - Chart */}
Active Nodes
Geographic Distribution
{componentLoading.metrics ? (
) : metrics.length === 0 ? (
No data available for the selected timeframe
) : (
format(new Date(date), "MMM d")}
fontSize={12}
tickMargin={10}
/>
[`${value} nodes`, 'Active Nodes']}
labelFormatter={(label) => format(new Date(label), "MMM d, yyyy")}
/>
)}
Geographic distribution view coming soon
{/* Bottom Section: Version Distribution + Active Peers */}
{/* Version Distribution */}
Version Distribution
{componentLoading.versions ? (
) : Object.keys(versionDistribution).length === 0 ? (
No version data available
) : (
<>
{paginatedVersions.map(([version, count]) => (
))}
>
)}
{/* Active Node IDs List */}
{componentLoading.nodes ? (
) : paginatedNodeIds.length === 0 ? (
{searchQuery ? "No matching node IDs found" : "No active node IDs"}
) : (
<>
{paginatedNodeIds.map((nodeId) => {
const node = nodeRecords[nodeId];
return (
);
})}
>
)}
)}
>
);
}