from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from typing import Any from uuid import UUID from django.db.models import Count from django.db.models import Prefetch from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.views.generic import TemplateView from django.views.generic import View from control_plane.local_test_runtime import build_test_django_local_url from control_plane.models import Deployment from control_plane.models import DeploymentStatus from control_plane.models import RuntimeService from control_plane.observability import JsonValue from control_plane.observability import load_test_deployment_diagnostics from control_plane.observability import probe_test_deployment_health if TYPE_CHECKING: from django.db.models import QuerySet from django.http import HttpRequest type RouteKwarg = str | UUID type DashboardContextValue = int | tuple[DeploymentCard, ...] | tuple[DeploymentStatusSummary, ...] type DetailContextValue = ( DashboardContextValue | Deployment | tuple[RuntimeService, ...] | str | dict[str, JsonValue] | None ) @dataclass(frozen=True, slots=True) class DeploymentStatusSummary: """Aggregate deployments by lifecycle state for dashboard cards.""" status: str label: str total: int @dataclass(frozen=True, slots=True) class DeploymentCard: """Small view model used by the dashboard templates.""" deployment: Deployment sentinel_url: str runtime_services: tuple[RuntimeService, ...] @property def runtime_ready_total(self) -> int: """Return total runtime services currently marked ready.""" return sum(runtime_service.status == "ready" for runtime_service in self.runtime_services) @property def runtime_failed_total(self) -> int: """Return total runtime services currently marked failed.""" return sum(runtime_service.status == "failed" for runtime_service in self.runtime_services) def _deployment_queryset() -> QuerySet[Deployment]: runtime_services = RuntimeService.objects.order_by("kind") return Deployment.objects.select_related("hosted_site__tenant").prefetch_related( Prefetch("runtime_services", queryset=runtime_services), ) class DeploymentDashboardView(TemplateView): """Render recent test deployments with links to diagnostics and sentinel probes.""" template_name = "control_plane/deployment_dashboard.html" def get_context_data(self, **kwargs: RouteKwarg) -> dict[str, DashboardContextValue]: """Build recent deployment cards plus aggregate status counts for the dashboard. Returns: Template context containing deployment cards and summary counters. """ context = super().get_context_data(**kwargs) deployments = tuple(_deployment_queryset().order_by("-created_at")[:24]) context.update( { "deployment_cards": tuple(_build_deployment_card(deployment) for deployment in deployments), "status_summaries": _build_status_summaries(), "running_total": sum(deployment.status == DeploymentStatus.RUNNING.value for deployment in deployments), "deployment_total": len(deployments), }, ) return context class DeploymentDetailView(TemplateView): """Render one deployment with persisted diagnostics, logs, and live health state.""" template_name = "control_plane/deployment_detail.html" def get_context_data(self, **kwargs: RouteKwarg) -> dict[str, DetailContextValue]: """Build one deployment view with persisted diagnostics and an initial health probe. Returns: Template context containing deployment metadata, diagnostics, and health state. """ context: dict[str, Any] = super().get_context_data(**kwargs) deployment: Deployment = get_object_or_404(_deployment_queryset(), pk=self.kwargs["deployment_id"]) runtime_services = tuple(deployment.runtime_services.all()) context.update( { "deployment": deployment, "runtime_services": runtime_services, "sentinel_url": build_test_django_local_url(deployment), "diagnostics": load_test_deployment_diagnostics(deployment), "health_probe": probe_test_deployment_health(deployment), }, ) return context class DeploymentHealthView(View): """Return live sentinel health JSON for one deployment.""" def get(self, request: HttpRequest, deployment_id: str) -> JsonResponse: """Return JSON probe state for one deployment sentinel endpoint.""" del request deployment = get_object_or_404(_deployment_queryset(), pk=deployment_id) return JsonResponse(probe_test_deployment_health(deployment)) class DeploymentDashboardHomeView(DeploymentDashboardView): """Alias the dashboard at the site root for local testing convenience.""" template_name = "control_plane/deployment_dashboard.html" def _build_deployment_card(deployment: Deployment) -> DeploymentCard: return DeploymentCard( deployment=deployment, sentinel_url=build_test_django_local_url(deployment), runtime_services=tuple(deployment.runtime_services.all()), ) def _build_status_summaries() -> tuple[DeploymentStatusSummary, ...]: summary_rows = tuple( Deployment.objects.values("status").annotate(total=Count("id")).order_by("status"), ) return tuple( DeploymentStatusSummary( status=status, label=_resolve_status_label(status), total=int(total), ) for status, total in ((row["status"], row["total"]) for row in summary_rows) ) def _resolve_status_label(status: str) -> str: for choice in DeploymentStatus: if choice.value == status: return choice.label return status.replace("-", " ").title()