system.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. """System information API routes."""
  2. import platform
  3. from datetime import datetime
  4. from pathlib import Path
  5. import psutil
  6. from fastapi import APIRouter, Depends
  7. from sqlalchemy import func, select
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from backend.app.core.auth import RequirePermissionIfAuthEnabled
  10. from backend.app.core.config import APP_VERSION, settings
  11. from backend.app.core.database import get_db
  12. from backend.app.core.permissions import Permission
  13. from backend.app.models.archive import PrintArchive
  14. from backend.app.models.filament import Filament
  15. from backend.app.models.printer import Printer
  16. from backend.app.models.project import Project
  17. from backend.app.models.smart_plug import SmartPlug
  18. from backend.app.models.user import User
  19. from backend.app.services.printer_manager import printer_manager
  20. router = APIRouter(prefix="/system", tags=["system"])
  21. def get_directory_size(path: Path) -> int:
  22. """Calculate total size of a directory in bytes."""
  23. total = 0
  24. try:
  25. for entry in path.rglob("*"):
  26. if entry.is_file():
  27. total += entry.stat().st_size
  28. except (PermissionError, OSError):
  29. pass # Return partial total if directory traversal is interrupted
  30. return total
  31. def format_bytes(bytes_value: int) -> str:
  32. """Format bytes to human-readable string."""
  33. for unit in ["B", "KB", "MB", "GB", "TB"]:
  34. if bytes_value < 1024:
  35. return f"{bytes_value:.1f} {unit}"
  36. bytes_value /= 1024
  37. return f"{bytes_value:.1f} PB"
  38. def format_uptime(seconds: float) -> str:
  39. """Format uptime in seconds to human-readable string."""
  40. days = int(seconds // 86400)
  41. hours = int((seconds % 86400) // 3600)
  42. minutes = int((seconds % 3600) // 60)
  43. parts = []
  44. if days > 0:
  45. parts.append(f"{days}d")
  46. if hours > 0:
  47. parts.append(f"{hours}h")
  48. if minutes > 0:
  49. parts.append(f"{minutes}m")
  50. return " ".join(parts) if parts else "< 1m"
  51. @router.get("/info")
  52. async def get_system_info(
  53. db: AsyncSession = Depends(get_db),
  54. _: User | None = RequirePermissionIfAuthEnabled(Permission.SYSTEM_READ),
  55. ):
  56. """Get comprehensive system information."""
  57. # Database stats
  58. archive_count = await db.scalar(select(func.count(PrintArchive.id)))
  59. printer_count = await db.scalar(select(func.count(Printer.id)))
  60. filament_count = await db.scalar(select(func.count(Filament.id)))
  61. project_count = await db.scalar(select(func.count(Project.id)))
  62. smart_plug_count = await db.scalar(select(func.count(SmartPlug.id)))
  63. # Archive stats by status
  64. completed_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "completed"))
  65. failed_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "failed"))
  66. printing_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "printing"))
  67. # Total print time
  68. total_print_time = (
  69. await db.scalar(
  70. select(func.sum(PrintArchive.print_time_seconds)).where(PrintArchive.print_time_seconds.isnot(None))
  71. )
  72. or 0
  73. )
  74. # Total filament used
  75. total_filament = (
  76. await db.scalar(
  77. select(func.sum(PrintArchive.filament_used_grams)).where(PrintArchive.filament_used_grams.isnot(None))
  78. )
  79. or 0
  80. )
  81. # Connected printers
  82. connected_printers = []
  83. for printer_id, client in printer_manager._clients.items():
  84. state = client.state
  85. if state and state.connected:
  86. # Get printer name and model from database
  87. result = await db.execute(select(Printer.name, Printer.model).where(Printer.id == printer_id))
  88. row = result.first()
  89. name = row[0] if row else f"Printer {printer_id}"
  90. model = row[1] if row else "unknown"
  91. connected_printers.append(
  92. {
  93. "id": printer_id,
  94. "name": name,
  95. "state": state.state,
  96. "model": model,
  97. }
  98. )
  99. # Storage info
  100. archive_dir = settings.archive_dir
  101. archive_size = get_directory_size(archive_dir) if archive_dir.exists() else 0
  102. # Database file size
  103. db_path = settings.base_dir / "bambuddy.db"
  104. db_size = db_path.stat().st_size if db_path.exists() else 0
  105. # Disk usage
  106. disk = psutil.disk_usage(str(settings.base_dir))
  107. # System info
  108. memory = psutil.virtual_memory()
  109. boot_time = datetime.fromtimestamp(psutil.boot_time())
  110. uptime_seconds = (datetime.now() - boot_time).total_seconds()
  111. # Python and system info
  112. import sys
  113. return {
  114. "app": {
  115. "version": APP_VERSION,
  116. "base_dir": str(settings.base_dir),
  117. "archive_dir": str(archive_dir),
  118. },
  119. "database": {
  120. "archives": archive_count,
  121. "archives_completed": completed_count,
  122. "archives_failed": failed_count,
  123. "archives_printing": printing_count,
  124. "printers": printer_count,
  125. "filaments": filament_count,
  126. "projects": project_count,
  127. "smart_plugs": smart_plug_count,
  128. "total_print_time_seconds": total_print_time,
  129. "total_print_time_formatted": format_uptime(total_print_time),
  130. "total_filament_grams": round(total_filament, 1),
  131. "total_filament_kg": round(total_filament / 1000, 2),
  132. },
  133. "printers": {
  134. "total": printer_count,
  135. "connected": len(connected_printers),
  136. "connected_list": connected_printers,
  137. },
  138. "storage": {
  139. "archive_size_bytes": archive_size,
  140. "archive_size_formatted": format_bytes(archive_size),
  141. "database_size_bytes": db_size,
  142. "database_size_formatted": format_bytes(db_size),
  143. "disk_total_bytes": disk.total,
  144. "disk_total_formatted": format_bytes(disk.total),
  145. "disk_used_bytes": disk.used,
  146. "disk_used_formatted": format_bytes(disk.used),
  147. "disk_free_bytes": disk.free,
  148. "disk_free_formatted": format_bytes(disk.free),
  149. "disk_percent_used": disk.percent,
  150. },
  151. "system": {
  152. "platform": platform.system(),
  153. "platform_release": platform.release(),
  154. "platform_version": platform.version(),
  155. "architecture": platform.machine(),
  156. "hostname": platform.node(),
  157. "python_version": sys.version.split()[0],
  158. "uptime_seconds": uptime_seconds,
  159. "uptime_formatted": format_uptime(uptime_seconds),
  160. "boot_time": boot_time.isoformat(),
  161. },
  162. "memory": {
  163. "total_bytes": memory.total,
  164. "total_formatted": format_bytes(memory.total),
  165. "available_bytes": memory.available,
  166. "available_formatted": format_bytes(memory.available),
  167. "used_bytes": memory.used,
  168. "used_formatted": format_bytes(memory.used),
  169. "percent_used": memory.percent,
  170. },
  171. "cpu": {
  172. "count": psutil.cpu_count(),
  173. "count_logical": psutil.cpu_count(logical=True),
  174. "percent": psutil.cpu_percent(interval=0.1),
  175. },
  176. }