Pārlūkot izejas kodu

feat: enhance dashboard and add output file existence check

Dashboard Improvements:
- Files Processed section now shows current task name and progress bar
- API Health section displays response time metrics
- Better visual feedback on dashboard with live progress tracking

API Service Improvements:
- Add outputFileExists() method to HandbrakeService for case-insensitive checking
- TaskQueueService now checks if output file exists before creating tasks
- Prevents unnecessary queue entries when output already exists under different case
- Logs warnings when attempting to create task for existing output file

This ensures better queue management and prevents duplicate work when
output files exist with case variations (e.g., Test1 vs TEST1).
Timothy Pomeroy 1 mēnesi atpakaļ
vecāks
revīzija
1cbd3ba724

+ 25 - 0
apps/service/src/handbrake.service.ts

@@ -14,6 +14,31 @@ export class HandbrakeService {
     private readonly db: DbService,
   ) {}
 
+  /**
+   * Check if output file exists (with case-insensitive directory matching)
+   * Useful for avoiding creating tasks when output already exists
+   */
+  outputFileExists(outputPath: string): boolean {
+    const outputDir = path.dirname(outputPath);
+    const outputFileName = path.basename(outputPath);
+
+    // Check if exact path exists
+    if (existsSync(outputPath)) {
+      return true;
+    }
+
+    // Check for case-insensitive directory match
+    const actualDir = this.findExistingDirCaseInsensitive(outputDir);
+    if (actualDir) {
+      const potentialPath = path.join(actualDir, outputFileName);
+      if (existsSync(potentialPath)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   /**
    * Find existing directory with case-insensitive matching
    * Returns the actual path if found, null otherwise

+ 8 - 0
apps/service/src/task-queue.service.ts

@@ -366,6 +366,14 @@ export class TaskQueueService implements OnModuleInit {
     priority?: number;
     status?: string;
   }) {
+    // Check if output file already exists (case-insensitive matching)
+    if (this.handbrake.outputFileExists(taskData.output)) {
+      this.logger.warn(
+        `Output file already exists: ${taskData.output}, skipping task creation`,
+      );
+      throw new Error(`Output file already exists: ${taskData.output}`);
+    }
+
     // Check if file already exists in database
     const existingFile = this.db.findFile(taskData.dataset, taskData.input);
     if (!existingFile) {

+ 22 - 3
apps/web/src/app/components/ApiHealth.tsx

@@ -1,11 +1,20 @@
 "use client";
 import { useQuery } from "@tanstack/react-query";
+import { useState } from "react";
 import { get } from "../../lib/api";
 
 export default function ApiHealth() {
+  const [responseTime, setResponseTime] = useState<number | null>(null);
+
   const { data, isLoading, error } = useQuery({
     queryKey: ["api", "health"],
-    queryFn: () => get("/health"),
+    queryFn: async () => {
+      const startTime = performance.now();
+      const result = await get("/health");
+      const endTime = performance.now();
+      setResponseTime(Math.round(endTime - startTime));
+      return result;
+    },
     refetchInterval: 30000 // Check every 30 seconds
   });
 
@@ -35,7 +44,7 @@ export default function ApiHealth() {
         </div>
       </div>
 
-      <div className="grid grid-cols-1 gap-2 text-sm">
+      <div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
         <div>
           <span className="font-medium text-gray-700 dark:text-gray-300">
             Status:
@@ -50,6 +59,16 @@ export default function ApiHealth() {
                 : "Issues Detected"}
           </span>
         </div>
+        {responseTime !== null && (
+          <div>
+            <span className="font-medium text-gray-700 dark:text-gray-300">
+              Response Time:
+            </span>
+            <span className="ml-2 text-gray-900 dark:text-gray-100">
+              {responseTime}ms
+            </span>
+          </div>
+        )}
         {lastChecked && (
           <div>
             <span className="font-medium text-gray-700 dark:text-gray-300">
@@ -61,7 +80,7 @@ export default function ApiHealth() {
           </div>
         )}
         {error && (
-          <div>
+          <div className="md:col-span-2">
             <span className="font-medium text-gray-700 dark:text-gray-300">
               Error:
             </span>

+ 36 - 1
apps/web/src/app/components/StatsSection.tsx

@@ -389,7 +389,7 @@ export default function StatsSection() {
               />
             </svg>
           </div>
-          <div>
+          <div className="flex-1">
             <div className="text-2xl font-bold text-white">
               {filesSuccessfulLoading ? (
                 <div className="flex justify-center">
@@ -402,6 +402,41 @@ export default function StatsSection() {
             <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="text-xs text-gray-400 mb-2">
+                        Current: {fileName}
+                      </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>