octoeverywhere.py 4.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """API routes for OctoEverywhere AI failure detection."""
  2. from typing import Literal
  3. from fastapi import APIRouter, Depends
  4. from pydantic import BaseModel
  5. from sqlalchemy import select
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from backend.app.core.auth import ApiKeyActor, RequestActor, RequestPrinterScope, RequirePermissionIfAuthEnabled
  8. from backend.app.core.database import get_db
  9. from backend.app.core.permissions import Permission
  10. from backend.app.core.printer_scope import PrinterScope
  11. from backend.app.models.printer import Printer
  12. from backend.app.models.user import User
  13. from backend.app.services.notification_service import notification_service
  14. from backend.app.services.octoeverywhere_detection import octoeverywhere_detection_service
  15. router = APIRouter(prefix="/octoeverywhere", tags=["octoeverywhere"])
  16. class TestConnectionRequest(BaseModel):
  17. # Omitted = test the saved key; empty = test with no key.
  18. api_key: str | None = None
  19. confidence: Literal["lowest", "low", "medium", "high", "highest"] | None = None
  20. @router.get("/status")
  21. async def get_status(
  22. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
  23. db: AsyncSession = Depends(get_db, scope="function"),
  24. printer_scope: PrinterScope = RequestPrinterScope,
  25. ):
  26. """Scheduler status, print quality, and recent detection history."""
  27. settings = await octoeverywhere_detection_service._load_settings()
  28. providers = await notification_service._get_providers_for_event(db, "on_ai_failure_detection", fleet_wide=True)
  29. providers = [provider for provider in providers if printer_scope.allows(provider.printer_id)]
  30. printers = select(Printer.id).where(Printer.is_active.is_(True))
  31. scope_filter = printer_scope.where_strict(Printer.id)
  32. if scope_filter is not None:
  33. printers = printers.where(scope_filter)
  34. if settings["enabled_printers"] is not None:
  35. printers = printers.where(Printer.id.in_(settings["enabled_printers"]))
  36. printer_ids = (await db.execute(printers)).scalars().all()
  37. # Use the same per-printer subscription rules as notification delivery.
  38. covered_printers = {provider.printer_id for provider in providers}
  39. uncovered_printers = (
  40. sorted(pid for pid in printer_ids if pid not in covered_printers) if None not in covered_printers else []
  41. )
  42. status = octoeverywhere_detection_service.get_status()
  43. return {
  44. **status,
  45. "per_printer": {pid: entry for pid, entry in status["per_printer"].items() if printer_scope.allows(int(pid))},
  46. "history": [entry for entry in status["history"] if printer_scope.allows(entry["printer_id"])],
  47. "enabled": settings["enabled"],
  48. "api_key_configured": bool(settings["api_key"]),
  49. "confidence": settings["confidence"],
  50. "action": settings["action"],
  51. "poll_interval": settings["poll_interval"],
  52. "notifications": {"configured": bool(providers), "uncovered_printers": uncovered_printers},
  53. }
  54. @router.get("/printer-status")
  55. async def get_printer_status(
  56. user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
  57. printer_scope: PrinterScope = RequestPrinterScope,
  58. actor: User | ApiKeyActor | None = RequestActor,
  59. ):
  60. """Live printer-card status without exposing provider configuration."""
  61. settings = await octoeverywhere_detection_service._load_settings()
  62. enabled_printers = settings["enabled_printers"]
  63. can_see_error = actor is None or actor.has_permission(Permission.SETTINGS_READ.value)
  64. per_printer = octoeverywhere_detection_service.get_per_printer()
  65. if not can_see_error:
  66. per_printer = {pid: {**entry, "error": None, "error_code": None} for pid, entry in per_printer.items()}
  67. per_printer = {pid: entry for pid, entry in per_printer.items() if printer_scope.allows(int(pid))}
  68. if enabled_printers is not None:
  69. enabled_printers = [pid for pid in enabled_printers if printer_scope.allows(int(pid))]
  70. return {
  71. "enabled": settings["enabled"],
  72. "monitored_printers": sorted(enabled_printers) if enabled_printers is not None else None,
  73. "per_printer": per_printer,
  74. "last_error": octoeverywhere_detection_service._last_error if can_see_error else None,
  75. "last_error_code": octoeverywhere_detection_service._last_error_code if can_see_error else None,
  76. }
  77. @router.post("/test-connection")
  78. async def test_connection(
  79. req: TestConnectionRequest,
  80. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  81. ):
  82. """Validate the key by creating a context, without uploading a camera image."""
  83. settings = await octoeverywhere_detection_service._load_settings()
  84. api_key = settings["api_key"] if req.api_key is None else req.api_key
  85. return await octoeverywhere_detection_service.test_connection(api_key, req.confidence or settings["confidence"])