|
|
@@ -1,138 +1,14 @@
|
|
|
"use client";
|
|
|
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
+import { 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";
|
|
|
+import FileWatcherCard from "./FileWatcherCard";
|
|
|
+import TaskProcessingCard from "./TaskProcessingCard";
|
|
|
+import FilesProcessedCard from "./FilesProcessedCard";
|
|
|
|
|
|
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) => {
|
|
|
@@ -183,344 +59,14 @@ export default function StatsSection() {
|
|
|
{/* API Health Widget */}
|
|
|
<ApiHealth />
|
|
|
|
|
|
- {/* 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;
|
|
|
+ {/* File Watcher Card */}
|
|
|
+ <FileWatcherCard />
|
|
|
|
|
|
- const progress = processingTask.progress || 0;
|
|
|
- const fileName = processingTask.input
|
|
|
- ? processingTask.input.split("/").pop()
|
|
|
- : "Unknown file";
|
|
|
+ {/* Task Processing Card */}
|
|
|
+ <TaskProcessingCard />
|
|
|
|
|
|
- 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>
|
|
|
+ {/* Files Processed Card */}
|
|
|
+ <FilesProcessedCard />
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|