"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 */}
Codex

Metrics

Testnet
Testnet Metrics {/* Image dimensions: 1200x630 (2:1.05 aspect ratio - optimal for social sharing) */} Testnet Metrics

The data displayed in this dashboard is collected from Codex nodes that use the{' '} Codex CLI {' '}for running a Codex alturistic node in the testnet.

Users agree to a privacy disclaimer before using the Codex CLI and the data collected will be used to understand the testnet statistics and help troubleshooting users who face difficulty in getting onboarded to Codex.

Don't wish to provide data?

You can still run a Codex node without providing any data. To do this, please follow the steps mentioned in the{' '} Codex documentation {' '}which does not use the Codex CLI.

Is there an incentive to run a Codex node?

Codex is currently in testnet and it is not incentivized. However, in the future, Codex may be incentivized as per the roadmap. But please bear in mind that no incentives are promised for testnet node operators.

I have a question or suggestion

The best way to get in touch with us is to join the{' '} Codex discord {' '}and ask your question in the #support channel.

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]) => (
{version} {count}
))}
)}
{/* Active Node IDs List */}

Active Node IDs

setSearchQuery(e.target.value)} placeholder="Search node IDs..." className="w-full sm:w-[200px] pl-7 sm:pl-9 pr-3 sm:pr-4 py-1.5 sm:py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-xs sm:text-sm placeholder:text-neutral-500 focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf] transition-colors outline-none" />
{componentLoading.nodes ? ( ) : paginatedNodeIds.length === 0 ? (

{searchQuery ? "No matching node IDs found" : "No active node IDs"}

) : ( <>
{paginatedNodeIds.map((nodeId) => { const node = nodeRecords[nodeId]; return (
Codex

Codex

Testnet

NODE ID

{nodeId}

VERSION

{node.version}

LAST ONLINE

{formatNodeTimestamp(node.timestamp)}

CONNECTED DHT PEERS

{node.peer_count}

Discord: {node.discord_user_id ? 'Linked' : 'Not linked'}
ERC20 Wallet: {node.wallet ? 'Linked' : 'Not linked'}
); })}
)}
)}
); }