140 lines
5.5 KiB
TypeScript
140 lines
5.5 KiB
TypeScript
"use client";
|
|
import { usePoll } from "@/lib/use-poll";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { CardSkeleton } from "@/components/skeleton";
|
|
|
|
interface TdarrWorker {
|
|
id: string;
|
|
type: string;
|
|
file: string;
|
|
percentage: number;
|
|
fps: number;
|
|
eta: string;
|
|
}
|
|
|
|
interface TdarrNode {
|
|
id: string;
|
|
name: string;
|
|
paused: boolean;
|
|
hardware: string;
|
|
workers: TdarrWorker[];
|
|
active: number;
|
|
}
|
|
|
|
interface TdarrCluster {
|
|
server_version?: string;
|
|
nodes: TdarrNode[];
|
|
total_active: number;
|
|
stats: {
|
|
total_files: number;
|
|
transcoded: number;
|
|
health_checked: number;
|
|
size_saved_gb: number;
|
|
queue_transcode: number;
|
|
error_transcode: number;
|
|
tdarr_score: string;
|
|
};
|
|
error?: string;
|
|
}
|
|
|
|
// Node hardware colors
|
|
const hwColors: Record<string, string> = {
|
|
"NVENC (RTX 5090)": "text-green-400",
|
|
"VAAPI (Radeon 760M)": "text-amber-400",
|
|
"QSV (Intel)": "text-cyan-400",
|
|
"CPU": "text-muted-foreground",
|
|
};
|
|
|
|
export function TdarrCard() {
|
|
const { data } = usePoll<TdarrCluster>("/api/tdarr/cluster", 10000); // 10s refresh for live worker updates
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2 flex flex-row items-center justify-between">
|
|
<CardTitle className="text-base font-semibold">Tdarr Cluster</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
{data && !data.error && (
|
|
<>
|
|
<Badge variant="secondary" className="text-xs bg-green-500/10 border border-green-500/20 text-green-400">
|
|
{data.total_active} active
|
|
</Badge>
|
|
{data.stats.error_transcode > 0 && (
|
|
<Badge variant="secondary" className="text-xs bg-red-500/10 border border-red-500/20 text-red-400">
|
|
{data.stats.error_transcode} errors
|
|
</Badge>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!data ? (
|
|
<CardSkeleton lines={5} />
|
|
) : data.error ? (
|
|
<p className="text-sm text-red-400">{data.error}</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Stats row */}
|
|
<div className="flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground">
|
|
<span>{data.stats.total_files.toLocaleString()} files</span>
|
|
<span className="text-green-400">{data.stats.transcoded.toLocaleString()} transcoded</span>
|
|
<span>{data.stats.health_checked.toLocaleString()} health checked</span>
|
|
<span className="text-amber-400">{data.stats.size_saved_gb} GB saved</span>
|
|
{data.stats.queue_transcode > 0 && (
|
|
<span className="text-blue-400">{data.stats.queue_transcode} in queue</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Nodes */}
|
|
<div className="space-y-3">
|
|
{data.nodes.map((node) => (
|
|
<div key={node.id} className="rounded-lg bg-white/[0.03] border border-white/[0.06] p-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`w-2 h-2 rounded-full ${node.active > 0 ? "bg-green-500" : node.paused ? "bg-amber-500" : "bg-gray-500"}`}
|
|
style={{ boxShadow: node.active > 0 ? "0 0 6px rgba(34,197,94,0.5)" : "none" }} />
|
|
<span className="text-sm font-medium">{node.name}</span>
|
|
<span className={`text-xs ${hwColors[node.hardware] ?? "text-muted-foreground"}`}>{node.hardware}</span>
|
|
</div>
|
|
{node.paused && <Badge variant="secondary" className="text-[10px] bg-amber-500/10 text-amber-400">Paused</Badge>}
|
|
</div>
|
|
|
|
{node.workers.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{node.workers.map((w) => (
|
|
<div key={w.id} className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-foreground/80 truncate max-w-[60%]">{w.file}</span>
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
<span className="text-green-400 font-mono">{w.fps} fps</span>
|
|
<span className="text-muted-foreground">{w.eta}</span>
|
|
<span className="text-foreground font-medium w-12 text-right">{w.percentage}%</span>
|
|
</div>
|
|
</div>
|
|
<div className="h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all duration-1000"
|
|
style={{
|
|
width: `${w.percentage}%`,
|
|
background: `linear-gradient(90deg, #3b82f6, ${w.percentage > 80 ? "#22c55e" : "#8b5cf6"})`,
|
|
boxShadow: "0 0 8px rgba(59, 130, 246, 0.3)",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground/50">Idle</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|