Files
homelab-optimized/dashboard/ui/components/tdarr-card.tsx
Gitea Mirror Bot e7652c8dab
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m3s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-20 01:32:01 UTC
2026-04-20 01:32:01 +00:00

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