"""System information API routes.""" import os import platform import psutil from datetime import datetime from pathlib import Path from fastapi import APIRouter, Depends from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from backend.app.core.config import settings, APP_VERSION from backend.app.core.database import get_db from backend.app.models.archive import PrintArchive from backend.app.models.printer import Printer from backend.app.models.filament import Filament from backend.app.models.project import Project from backend.app.models.smart_plug import SmartPlug from backend.app.services.printer_manager import printer_manager router = APIRouter(prefix="/system", tags=["system"]) def get_directory_size(path: Path) -> int: """Calculate total size of a directory in bytes.""" total = 0 try: for entry in path.rglob('*'): if entry.is_file(): total += entry.stat().st_size except (PermissionError, OSError): pass return total def format_bytes(bytes_value: int) -> str: """Format bytes to human-readable string.""" for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if bytes_value < 1024: return f"{bytes_value:.1f} {unit}" bytes_value /= 1024 return f"{bytes_value:.1f} PB" def format_uptime(seconds: float) -> str: """Format uptime in seconds to human-readable string.""" days = int(seconds // 86400) hours = int((seconds % 86400) // 3600) minutes = int((seconds % 3600) // 60) parts = [] if days > 0: parts.append(f"{days}d") if hours > 0: parts.append(f"{hours}h") if minutes > 0: parts.append(f"{minutes}m") return " ".join(parts) if parts else "< 1m" @router.get("/info") async def get_system_info(db: AsyncSession = Depends(get_db)): """Get comprehensive system information.""" # Database stats archive_count = await db.scalar(select(func.count(PrintArchive.id))) printer_count = await db.scalar(select(func.count(Printer.id))) filament_count = await db.scalar(select(func.count(Filament.id))) project_count = await db.scalar(select(func.count(Project.id))) smart_plug_count = await db.scalar(select(func.count(SmartPlug.id))) # Archive stats by status completed_count = await db.scalar( select(func.count(PrintArchive.id)).where(PrintArchive.status == "completed") ) failed_count = await db.scalar( select(func.count(PrintArchive.id)).where(PrintArchive.status == "failed") ) printing_count = await db.scalar( select(func.count(PrintArchive.id)).where(PrintArchive.status == "printing") ) # Total print time total_print_time = await db.scalar( select(func.sum(PrintArchive.print_time_seconds)).where( PrintArchive.print_time_seconds.isnot(None) ) ) or 0 # Total filament used total_filament = await db.scalar( select(func.sum(PrintArchive.filament_used_grams)).where( PrintArchive.filament_used_grams.isnot(None) ) ) or 0 # Connected printers connected_printers = [] for printer_id, client in printer_manager._clients.items(): state = client.state if state and state.connected: # Get printer name and model from database result = await db.execute( select(Printer.name, Printer.model).where(Printer.id == printer_id) ) row = result.first() name = row[0] if row else f"Printer {printer_id}" model = row[1] if row else "unknown" connected_printers.append({ "id": printer_id, "name": name, "state": state.state, "model": model, }) # Storage info archive_dir = settings.archive_dir archive_size = get_directory_size(archive_dir) if archive_dir.exists() else 0 # Database file size db_path = settings.base_dir / "bambuddy.db" db_size = db_path.stat().st_size if db_path.exists() else 0 # Disk usage disk = psutil.disk_usage(str(settings.base_dir)) # System info memory = psutil.virtual_memory() boot_time = datetime.fromtimestamp(psutil.boot_time()) uptime_seconds = (datetime.now() - boot_time).total_seconds() # Python and system info import sys return { "app": { "version": APP_VERSION, "base_dir": str(settings.base_dir), "archive_dir": str(archive_dir), }, "database": { "archives": archive_count, "archives_completed": completed_count, "archives_failed": failed_count, "archives_printing": printing_count, "printers": printer_count, "filaments": filament_count, "projects": project_count, "smart_plugs": smart_plug_count, "total_print_time_seconds": total_print_time, "total_print_time_formatted": format_uptime(total_print_time), "total_filament_grams": round(total_filament, 1), "total_filament_kg": round(total_filament / 1000, 2), }, "printers": { "total": printer_count, "connected": len(connected_printers), "connected_list": connected_printers, }, "storage": { "archive_size_bytes": archive_size, "archive_size_formatted": format_bytes(archive_size), "database_size_bytes": db_size, "database_size_formatted": format_bytes(db_size), "disk_total_bytes": disk.total, "disk_total_formatted": format_bytes(disk.total), "disk_used_bytes": disk.used, "disk_used_formatted": format_bytes(disk.used), "disk_free_bytes": disk.free, "disk_free_formatted": format_bytes(disk.free), "disk_percent_used": disk.percent, }, "system": { "platform": platform.system(), "platform_release": platform.release(), "platform_version": platform.version(), "architecture": platform.machine(), "hostname": platform.node(), "python_version": sys.version.split()[0], "uptime_seconds": uptime_seconds, "uptime_formatted": format_uptime(uptime_seconds), "boot_time": boot_time.isoformat(), }, "memory": { "total_bytes": memory.total, "total_formatted": format_bytes(memory.total), "available_bytes": memory.available, "available_formatted": format_bytes(memory.available), "used_bytes": memory.used, "used_formatted": format_bytes(memory.used), "percent_used": memory.percent, }, "cpu": { "count": psutil.cpu_count(), "count_logical": psutil.cpu_count(logical=True), "percent": psutil.cpu_percent(interval=0.1), }, }