mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 11:47:46 +02:00
- 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+)
196 lines
6.9 KiB
Python
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),
|
|
},
|
|
}
|