| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- "use client";
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
- import { useEffect } from "react";
- import toast from "react-hot-toast";
- import { get, post } from "../../lib/api";
- import ApiHealth from "./ApiHealth";
- export default function StatsSection() {
- const queryClient = useQueryClient();
- const { data: tasks, isLoading: _tasksLoading } = useQuery({
- queryKey: ["tasks"],
- queryFn: () => get("/tasks"),
- });
- const { data: filesSuccessful, isLoading: filesSuccessfulLoading } = useQuery(
- {
- queryKey: ["files-stats-successful"],
- queryFn: () => get("/files/stats/successful"),
- }
- );
- const { data: filesProcessedTotal, isLoading: filesProcessedLoading } =
- useQuery({
- queryKey: ["files-stats-processed"],
- queryFn: () => get("/files/stats/processed"),
- });
- const { data: _datasets, isLoading: _datasetsLoading } = useQuery({
- queryKey: ["datasets"],
- queryFn: () => get("/files"),
- });
- const { data: settings, isLoading: settingsLoading } = useQuery({
- queryKey: ["settings", "datasets"],
- queryFn: () => get("/config/settings/datasets"),
- });
- const { data: watcherStatus, isLoading: watcherLoading } = useQuery({
- queryKey: ["watcher", "status"],
- queryFn: () => get("/watcher/status"),
- });
- const { data: taskProcessingStatus, isLoading: taskProcessingLoading } =
- useQuery({
- queryKey: ["tasks", "processing-status"],
- queryFn: () => get("/tasks/processing-status"),
- });
- const { data: queueStatus, isLoading: _queueLoading } = useQuery({
- queryKey: ["tasks", "queue", "status"],
- queryFn: () => get("/tasks/queue/status"),
- });
- const { data: apiHealth, isLoading: apiHealthLoading } = useQuery({
- queryKey: ["api", "health"],
- queryFn: () => get("/health"),
- refetchInterval: 30000,
- });
- // Mutations for controlling services
- const startWatcherMutation = useMutation({
- mutationFn: () => post("/watcher/start"),
- onSuccess: () => {
- toast.success("File watcher started");
- // Invalidate and refetch to ensure status updates immediately
- queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
- setTimeout(() => {
- queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
- }, 100);
- },
- onError: () => {
- toast.error("Failed to start file watcher");
- },
- });
- const stopWatcherMutation = useMutation({
- mutationFn: () => post("/watcher/stop"),
- onSuccess: () => {
- toast.success("File watcher stopped");
- // Invalidate and refetch to ensure status updates immediately
- queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
- setTimeout(() => {
- queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
- }, 100);
- },
- onError: () => {
- toast.error("Failed to stop file watcher");
- },
- });
- const startTaskProcessingMutation = useMutation({
- mutationFn: () => post("/tasks/start-processing"),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: ["tasks", "processing-status"],
- });
- queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
- toast.success("Task processing started");
- },
- onError: () => {
- toast.error("Failed to start task processing");
- },
- });
- const stopTaskProcessingMutation = useMutation({
- mutationFn: () => post("/tasks/stop-processing"),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: ["tasks", "processing-status"],
- });
- queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
- toast.success("Task processing stopped");
- },
- onError: () => {
- toast.error("Failed to stop task processing");
- },
- });
- const _tasksRunning = tasks?.length || 0;
- const filesProcessed = filesSuccessful || 0;
- const totalProcessed = filesProcessedTotal || 0;
- const successRate =
- totalProcessed > 0
- ? Math.round((filesProcessed / totalProcessed) * 100)
- : 0;
- const activeWatchers = settings
- ? Object.values(settings).filter((dataset: any) => dataset.enabled === true)
- .length
- : 0;
- const isApiHealthy = apiHealth?.status === "healthy";
- const isWatcherActive = watcherStatus?.isWatching;
- const isTaskProcessingActive = taskProcessingStatus?.isProcessing;
- // Listen for WebSocket updates to refresh stats
- useEffect(() => {
- const handleTaskUpdate = (event: CustomEvent) => {
- const taskData = event.detail;
- // Refresh task-related queries when tasks are updated
- if (
- taskData.type === "progress" ||
- taskData.type === "completed" ||
- taskData.type === "failed"
- ) {
- queryClient.invalidateQueries({ queryKey: ["tasks"] });
- queryClient.invalidateQueries({
- queryKey: ["tasks", "processing-status"],
- });
- queryClient.invalidateQueries({
- queryKey: ["tasks", "queue", "status"],
- });
- }
- };
- const handleFileUpdate = (event: CustomEvent) => {
- const fileData = event.detail;
- // Refresh file stats when files are processed
- if (fileData.type === "processed" || fileData.type === "success") {
- queryClient.invalidateQueries({ queryKey: ["files-stats-successful"] });
- queryClient.invalidateQueries({ queryKey: ["files-stats-processed"] });
- }
- };
- window.addEventListener("taskUpdate", handleTaskUpdate as EventListener);
- window.addEventListener("fileUpdate", handleFileUpdate as EventListener);
- return () => {
- window.removeEventListener(
- "taskUpdate",
- handleTaskUpdate as EventListener
- );
- window.removeEventListener(
- "fileUpdate",
- handleFileUpdate as EventListener
- );
- };
- }, [queryClient]);
- return (
- <div className="space-y-6">
- {/* API Health Widget */}
- <ApiHealth />
- {/* Stats Grid */}
- <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
- {/* File Watcher */}
- <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-x-3">
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/20 ring-1 ring-indigo-500/30">
- <svg
- className="h-6 w-6 text-indigo-400"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth="1.5"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M2.457 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.543-7z"
- />
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M12 9a3 3 0 100 6 3 3 0 000-6z"
- />
- </svg>
- </div>
- <div>
- <div className="text-2xl font-bold text-white">
- {watcherLoading ? (
- <div className="flex justify-center">
- <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
- </div>
- ) : (
- <span
- className={
- isWatcherActive ? "text-green-400" : "text-red-400"
- }
- >
- {isWatcherActive ? "Active" : "Idle"}
- </span>
- )}
- </div>
- <div className="text-sm font-medium text-gray-400">
- File Watcher
- </div>
- <div className="text-xs text-gray-500 mt-1">
- {settingsLoading ? "..." : `${activeWatchers} datasets`}
- </div>
- </div>
- </div>
- <div className="flex gap-2">
- <button
- onClick={() =>
- isWatcherActive
- ? stopWatcherMutation.mutate()
- : startWatcherMutation.mutate()
- }
- disabled={
- watcherLoading ||
- startWatcherMutation.isPending ||
- stopWatcherMutation.isPending
- }
- className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
- isWatcherActive
- ? "bg-red-600 hover:bg-red-700 text-white"
- : "bg-green-600 hover:bg-green-700 text-white"
- } disabled:opacity-50`}
- >
- {startWatcherMutation.isPending || stopWatcherMutation.isPending
- ? "..."
- : isWatcherActive
- ? "Stop"
- : "Start"}
- </button>
- </div>
- </div>
- {watcherStatus?.watches && watcherStatus.watches.length > 0 && (
- <div className="mt-4 pt-4 border-t border-white/10">
- <div className="text-xs text-gray-400 mb-2">Watching:</div>
- <div className="flex flex-wrap gap-1">
- {watcherStatus.watches
- .slice(0, 3)
- .map((watch: any, index: number) => (
- <span
- key={index}
- className="text-xs bg-white/10 px-2 py-1 rounded text-gray-300"
- >
- {typeof watch === "string"
- ? watch.split("/").pop()
- : watch.path?.split("/").pop() || "Unknown"}
- </span>
- ))}
- {watcherStatus.watches.length > 3 && (
- <span className="text-xs text-gray-500">
- +{watcherStatus.watches.length - 3} more
- </span>
- )}
- </div>
- </div>
- )}
- </div>
- {/* Task Processing */}
- <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-x-3">
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/20 ring-1 ring-emerald-500/30">
- <svg
- className="h-6 w-6 text-emerald-400"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth="1.5"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
- />
- </svg>
- </div>
- <div>
- <div className="text-2xl font-bold text-white">
- {taskProcessingLoading ? (
- <div className="flex justify-center">
- <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
- </div>
- ) : (
- <span
- className={
- isTaskProcessingActive
- ? "text-green-400"
- : "text-red-400"
- }
- >
- {isTaskProcessingActive ? "Active" : "Idle"}
- </span>
- )}
- </div>
- <div className="text-sm font-medium text-gray-400">
- Task Processing
- </div>
- <div className="text-xs text-gray-500 mt-1">
- {filesSuccessfulLoading || filesProcessedLoading
- ? "..."
- : `${successRate}% success`}
- </div>
- </div>
- </div>
- <div className="flex gap-2">
- <button
- onClick={() =>
- isTaskProcessingActive
- ? stopTaskProcessingMutation.mutate()
- : startTaskProcessingMutation.mutate()
- }
- disabled={
- taskProcessingLoading ||
- startTaskProcessingMutation.isPending ||
- stopTaskProcessingMutation.isPending
- }
- className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
- isTaskProcessingActive
- ? "bg-red-600 hover:bg-red-700 text-white"
- : "bg-green-600 hover:bg-green-700 text-white"
- } disabled:opacity-50`}
- >
- {startTaskProcessingMutation.isPending ||
- stopTaskProcessingMutation.isPending
- ? "..."
- : isTaskProcessingActive
- ? "Stop"
- : "Start"}
- </button>
- </div>
- </div>
- {queueStatus && (
- <div className="mt-4 pt-4 border-t border-white/10">
- <div className="grid grid-cols-4 gap-2 text-xs">
- <div className="text-center">
- <div className="text-gray-400">Pending</div>
- <div className="text-white font-medium">
- {queueStatus.pending || 0}
- </div>
- </div>
- <div className="text-center">
- <div className="text-gray-400">Active</div>
- <div className="text-white font-medium">
- {queueStatus.processing || 0}
- </div>
- </div>
- <div className="text-center">
- <div className="text-gray-400">Done</div>
- <div className="text-white font-medium">
- {queueStatus.completed || 0}
- </div>
- </div>
- <div className="text-center">
- <div className="text-gray-400">Failed</div>
- <div className="text-white font-medium">
- {queueStatus.failed || 0}
- </div>
- </div>
- </div>
- </div>
- )}
- </div>
- {/* API Health */}
- <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
- <div className="flex items-center gap-x-3">
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/20 ring-1 ring-amber-500/30">
- <svg
- className="h-6 w-6 text-amber-400"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth="1.5"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
- />
- </svg>
- </div>
- <div>
- <div className="text-2xl font-bold text-white">
- {apiHealthLoading ? (
- <div className="flex justify-center">
- <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
- </div>
- ) : (
- <span
- className={isApiHealthy ? "text-green-400" : "text-red-400"}
- >
- {isApiHealthy ? "Healthy" : "Issues"}
- </span>
- )}
- </div>
- <div className="text-sm font-medium text-gray-400">
- API Health
- </div>
- <div className="text-xs text-gray-500 mt-1">
- {apiHealth?.datetime
- ? new Date(apiHealth.datetime).toLocaleTimeString()
- : "Checking..."}
- </div>
- </div>
- </div>
- </div>
- {/* Files Processed */}
- <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
- <div className="flex items-center gap-x-3">
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-rose-500/20 ring-1 ring-rose-500/30">
- <svg
- className="h-6 w-6 text-rose-400"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth="1.5"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
- />
- </svg>
- </div>
- <div className="flex-1">
- <div className="text-2xl font-bold text-white">
- {filesSuccessfulLoading ? (
- <div className="flex justify-center">
- <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
- </div>
- ) : (
- filesProcessed.toLocaleString()
- )}
- </div>
- <div className="text-sm font-medium text-gray-400">
- Files Processed
- </div>
- {/* Current Task Progress */}
- {tasks && tasks.length > 0 && (
- <div className="mt-3 pt-3 border-t border-white/10">
- {(() => {
- const processingTask = tasks.find(
- (t: any) => t.status === "processing"
- );
- if (!processingTask) return null;
- const progress = processingTask.progress || 0;
- const fileName = processingTask.input
- ? processingTask.input.split("/").pop()
- : "Unknown file";
- return (
- <div>
- <div className="flex items-center gap-2 mb-2">
- <svg
- className="h-4 w-4 text-gray-400 flex-shrink-0"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth="1.5"
- stroke="currentColor"
- >
- <title>{fileName}</title>
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
- />
- </svg>
- <span
- className="text-xs text-gray-400 truncate"
- title={fileName}
- >
- Processing...
- </span>
- </div>
- <div className="flex items-center gap-2">
- <div className="flex-1 bg-white/10 rounded-full h-2 overflow-hidden">
- <div
- className="h-full bg-gradient-to-r from-rose-400 to-rose-500 transition-all duration-300"
- style={{ width: `${progress}%` }}
- />
- </div>
- <div className="text-xs font-medium text-rose-400 w-8 text-right">
- {progress}%
- </div>
- </div>
- </div>
- );
- })()}
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
|