diff --git a/CHANGELOG.md b/CHANGELOG.md index c78d30451..8b9338bb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ All notable changes to Bambuddy will be documented in this file. - Added locale parity test to ensure English and German stay in sync ### Fixed +- **Monthly Comparison Calculation Off** (Issue #229): + - Fixed filament statistics not accounting for quantity multiplier + - Monthly comparison chart now correctly multiplies `filament_used_grams` by `quantity` + - Daily and weekly charts also now account for quantity + - Filament type breakdown includes quantity in calculations + - Backend stats endpoint (`/archives/stats`) and Prometheus metrics also fixed + - Prints count now shows total items (sum of quantities) instead of archive count - **Authentication Required for Downloads** (Issue #231): - Fixed support bundle download returning 401 Unauthorized when auth is enabled - Fixed archive export (CSV/XLSX) failing with authentication enabled diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 4d34a0508..e2e73c844 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -451,7 +451,10 @@ async def get_archive_stats(db: AsyncSession = Depends(get_db)): total_seconds += print_time_seconds total_time = total_seconds / 3600 # Convert to hours - filament_result = await db.execute(select(func.sum(PrintArchive.filament_used_grams))) + # Multiply filament by quantity to account for multiple items printed + filament_result = await db.execute( + select(func.sum(PrintArchive.filament_used_grams * func.coalesce(PrintArchive.quantity, 1))) + ) total_filament = filament_result.scalar() or 0 cost_result = await db.execute(select(func.sum(PrintArchive.cost))) diff --git a/backend/app/api/routes/metrics.py b/backend/app/api/routes/metrics.py index 57002d93a..757f531dc 100644 --- a/backend/app/api/routes/metrics.py +++ b/backend/app/api/routes/metrics.py @@ -362,11 +362,13 @@ async def get_metrics( ) lines.append(f"bambuddy_printer_prints_total{labels} {count}") - # Total filament used + # Total filament used (multiply by quantity to account for multiple items printed) lines.append("") lines.append("# HELP bambuddy_filament_used_grams Total filament used in grams") lines.append("# TYPE bambuddy_filament_used_grams counter") - result = await db.execute(select(func.coalesce(func.sum(PrintArchive.filament_used_grams), 0))) + result = await db.execute( + select(func.coalesce(func.sum(PrintArchive.filament_used_grams * func.coalesce(PrintArchive.quantity, 1)), 0)) + ) total_filament = result.scalar() or 0 lines.append(f"bambuddy_filament_used_grams {total_filament:.1f}") diff --git a/frontend/src/components/FilamentTrends.tsx b/frontend/src/components/FilamentTrends.tsx index e20d38597..594ef61c7 100644 --- a/frontend/src/components/FilamentTrends.tsx +++ b/frontend/src/components/FilamentTrends.tsx @@ -61,9 +61,10 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const existing = dataMap.get(key) || { date: key, filament: 0, cost: 0, prints: 0 }; - existing.filament += archive.filament_used_grams || 0; + const qty = archive.quantity || 1; + existing.filament += (archive.filament_used_grams || 0) * qty; existing.cost += archive.cost || 0; - existing.prints += 1; + existing.prints += qty; dataMap.set(key, existing); }); @@ -89,9 +90,10 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps const key = `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`; const existing = dataMap.get(key) || { week: key, filament: 0, cost: 0, prints: 0 }; - existing.filament += archive.filament_used_grams || 0; + const qty = archive.quantity || 1; + existing.filament += (archive.filament_used_grams || 0) * qty; existing.cost += archive.cost || 0; - existing.prints += 1; + existing.prints += qty; dataMap.set(key, existing); }); @@ -110,10 +112,11 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps filteredArchives.forEach(archive => { const type = archive.filament_type || 'Unknown'; + const qty = archive.quantity || 1; // Handle multiple types (e.g., "PLA, PETG") const types = type.split(', '); types.forEach(t => { - const grams = (archive.filament_used_grams || 0) / types.length; + const grams = ((archive.filament_used_grams || 0) * qty) / types.length; dataMap.set(t, (dataMap.get(t) || 0) + grams); }); }); @@ -140,9 +143,9 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps months.push({ month: monthStr, - filament: Math.round(monthArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0)), + filament: Math.round(monthArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0) * (a.quantity || 1), 0)), cost: monthArchives.reduce((sum, a) => sum + (a.cost || 0), 0), - prints: monthArchives.length, + prints: monthArchives.reduce((sum, a) => sum + (a.quantity || 1), 0), }); } @@ -150,8 +153,9 @@ export function FilamentTrends({ archives, currency = '$' }: FilamentTrendsProps }, [archives]); const chartData = timeRange === '7d' || timeRange === '30d' ? dailyData : weeklyData; - const totalFilament = filteredArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0), 0); + const totalFilament = filteredArchives.reduce((sum, a) => sum + (a.filament_used_grams || 0) * (a.quantity || 1), 0); const totalCost = filteredArchives.reduce((sum, a) => sum + (a.cost || 0), 0); + const totalPrints = filteredArchives.reduce((sum, a) => sum + (a.quantity || 1), 0); return (
Period Cost
{currency}{totalCost.toFixed(2)}
-{filteredArchives.length} prints
+{totalPrints} prints
Avg per Print
- {filteredArchives.length > 0 - ? (totalFilament / filteredArchives.length).toFixed(0) + {totalPrints > 0 + ? (totalFilament / totalPrints).toFixed(0) : 0}g
- {currency}{filteredArchives.length > 0 ? (totalCost / filteredArchives.length).toFixed(2) : '0.00'} avg + {currency}{totalPrints > 0 ? (totalCost / totalPrints).toFixed(2) : '0.00'} avg