Removed the /ui basePath from Next.js configuration to serve the app at root path: - Removed basePath: "/ui" from next.config.ts - Updated all API route calls from /ui/api/* to /api/* - App now accessible at root path instead of /ui subdirectory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
174 lines
5.4 KiB
TypeScript
174 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Header } from "@/components/header";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
Activity,
|
|
FolderKanban,
|
|
PlayCircle,
|
|
Clock,
|
|
CheckCircle2,
|
|
} from "lucide-react";
|
|
import { DaemonStatus } from "@/lib/types";
|
|
|
|
export default function DashboardPage() {
|
|
const { data: daemonStatus, isLoading: isDaemonLoading } = useQuery({
|
|
queryKey: ["daemon-status"],
|
|
queryFn: async (): Promise<DaemonStatus> => {
|
|
const res = await fetch("/api/scrapyd/daemon");
|
|
if (!res.ok) throw new Error("Failed to fetch daemon status");
|
|
return res.json();
|
|
},
|
|
});
|
|
|
|
const { data: projects, isLoading: isProjectsLoading } = useQuery({
|
|
queryKey: ["projects"],
|
|
queryFn: async () => {
|
|
const res = await fetch("/api/scrapyd/projects");
|
|
if (!res.ok) throw new Error("Failed to fetch projects");
|
|
return res.json();
|
|
},
|
|
});
|
|
|
|
const stats = [
|
|
{
|
|
title: "Running Jobs",
|
|
value: daemonStatus?.running ?? 0,
|
|
icon: PlayCircle,
|
|
color: "text-green-500",
|
|
bgColor: "bg-green-500/10",
|
|
},
|
|
{
|
|
title: "Pending Jobs",
|
|
value: daemonStatus?.pending ?? 0,
|
|
icon: Clock,
|
|
color: "text-yellow-500",
|
|
bgColor: "bg-yellow-500/10",
|
|
},
|
|
{
|
|
title: "Finished Jobs",
|
|
value: daemonStatus?.finished ?? 0,
|
|
icon: CheckCircle2,
|
|
color: "text-blue-500",
|
|
bgColor: "bg-blue-500/10",
|
|
},
|
|
{
|
|
title: "Total Projects",
|
|
value: projects?.projects?.length ?? 0,
|
|
icon: FolderKanban,
|
|
color: "text-purple-500",
|
|
bgColor: "bg-purple-500/10",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Header
|
|
title="Dashboard"
|
|
description="Monitor your Scrapyd instance and scraping jobs"
|
|
/>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
{stats.map((stat, index) => (
|
|
<Card key={index}>
|
|
<CardHeader className="flex flex-row items-center justify-between gap-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
{stat.title}
|
|
</CardTitle>
|
|
<div className={`rounded-full p-2 ${stat.bgColor}`}>
|
|
<stat.icon className={`h-4 w-4 ${stat.color}`} />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isDaemonLoading || isProjectsLoading ? (
|
|
<Skeleton className="h-8 w-16" />
|
|
) : (
|
|
<div className="text-2xl font-bold">{stat.value}</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* System Status Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle>System Status</CardTitle>
|
|
<Badge variant="outline" className="flex items-center gap-1">
|
|
<Activity className="h-3 w-3" />
|
|
{isDaemonLoading ? (
|
|
<Skeleton className="h-4 w-12" />
|
|
) : (
|
|
daemonStatus?.status
|
|
)}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isDaemonLoading ? (
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-3/4" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Node Name:</span>
|
|
<span className="font-mono">{daemonStatus?.node_name}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Total Jobs:</span>
|
|
<span>
|
|
{(daemonStatus?.running ?? 0) +
|
|
(daemonStatus?.pending ?? 0) +
|
|
(daemonStatus?.finished ?? 0)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Projects Overview */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Projects Overview</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isProjectsLoading ? (
|
|
<div className="space-y-2">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-8 w-full" />
|
|
))}
|
|
</div>
|
|
) : projects?.projects?.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{projects.projects.map((project: string) => (
|
|
<div
|
|
key={project}
|
|
className="flex items-center justify-between rounded-lg border p-3"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<FolderKanban className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">{project}</span>
|
|
</div>
|
|
<Badge variant="secondary">Active</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-center text-muted-foreground">
|
|
No projects found. Upload a project to get started.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|