from __future__ import annotations from datetime import timedelta from typing import Any from django.conf import settings from django.core.cache import cache from django.db.models import Count, Q, Sum from django.utils import timezone from apps.jobs.models import JobPosting, ScoreRun from apps.jobs.services.relevance import it_relevance_query from apps.notifications.models import DigestOutbox, ReminderOutbox from apps.profiles.models import SearchProfile from apps.sources.models import Source, SourceRun from ..health import readiness from ..tasks import RUNTIME_HEARTBEAT_KEY def _runtime_heartbeat_state() -> dict[str, Any]: if settings.CELERY_TASK_ALWAYS_EAGER: return {"label": "Eager/lokaal", "healthy": True, "observed_at": None} raw_timestamp = cache.get(RUNTIME_HEARTBEAT_KEY) if not raw_timestamp: return {"label": "Geen recente heartbeat", "healthy": False, "observed_at": None} try: observed_at = timezone.datetime.fromisoformat(str(raw_timestamp)) if timezone.is_naive(observed_at): observed_at = timezone.make_aware(observed_at) except (TypeError, ValueError): return {"label": "Ongeldige heartbeat", "healthy": False, "observed_at": None} age_seconds = max(0, int((timezone.now() - observed_at).total_seconds())) return { "label": f"Actief ยท {age_seconds}s geleden" if age_seconds <= 150 else "Heartbeat verouderd", "healthy": age_seconds <= 150, "observed_at": observed_at, } def build_automation_cockpit() -> dict[str, Any]: """Build an honest operational read model without probing unsafe hidden endpoints.""" since = timezone.now() - timedelta(hours=24) recent_runs = SourceRun.objects.filter(started_at__gte=since) run_metrics = recent_runs.aggregate( discovered=Sum("discovered_count"), extracted=Sum("extracted_count"), created=Sum("created_count"), updated=Sum("updated_count"), duplicates=Sum("duplicate_count"), failures=Count("id", filter=Q(status=SourceRun.Status.FAILED)), ) notification_counts = { "pending": DigestOutbox.objects.filter(status=DigestOutbox.Status.PENDING).count() + ReminderOutbox.objects.filter(status=ReminderOutbox.Status.PENDING).count(), "failed": DigestOutbox.objects.filter(status=DigestOutbox.Status.FAILED).count() + ReminderOutbox.objects.filter(status=ReminderOutbox.Status.FAILED).count(), "sent_24h": DigestOutbox.objects.filter( status=DigestOutbox.Status.SENT, sent_at__gte=since ).count() + ReminderOutbox.objects.filter( status=ReminderOutbox.Status.SENT, sent_at__gte=since ).count(), } source_total = Source.objects.count() source_active = Source.objects.filter(status=Source.Status.ACTIVE).count() active_jobs = JobPosting.objects.filter(status=JobPosting.Status.ACTIVE) phase_rows = [ {"key": "discover", "label": "Ontdekken", "value": source_total, "unit": "bronnen"}, {"key": "fetch", "label": "Ophalen", "value": recent_runs.count(), "unit": "runs / 24u"}, { "key": "extract", "label": "Extraheren", "value": run_metrics["extracted"] or 0, "unit": "items", }, { "key": "normalize", "label": "Normaliseren", "value": (run_metrics["created"] or 0) + (run_metrics["updated"] or 0), "unit": "records", }, { "key": "dedupe", "label": "Dedupliceren", "value": run_metrics["duplicates"] or 0, "unit": "herkend", }, { "key": "filter", "label": "IT-filter", "value": active_jobs.exclude(it_relevance_query()).count(), "unit": "uit beeld", }, { "key": "analyze", "label": "Analyseren", "value": ScoreRun.objects.filter(created_at__gte=since).count(), "unit": "scores / 24u", }, { "key": "notify", "label": "Notificeren", "value": notification_counts["sent_24h"], "unit": "verzonden / 24u", }, ] runtime_heartbeat = _runtime_heartbeat_state() return { "health": readiness(), "source_total": source_total, "source_active": source_active, "run_metrics": run_metrics, "run_count": recent_runs.count(), "active_jobs": active_jobs.filter(it_relevance_query()).count(), "review_jobs": ScoreRun.objects.filter( recommendation__in=[ScoreRun.Recommendation.STRONG, ScoreRun.Recommendation.POSSIBLE], job__status=JobPosting.Status.ACTIVE, ).count(), "notification_counts": notification_counts, "phase_rows": phase_rows, "recent_runs": list( SourceRun.objects.select_related("source").order_by("-started_at")[:10] ), "source_rows": Source.objects.annotate(run_count=Count("runs")).order_by("name")[:12], "queue_state": "Eager/lokaal" if settings.CELERY_TASK_ALWAYS_EAGER else "Diepte niet beschikbaar", "runtime_heartbeat": runtime_heartbeat, "ai_state": "Geconfigureerd" if settings.OLLAMA_ENABLED and settings.OLLAMA_MODEL else "Niet geconfigureerd", "ai_profiles": SearchProfile.objects.filter(ai_scoring_enabled=True).count(), "browser_state": "Geen aparte browserextractor geconfigureerd", }