from __future__ import annotations import uuid from fastapi import APIRouter, BackgroundTasks, HTTPException from app.core.command_runner import CommandRunner from app.core.installer import AptInstaller from app.core.service_manager import ServiceManager from app.core.task_runner import TaskRunner from app.models.schemas import InstallRequest, RemoveRequest, UpdateRequest from app.storage.repository import Repository router = APIRouter(prefix="/apps", tags=["apps"]) repository = Repository() task_runner = TaskRunner(repository) def _task_id(prefix: str) -> str: return f"task_{prefix}_{uuid.uuid4().hex[:12]}" @router.get("/installed") def installed_apps() -> list[dict]: apps = repository.list_installed_apps() command_runner = CommandRunner(repository) service_manager = ServiceManager(command_runner) apt_installer = AptInstaller(command_runner) for app in apps: service_checks: list[dict] = [] component_statuses: list[str] = [] for component in repository.list_installed_components(app["app_id"]): check_status = component.get("service_check_status", "not-checked") if check_status != "not-checked": component_statuses.append(check_status) for check in component.get("service_checks", []): service_name = str(check.get("serviceName") or "").strip() if not service_name: continue current_check = service_manager.get_service_status(service_name) if current_check.get("status") == "unknown" and current_check.get("errorMessage"): current_check = dict(check) current_check["stale"] = True elif check.get("readinessType") == "postgresql": readiness_status = "ready" if apt_installer.is_postgresql_ready() else "failed" current_check["readinessType"] = "postgresql" current_check["readinessStatus"] = readiness_status current_check["healthy"] = bool(current_check.get("healthy")) and readiness_status == "ready" service_checks.append( { **current_check, "componentId": component["component_id"], "packageName": component.get("package_name"), } ) if service_checks and any(not check.get("healthy") for check in service_checks): overall_status = "unhealthy" elif service_checks: overall_status = "healthy" elif "unhealthy" in component_statuses: overall_status = "unhealthy" elif "checking" in component_statuses: overall_status = "checking" elif "healthy" in component_statuses: overall_status = "healthy" elif "not-applicable" in component_statuses: overall_status = "not-applicable" else: overall_status = "not-checked" app["serviceCheckStatus"] = overall_status app["serviceChecks"] = service_checks return apps @router.post("/install") def install_app(request: InstallRequest, background_tasks: BackgroundTasks) -> dict[str, str]: if not request.version: raise HTTPException(status_code=400, detail="version is required") task_id = _task_id("install") repository.create_task(task_id, "install", request.app_id, request.app_name) background_tasks.add_task(task_runner.run_install, task_id, request, "install") return {"taskId": task_id, "status": "queued"} @router.post("/update") def update_app(request: UpdateRequest, background_tasks: BackgroundTasks) -> dict[str, str]: if not request.version: raise HTTPException(status_code=400, detail="version or targetVersion is required") task_id = _task_id("update") repository.create_task(task_id, "update", request.app_id, request.app_name) background_tasks.add_task(task_runner.run_install, task_id, request, "update") return {"taskId": task_id, "status": "queued"} @router.post("/remove") def remove_app(request: RemoveRequest, background_tasks: BackgroundTasks) -> dict[str, str]: if not request.package_name and not repository.list_installed_components(request.app_id): raise HTTPException(status_code=400, detail="packageName is required when app is not tracked locally") task_id = _task_id("remove") repository.create_task(task_id, "remove", request.app_id, request.app_id) background_tasks.add_task(task_runner.run_remove, task_id, request) return {"taskId": task_id, "status": "queued"}