- 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>
268 lines
9.1 KiB
TypeScript
268 lines
9.1 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,
|
|
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>
|
|
);
|
|
}
|