diff --git a/inventory/src/pages/Settings.tsx b/inventory/src/pages/Settings.tsx index 0b6c36b..2380b2a 100644 --- a/inventory/src/pages/Settings.tsx +++ b/inventory/src/pages/Settings.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; @@ -58,6 +58,217 @@ export function Settings() { purchaseOrders: 10000 }); + // Helper to connect to event source + const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset') => { + if (eventSource) { + eventSource.close(); + } + + console.log('Connecting to event source:', type); // Debug log + const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, { + withCredentials: true + }); + setEventSource(source); + + source.onopen = () => { + console.log('Event source connected:', type); // Debug log + }; + + source.onerror = () => { + console.log('Event source error:', type, source.readyState); // Debug log + if (source.readyState === EventSource.CLOSED) { + source.close(); + setEventSource(null); + + // Only reset states if we're not in a completed state + const progress = type === 'update' ? updateProgress : + type === 'import' ? importProgress : + resetProgress; + + if (progress?.status !== 'complete') { + // Reset the appropriate state based on type + if (type === 'update') { + setIsUpdating(false); + setUpdateProgress(null); + } else if (type === 'import') { + setIsImporting(false); + setImportProgress(null); + } else if (type === 'reset') { + setIsResetting(false); + setResetProgress(null); + } + } + } + }; + + source.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('Event source message:', type, data); // Debug log + let progressData = data.progress ? + (typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress) + : data; + + // Update the appropriate progress state based on type + const setProgress = type === 'update' ? setUpdateProgress : + type === 'import' ? setImportProgress : + setResetProgress; + + // Also set the operation state if we're getting progress + if (progressData.status === 'running') { + if (type === 'update') setIsUpdating(true); + else if (type === 'import') setIsImporting(true); + else if (type === 'reset') setIsResetting(true); + } + + setProgress(prev => { + // If we're getting a new operation, clear out old messages + if (progressData.operation && progressData.operation !== prev?.operation) { + return { + status: progressData.status || 'running', + operation: progressData.operation, + current: progressData.current !== undefined ? Number(progressData.current) : undefined, + total: progressData.total !== undefined ? Number(progressData.total) : undefined, + rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined, + percentage: progressData.percentage, + elapsed: progressData.elapsed, + remaining: progressData.remaining, + message: progressData.message, + error: progressData.error, + added: progressData.added, + updated: progressData.updated, + skipped: progressData.skipped, + duration: progressData.duration + }; + } + + // Otherwise update existing state + return { + ...prev, + status: progressData.status || prev?.status || 'running', + operation: progressData.operation || prev?.operation, + current: progressData.current !== undefined ? Number(progressData.current) : prev?.current, + total: progressData.total !== undefined ? Number(progressData.total) : prev?.total, + rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate, + percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage, + elapsed: progressData.elapsed || prev?.elapsed, + remaining: progressData.remaining || prev?.remaining, + error: progressData.error || prev?.error, + message: progressData.message || prev?.message, + added: progressData.added !== undefined ? progressData.added : prev?.added, + updated: progressData.updated !== undefined ? progressData.updated : prev?.updated, + skipped: progressData.skipped !== undefined ? progressData.skipped : prev?.skipped, + duration: progressData.duration || prev?.duration + }; + }); + + if (progressData.status === 'complete') { + source.close(); + setEventSource(null); + + // Reset the appropriate state based on type + if (type === 'update') { + setIsUpdating(false); + setUpdateProgress(null); + } else if (type === 'import') { + setIsImporting(false); + setImportProgress(null); + } else if (type === 'reset') { + setIsResetting(false); + setResetProgress(null); + } + + if (!progressData.operation?.includes('cancelled')) { + handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`); + } + } else if (progressData.status === 'error') { + source.close(); + setEventSource(null); + + // Reset the appropriate state based on type + if (type === 'update') { + setIsUpdating(false); + } else if (type === 'import') { + setIsImporting(false); + } else if (type === 'reset') { + setIsResetting(false); + } + + handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error'); + } + } catch (error) { + console.error('Error parsing event data:', error); // Debug log + } + }; + }, []); // Remove dependencies that might prevent initial connection + + // Check for active operations on mount + useEffect(() => { + console.log('Checking status on mount'); // Debug log + const checkStatus = async () => { + try { + const response = await fetch(`${config.apiUrl}/csv/status`, { + credentials: 'include' + }); + const data = await response.json(); + console.log('Status check response:', data); // Debug log + + if (data.active) { + // Try to determine the operation type from progress if available + let operationType: 'update' | 'import' | 'reset' | null = null; + + if (data.progress?.operation) { + const operation = data.progress.operation.toLowerCase(); + if (operation.includes('update')) operationType = 'update'; + else if (operation.includes('import')) operationType = 'import'; + else if (operation.includes('reset')) operationType = 'reset'; + } else { + // If no progress data, try to connect to import stream by default + // since that's the most common long-running operation + operationType = 'import'; + } + + if (operationType) { + // Set initial state + if (operationType === 'update') { + setIsUpdating(true); + if (data.progress) { + setUpdateProgress({ + ...data.progress, + status: data.progress.status || 'running' + }); + } + } else if (operationType === 'import') { + setIsImporting(true); + if (data.progress) { + setImportProgress({ + ...data.progress, + status: data.progress.status || 'running' + }); + } + } else if (operationType === 'reset') { + setIsResetting(true); + if (data.progress) { + setResetProgress({ + ...data.progress, + status: data.progress.status || 'running' + }); + } + } + + // Connect to the appropriate event source + console.log('Connecting to event source for active operation:', operationType); + connectToEventSource(operationType); + } + } + } catch (error) { + console.error('Failed to check operation status:', error); + } + }; + + checkStatus(); + }, []); // Remove connectToEventSource dependency to ensure it runs on mount + // Clean up function to reset state const handleCancel = async () => { @@ -96,104 +307,22 @@ export function Settings() { setUpdateProgress({ status: 'running', operation: 'Starting CSV update' }); try { - // Set up SSE connection for progress updates first - if (eventSource) { - eventSource.close(); - setEventSource(null); - } + // Connect to SSE for progress updates + connectToEventSource('update'); - // Set up SSE connection for progress updates - const source = new EventSource(`${config.apiUrl}/csv/update/progress`, { - withCredentials: true - }); - setEventSource(source); - - // Add event listeners for all SSE events - source.onopen = () => {}; - - source.onerror = () => { - if (source.readyState === EventSource.CLOSED) { - source.close(); - setEventSource(null); - setIsUpdating(false); - // Only show connection error if we're not in a cancelled state - if (!updateProgress?.operation?.includes('cancelled')) { - setUpdateProgress(prev => ({ - ...prev, - status: 'error', - error: 'Connection to server lost' - })); - } - } - }; - - source.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - let progressData = data.progress ? - (typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress) - : data; - - setUpdateProgress(prev => { - // If we're getting a new operation, clear out old messages - if (progressData.operation && progressData.operation !== prev?.operation) { - return { - status: progressData.status || 'running', - operation: progressData.operation, - current: progressData.current !== undefined ? Number(progressData.current) : undefined, - total: progressData.total !== undefined ? Number(progressData.total) : undefined, - rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined, - percentage: progressData.percentage, - elapsed: progressData.elapsed, - remaining: progressData.remaining, - message: progressData.message, - error: progressData.error - }; - } - - // Otherwise update existing state - return { - ...prev, - status: progressData.status || prev?.status || 'running', - operation: progressData.operation || prev?.operation, - current: progressData.current !== undefined ? Number(progressData.current) : prev?.current, - total: progressData.total !== undefined ? Number(progressData.total) : prev?.total, - rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate, - percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage, - elapsed: progressData.elapsed || prev?.elapsed, - remaining: progressData.remaining || prev?.remaining, - error: progressData.error || prev?.error, - message: progressData.message || prev?.message - }; - }); - - if (progressData.status === 'complete') { - source.close(); - setEventSource(null); - setIsUpdating(false); - setUpdateProgress(null); - if (!progressData.operation?.includes('cancelled')) { - handleComplete('CSV update'); - } - } else if (progressData.status === 'error') { - source.close(); - setEventSource(null); - setIsUpdating(false); - handleError('CSV update', progressData.error || 'Unknown error'); - } - } catch (error) { - // Silently handle parsing errors - } - }; - - // Now make the update request + // Make the update request const response = await fetch(`${config.apiUrl}/csv/update`, { method: 'POST', credentials: 'include' }); if (!response.ok) { - throw new Error(`Failed to update CSV files: ${response.status} ${response.statusText}`); + const data = await response.json().catch(() => ({})); + if (data.error === 'Import already in progress') { + // If there's already an import, just let the SSE connection handle showing progress + return; + } + throw new Error(data.error || `Failed to update CSV files: ${response.status} ${response.statusText}`); } } catch (error) { if (eventSource) { @@ -211,99 +340,10 @@ export function Settings() { setImportProgress({ status: 'running', operation: 'Starting import process' }); try { - // Set up SSE connection for progress updates first - if (eventSource) { - eventSource.close(); - setEventSource(null); - } + // Connect to SSE for progress updates + connectToEventSource('import'); - // Set up SSE connection for progress updates - const source = new EventSource(`${config.apiUrl}/csv/import/progress`, { - withCredentials: true - }); - setEventSource(source); - - // Add event listeners for all SSE events - source.onopen = () => {}; - - source.onerror = () => { - if (source.readyState === EventSource.CLOSED) { - source.close(); - setEventSource(null); - setIsImporting(false); - // Only show connection error if we're not in a cancelled state - if (!importProgress?.operation?.includes('cancelled') && importProgress?.status !== 'complete') { - setImportProgress(prev => ({ - ...prev, - status: 'error', - error: 'Connection to server lost' - })); - } - } - }; - - source.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - let progressData = data.progress ? - (typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress) - : data; - - setImportProgress(prev => { - // If we're getting a new operation, clear out old messages - if (progressData.operation && progressData.operation !== prev?.operation) { - return { - status: progressData.status || 'running', - operation: progressData.operation, - current: progressData.current !== undefined ? Number(progressData.current) : undefined, - total: progressData.total !== undefined ? Number(progressData.total) : undefined, - rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined, - percentage: progressData.percentage, - elapsed: progressData.elapsed, - remaining: progressData.remaining, - message: progressData.message, - error: progressData.error - }; - } - - // Otherwise update existing state - return { - ...prev, - status: progressData.status || prev?.status || 'running', - operation: progressData.operation || prev?.operation, - current: progressData.current !== undefined ? Number(progressData.current) : prev?.current, - total: progressData.total !== undefined ? Number(progressData.total) : prev?.total, - rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate, - percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage, - elapsed: progressData.elapsed || prev?.elapsed, - remaining: progressData.remaining || prev?.remaining, - error: progressData.error || prev?.error, - message: progressData.message || prev?.message - }; - }); - - if (progressData.status === 'complete') { - source.close(); - setEventSource(null); - setIsUpdating(false); - setIsImporting(false); - setImportProgress(null); - if (!progressData.operation?.includes('cancelled')) { - handleComplete('Data import'); - } - } else if (progressData.status === 'error') { - source.close(); - setEventSource(null); - setIsUpdating(false); - setIsImporting(false); - handleError('Data import', progressData.error || 'Unknown error'); - } - } catch (error) { - // Silently handle parsing errors - } - }; - - // Now make the import request + // Make the import request const response = await fetch(`${config.apiUrl}/csv/import`, { method: 'POST', headers: { @@ -314,7 +354,12 @@ export function Settings() { }); if (!response.ok) { - throw new Error('Failed to start CSV import'); + const data = await response.json().catch(() => ({})); + if (data.error === 'Import already in progress') { + // If there's already an import, just let the SSE connection handle showing progress + return; + } + throw new Error(data.error || 'Failed to start CSV import'); } } catch (error) { if (eventSource) { @@ -332,104 +377,22 @@ export function Settings() { setResetProgress({ status: 'running', operation: 'Starting database reset' }); try { - // Set up SSE connection for progress updates first - if (eventSource) { - eventSource.close(); - setEventSource(null); - } + // Connect to SSE for progress updates + connectToEventSource('reset'); - // Set up SSE connection for progress updates - const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, { - withCredentials: true - }); - setEventSource(source); - - // Add event listeners for all SSE events - source.onopen = () => {}; - - source.onerror = () => { - if (source.readyState === EventSource.CLOSED) { - source.close(); - setEventSource(null); - setIsResetting(false); - // Only show connection error if we're not in a cancelled state - if (!resetProgress?.operation?.includes('cancelled')) { - setResetProgress(prev => ({ - ...prev, - status: 'error', - error: 'Connection to server lost' - })); - } - } - }; - - source.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - let progressData = data.progress ? - (typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress) - : data; - - setResetProgress(prev => { - // If we're getting a new operation, clear out old messages - if (progressData.operation && progressData.operation !== prev?.operation) { - return { - status: progressData.status || 'running', - operation: progressData.operation, - current: progressData.current !== undefined ? Number(progressData.current) : undefined, - total: progressData.total !== undefined ? Number(progressData.total) : undefined, - rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined, - percentage: progressData.percentage, - elapsed: progressData.elapsed, - remaining: progressData.remaining, - message: progressData.message, - error: progressData.error - }; - } - - // Otherwise update existing state - return { - ...prev, - status: progressData.status || prev?.status || 'running', - operation: progressData.operation || prev?.operation, - current: progressData.current !== undefined ? Number(progressData.current) : prev?.current, - total: progressData.total !== undefined ? Number(progressData.total) : prev?.total, - rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate, - percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage, - elapsed: progressData.elapsed || prev?.elapsed, - remaining: progressData.remaining || prev?.remaining, - error: progressData.error || prev?.error, - message: progressData.message || prev?.message - }; - }); - - if (progressData.status === 'complete') { - source.close(); - setEventSource(null); - setIsResetting(false); - setResetProgress(null); - if (!progressData.operation?.includes('cancelled')) { - handleComplete('Database reset'); - } - } else if (progressData.status === 'error') { - source.close(); - setEventSource(null); - setIsResetting(false); - handleError('Database reset', progressData.error || 'Unknown error'); - } - } catch (error) { - // Silently handle parsing errors - } - }; - - // Now make the reset request + // Make the reset request const response = await fetch(`${config.apiUrl}/csv/reset`, { method: 'POST', credentials: 'include' }); if (!response.ok) { - throw new Error('Failed to reset database'); + const data = await response.json().catch(() => ({})); + if (data.error === 'Import already in progress') { + // If there's already an import, just let the SSE connection handle showing progress + return; + } + throw new Error(data.error || 'Failed to reset database'); } } catch (error) { if (eventSource) { @@ -446,6 +409,7 @@ export function Settings() { useEffect(() => { return () => { if (eventSource) { + console.log('Cleaning up event source'); // Debug log eventSource.close(); } }; @@ -466,13 +430,47 @@ export function Settings() { {percentage !== null && ( <> -
+
{progress.current && progress.total && ( - {progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''} +
+ Progress: + {progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''} +
+ )} + {(progress.elapsed || progress.remaining) && ( +
+ Time: + + {progress.elapsed && `${progress.elapsed} elapsed`} + {progress.elapsed && progress.remaining && ' - '} + {progress.remaining && `${progress.remaining} remaining`} + +
+ )} + {(progress.added !== undefined || progress.updated !== undefined || progress.skipped !== undefined) && ( +
+ Results: + + {progress.added !== undefined && `${progress.added.toLocaleString()} added`} + {progress.added !== undefined && progress.updated !== undefined && ', '} + {progress.updated !== undefined && `${progress.updated.toLocaleString()} updated`} + {((progress.added !== undefined || progress.updated !== undefined) && progress.skipped !== undefined) && ', '} + {progress.skipped !== undefined && `${progress.skipped.toLocaleString()} skipped`} + +
+ )} + {progress.duration && ( +
+ Duration: + {progress.duration} +
)}
)} + {progress.message && ( +
{progress.message}
+ )}
); };