"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, } 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 supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ); const ITEMS_PER_PAGE = 5; 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 = activePeerIds.filter(peerId => peerId.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 { // Fetch metrics const { data: metricsData, error: metricsError } = await supabase .from("metrics") .select("*") .order("date", { ascending: true }) .limit(timeframe === "7d" ? 7 : timeframe === "30d" ? 30 : 365); 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 currentActiveNodes = activeNodes.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))]; const totalNodes = metrics.reduce((acc, day) => acc + day.new_records_count, 0); 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); return ( <> Codex Metrics
{/* Header */}
Codex

Metrics

Testnet
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.

{error ? ( ) : (
{/* Top Section: Stats + Graph */}
{/* Left Column - Stats Cards */}
{[ { title: "Active Nodes", value: currentActiveNodes, Icon: Users, delay: 0, isLoading: componentLoading.nodes, }, { title: "Average Peer Count", value: averagePeerCount, Icon: Network, delay: 0.1, isLoading: componentLoading.nodes, }, { title: "Total Nodes", value: totalNodes, Icon: Database, delay: 0.2, isLoading: componentLoading.metrics, }, { 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 Peer IDs List */}

Active Peer IDs

setSearchQuery(e.target.value)} className="w-full bg-neutral-800 border border-neutral-700 rounded-lg px-4 py-2 text-sm placeholder-neutral-500 focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf] transition-colors outline-none" />
{componentLoading.peers ? ( ) : activePeerIds.length === 0 ? (

No active peers available

) : isSearching ? (
) : searchQuery && searchResults.length === 0 ? (

No matching peer IDs found

) : ( <>
{paginatedPeerIds.map((peerId, index) => ( {peerId} ))}
)}
)}
); }