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,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

View File

@@ -63,104 +63,264 @@ export function Settings() {
const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false);
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
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 (
<div className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground">
@@ -516,17 +666,18 @@ export function Settings() {
</span>
</div>
)}
{progress.duration && (
<div className="flex justify-between">
<span>Duration:</span>
<span>{progress.duration}</span>
{message && (
<div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)}
</div>
</>
)}
{progress.message && (
<div className="text-xs text-muted-foreground">{progress.message}</div>
{!percentage && message && (
<div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)}
</div>
);
@@ -761,14 +912,21 @@ export function Settings() {
className="flex-1 min-w-[140px]"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
Reset Database
{isResetting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Resetting Database...
</>
) : (
<>Reset Database</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogTitle>Reset Database</AlertDialogTitle>
<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>
</AlertDialogHeader>
<AlertDialogFooter>