Skip to main content
Glama
page.tsx8.49 kB
'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { BarChart3, Database, Zap, Users, TrendingUp, Play } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { api } from '@/lib/api'; interface AdminStats { totalLibraries: number; totalUsers: number; apiRequests: number; activeCrawls: number; crawlsCompleted: number; docsIndexed: number; } interface CrawlJob { id: string; libraryId: string; libraryName: string; status: 'pending' | 'running' | 'completed' | 'failed'; progress: number; pagesScraped: number; pagesIndexed: number; startedAt: string; completedAt?: string; error?: string; } export default function AdminPage() { const router = useRouter(); const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); const [stats, setStats] = useState<AdminStats | null>(null); const [jobs, setJobs] = useState<CrawlJob[]>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); // Check authorization useEffect(() => { if (!authLoading && (!isAuthenticated || !user || user.tier === 'free')) { router.push('/dashboard'); } }, [isAuthenticated, user, authLoading, router]); // Fetch admin data useEffect(() => { const fetchAdminData = async () => { try { setIsLoading(true); const [statsRes, jobsRes] = await Promise.all([ api.getStats(), api.getCrawlJobs(), ]); if (statsRes.data) { setStats(statsRes.data as AdminStats); } if (jobsRes.data) { setJobs(jobsRes.data as CrawlJob[]); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load admin data'); } finally { setIsLoading(false); } }; if (isAuthenticated && user && user.tier !== 'free') { fetchAdminData(); // Refresh every 10 seconds const interval = setInterval(fetchAdminData, 10000); return () => clearInterval(interval); } }, [isAuthenticated, user]); if (authLoading || isLoading) { return ( <main className="min-h-screen bg-gradient-to-b from-black via-purple-900/10 to-black flex items-center justify-center pt-20"> <div className="text-gray-400">Loading...</div> </main> ); } if (!isAuthenticated || !user || user.tier === 'free') { return null; } return ( <main className="min-h-screen bg-gradient-to-b from-black via-purple-900/10 to-black pt-20 pb-12"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> {/* Header */} <div className="mb-12"> <h1 className="text-4xl font-bold text-white mb-2">Admin Panel</h1> <p className="text-gray-400">System monitoring and crawler management</p> </div> {/* Stats Grid */} {stats && ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12"> {[ { icon: Database, label: 'Libraries', value: stats.totalLibraries, color: 'purple', }, { icon: Users, label: 'Users', value: stats.totalUsers, color: 'pink', }, { icon: Zap, label: 'API Requests', value: stats.apiRequests.toLocaleString(), color: 'orange', }, { icon: TrendingUp, label: 'Docs Indexed', value: stats.docsIndexed.toLocaleString(), color: 'green', }, { icon: Play, label: 'Active Crawls', value: stats.activeCrawls, color: 'blue', }, { icon: BarChart3, label: 'Completed', value: stats.crawlsCompleted, color: 'indigo', }, ].map((stat, idx) => { const Icon = stat.icon; return ( <div key={idx} className="card"> <div className="flex items-start justify-between"> <div> <p className="text-gray-400 text-sm mb-2">{stat.label}</p> <p className="text-3xl font-bold text-white">{stat.value}</p> </div> <div className={`p-3 rounded-lg bg-${stat.color}-600/20 text-${stat.color}-400`} > <Icon className="w-6 h-6" /> </div> </div> </div> ); })} </div> )} {/* Error Message */} {error && ( <div className="p-4 rounded-lg bg-red-600/20 border border-red-600/50 mb-8"> <p className="text-red-300">{error}</p> </div> )} {/* Crawl Jobs */} <div className="card"> <h2 className="text-2xl font-bold text-white mb-6">Crawler Jobs</h2> {jobs.length === 0 ? ( <div className="text-center py-8"> <p className="text-gray-400">No crawl jobs yet</p> </div> ) : ( <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b border-gray-800"> <th className="text-left py-3 px-4 font-semibold text-gray-300"> Library </th> <th className="text-left py-3 px-4 font-semibold text-gray-300"> Status </th> <th className="text-left py-3 px-4 font-semibold text-gray-300"> Progress </th> <th className="text-left py-3 px-4 font-semibold text-gray-300"> Pages </th> <th className="text-left py-3 px-4 font-semibold text-gray-300"> Started </th> </tr> </thead> <tbody> {jobs.map((job) => ( <tr key={job.id} className="border-b border-gray-800/50 hover:bg-gray-900/30"> <td className="py-3 px-4 text-white">{job.libraryName}</td> <td className="py-3 px-4"> <span className={`px-3 py-1 rounded-full text-xs font-semibold ${ job.status === 'completed' ? 'bg-green-600/20 text-green-300' : job.status === 'running' ? 'bg-blue-600/20 text-blue-300' : job.status === 'failed' ? 'bg-red-600/20 text-red-300' : 'bg-gray-600/20 text-gray-300' }`} > {job.status} </span> </td> <td className="py-3 px-4"> <div className="flex items-center gap-2"> <div className="w-20 h-2 bg-gray-800 rounded-full overflow-hidden"> <div className="h-full bg-gradient-to-r from-purple-600 to-pink-600 transition-all duration-300" style={{ width: `${job.progress}%` }} /> </div> <span className="text-xs text-gray-400">{job.progress}%</span> </div> </td> <td className="py-3 px-4 text-gray-300 text-sm"> {job.pagesScraped}/{job.pagesIndexed} </td> <td className="py-3 px-4 text-gray-400 text-sm"> {new Date(job.startedAt).toLocaleDateString()} </td> </tr> ))} </tbody> </table> </div> )} </div> </div> </main> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aiatamai/atamai-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server