Fix initial errors in calculate metrics and get progress working in frontend

This commit is contained in:
2025-01-12 14:12:18 -05:00
parent 3e855eeb2b
commit ac8563325a
3 changed files with 148 additions and 67 deletions

View File

@@ -43,15 +43,21 @@ function calculateRate(startTime, current) {
// Helper function to output progress // Helper function to output progress
function outputProgress(data) { function outputProgress(data) {
process.stdout.write(JSON.stringify(data) + '\n'); // Format as SSE event
const event = {
progress: data
};
process.stdout.write(JSON.stringify(event) + '\n');
} }
// Helper function to log errors // Helper function to log errors
function logError(error, context) { function logError(error, context) {
console.error(JSON.stringify({ console.error(JSON.stringify({
status: 'error', progress: {
error: error.message || error, status: 'error',
context error: error.message || error,
context
}
})); }));
} }
@@ -71,15 +77,19 @@ let isCancelled = false;
function cancelCalculation() { function cancelCalculation() {
isCancelled = true; isCancelled = true;
process.stdout.write(JSON.stringify({ // Format as SSE event
status: 'cancelled', const event = {
operation: 'Calculation cancelled', progress: {
current: 0, status: 'cancelled',
total: 0, operation: 'Calculation cancelled',
elapsed: null, current: 0,
remaining: null, total: 0,
rate: 0 elapsed: null,
}) + '\n'); remaining: null,
rate: 0
}
};
process.stdout.write(JSON.stringify(event) + '\n');
process.exit(0); process.exit(0);
} }
@@ -99,7 +109,7 @@ async function calculateMetrics() {
const [countResult] = await connection.query('SELECT COUNT(*) as total FROM products'); const [countResult] = await connection.query('SELECT COUNT(*) as total FROM products');
totalProducts = countResult[0].total; totalProducts = countResult[0].total;
// Initial progress // Initial progress with percentage
outputProgress({ outputProgress({
status: 'running', status: 'running',
operation: 'Processing products', operation: 'Processing products',
@@ -107,7 +117,8 @@ async function calculateMetrics() {
total: totalProducts, total: totalProducts,
elapsed: '0s', elapsed: '0s',
remaining: 'Calculating...', remaining: 'Calculating...',
rate: 0 rate: 0,
percentage: '0'
}); });
// Process in batches of 100 // Process in batches of 100
@@ -128,7 +139,8 @@ async function calculateMetrics() {
total: totalProducts, total: totalProducts,
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: estimateRemaining(startTime, processedCount, totalProducts), remaining: estimateRemaining(startTime, processedCount, totalProducts),
rate: calculateRate(startTime, processedCount) rate: calculateRate(startTime, processedCount),
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
}); });
// Process the batch // Process the batch
@@ -175,6 +187,14 @@ async function calculateMetrics() {
const weekly_sales_avg = metrics.total_quantity_sold ? metrics.total_quantity_sold / 4 : 0; const weekly_sales_avg = metrics.total_quantity_sold ? metrics.total_quantity_sold / 4 : 0;
const monthly_sales_avg = metrics.total_quantity_sold || 0; const monthly_sales_avg = metrics.total_quantity_sold || 0;
// Calculate margin percent with proper handling of edge cases
let margin_percent = 0;
if (metrics.total_revenue && metrics.total_revenue > 0) {
margin_percent = ((metrics.total_revenue - metrics.total_cost) / metrics.total_revenue) * 100;
// Handle -Infinity or Infinity cases
margin_percent = isFinite(margin_percent) ? margin_percent : 0;
}
// Update product metrics // Update product metrics
await connection.query(` await connection.query(`
INSERT INTO product_metrics ( INSERT INTO product_metrics (
@@ -236,7 +256,7 @@ async function calculateMetrics() {
weekly_sales_avg ? stock.stock_quantity / weekly_sales_avg : null, weekly_sales_avg ? stock.stock_quantity / weekly_sales_avg : null,
Math.ceil(daily_sales_avg * 14), // 14 days reorder point Math.ceil(daily_sales_avg * 14), // 14 days reorder point
Math.ceil(daily_sales_avg * 7), // 7 days safety stock Math.ceil(daily_sales_avg * 7), // 7 days safety stock
metrics.total_revenue ? ((metrics.total_revenue - metrics.total_cost) / metrics.total_revenue) * 100 : 0, margin_percent, // Use the properly handled margin percent
metrics.total_revenue || 0, metrics.total_revenue || 0,
purchases.avg_lead_time_days || 0, purchases.avg_lead_time_days || 0,
purchases.last_purchase_date, purchases.last_purchase_date,
@@ -257,7 +277,8 @@ async function calculateMetrics() {
total: totalProducts, total: totalProducts,
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: estimateRemaining(startTime, totalProducts, totalProducts), remaining: estimateRemaining(startTime, totalProducts, totalProducts),
rate: calculateRate(startTime, totalProducts) rate: calculateRate(startTime, totalProducts),
percentage: '100'
}); });
// Calculate ABC classification // Calculate ABC classification
@@ -288,7 +309,8 @@ async function calculateMetrics() {
total: totalProducts, total: totalProducts,
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: estimateRemaining(startTime, totalProducts, totalProducts), remaining: estimateRemaining(startTime, totalProducts, totalProducts),
rate: calculateRate(startTime, totalProducts) rate: calculateRate(startTime, totalProducts),
percentage: '100'
}); });
// Calculate time-based aggregates // Calculate time-based aggregates
@@ -386,7 +408,8 @@ async function calculateMetrics() {
total: totalProducts, total: totalProducts,
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: estimateRemaining(startTime, totalProducts, totalProducts), remaining: estimateRemaining(startTime, totalProducts, totalProducts),
rate: calculateRate(startTime, totalProducts) rate: calculateRate(startTime, totalProducts),
percentage: '100'
}); });
// Calculate vendor metrics // Calculate vendor metrics
@@ -428,7 +451,8 @@ async function calculateMetrics() {
total: totalProducts, total: totalProducts,
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: '0s', remaining: '0s',
rate: calculateRate(startTime, totalProducts) rate: calculateRate(startTime, totalProducts),
percentage: '100'
}); });
} catch (error) { } catch (error) {
@@ -440,7 +464,8 @@ async function calculateMetrics() {
total: totalProducts || 0, // Use 0 if not yet defined total: totalProducts || 0, // Use 0 if not yet defined
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: null, remaining: null,
rate: calculateRate(startTime, processedCount) rate: calculateRate(startTime, processedCount),
percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1)
}); });
} else { } else {
outputProgress({ outputProgress({
@@ -450,7 +475,8 @@ async function calculateMetrics() {
total: totalProducts || 0, // Use 0 if not yet defined total: totalProducts || 0, // Use 0 if not yet defined
elapsed: formatElapsedTime(startTime), elapsed: formatElapsedTime(startTime),
remaining: null, remaining: null,
rate: calculateRate(startTime, processedCount) rate: calculateRate(startTime, processedCount),
percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1)
}); });
} }
throw error; throw error;

View File

@@ -357,8 +357,8 @@ router.post('/cancel', (req, res) => {
} }
try { try {
// Kill the process // Kill the process with SIGTERM signal
activeImport.kill(); activeImport.kill('SIGTERM');
// Clean up // Clean up
activeImport = null; activeImport = null;
@@ -383,6 +383,9 @@ router.post('/cancel', (req, res) => {
case 'reset': case 'reset':
sendProgressToClients(resetClients, cancelMessage); sendProgressToClients(resetClients, cancelMessage);
break; break;
case 'calculate-metrics':
sendProgressToClients(calculateMetricsClients, cancelMessage);
break;
} }
res.json({ success: true }); res.json({ success: true });
@@ -532,51 +535,98 @@ router.post('/reset-metrics', async (req, res) => {
// Add calculate-metrics endpoint // Add calculate-metrics endpoint
router.post('/calculate-metrics', async (req, res) => { router.post('/calculate-metrics', async (req, res) => {
if (activeImport) { if (activeImport) {
res.status(400).json({ error: 'Operation already in progress' }); return res.status(409).json({ error: 'Import already in progress' });
return;
} }
try { try {
// Set active import to prevent concurrent operations const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'calculate-metrics.js');
activeImport = {
type: 'calculate-metrics', if (!require('fs').existsSync(scriptPath)) {
status: 'running', return res.status(500).json({ error: 'Calculate metrics script not found' });
operation: 'Starting metrics calculation' }
};
activeImport = spawn('node', [scriptPath]);
// Send initial response let wasCancelled = false;
res.status(200).json({ message: 'Metrics calculation started' });
activeImport.stdout.on('data', (data) => {
// Send initial progress through SSE const output = data.toString().trim();
sendProgressToClients(calculateMetricsClients, {
status: 'running', try {
operation: 'Starting metrics calculation', // Try to parse as JSON
percentage: '0' const jsonData = JSON.parse(output);
sendProgressToClients(calculateMetricsClients, {
status: 'running',
...jsonData
});
} catch (e) {
// If not JSON, send as plain progress
sendProgressToClients(calculateMetricsClients, {
status: 'running',
progress: output
});
}
}); });
// Run the metrics calculation script activeImport.stderr.on('data', (data) => {
const calculateMetrics = require('../../scripts/calculate-metrics'); if (wasCancelled) return; // Don't send errors if cancelled
await calculateMetrics();
const error = data.toString().trim();
// Send completion through SSE try {
sendProgressToClients(calculateMetricsClients, { // Try to parse as JSON
status: 'complete', const jsonData = JSON.parse(error);
operation: 'Metrics calculation completed', sendProgressToClients(calculateMetricsClients, {
percentage: '100' status: 'error',
...jsonData
});
} catch {
sendProgressToClients(calculateMetricsClients, {
status: 'error',
error
});
}
}); });
activeImport = null; await new Promise((resolve, reject) => {
activeImport.on('close', (code, signal) => {
wasCancelled = signal === 'SIGTERM' || code === 143;
activeImport = null;
importProgress = null;
if (code === 0 || wasCancelled) {
if (wasCancelled) {
sendProgressToClients(calculateMetricsClients, {
status: 'cancelled',
operation: 'Operation cancelled'
});
} else {
sendProgressToClients(calculateMetricsClients, {
status: 'complete',
operation: 'Metrics calculation complete'
});
}
resolve();
} else {
reject(new Error(`Metrics calculation process exited with code ${code}`));
}
});
});
res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Error during metrics calculation:', error); console.error('Error calculating metrics:', error);
// Send error through SSE
sendProgressToClients(calculateMetricsClients, {
status: 'error',
error: error.message || 'Failed to calculate metrics'
});
activeImport = null; activeImport = null;
res.status(500).json({ error: error.message || 'Failed to calculate metrics' }); importProgress = null;
// Only send error if it wasn't a cancellation
if (!error.message?.includes('code 143') && !error.message?.includes('SIGTERM')) {
sendProgressToClients(calculateMetricsClients, {
status: 'error',
error: error.message
});
res.status(500).json({ error: 'Failed to calculate metrics', details: error.message });
} else {
res.json({ success: true });
}
} }
}); });

View File

@@ -254,8 +254,8 @@ export function Settings() {
} }
// Handle completion // Handle completion
if (progressData.status === 'complete') { if (progressData.status === 'complete' || progressData.status === 'cancelled') {
console.log(`Operation ${type} completed`); console.log(`Operation ${type} completed or cancelled`);
// For import, only close connection when both operations are complete // For import, only close connection when both operations are complete
if (type === 'import') { if (type === 'import') {
@@ -433,11 +433,14 @@ export function Settings() {
}, [eventSource]); }, [eventSource]);
const handleCancel = async () => { const handleCancel = async () => {
// Determine which operation is running first // Determine which operation is running
const operation = isImporting ? 'Import' : isUpdating ? 'Update' : 'Reset'; const operation = isImporting ? 'import' :
isUpdating ? 'update' :
isResetting ? 'reset' :
isCalculatingMetrics ? 'calculate-metrics' : 'reset';
// Show cancellation toast immediately // Show cancellation toast immediately
toast.warning(`${operation} cancelled`); toast.warning(`${operation.charAt(0).toUpperCase() + operation.slice(1)} cancelled`);
// Clean up everything immediately // Clean up everything immediately
if (eventSource) { if (eventSource) {
@@ -447,13 +450,15 @@ export function Settings() {
setIsUpdating(false); setIsUpdating(false);
setIsImporting(false); setIsImporting(false);
setIsResetting(false); setIsResetting(false);
setIsCalculatingMetrics(false);
setUpdateProgress(null); setUpdateProgress(null);
setImportProgress(null); setImportProgress(null);
setResetProgress(null); setResetProgress(null);
setMetricsProgress(null);
// Fire and forget the cancel request with the operation type // Fire and forget the cancel request with the operation type
try { try {
await fetch(`${config.apiUrl}/csv/cancel?operation=${operation.toLowerCase()}`, { await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, {
method: 'POST', method: 'POST',
credentials: 'include' credentials: 'include'
}); });