Files
VacatureRadar/apps/core/services/automation.py
T
Jens 5e2453c29a
deploy / deploy (push) Canceled after 0s
feat: professionalize platform release 0.3.13
2026-07-29 16:35:37 +02:00

142 lines
5.5 KiB
Python

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",
}