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:
267
app/(dashboard)/system/page.tsx
Normal file
267
app/(dashboard)/system/page.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
"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,
|
||||
Server,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
PlayCircle,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
import { DaemonStatus, ListProjects } from "@/lib/types";
|
||||
|
||||
export default function SystemPage() {
|
||||
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();
|
||||
},
|
||||
refetchInterval: 10000, // Refresh every 10 seconds
|
||||
});
|
||||
|
||||
const { data: projects, isLoading: isProjectsLoading } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async (): Promise<ListProjects> => {
|
||||
const res = await fetch("/ui/api/scrapyd/projects");
|
||||
if (!res.ok) throw new Error("Failed to fetch projects");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
const systemMetrics = [
|
||||
{
|
||||
label: "Daemon Status",
|
||||
value: daemonStatus?.status || "unknown",
|
||||
icon: Activity,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
},
|
||||
{
|
||||
label: "Running Jobs",
|
||||
value: daemonStatus?.running ?? 0,
|
||||
icon: PlayCircle,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
},
|
||||
{
|
||||
label: "Pending Jobs",
|
||||
value: daemonStatus?.pending ?? 0,
|
||||
icon: Clock,
|
||||
color: "text-yellow-500",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
},
|
||||
{
|
||||
label: "Finished Jobs",
|
||||
value: daemonStatus?.finished ?? 0,
|
||||
icon: CheckCircle2,
|
||||
color: "text-purple-500",
|
||||
bgColor: "bg-purple-500/10",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Header
|
||||
title="System Status"
|
||||
description="Monitor Scrapyd daemon health and metrics"
|
||||
/>
|
||||
|
||||
{/* Status Overview */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Daemon Information</CardTitle>
|
||||
{isDaemonLoading ? (
|
||||
<Skeleton className="h-6 w-16" />
|
||||
) : (
|
||||
<Badge
|
||||
variant={daemonStatus?.status === "ok" ? "default" : "destructive"}
|
||||
className="gap-1"
|
||||
>
|
||||
{daemonStatus?.status === "ok" ? (
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
) : (
|
||||
<Activity className="h-3 w-3" />
|
||||
)}
|
||||
{daemonStatus?.status?.toUpperCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isDaemonLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-6 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Node Name</span>
|
||||
</div>
|
||||
<code className="rounded bg-muted px-2 py-1 text-sm">
|
||||
{daemonStatus?.node_name}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-b pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Projects</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold">
|
||||
{isProjectsLoading ? (
|
||||
<Skeleton className="h-5 w-8" />
|
||||
) : (
|
||||
projects?.projects?.length ?? 0
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Jobs (All Time)</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold">
|
||||
{(daemonStatus?.running ?? 0) +
|
||||
(daemonStatus?.pending ?? 0) +
|
||||
(daemonStatus?.finished ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Metrics Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{systemMetrics.map((metric, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{metric.label}
|
||||
</CardTitle>
|
||||
<div className={`rounded-full p-2 ${metric.bgColor}`}>
|
||||
<metric.icon className={`h-4 w-4 ${metric.color}`} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isDaemonLoading ? (
|
||||
<Skeleton className="h-8 w-20" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{metric.value}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Job Queue Status */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Job Queue Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isDaemonLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full bg-blue-500/10 p-2">
|
||||
<PlayCircle className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Running Jobs</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Currently executing
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-bold">
|
||||
{daemonStatus?.running}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full bg-yellow-500/10 p-2">
|
||||
<Clock className="h-5 w-5 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Pending Jobs</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Waiting in queue
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-bold">
|
||||
{daemonStatus?.pending}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full bg-purple-500/10 p-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Finished Jobs</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Completed successfully
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-bold">
|
||||
{daemonStatus?.finished}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Environment Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Environment Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Scrapyd URL:</span>
|
||||
<code className="rounded bg-muted px-2 py-1 text-xs">
|
||||
{process.env.NEXT_PUBLIC_SCRAPYD_URL || "https://scrapy.pivoine.art"}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">UI Version:</span>
|
||||
<Badge variant="outline">1.0.0</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Refresh Interval:</span>
|
||||
<span>10 seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user