|
|
@@ -1,8 +1,11 @@
|
|
|
"use client";
|
|
|
-import { useQuery } from "@tanstack/react-query";
|
|
|
-import { get } from "../../lib/api";
|
|
|
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
+import toast from "react-hot-toast";
|
|
|
+import { get, post } from "../../lib/api";
|
|
|
|
|
|
export default function StatsSection() {
|
|
|
+ const queryClient = useQueryClient();
|
|
|
+
|
|
|
const { data: tasks, isLoading: tasksLoading } = useQuery({
|
|
|
queryKey: ["tasks"],
|
|
|
queryFn: () => get("/tasks")
|
|
|
@@ -31,6 +34,79 @@ export default function StatsSection() {
|
|
|
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: () => {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
|
|
|
+ toast.success("File watcher started");
|
|
|
+ },
|
|
|
+ onError: () => {
|
|
|
+ toast.error("Failed to start file watcher");
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const stopWatcherMutation = useMutation({
|
|
|
+ mutationFn: () => post("/watcher/stop"),
|
|
|
+ onSuccess: () => {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
|
|
|
+ toast.success("File watcher stopped");
|
|
|
+ },
|
|
|
+ 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;
|
|
|
@@ -43,81 +119,217 @@ export default function StatsSection() {
|
|
|
.length
|
|
|
: 0;
|
|
|
|
|
|
+ const isApiHealthy = apiHealth?.status === "healthy";
|
|
|
+ const isWatcherActive = watcherStatus?.isWatching;
|
|
|
+ const isTaskProcessingActive = taskProcessingStatus?.isProcessing;
|
|
|
+
|
|
|
return (
|
|
|
<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 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"
|
|
|
+ <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`}
|
|
|
>
|
|
|
- <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>
|
|
|
+ {startWatcherMutation.isPending || stopWatcherMutation.isPending
|
|
|
+ ? "..."
|
|
|
+ : isWatcherActive
|
|
|
+ ? "Stop"
|
|
|
+ : "Start"}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <div>
|
|
|
- <div className="text-2xl font-bold text-white">
|
|
|
- {datasetsLoading || settingsLoading ? (
|
|
|
- <div className="flex justify-center">
|
|
|
- <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- activeWatchers
|
|
|
+ </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 className="text-sm font-medium text-gray-400">
|
|
|
- Active Watchers
|
|
|
- </div>
|
|
|
</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 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"
|
|
|
+ <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`}
|
|
|
>
|
|
|
- <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>
|
|
|
+ {startTaskProcessingMutation.isPending ||
|
|
|
+ stopTaskProcessingMutation.isPending
|
|
|
+ ? "..."
|
|
|
+ : isTaskProcessingActive
|
|
|
+ ? "Stop"
|
|
|
+ : "Start"}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <div>
|
|
|
- <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>
|
|
|
+ {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>
|
|
|
- ) : (
|
|
|
- filesProcessed.toLocaleString()
|
|
|
- )}
|
|
|
- </div>
|
|
|
- <div className="text-sm font-medium text-gray-400">
|
|
|
- Files Processed
|
|
|
+ </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>
|
|
|
+ )}
|
|
|
</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">
|
|
|
@@ -131,27 +343,35 @@ export default function StatsSection() {
|
|
|
<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"
|
|
|
+ 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">
|
|
|
- {filesSuccessfulLoading || filesProcessedLoading ? (
|
|
|
+ {apiHealthLoading ? (
|
|
|
<div className="flex justify-center">
|
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
|
|
</div>
|
|
|
) : (
|
|
|
- `${successRate}%`
|
|
|
+ <span
|
|
|
+ className={isApiHealthy ? "text-green-400" : "text-red-400"}
|
|
|
+ >
|
|
|
+ {isApiHealthy ? "Healthy" : "Issues"}
|
|
|
+ </span>
|
|
|
)}
|
|
|
</div>
|
|
|
- <div className="text-sm font-medium text-gray-400">
|
|
|
- Success Rate
|
|
|
+ <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">
|
|
|
@@ -165,22 +385,22 @@ export default function StatsSection() {
|
|
|
<path
|
|
|
strokeLinecap="round"
|
|
|
strokeLinejoin="round"
|
|
|
- d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
|
|
+ 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>
|
|
|
<div className="text-2xl font-bold text-white">
|
|
|
- {tasksLoading ? (
|
|
|
+ {filesSuccessfulLoading ? (
|
|
|
<div className="flex justify-center">
|
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
|
|
</div>
|
|
|
) : (
|
|
|
- tasksRunning
|
|
|
+ filesProcessed.toLocaleString()
|
|
|
)}
|
|
|
</div>
|
|
|
<div className="text-sm font-medium text-gray-400">
|
|
|
- Tasks Running
|
|
|
+ Files Processed
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|