From 6e1a8cf17d78c54391965cbf3f70319a3867b3df Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 12 Jan 2025 01:08:32 -0500 Subject: [PATCH] Fix up frontend for update/import/reset scripts --- inventory-server/scripts/import-csv.js | 28 +- inventory/src/pages/Settings.tsx | 384 +++++++++++++++++-------- 2 files changed, 294 insertions(+), 118 deletions(-) diff --git a/inventory-server/scripts/import-csv.js b/inventory-server/scripts/import-csv.js index 9ab0e8a..10d8117 100644 --- a/inventory-server/scripts/import-csv.js +++ b/inventory-server/scripts/import-csv.js @@ -1061,12 +1061,30 @@ async function main() { // Check if tables exist, if not create them outputProgress({ operation: 'Checking database schema', - message: 'Creating tables if needed...' + message: 'Verifying tables exist...' }); - - const schemaSQL = fs.readFileSync(path.join(__dirname, '../db/schema.sql'), 'utf8'); - await pool.query(schemaSQL); - + + const connection = await pool.getConnection(); + try { + // Check if products table exists as a proxy for schema being initialized + const [tables] = await connection.query( + 'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = ? AND table_name = ?', + [dbConfig.database, 'products'] + ); + + if (tables[0].count === 0) { + outputProgress({ + operation: 'Creating database schema', + message: 'Tables not found, creating schema...' + }); + + const schemaSQL = fs.readFileSync(path.join(__dirname, '../db/schema.sql'), 'utf8'); + await connection.query(schemaSQL); + } + } finally { + connection.release(); + } + // Step 1: Import all data first try { // Import products first since they're referenced by other tables diff --git a/inventory/src/pages/Settings.tsx b/inventory/src/pages/Settings.tsx index e960968..093148d 100644 --- a/inventory/src/pages/Settings.tsx +++ b/inventory/src/pages/Settings.tsx @@ -63,104 +63,264 @@ export function Settings() { const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false); const [metricsProgress, setMetricsProgress] = useState(null); + // Update the message handlers to use toast + const handleComplete = (operation: string) => { + toast.success(`${operation} completed successfully`); + }; + + const handleError = (operation: string, error: string) => { + // Skip error toast if we're cancelling or if it's a cancellation error + if (error.includes('cancelled') || + error.includes('Process exited with code 143') || + error.includes('Operation cancelled') || + error.includes('500 Internal Server Error') || + // Skip "Failed to start" errors if we have active progress + (error.includes('Failed to start CSV import') && (importProgress || purchaseOrdersProgress)) || + // Skip connection errors if we have active progress + (error.includes('Failed to fetch') && (importProgress || purchaseOrdersProgress))) { + return; + } + toast.error(`${operation} failed: ${error}`); + }; + // Helper to connect to event source const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => { + console.log(`Setting up EventSource for ${type}...`); + + // Clean up existing connection first if (eventSource) { + console.log('Closing existing event source'); eventSource.close(); + setEventSource(null); } - const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, { - withCredentials: true - }); - setEventSource(source); + try { + console.log(`Creating new EventSource for ${config.apiUrl}/csv/${type}/progress`); + const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, { + withCredentials: true + }); + + // Set up handlers before setting state + source.onopen = () => { + console.log('EventSource connected successfully'); + // Set event source state only after successful connection + setEventSource(source); + }; - 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; - - // For non-import operations, use the existing logic - const setProgress = type === 'update' ? setUpdateProgress : - type === 'reset' ? setResetProgress : - type === 'reset-metrics' ? setResetMetricsProgress : - type === 'calculate-metrics' ? setMetricsProgress : - setImportProgress; - - setProgress(prev => ({ - ...prev, - status: progressData.status || 'running', - operation: progressData.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 - })); - - // Set 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); - else if (type === 'reset-metrics') setIsResettingMetrics(true); - else if (type === 'calculate-metrics') setIsCalculatingMetrics(true); - } - - if (progressData.status === 'complete') { - source.close(); - setEventSource(null); - - // Reset the appropriate state based on type - if (type === 'update') { + source.onerror = (event) => { + console.error('EventSource failed:', event); + source.close(); + + // Reset states based on type + switch (type) { + case 'update': setIsUpdating(false); setUpdateProgress(null); - } else if (type === 'import') { + break; + case 'import': setIsImporting(false); setImportProgress(null); - setPurchaseOrdersProgress(null); - } else if (type === 'reset') { + break; + case 'reset': setIsResetting(false); setResetProgress(null); - } else if (type === 'reset-metrics') { + break; + case 'reset-metrics': setIsResettingMetrics(false); setResetMetricsProgress(null); - } else if (type === 'calculate-metrics') { + break; + case 'calculate-metrics': setIsCalculatingMetrics(false); setMetricsProgress(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); - } else if (type === 'reset-metrics') { - setIsResettingMetrics(false); - } else if (type === 'calculate-metrics') { - setIsCalculatingMetrics(false); - } - - handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error'); + break; } - } catch (error) { - console.error('Error parsing event data:', error); + + setEventSource(null); + handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Lost connection to server'); + }; + + source.onmessage = (event) => { + try { + console.log(`Received message for ${type}:`, event.data); + + // First parse the outer message + const data = JSON.parse(event.data); + + // If we have a progress field that's a string containing multiple JSON objects + if (data.progress && typeof data.progress === 'string') { + // Split the progress string into separate JSON objects + const progressMessages = data.progress + .split('\n') + .filter(Boolean) + .map((message: string) => { + try { + return JSON.parse(message); + } catch (err) { + console.warn('Failed to parse progress message:', message, err); + return null; + } + }) + .filter((msg: unknown): msg is ImportProgress => msg !== null); + + // Process each progress message + progressMessages.forEach((progressData: ImportProgress) => { + handleProgressUpdate(type, progressData, source); + }); + } else { + // Handle single message case + const progressData = data.progress || data; + handleProgressUpdate(type, progressData, source); + } + } catch (error) { + console.error('Error parsing event data:', error, event.data); + handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Failed to parse server response'); + } + }; + + } catch (error) { + console.error('Failed to set up EventSource:', error); + + // Reset operation state + switch (type) { + case 'update': + setIsUpdating(false); + setUpdateProgress(null); + break; + case 'import': + setIsImporting(false); + setImportProgress(null); + break; + case 'reset': + setIsResetting(false); + setResetProgress(null); + break; + case 'reset-metrics': + setIsResettingMetrics(false); + setResetMetricsProgress(null); + break; + case 'calculate-metrics': + setIsCalculatingMetrics(false); + setMetricsProgress(null); + break; } + + handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Failed to connect to server'); + } + }, [eventSource, handleComplete, handleError]); + + // Helper function to process a single progress update + const handleProgressUpdate = ( + type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics', + progressData: any, + source: EventSource + ) => { + console.log('Processing progress data:', progressData); + + // If message is an object, stringify it before setting state + const processedData = { + ...progressData, + message: progressData.message && typeof progressData.message === 'object' + ? JSON.stringify(progressData.message, null, 2) + : progressData.message, + status: progressData.status || 'running' }; - }, []); + + // Update progress state based on type + switch (type) { + case 'update': + setUpdateProgress(prev => ({ + ...prev, + ...processedData + })); + break; + case 'reset': + setResetProgress(prev => ({ + ...prev, + ...processedData + })); + break; + case 'reset-metrics': + setResetMetricsProgress(prev => ({ + ...prev, + ...processedData + })); + break; + case 'calculate-metrics': + setMetricsProgress(prev => ({ + ...prev, + ...processedData + })); + break; + default: + setImportProgress(prev => ({ + ...prev, + ...processedData + })); + } + + // Handle completion + if (progressData.status === 'complete') { + console.log(`Operation ${type} completed`); + source.close(); + setEventSource(null); + + // Reset operation state + switch (type) { + case 'update': + setIsUpdating(false); + setUpdateProgress(null); + break; + case 'import': + setIsImporting(false); + setImportProgress(null); + setPurchaseOrdersProgress(null); + break; + case 'reset': + setIsResetting(false); + setResetProgress(null); + break; + case 'reset-metrics': + setIsResettingMetrics(false); + setResetMetricsProgress(null); + break; + case 'calculate-metrics': + setIsCalculatingMetrics(false); + setMetricsProgress(null); + break; + } + + if (!progressData.operation?.includes('cancelled')) { + handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`); + } + } + // Handle errors + else if (progressData.status === 'error') { + console.error(`Operation ${type} failed:`, progressData.error); + source.close(); + setEventSource(null); + + // Reset operation state + switch (type) { + case 'update': + setIsUpdating(false); + break; + case 'import': + setIsImporting(false); + break; + case 'reset': + setIsResetting(false); + break; + case 'reset-metrics': + setIsResettingMetrics(false); + break; + case 'calculate-metrics': + setIsCalculatingMetrics(false); + break; + } + + handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error'); + } + }; // Check for active operations on mount useEffect(() => { @@ -362,8 +522,13 @@ export function Settings() { }; const handleResetDB = async () => { + // Set initial state setIsResetting(true); - setResetProgress({ status: 'running', operation: 'Starting database reset' }); + setResetProgress({ + status: 'running', + operation: 'Starting database reset', + percentage: '0' + }); try { // Connect to SSE for progress updates @@ -377,13 +542,11 @@ export function Settings() { if (!response.ok) { 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'); + console.error('Reset request failed:', response.status, response.statusText, data); + throw new Error(data.error || `Failed to reset database: ${response.status} ${response.statusText}`); } } catch (error) { + console.error('Reset error:', error); if (eventSource) { eventSource.close(); setEventSource(null); @@ -452,32 +615,19 @@ export function Settings() { } }; - // Update the message handlers to use toast - const handleComplete = (operation: string) => { - toast.success(`${operation} completed successfully`); - }; - - const handleError = (operation: string, error: string) => { - // Skip error toast if we're cancelling or if it's a cancellation error - if (error.includes('cancelled') || - error.includes('Process exited with code 143') || - error.includes('Operation cancelled') || - error.includes('500 Internal Server Error') || - // Skip "Failed to start" errors if we have active progress - (error.includes('Failed to start CSV import') && (importProgress || purchaseOrdersProgress)) || - // Skip connection errors if we have active progress - (error.includes('Failed to fetch') && (importProgress || purchaseOrdersProgress))) { - return; - } - toast.error(`${operation} failed: ${error}`); - }; - const renderProgress = (progress: ImportProgress | null) => { if (!progress) return null; let percentage = progress.percentage ? parseFloat(progress.percentage) : (progress.current && progress.total) ? (progress.current / progress.total) * 100 : null; + // Handle object messages by converting them to strings + const message = progress.message ? + (typeof progress.message === 'object' ? + JSON.stringify(progress.message, null, 2) : + progress.message + ) : null; + return (
@@ -516,17 +666,18 @@ export function Settings() {
)} - {progress.duration && ( -
- Duration: - {progress.duration} + {message && ( +
+ {message}
)}
)} - {progress.message && ( -
{progress.message}
+ {!percentage && message && ( +
+ {message} +
)}
); @@ -761,14 +912,21 @@ export function Settings() { className="flex-1 min-w-[140px]" disabled={isResetting || isImporting || isUpdating || isResettingMetrics} > - Reset Database + {isResetting ? ( + <> + + Resetting Database... + + ) : ( + <>Reset Database + )} - Are you absolutely sure? + Reset Database - This action cannot be undone. This will permanently delete all data from the database. + This will delete all data in the database and recreate the tables. This action cannot be undone.