This commit is contained in:
Joakim Hellsén 2026-04-27 20:43:26 +02:00
commit a7a5b5c8ea
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
43 changed files with 5531 additions and 9 deletions

163
control_plane/views.py Normal file
View file

@ -0,0 +1,163 @@
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()