From 3ffe0326359581fa093137fd3ccce4d9529b99ea Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 21 Dec 2025 09:56:34 +0100 Subject: [PATCH] - Fix total print hours to include all prints --- backend/app/api/routes/maintenance.py | 146 ++++++++++++-------------- 1 file changed, 65 insertions(+), 81 deletions(-) diff --git a/backend/app/api/routes/maintenance.py b/backend/app/api/routes/maintenance.py index aa88e4c44..d7eea1672 100644 --- a/backend/app/api/routes/maintenance.py +++ b/backend/app/api/routes/maintenance.py @@ -2,30 +2,28 @@ import logging from datetime import datetime -from typing import List from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy import select, func +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from backend.app.core.database import get_db -from backend.app.models.maintenance import MaintenanceType, PrinterMaintenance, MaintenanceHistory -from backend.app.models.printer import Printer from backend.app.models.archive import PrintArchive -from backend.app.services.notification_service import notification_service +from backend.app.models.maintenance import MaintenanceHistory, MaintenanceType, PrinterMaintenance +from backend.app.models.printer import Printer from backend.app.schemas.maintenance import ( - MaintenanceTypeCreate, - MaintenanceTypeUpdate, - MaintenanceTypeResponse, - PrinterMaintenanceCreate, - PrinterMaintenanceUpdate, - PrinterMaintenanceResponse, MaintenanceHistoryResponse, MaintenanceStatus, - PrinterMaintenanceOverview, + MaintenanceTypeCreate, + MaintenanceTypeResponse, + MaintenanceTypeUpdate, PerformMaintenanceRequest, + PrinterMaintenanceOverview, + PrinterMaintenanceResponse, + PrinterMaintenanceUpdate, ) +from backend.app.services.notification_service import notification_service logger = logging.getLogger(__name__) @@ -73,20 +71,20 @@ DEFAULT_MAINTENANCE_TYPES = [ async def get_printer_total_hours(db: AsyncSession, printer_id: int) -> float: - """Calculate total print hours for a printer from archives plus offset.""" - # Get archive hours + """Calculate total print hours for a printer from archives plus offset. + + Includes ALL prints (completed, failed, cancelled) since the printer + components are being used regardless of print outcome. + """ + # Get archive hours (all prints, not just completed) result = await db.execute( - select(func.sum(PrintArchive.print_time_seconds)) - .where(PrintArchive.printer_id == printer_id) - .where(PrintArchive.status == "completed") + select(func.sum(PrintArchive.print_time_seconds)).where(PrintArchive.printer_id == printer_id) ) total_seconds = result.scalar() or 0 archive_hours = total_seconds / 3600.0 # Get printer offset - result = await db.execute( - select(Printer.print_hours_offset).where(Printer.id == printer_id) - ) + result = await db.execute(select(Printer.print_hours_offset).where(Printer.id == printer_id)) offset = result.scalar() or 0.0 return archive_hours + offset @@ -94,9 +92,7 @@ async def get_printer_total_hours(db: AsyncSession, printer_id: int) -> float: async def ensure_default_types(db: AsyncSession) -> None: """Ensure default maintenance types exist.""" - result = await db.execute( - select(MaintenanceType).where(MaintenanceType.is_system == True) - ) + result = await db.execute(select(MaintenanceType).where(MaintenanceType.is_system.is_(True))) existing = result.scalars().all() existing_names = {t.name for t in existing} @@ -116,13 +112,12 @@ async def ensure_default_types(db: AsyncSession) -> None: # ============== Maintenance Types ============== -@router.get("/types", response_model=List[MaintenanceTypeResponse]) + +@router.get("/types", response_model=list[MaintenanceTypeResponse]) async def get_maintenance_types(db: AsyncSession = Depends(get_db)): """Get all maintenance types.""" await ensure_default_types(db) - result = await db.execute( - select(MaintenanceType).order_by(MaintenanceType.is_system.desc(), MaintenanceType.name) - ) + result = await db.execute(select(MaintenanceType).order_by(MaintenanceType.is_system.desc(), MaintenanceType.name)) return result.scalars().all() @@ -153,9 +148,7 @@ async def update_maintenance_type( db: AsyncSession = Depends(get_db), ): """Update a maintenance type.""" - result = await db.execute( - select(MaintenanceType).where(MaintenanceType.id == type_id) - ) + result = await db.execute(select(MaintenanceType).where(MaintenanceType.id == type_id)) maint_type = result.scalar_one_or_none() if not maint_type: raise HTTPException(status_code=404, detail="Maintenance type not found") @@ -175,9 +168,7 @@ async def delete_maintenance_type( db: AsyncSession = Depends(get_db), ): """Delete a custom maintenance type.""" - result = await db.execute( - select(MaintenanceType).where(MaintenanceType.id == type_id) - ) + result = await db.execute(select(MaintenanceType).where(MaintenanceType.id == type_id)) maint_type = result.scalar_one_or_none() if not maint_type: raise HTTPException(status_code=404, detail="Maintenance type not found") @@ -192,6 +183,7 @@ async def delete_maintenance_type( # ============== Printer Maintenance ============== + async def _get_printer_maintenance_internal( printer_id: int, db: AsyncSession, @@ -201,9 +193,7 @@ async def _get_printer_maintenance_internal( await ensure_default_types(db) # Get printer - result = await db.execute( - select(Printer).where(Printer.id == printer_id) - ) + result = await db.execute(select(Printer).where(Printer.id == printer_id)) printer = result.scalar_one_or_none() if not printer: raise HTTPException(status_code=404, detail="Printer not found") @@ -230,12 +220,12 @@ async def _get_printer_maintenance_internal( for maint_type in all_types: item = existing_items.get(maint_type.id) - default_interval_type = getattr(maint_type, 'interval_type', 'hours') or 'hours' + default_interval_type = getattr(maint_type, "interval_type", "hours") or "hours" if item: interval = item.custom_interval_hours or maint_type.default_interval_hours # Use custom interval type if set, otherwise use type's default - interval_type = getattr(item, 'custom_interval_type', None) or default_interval_type + interval_type = getattr(item, "custom_interval_type", None) or default_interval_type enabled = item.enabled last_performed_hours = item.last_performed_hours last_performed_at = item.last_performed_at @@ -294,25 +284,27 @@ async def _get_printer_maintenance_internal( elif is_warning: warning_count += 1 - maintenance_items.append(MaintenanceStatus( - id=item_id, - printer_id=printer_id, - printer_name=printer.name, - maintenance_type_id=maint_type.id, - maintenance_type_name=maint_type.name, - maintenance_type_icon=maint_type.icon, - enabled=enabled, - interval_hours=interval, - interval_type=interval_type, - current_hours=total_hours, - hours_since_maintenance=hours_since, - hours_until_due=hours_until, - days_since_maintenance=days_since if interval_type == "days" else None, - days_until_due=days_until if interval_type == "days" else None, - is_due=is_due, - is_warning=is_warning, - last_performed_at=last_performed_at, - )) + maintenance_items.append( + MaintenanceStatus( + id=item_id, + printer_id=printer_id, + printer_name=printer.name, + maintenance_type_id=maint_type.id, + maintenance_type_name=maint_type.name, + maintenance_type_icon=maint_type.icon, + enabled=enabled, + interval_hours=interval, + interval_type=interval_type, + current_hours=total_hours, + hours_since_maintenance=hours_since, + hours_until_due=hours_until, + days_since_maintenance=days_since if interval_type == "days" else None, + days_until_due=days_until if interval_type == "days" else None, + is_due=is_due, + is_warning=is_warning, + last_performed_at=last_performed_at, + ) + ) if commit: await db.commit() @@ -336,14 +328,12 @@ async def get_printer_maintenance( return await _get_printer_maintenance_internal(printer_id, db, commit=True) -@router.get("/overview", response_model=List[PrinterMaintenanceOverview]) +@router.get("/overview", response_model=list[PrinterMaintenanceOverview]) async def get_all_maintenance_overview(db: AsyncSession = Depends(get_db)): """Get maintenance overview for all active printers.""" await ensure_default_types(db) - result = await db.execute( - select(Printer).where(Printer.is_active == True) - ) + result = await db.execute(select(Printer).where(Printer.is_active.is_(True))) printers = result.scalars().all() overviews = [] @@ -400,9 +390,7 @@ async def perform_maintenance( raise HTTPException(status_code=404, detail="Maintenance item not found") # Get printer for name - result = await db.execute( - select(Printer).where(Printer.id == item.printer_id) - ) + result = await db.execute(select(Printer).where(Printer.id == item.printer_id)) printer = result.scalar_one() # Get current hours @@ -424,7 +412,7 @@ async def perform_maintenance( # Calculate status interval = item.custom_interval_hours or item.maintenance_type.default_interval_hours - interval_type = getattr(item.maintenance_type, 'interval_type', 'hours') or 'hours' + interval_type = getattr(item.maintenance_type, "interval_type", "hours") or "hours" hours_since = current_hours - item.last_performed_hours hours_until = interval - hours_since @@ -449,7 +437,7 @@ async def perform_maintenance( ) -@router.get("/items/{item_id}/history", response_model=List[MaintenanceHistoryResponse]) +@router.get("/items/{item_id}/history", response_model=list[MaintenanceHistoryResponse]) async def get_maintenance_history( item_id: int, db: AsyncSession = Depends(get_db), @@ -468,9 +456,7 @@ async def get_maintenance_summary(db: AsyncSession = Depends(get_db)): """Get a summary of maintenance status across all printers.""" await ensure_default_types(db) - result = await db.execute( - select(Printer).where(Printer.is_active == True) - ) + result = await db.execute(select(Printer).where(Printer.is_active.is_(True))) printers = result.scalars().all() total_due = 0 @@ -482,12 +468,14 @@ async def get_maintenance_summary(db: AsyncSession = Depends(get_db)): total_due += overview.due_count total_warning += overview.warning_count if overview.due_count > 0 or overview.warning_count > 0: - printers_with_issues.append({ - "printer_id": printer.id, - "printer_name": printer.name, - "due_count": overview.due_count, - "warning_count": overview.warning_count, - }) + printers_with_issues.append( + { + "printer_id": printer.id, + "printer_name": printer.name, + "due_count": overview.due_count, + "warning_count": overview.warning_count, + } + ) return { "total_due": total_due, @@ -504,9 +492,7 @@ async def set_printer_hours( ): """Set the total print hours for a printer (adjusts offset to match).""" # Get printer - result = await db.execute( - select(Printer).where(Printer.id == printer_id) - ) + result = await db.execute(select(Printer).where(Printer.id == printer_id)) printer = result.scalar_one_or_none() if not printer: raise HTTPException(status_code=404, detail="Printer not found") @@ -541,9 +527,7 @@ async def set_printer_hours( ] if items_needing_attention: - await notification_service.on_maintenance_due( - printer_id, printer.name, items_needing_attention, db - ) + await notification_service.on_maintenance_due(printer_id, printer.name, items_needing_attention, db) logger.info( f"Sent maintenance notification for printer {printer_id}: " f"{len(items_needing_attention)} items need attention"