"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, } from "lucide-react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; // 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 (
{/* Header */}
Codex

Codex Metrics

{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 Over Time

{componentLoading.metrics ? (
) : metrics.length === 0 ? (

No data available for the selected timeframe

) : (
format(new Date(date), "MMM d")} fontSize={12} tickMargin={10} />
)}
{/* 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} ))}
)}
)}
); }