Fix up frontend for update/import/reset scripts

This commit is contained in:
2025-01-12 01:08:32 -05:00
parent 03ad15c731
commit 6e1a8cf17d
2 changed files with 294 additions and 118 deletions

View File

@@ -1061,11 +1061,29 @@ async function main() {
// Check if tables exist, if not create them // Check if tables exist, if not create them
outputProgress({ outputProgress({
operation: 'Checking database schema', operation: 'Checking database schema',
message: 'Creating tables if needed...' message: 'Verifying tables exist...'
});
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'); const schemaSQL = fs.readFileSync(path.join(__dirname, '../db/schema.sql'), 'utf8');
await pool.query(schemaSQL); await connection.query(schemaSQL);
}
} finally {
connection.release();
}
// Step 1: Import all data first // Step 1: Import all data first
try { try {

View File

@@ -63,104 +63,264 @@ export function Settings() {
const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false); const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false);
const [metricsProgress, setMetricsProgress] = useState<ImportProgress | null>(null); const [metricsProgress, setMetricsProgress] = useState<ImportProgress | null>(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 // Helper to connect to event source
const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => { 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) { if (eventSource) {
console.log('Closing existing event source');
eventSource.close(); eventSource.close();
setEventSource(null);
} }
try {
console.log(`Creating new EventSource for ${config.apiUrl}/csv/${type}/progress`);
const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, { const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, {
withCredentials: true 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); setEventSource(source);
};
source.onerror = (event) => {
console.error('EventSource failed:', event);
source.close();
// Reset states based on type
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;
}
setEventSource(null);
handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Lost connection to server');
};
source.onmessage = (event) => { source.onmessage = (event) => {
try { try {
console.log(`Received message for ${type}:`, event.data);
// First parse the outer message
const data = JSON.parse(event.data); 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 // If we have a progress field that's a string containing multiple JSON objects
const setProgress = type === 'update' ? setUpdateProgress : if (data.progress && typeof data.progress === 'string') {
type === 'reset' ? setResetProgress : // Split the progress string into separate JSON objects
type === 'reset-metrics' ? setResetMetricsProgress : const progressMessages = data.progress
type === 'calculate-metrics' ? setMetricsProgress : .split('\n')
setImportProgress; .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);
setProgress(prev => ({ // Process each progress message
...prev, progressMessages.forEach((progressData: ImportProgress) => {
status: progressData.status || 'running', handleProgressUpdate(type, progressData, source);
operation: progressData.operation, });
current: progressData.current !== undefined ? Number(progressData.current) : prev?.current, } else {
total: progressData.total !== undefined ? Number(progressData.total) : prev?.total, // Handle single message case
rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate, const progressData = data.progress || data;
percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage, handleProgressUpdate(type, progressData, source);
elapsed: progressData.elapsed || prev?.elapsed, }
remaining: progressData.remaining || prev?.remaining, } catch (error) {
error: progressData.error || prev?.error, console.error('Error parsing event data:', error, event.data);
message: progressData.message || prev?.message handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Failed to parse server response');
})); }
};
// Set operation state if we're getting progress } catch (error) {
if (progressData.status === 'running') { console.error('Failed to set up EventSource:', error);
if (type === 'update') setIsUpdating(true);
else if (type === 'import') setIsImporting(true); // Reset operation state
else if (type === 'reset') setIsResetting(true); switch (type) {
else if (type === 'reset-metrics') setIsResettingMetrics(true); case 'update':
else if (type === 'calculate-metrics') setIsCalculatingMetrics(true); 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') { if (progressData.status === 'complete') {
console.log(`Operation ${type} completed`);
source.close(); source.close();
setEventSource(null); setEventSource(null);
// Reset the appropriate state based on type // Reset operation state
if (type === 'update') { switch (type) {
case 'update':
setIsUpdating(false); setIsUpdating(false);
setUpdateProgress(null); setUpdateProgress(null);
} else if (type === 'import') { break;
case 'import':
setIsImporting(false); setIsImporting(false);
setImportProgress(null); setImportProgress(null);
setPurchaseOrdersProgress(null); setPurchaseOrdersProgress(null);
} else if (type === 'reset') { break;
case 'reset':
setIsResetting(false); setIsResetting(false);
setResetProgress(null); setResetProgress(null);
} else if (type === 'reset-metrics') { break;
case 'reset-metrics':
setIsResettingMetrics(false); setIsResettingMetrics(false);
setResetMetricsProgress(null); setResetMetricsProgress(null);
} else if (type === 'calculate-metrics') { break;
case 'calculate-metrics':
setIsCalculatingMetrics(false); setIsCalculatingMetrics(false);
setMetricsProgress(null); setMetricsProgress(null);
break;
} }
if (!progressData.operation?.includes('cancelled')) { if (!progressData.operation?.includes('cancelled')) {
handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`); handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`);
} }
} else if (progressData.status === 'error') { }
// Handle errors
else if (progressData.status === 'error') {
console.error(`Operation ${type} failed:`, progressData.error);
source.close(); source.close();
setEventSource(null); setEventSource(null);
// Reset the appropriate state based on type // Reset operation state
if (type === 'update') { switch (type) {
case 'update':
setIsUpdating(false); setIsUpdating(false);
} else if (type === 'import') { break;
case 'import':
setIsImporting(false); setIsImporting(false);
} else if (type === 'reset') { break;
case 'reset':
setIsResetting(false); setIsResetting(false);
} else if (type === 'reset-metrics') { break;
case 'reset-metrics':
setIsResettingMetrics(false); setIsResettingMetrics(false);
} else if (type === 'calculate-metrics') { break;
case 'calculate-metrics':
setIsCalculatingMetrics(false); setIsCalculatingMetrics(false);
break;
} }
handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error'); handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error');
} }
} catch (error) {
console.error('Error parsing event data:', error);
}
}; };
}, []);
// Check for active operations on mount // Check for active operations on mount
useEffect(() => { useEffect(() => {
@@ -362,8 +522,13 @@ export function Settings() {
}; };
const handleResetDB = async () => { const handleResetDB = async () => {
// Set initial state
setIsResetting(true); setIsResetting(true);
setResetProgress({ status: 'running', operation: 'Starting database reset' }); setResetProgress({
status: 'running',
operation: 'Starting database reset',
percentage: '0'
});
try { try {
// Connect to SSE for progress updates // Connect to SSE for progress updates
@@ -377,13 +542,11 @@ export function Settings() {
if (!response.ok) { if (!response.ok) {
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (data.error === 'Import already in progress') { console.error('Reset request failed:', response.status, response.statusText, data);
// If there's already an import, just let the SSE connection handle showing progress throw new Error(data.error || `Failed to reset database: ${response.status} ${response.statusText}`);
return;
}
throw new Error(data.error || 'Failed to reset database');
} }
} catch (error) { } catch (error) {
console.error('Reset error:', error);
if (eventSource) { if (eventSource) {
eventSource.close(); eventSource.close();
setEventSource(null); 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) => { const renderProgress = (progress: ImportProgress | null) => {
if (!progress) return null; if (!progress) return null;
let percentage = progress.percentage ? parseFloat(progress.percentage) : let percentage = progress.percentage ? parseFloat(progress.percentage) :
(progress.current && progress.total) ? (progress.current / progress.total) * 100 : null; (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 ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground"> <div className="flex justify-between text-sm text-muted-foreground">
@@ -516,17 +666,18 @@ export function Settings() {
</span> </span>
</div> </div>
)} )}
{progress.duration && ( {message && (
<div className="flex justify-between"> <div className="whitespace-pre-wrap font-mono text-xs">
<span>Duration:</span> {message}
<span>{progress.duration}</span>
</div> </div>
)} )}
</div> </div>
</> </>
)} )}
{progress.message && ( {!percentage && message && (
<div className="text-xs text-muted-foreground">{progress.message}</div> <div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)} )}
</div> </div>
); );
@@ -761,14 +912,21 @@ export function Settings() {
className="flex-1 min-w-[140px]" className="flex-1 min-w-[140px]"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics} disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
> >
Reset Database {isResetting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Resetting Database...
</>
) : (
<>Reset Database</>
)}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogTitle>Reset Database</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
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.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>