فهرست منبع

Fix API restart button visibility and watcher status update delays

- Add ApiHealth component import to StatsSection so restart button appears on dashboard
- Wrap stats grid with space-y-6 container for proper spacing between widgets
- Fix watcher start/stop status updates by adding explicit refetch with 100ms delay
- Apply fix across all watcher control components (StatsSection, WatcherControls, WatcherStatus)
- Fix TypeScript type checking issues in DuplicateList.tsx and indexing/page.tsx for object property access
Timothy Pomeroy 3 هفته پیش
والد
کامیت
99758c1818

+ 5 - 3
apps/service/src/app.controller.ts

@@ -3,13 +3,12 @@ import {
   Controller,
   Controller,
   Delete,
   Delete,
   Get,
   Get,
+  INestApplication,
   Param,
   Param,
   Post,
   Post,
   Put,
   Put,
   Query,
   Query,
-  Inject,
 } from '@nestjs/common';
 } from '@nestjs/common';
-import { INestApplication } from '@nestjs/common';
 import * as fs from 'fs';
 import * as fs from 'fs';
 import * as path from 'path';
 import * as path from 'path';
 import { AppService } from './app.service';
 import { AppService } from './app.service';
@@ -86,7 +85,10 @@ export class AppController {
   @Post('restart')
   @Post('restart')
   async restart() {
   async restart() {
     if (!this.app) {
     if (!this.app) {
-      return { success: false, message: 'Application not properly initialized' };
+      return {
+        success: false,
+        message: 'Application not properly initialized',
+      };
     }
     }
 
 
     // Schedule restart for next tick to allow response to complete
     // Schedule restart for next tick to allow response to complete

+ 1 - 1
apps/service/src/main.ts

@@ -1,6 +1,6 @@
 import { NestFactory } from '@nestjs/core';
 import { NestFactory } from '@nestjs/core';
-import { AppModule } from './app.module';
 import { AppController } from './app.controller';
 import { AppController } from './app.controller';
+import { AppModule } from './app.module';
 
 
 async function bootstrap() {
 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
   const app = await NestFactory.create(AppModule);

+ 7 - 6
apps/web/src/app/components/ApiHealth.tsx

@@ -1,9 +1,9 @@
 "use client";
 "use client";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { useState } from "react";
-import { get, post } from "../../lib/api";
 import { ArrowPathIcon } from "@heroicons/react/24/outline";
 import { ArrowPathIcon } from "@heroicons/react/24/outline";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
 import toast from "react-hot-toast";
 import toast from "react-hot-toast";
+import { get, post } from "../../lib/api";
 
 
 export default function ApiHealth() {
 export default function ApiHealth() {
   const [responseTime, setResponseTime] = useState<number | null>(null);
   const [responseTime, setResponseTime] = useState<number | null>(null);
@@ -19,7 +19,7 @@ export default function ApiHealth() {
       setResponseTime(Math.round(endTime - startTime));
       setResponseTime(Math.round(endTime - startTime));
       return result;
       return result;
     },
     },
-    refetchInterval: 30000 // Check every 30 seconds
+    refetchInterval: 30000, // Check every 30 seconds
   });
   });
 
 
   const restartMutation = useMutation({
   const restartMutation = useMutation({
@@ -34,7 +34,7 @@ export default function ApiHealth() {
     },
     },
     onError: (error: any) => {
     onError: (error: any) => {
       toast.error("Failed to restart API service");
       toast.error("Failed to restart API service");
-    }
+    },
   });
   });
 
 
   const isHealthy = data?.status === "healthy";
   const isHealthy = data?.status === "healthy";
@@ -128,7 +128,8 @@ export default function ApiHealth() {
           <div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 max-w-sm mx-4">
           <div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 max-w-sm mx-4">
             <h2 className="text-lg font-semibold mb-2">Restart API Service?</h2>
             <h2 className="text-lg font-semibold mb-2">Restart API Service?</h2>
             <p className="text-gray-600 dark:text-gray-400 text-sm mb-6">
             <p className="text-gray-600 dark:text-gray-400 text-sm mb-6">
-              This will gracefully restart the API service. The service will be temporarily unavailable.
+              This will gracefully restart the API service. The service will be
+              temporarily unavailable.
             </p>
             </p>
             <div className="flex justify-end gap-3">
             <div className="flex justify-end gap-3">
               <button
               <button

+ 311 - 292
apps/web/src/app/components/StatsSection.tsx

@@ -3,109 +3,118 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 import { useEffect } from "react";
 import { useEffect } from "react";
 import toast from "react-hot-toast";
 import toast from "react-hot-toast";
 import { get, post } from "../../lib/api";
 import { get, post } from "../../lib/api";
+import ApiHealth from "./ApiHealth";
 
 
 export default function StatsSection() {
 export default function StatsSection() {
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
 
 
   const { data: tasks, isLoading: _tasksLoading } = useQuery({
   const { data: tasks, isLoading: _tasksLoading } = useQuery({
     queryKey: ["tasks"],
     queryKey: ["tasks"],
-    queryFn: () => get("/tasks")
+    queryFn: () => get("/tasks"),
   });
   });
 
 
   const { data: filesSuccessful, isLoading: filesSuccessfulLoading } = useQuery(
   const { data: filesSuccessful, isLoading: filesSuccessfulLoading } = useQuery(
     {
     {
       queryKey: ["files-stats-successful"],
       queryKey: ["files-stats-successful"],
-      queryFn: () => get("/files/stats/successful")
+      queryFn: () => get("/files/stats/successful"),
     }
     }
   );
   );
 
 
   const { data: filesProcessedTotal, isLoading: filesProcessedLoading } =
   const { data: filesProcessedTotal, isLoading: filesProcessedLoading } =
     useQuery({
     useQuery({
       queryKey: ["files-stats-processed"],
       queryKey: ["files-stats-processed"],
-      queryFn: () => get("/files/stats/processed")
+      queryFn: () => get("/files/stats/processed"),
     });
     });
 
 
   const { data: _datasets, isLoading: _datasetsLoading } = useQuery({
   const { data: _datasets, isLoading: _datasetsLoading } = useQuery({
     queryKey: ["datasets"],
     queryKey: ["datasets"],
-    queryFn: () => get("/files")
+    queryFn: () => get("/files"),
   });
   });
 
 
   const { data: settings, isLoading: settingsLoading } = useQuery({
   const { data: settings, isLoading: settingsLoading } = useQuery({
     queryKey: ["settings", "datasets"],
     queryKey: ["settings", "datasets"],
-    queryFn: () => get("/config/settings/datasets")
+    queryFn: () => get("/config/settings/datasets"),
   });
   });
 
 
   const { data: watcherStatus, isLoading: watcherLoading } = useQuery({
   const { data: watcherStatus, isLoading: watcherLoading } = useQuery({
     queryKey: ["watcher", "status"],
     queryKey: ["watcher", "status"],
-    queryFn: () => get("/watcher/status")
+    queryFn: () => get("/watcher/status"),
   });
   });
 
 
   const { data: taskProcessingStatus, isLoading: taskProcessingLoading } =
   const { data: taskProcessingStatus, isLoading: taskProcessingLoading } =
     useQuery({
     useQuery({
       queryKey: ["tasks", "processing-status"],
       queryKey: ["tasks", "processing-status"],
-      queryFn: () => get("/tasks/processing-status")
+      queryFn: () => get("/tasks/processing-status"),
     });
     });
 
 
   const { data: queueStatus, isLoading: _queueLoading } = useQuery({
   const { data: queueStatus, isLoading: _queueLoading } = useQuery({
     queryKey: ["tasks", "queue", "status"],
     queryKey: ["tasks", "queue", "status"],
-    queryFn: () => get("/tasks/queue/status")
+    queryFn: () => get("/tasks/queue/status"),
   });
   });
 
 
   const { data: apiHealth, isLoading: apiHealthLoading } = useQuery({
   const { data: apiHealth, isLoading: apiHealthLoading } = useQuery({
     queryKey: ["api", "health"],
     queryKey: ["api", "health"],
     queryFn: () => get("/health"),
     queryFn: () => get("/health"),
-    refetchInterval: 30000
+    refetchInterval: 30000,
   });
   });
 
 
   // Mutations for controlling services
   // Mutations for controlling services
   const startWatcherMutation = useMutation({
   const startWatcherMutation = useMutation({
     mutationFn: () => post("/watcher/start"),
     mutationFn: () => post("/watcher/start"),
     onSuccess: () => {
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
       toast.success("File watcher started");
       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: () => {
     onError: () => {
       toast.error("Failed to start file watcher");
       toast.error("Failed to start file watcher");
-    }
+    },
   });
   });
 
 
   const stopWatcherMutation = useMutation({
   const stopWatcherMutation = useMutation({
     mutationFn: () => post("/watcher/stop"),
     mutationFn: () => post("/watcher/stop"),
     onSuccess: () => {
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
       toast.success("File watcher stopped");
       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: () => {
     onError: () => {
       toast.error("Failed to stop file watcher");
       toast.error("Failed to stop file watcher");
-    }
+    },
   });
   });
 
 
   const startTaskProcessingMutation = useMutation({
   const startTaskProcessingMutation = useMutation({
     mutationFn: () => post("/tasks/start-processing"),
     mutationFn: () => post("/tasks/start-processing"),
     onSuccess: () => {
     onSuccess: () => {
       queryClient.invalidateQueries({
       queryClient.invalidateQueries({
-        queryKey: ["tasks", "processing-status"]
+        queryKey: ["tasks", "processing-status"],
       });
       });
       queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
       queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
       toast.success("Task processing started");
       toast.success("Task processing started");
     },
     },
     onError: () => {
     onError: () => {
       toast.error("Failed to start task processing");
       toast.error("Failed to start task processing");
-    }
+    },
   });
   });
 
 
   const stopTaskProcessingMutation = useMutation({
   const stopTaskProcessingMutation = useMutation({
     mutationFn: () => post("/tasks/stop-processing"),
     mutationFn: () => post("/tasks/stop-processing"),
     onSuccess: () => {
     onSuccess: () => {
       queryClient.invalidateQueries({
       queryClient.invalidateQueries({
-        queryKey: ["tasks", "processing-status"]
+        queryKey: ["tasks", "processing-status"],
       });
       });
       queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
       queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
       toast.success("Task processing stopped");
       toast.success("Task processing stopped");
     },
     },
     onError: () => {
     onError: () => {
       toast.error("Failed to stop task processing");
       toast.error("Failed to stop task processing");
-    }
+    },
   });
   });
 
 
   const _tasksRunning = tasks?.length || 0;
   const _tasksRunning = tasks?.length || 0;
@@ -136,10 +145,10 @@ export default function StatsSection() {
       ) {
       ) {
         queryClient.invalidateQueries({ queryKey: ["tasks"] });
         queryClient.invalidateQueries({ queryKey: ["tasks"] });
         queryClient.invalidateQueries({
         queryClient.invalidateQueries({
-          queryKey: ["tasks", "processing-status"]
+          queryKey: ["tasks", "processing-status"],
         });
         });
         queryClient.invalidateQueries({
         queryClient.invalidateQueries({
-          queryKey: ["tasks", "queue", "status"]
+          queryKey: ["tasks", "queue", "status"],
         });
         });
       }
       }
     };
     };
@@ -169,14 +178,224 @@ export default function StatsSection() {
   }, [queryClient]);
   }, [queryClient]);
 
 
   return (
   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 justify-between">
+    <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 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">
+            <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/20 ring-1 ring-amber-500/30">
               <svg
               <svg
-                className="h-6 w-6 text-indigo-400"
+                className="h-6 w-6 text-amber-400"
                 fill="none"
                 fill="none"
                 viewBox="0 0 24 24"
                 viewBox="0 0 24 24"
                 strokeWidth="1.5"
                 strokeWidth="1.5"
@@ -185,98 +404,42 @@ export default function StatsSection() {
                 <path
                 <path
                   strokeLinecap="round"
                   strokeLinecap="round"
                   strokeLinejoin="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"
+                  d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                 />
                 />
               </svg>
               </svg>
             </div>
             </div>
             <div>
             <div>
               <div className="text-2xl font-bold text-white">
               <div className="text-2xl font-bold text-white">
-                {watcherLoading ? (
+                {apiHealthLoading ? (
                   <div className="flex justify-center">
                   <div className="flex justify-center">
                     <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
                     <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
                   </div>
                   </div>
                 ) : (
                 ) : (
                   <span
                   <span
-                    className={
-                      isWatcherActive ? "text-green-400" : "text-red-400"
-                    }
+                    className={isApiHealthy ? "text-green-400" : "text-red-400"}
                   >
                   >
-                    {isWatcherActive ? "Active" : "Idle"}
+                    {isApiHealthy ? "Healthy" : "Issues"}
                   </span>
                   </span>
                 )}
                 )}
               </div>
               </div>
               <div className="text-sm font-medium text-gray-400">
               <div className="text-sm font-medium text-gray-400">
-                File Watcher
+                API Health
               </div>
               </div>
               <div className="text-xs text-gray-500 mt-1">
               <div className="text-xs text-gray-500 mt-1">
-                {settingsLoading ? "..." : `${activeWatchers} datasets`}
+                {apiHealth?.datetime
+                  ? new Date(apiHealth.datetime).toLocaleTimeString()
+                  : "Checking..."}
               </div>
               </div>
             </div>
             </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>
         </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">
+        {/* 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 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">
+            <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-rose-500/20 ring-1 ring-rose-500/30">
               <svg
               <svg
-                className="h-6 w-6 text-emerald-400"
+                className="h-6 w-6 text-rose-400"
                 fill="none"
                 fill="none"
                 viewBox="0 0 24 24"
                 viewBox="0 0 24 24"
                 strokeWidth="1.5"
                 strokeWidth="1.5"
@@ -285,222 +448,78 @@ export default function StatsSection() {
                 <path
                 <path
                   strokeLinecap="round"
                   strokeLinecap="round"
                   strokeLinejoin="round"
                   strokeLinejoin="round"
-                  d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+                  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>
               </svg>
             </div>
             </div>
-            <div>
+            <div className="flex-1">
               <div className="text-2xl font-bold text-white">
               <div className="text-2xl font-bold text-white">
-                {taskProcessingLoading ? (
+                {filesSuccessfulLoading ? (
                   <div className="flex justify-center">
                   <div className="flex justify-center">
                     <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
                     <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
                   </div>
                   </div>
                 ) : (
                 ) : (
-                  <span
-                    className={
-                      isTaskProcessingActive ? "text-green-400" : "text-red-400"
-                    }
-                  >
-                    {isTaskProcessingActive ? "Active" : "Idle"}
-                  </span>
+                  filesProcessed.toLocaleString()
                 )}
                 )}
               </div>
               </div>
               <div className="text-sm font-medium text-gray-400">
               <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>
+                Files Processed
               </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;
+              {/* 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";
+                    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}%` }}
-                          />
+                    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>
-                        <div className="text-xs font-medium text-rose-400 w-8 text-right">
-                          {progress}%
+                        <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>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 10 - 2
apps/web/src/app/components/WatcherControls.tsx

@@ -16,13 +16,17 @@ export default function WatcherControls() {
   const startMutation = useMutation({
   const startMutation = useMutation({
     mutationFn: () => post("/watcher/start"),
     mutationFn: () => post("/watcher/start"),
     onSuccess: () => {
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
       toast.success("File watcher started successfully");
       toast.success("File watcher started successfully");
       addNotification({
       addNotification({
         type: "success",
         type: "success",
         title: "File Watcher Started",
         title: "File Watcher Started",
         message: "The file watcher has been started successfully.",
         message: "The file watcher has been started successfully.",
       });
       });
+      // Invalidate and refetch to ensure status updates immediately
+      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
+      setTimeout(() => {
+        queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
+      }, 100);
     },
     },
     onError: () => {
     onError: () => {
       toast.error("Failed to start file watcher");
       toast.error("Failed to start file watcher");
@@ -32,13 +36,17 @@ export default function WatcherControls() {
   const stopMutation = useMutation({
   const stopMutation = useMutation({
     mutationFn: () => post("/watcher/stop"),
     mutationFn: () => post("/watcher/stop"),
     onSuccess: () => {
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
       toast.success("File watcher stopped successfully");
       toast.success("File watcher stopped successfully");
       addNotification({
       addNotification({
         type: "success",
         type: "success",
         title: "File Watcher Stopped",
         title: "File Watcher Stopped",
         message: "The file watcher has been stopped successfully.",
         message: "The file watcher has been stopped successfully.",
       });
       });
+      // Invalidate and refetch to ensure status updates immediately
+      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
+      setTimeout(() => {
+        queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
+      }, 100);
     },
     },
     onError: () => {
     onError: () => {
       toast.error("Failed to stop file watcher");
       toast.error("Failed to stop file watcher");

+ 10 - 2
apps/web/src/app/components/WatcherStatus.tsx

@@ -27,25 +27,33 @@ export default function WatcherStatus() {
   const startMutation = useMutation({
   const startMutation = useMutation({
     mutationFn: () => post("/watcher/start"),
     mutationFn: () => post("/watcher/start"),
     onSuccess: () => {
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
       toast.success("Watcher started successfully");
       toast.success("Watcher started successfully");
       addNotification({
       addNotification({
         type: "success",
         type: "success",
         title: "Watcher Started",
         title: "Watcher Started",
         message: "The file watcher has been started successfully."
         message: "The file watcher has been started successfully."
       });
       });
+      // Invalidate and refetch to ensure status updates immediately
+      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
+      setTimeout(() => {
+        queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
+      }, 100);
     }
     }
   });
   });
   const stopMutation = useMutation({
   const stopMutation = useMutation({
     mutationFn: () => post("/watcher/stop"),
     mutationFn: () => post("/watcher/stop"),
     onSuccess: () => {
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
       toast.success("Watcher stopped successfully");
       toast.success("Watcher stopped successfully");
       addNotification({
       addNotification({
         type: "success",
         type: "success",
         title: "Watcher Stopped",
         title: "Watcher Stopped",
         message: "The file watcher has been stopped successfully."
         message: "The file watcher has been stopped successfully."
       });
       });
+      // Invalidate and refetch to ensure status updates immediately
+      queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
+      setTimeout(() => {
+        queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
+      }, 100);
     }
     }
   });
   });
 
 

+ 3 - 2
apps/web/src/app/duplicates/DuplicateList.tsx

@@ -106,9 +106,10 @@ export default function DuplicateList() {
         if (
         if (
           value &&
           value &&
           typeof value === "object" &&
           typeof value === "object" &&
-          typeof value.destination === "string"
+          "destination" in value &&
+          typeof (value as any).destination === "string"
         ) {
         ) {
-          if (value.destination.trim()) return value.destination as string;
+          if ((value as any).destination.trim()) return (value as any).destination as string;
         }
         }
       }
       }
       return undefined;
       return undefined;

+ 3 - 2
apps/web/src/app/indexing/page.tsx

@@ -58,9 +58,10 @@ export default function IndexManagementPage() {
         if (
         if (
           value &&
           value &&
           typeof value === "object" &&
           typeof value === "object" &&
-          typeof value.destination === "string"
+          "destination" in value &&
+          typeof (value as any).destination === "string"
         ) {
         ) {
-          if (value.destination.trim()) return value.destination as string;
+          if ((value as any).destination.trim()) return (value as any).destination as string;
         }
         }
       }
       }
       return undefined;
       return undefined;

BIN
data/database.db-shm