"""Update management API endpoints. Refactored to use UpdateService for all business logic. Endpoints are now thin adapters that convert HTTP requests/responses. Supports both legacy whole-system operations and per-component operations. """ from typing import Optional, List from fastapi import APIRouter, HTTPException, Path from pydantic import BaseModel from ..services.container import container from ..managers.ble_manager import get_ble_manager, NotificationType router = APIRouter() # --------------------------------------------------------------------------- # Response / request models # --------------------------------------------------------------------------- class ComponentUpdateInfo(BaseModel): """Per-component update information.""" component_id: str current_version: Optional[str] = None available_version: str changelog: str = "" download_size: Optional[int] = None compatible: bool = True incompatible_reason: Optional[str] = None class PluginUpdateInfo(BaseModel): """Per-plugin update information.""" plugin_id: str current_version: str available_version: str changelog: str = "" download_size: Optional[int] = None source_repo: str = "" class UpdateCheckResponse(BaseModel): """Response model for update check.""" update_available: bool current_version: str latest_version: Optional[str] = None release_date: Optional[str] = None changelog: Optional[str] = None is_prerelease: bool = False download_size: Optional[int] = None message: Optional[str] = None components: List[ComponentUpdateInfo] = [] plugins: List[PluginUpdateInfo] = [] class UpdateProgressResponse(BaseModel): """Response model for update progress.""" status: str current_version: str available_version: Optional[str] = None download_progress: float = 0.0 error_message: Optional[str] = None last_check: Optional[str] = None active_component: Optional[str] = None components: List[dict] = [] class ReleaseNotesRequest(BaseModel): """Request model for release notes.""" version: Optional[str] = None class ComponentDownloadRequest(BaseModel): """Optional request body for selective download.""" components: Optional[List[str]] = None # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _service_error_to_http_status(error_code: str) -> int: """Map service error codes to HTTP status codes.""" codes = { "update_check_error": 500, "no_update_available": 400, "download_failed": 500, "update_download_error": 500, "no_update_downloaded": 400, "installation_failed": 500, "update_install_error": 500, "progress_error": 500, "release_notes_error": 500, "check_download_error": 500, "full_update_error": 500, "manifest_error": 500, "component_not_available": 400, "component_download_error": 500, "component_install_error": 500, "rollback_error": 500, } return codes.get(error_code, 500) def _raise_on_error(result): """Raise HTTPException if result indicates failure.""" if not result.success: raise HTTPException( status_code=_service_error_to_http_status(result.error.code), detail=result.error.message, ) # --------------------------------------------------------------------------- # Legacy endpoints (backward-compatible) # --------------------------------------------------------------------------- @router.get("/check", response_model=UpdateCheckResponse) async def check_for_updates(): """Check for available updates (components + plugins).""" result = await container.update_service.check_for_updates() _raise_on_error(result) # BLE notification if result.data.get("update_available"): try: ble_manager = get_ble_manager() await ble_manager.send_notification( NotificationType.UPDATE_AVAILABLE, f"Update available: v{result.data['latest_version']}", {"version": result.data["latest_version"]}, ) except Exception: pass return UpdateCheckResponse(**result.data) @router.get("/progress", response_model=UpdateProgressResponse) async def get_update_progress(): """Get current update progress.""" result = await container.update_service.get_progress() _raise_on_error(result) return UpdateProgressResponse(**result.data) @router.post("/download") async def download_update(body: Optional[ComponentDownloadRequest] = None): """Download available updates. Without a body, downloads all available components (legacy behavior). With ``{"components": ["frontend"]}``, downloads only the specified ones. """ if body and body.components: results = {} for comp_id in body.components: r = await container.update_service.download_component(comp_id) _raise_on_error(r) results[comp_id] = r.data return {"message": "Components downloaded", "components": results} result = await container.update_service.download_update() _raise_on_error(result) return {"message": result.data["message"]} @router.post("/install") async def install_update(body: Optional[ComponentDownloadRequest] = None): """Install downloaded updates. Without a body, installs all downloaded components (legacy behavior). With ``{"components": ["frontend"]}``, installs only the specified ones. """ if body and body.components: results = {} for comp_id in body.components: r = await container.update_service.install_component(comp_id) _raise_on_error(r) results[comp_id] = r.data return {"message": "Components installed", "components": results} result = await container.update_service.install_update() _raise_on_error(result) # BLE notification try: ble_manager = get_ble_manager() await ble_manager.send_notification( NotificationType.UPDATE_COMPLETE, "Update installed successfully", {"restart_required": True}, ) except Exception: pass return { "message": result.data["message"], "restart_required": result.data.get("requires_restart", True), } @router.post("/release-notes", response_model=dict) async def get_release_notes(request: ReleaseNotesRequest): """Get release notes for a specific version.""" result = await container.update_service.get_release_notes(request.version) _raise_on_error(result) return { "version": result.data["version"], "notes": result.data["release_notes"], } @router.get("/current-version") async def get_current_version(): """Get current system version.""" result = await container.update_service.get_progress() _raise_on_error(result) return { "version": result.data["current_version"], "last_check": result.data["last_check"], } # --------------------------------------------------------------------------- # Component-level endpoints # --------------------------------------------------------------------------- @router.get("/components") async def get_installed_components(): """Get installed component manifest.""" result = await container.update_service.get_installed_components() _raise_on_error(result) return result.data @router.post("/components/{component_id}/download") async def download_component( component_id: str = Path(description="Component ID (pm3, frontend, backend, theme)"), ): """Download a single component update.""" result = await container.update_service.download_component(component_id) _raise_on_error(result) return result.data @router.post("/components/{component_id}/install") async def install_component( component_id: str = Path(description="Component ID"), ): """Install a single downloaded component.""" result = await container.update_service.install_component(component_id) _raise_on_error(result) return result.data @router.post("/components/{component_id}/rollback") async def rollback_component( component_id: str = Path(description="Component ID"), ): """Rollback a component to its backup.""" result = await container.update_service.rollback_component(component_id) _raise_on_error(result) return result.data