Files
scrapy-ui/app/(dashboard)/system/page.tsx
Sebastian Krüger 971ef5426d 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>
2025-11-05 03:32:14 +01:00

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>
);
}