163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
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()
|