From ac8563325aed4dd43633afc0769d8eb0c531dfee Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 12 Jan 2025 14:12:18 -0500 Subject: [PATCH] Fix initial errors in calculate metrics and get progress working in frontend --- inventory-server/scripts/calculate-metrics.js | 72 ++++++---- inventory-server/src/routes/csv.js | 126 ++++++++++++------ inventory/src/pages/Settings.tsx | 17 ++- 3 files changed, 148 insertions(+), 67 deletions(-) diff --git a/inventory-server/scripts/calculate-metrics.js b/inventory-server/scripts/calculate-metrics.js index ffcb74d..24f00cf 100644 --- a/inventory-server/scripts/calculate-metrics.js +++ b/inventory-server/scripts/calculate-metrics.js @@ -43,15 +43,21 @@ function calculateRate(startTime, current) { // Helper function to output progress 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 function logError(error, context) { console.error(JSON.stringify({ - status: 'error', - error: error.message || error, - context + progress: { + status: 'error', + error: error.message || error, + context + } })); } @@ -71,15 +77,19 @@ let isCancelled = false; function cancelCalculation() { isCancelled = true; - process.stdout.write(JSON.stringify({ - status: 'cancelled', - operation: 'Calculation cancelled', - current: 0, - total: 0, - elapsed: null, - remaining: null, - rate: 0 - }) + '\n'); + // Format as SSE event + const event = { + progress: { + status: 'cancelled', + operation: 'Calculation cancelled', + current: 0, + total: 0, + elapsed: null, + remaining: null, + rate: 0 + } + }; + process.stdout.write(JSON.stringify(event) + '\n'); process.exit(0); } @@ -99,7 +109,7 @@ async function calculateMetrics() { const [countResult] = await connection.query('SELECT COUNT(*) as total FROM products'); totalProducts = countResult[0].total; - // Initial progress + // Initial progress with percentage outputProgress({ status: 'running', operation: 'Processing products', @@ -107,7 +117,8 @@ async function calculateMetrics() { total: totalProducts, elapsed: '0s', remaining: 'Calculating...', - rate: 0 + rate: 0, + percentage: '0' }); // Process in batches of 100 @@ -128,7 +139,8 @@ async function calculateMetrics() { total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, processedCount, totalProducts), - rate: calculateRate(startTime, processedCount) + rate: calculateRate(startTime, processedCount), + percentage: ((processedCount / totalProducts) * 100).toFixed(1) }); // 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 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 await connection.query(` INSERT INTO product_metrics ( @@ -236,7 +256,7 @@ async function calculateMetrics() { 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 * 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, purchases.avg_lead_time_days || 0, purchases.last_purchase_date, @@ -257,7 +277,8 @@ async function calculateMetrics() { total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, totalProducts, totalProducts), - rate: calculateRate(startTime, totalProducts) + rate: calculateRate(startTime, totalProducts), + percentage: '100' }); // Calculate ABC classification @@ -288,7 +309,8 @@ async function calculateMetrics() { total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, totalProducts, totalProducts), - rate: calculateRate(startTime, totalProducts) + rate: calculateRate(startTime, totalProducts), + percentage: '100' }); // Calculate time-based aggregates @@ -386,7 +408,8 @@ async function calculateMetrics() { total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, totalProducts, totalProducts), - rate: calculateRate(startTime, totalProducts) + rate: calculateRate(startTime, totalProducts), + percentage: '100' }); // Calculate vendor metrics @@ -428,7 +451,8 @@ async function calculateMetrics() { total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: '0s', - rate: calculateRate(startTime, totalProducts) + rate: calculateRate(startTime, totalProducts), + percentage: '100' }); } catch (error) { @@ -440,7 +464,8 @@ async function calculateMetrics() { total: totalProducts || 0, // Use 0 if not yet defined elapsed: formatElapsedTime(startTime), remaining: null, - rate: calculateRate(startTime, processedCount) + rate: calculateRate(startTime, processedCount), + percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1) }); } else { outputProgress({ @@ -450,7 +475,8 @@ async function calculateMetrics() { total: totalProducts || 0, // Use 0 if not yet defined elapsed: formatElapsedTime(startTime), remaining: null, - rate: calculateRate(startTime, processedCount) + rate: calculateRate(startTime, processedCount), + percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1) }); } throw error; diff --git a/inventory-server/src/routes/csv.js b/inventory-server/src/routes/csv.js index 4db81af..c052644 100644 --- a/inventory-server/src/routes/csv.js +++ b/inventory-server/src/routes/csv.js @@ -357,8 +357,8 @@ router.post('/cancel', (req, res) => { } try { - // Kill the process - activeImport.kill(); + // Kill the process with SIGTERM signal + activeImport.kill('SIGTERM'); // Clean up activeImport = null; @@ -383,6 +383,9 @@ router.post('/cancel', (req, res) => { case 'reset': sendProgressToClients(resetClients, cancelMessage); break; + case 'calculate-metrics': + sendProgressToClients(calculateMetricsClients, cancelMessage); + break; } res.json({ success: true }); @@ -532,51 +535,98 @@ router.post('/reset-metrics', async (req, res) => { // Add calculate-metrics endpoint router.post('/calculate-metrics', async (req, res) => { if (activeImport) { - res.status(400).json({ error: 'Operation already in progress' }); - return; + return res.status(409).json({ error: 'Import already in progress' }); } try { - // Set active import to prevent concurrent operations - activeImport = { - type: 'calculate-metrics', - status: 'running', - operation: 'Starting metrics calculation' - }; - - // Send initial response - res.status(200).json({ message: 'Metrics calculation started' }); - - // Send initial progress through SSE - sendProgressToClients(calculateMetricsClients, { - status: 'running', - operation: 'Starting metrics calculation', - percentage: '0' + const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'calculate-metrics.js'); + + if (!require('fs').existsSync(scriptPath)) { + return res.status(500).json({ error: 'Calculate metrics script not found' }); + } + + activeImport = spawn('node', [scriptPath]); + let wasCancelled = false; + + activeImport.stdout.on('data', (data) => { + const output = data.toString().trim(); + + try { + // Try to parse as JSON + 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 - const calculateMetrics = require('../../scripts/calculate-metrics'); - await calculateMetrics(); - - // Send completion through SSE - sendProgressToClients(calculateMetricsClients, { - status: 'complete', - operation: 'Metrics calculation completed', - percentage: '100' + activeImport.stderr.on('data', (data) => { + if (wasCancelled) return; // Don't send errors if cancelled + + const error = data.toString().trim(); + try { + // Try to parse as JSON + const jsonData = JSON.parse(error); + sendProgressToClients(calculateMetricsClients, { + 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) { - console.error('Error during metrics calculation:', error); - - // Send error through SSE - sendProgressToClients(calculateMetricsClients, { - status: 'error', - error: error.message || 'Failed to calculate metrics' - }); - + console.error('Error calculating metrics:', error); 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 }); + } } }); diff --git a/inventory/src/pages/Settings.tsx b/inventory/src/pages/Settings.tsx index 99b56b2..eb29634 100644 --- a/inventory/src/pages/Settings.tsx +++ b/inventory/src/pages/Settings.tsx @@ -254,8 +254,8 @@ export function Settings() { } // Handle completion - if (progressData.status === 'complete') { - console.log(`Operation ${type} completed`); + if (progressData.status === 'complete' || progressData.status === 'cancelled') { + console.log(`Operation ${type} completed or cancelled`); // For import, only close connection when both operations are complete if (type === 'import') { @@ -433,11 +433,14 @@ export function Settings() { }, [eventSource]); const handleCancel = async () => { - // Determine which operation is running first - const operation = isImporting ? 'Import' : isUpdating ? 'Update' : 'Reset'; + // Determine which operation is running + const operation = isImporting ? 'import' : + isUpdating ? 'update' : + isResetting ? 'reset' : + isCalculatingMetrics ? 'calculate-metrics' : 'reset'; // Show cancellation toast immediately - toast.warning(`${operation} cancelled`); + toast.warning(`${operation.charAt(0).toUpperCase() + operation.slice(1)} cancelled`); // Clean up everything immediately if (eventSource) { @@ -447,13 +450,15 @@ export function Settings() { setIsUpdating(false); setIsImporting(false); setIsResetting(false); + setIsCalculatingMetrics(false); setUpdateProgress(null); setImportProgress(null); setResetProgress(null); + setMetricsProgress(null); // Fire and forget the cancel request with the operation type try { - await fetch(`${config.apiUrl}/csv/cancel?operation=${operation.toLowerCase()}`, { + await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, { method: 'POST', credentials: 'include' });