Files
bambuddy/backend/app/api/routes/system.py
T
maziggy 83cbac04b7 - Add interactive API Browser to Settings > API Keys
- New APIBrowser component with full OpenAPI schema integration
    - Fetches and parses /openapi.json automatically
    - Groups endpoints by API tags (printers, archives, settings, etc.)
    - Expandable endpoint sections with color-coded method badges
    - Path parameter, query parameter, and JSON body editors
    - Auto-populates request body with schema examples
    - Live API request execution with response display
    - Response shows status code, timing, and formatted JSON
    - Copy response button with clipboard fallback
    - Search to filter endpoints across all categories
    - Expand All / Collapse All buttons
    - Link to Swagger UI (/docs)

  - Two-column layout for API Keys tab
    - Left: API key management + webhook documentation
    - Right: API Browser with dedicated test key input

  - Parameter validation
    - Shows warning for missing required parameters
    - Validates before sending requests to avoid 422 errors

  - UX improvements
    - "Use in API Browser" button on newly created keys
    - Responsive layout (stacked on mobile, side-by-side on xl+)
2026-01-02 15:00:21 +01:00

196 lines
6.9 KiB
Python

"""System information API routes."""
import platform
from datetime import datetime
from pathlib import Path
import psutil
from fastapi import APIRouter, Depends
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.config import APP_VERSION, settings
from backend.app.core.database import get_db
from backend.app.models.archive import PrintArchive
from backend.app.models.filament import Filament
from backend.app.models.printer import Printer
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),
},
}