feat: initial commit - Scrapyd UI web interface
- Next.js 16.0.1 + React 19.2.0 + Tailwind CSS 4.1.16 - Complete Scrapyd API integration (all 12 endpoints) - Dashboard with real-time job monitoring - Projects management (upload, list, delete) - Spiders management with scheduling - Jobs monitoring with filtering and cancellation - System status monitoring - Dark/light theme toggle with next-themes - Server-side authentication via environment variables - Docker deployment with multi-stage builds - GitHub Actions CI/CD workflow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
173
app/(dashboard)/page.tsx
Normal file
173
app/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
"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("/ui/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("/ui/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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user