Fix up frontend for update/import/reset scripts
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user