from __future__ import annotations from datetime import timedelta from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.db.models import Avg, Count, Q, Sum from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views import View from core.models import ( AIRunLog, BusinessPlanDocument, BusinessPlanRevision, CommandAction, CommandChannelBinding, CommandProfile, TranslationBridge, TranslationEventLog, ) from core.translation.engine import parse_quick_mode_title class CommandRoutingSettings(LoginRequiredMixin, View): template_name = "pages/command-routing.html" @staticmethod def _normalize_action_positions(profile): rows = list(profile.actions.order_by("position", "id")) for idx, row in enumerate(rows): if row.position != idx: row.position = idx row.save(update_fields=["position", "updated_at"]) return rows def _context(self, request): profiles = ( CommandProfile.objects.filter(user=request.user) .prefetch_related("channel_bindings", "actions") .order_by("slug") ) documents = BusinessPlanDocument.objects.filter(user=request.user).order_by( "-updated_at" )[:30] return { "profiles": profiles, "documents": documents, "channel_services": ("web", "xmpp", "signal", "whatsapp"), "directions": ("ingress", "egress", "scratchpad_mirror"), "action_types": ("extract_bp", "post_result", "save_document"), } def get(self, request): return render(request, self.template_name, self._context(request)) def post(self, request): action = str(request.POST.get("action") or "").strip() if action == "profile_create": slug = str(request.POST.get("slug") or "bp").strip().lower() or "bp" profile, _ = CommandProfile.objects.get_or_create( user=request.user, slug=slug, defaults={ "name": str(request.POST.get("name") or "Business Plan").strip() or "Business Plan", "enabled": True, "trigger_token": str( request.POST.get("trigger_token") or "#bp#" ).strip() or "#bp#", "template_text": str(request.POST.get("template_text") or ""), }, ) CommandAction.objects.get_or_create( profile=profile, action_type="extract_bp", defaults={"enabled": True, "position": 0}, ) CommandAction.objects.get_or_create( profile=profile, action_type="save_document", defaults={"enabled": True, "position": 1}, ) CommandAction.objects.get_or_create( profile=profile, action_type="post_result", defaults={"enabled": True, "position": 2}, ) return redirect("command_routing") if action == "profile_update": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) profile.name = str(request.POST.get("name") or profile.name).strip() profile.enabled = bool(request.POST.get("enabled")) profile.trigger_token = ( str(request.POST.get("trigger_token") or profile.trigger_token).strip() or "#bp#" ) profile.reply_required = bool(request.POST.get("reply_required")) profile.exact_match_only = bool(request.POST.get("exact_match_only")) profile.template_text = str(request.POST.get("template_text") or "") profile.visibility_mode = ( str(request.POST.get("visibility_mode") or "status_in_source").strip() or "status_in_source" ) profile.save() return redirect("command_routing") if action == "profile_delete": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) profile.delete() return redirect("command_routing") if action == "binding_create": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) CommandChannelBinding.objects.create( profile=profile, direction=str(request.POST.get("direction") or "ingress").strip(), service=str(request.POST.get("service") or "web").strip(), channel_identifier=str( request.POST.get("channel_identifier") or "" ).strip(), enabled=bool(request.POST.get("enabled") or "1"), ) return redirect("command_routing") if action == "binding_delete": binding = get_object_or_404( CommandChannelBinding, id=request.POST.get("binding_id"), profile__user=request.user, ) binding.delete() return redirect("command_routing") if action == "action_update": row = get_object_or_404( CommandAction, id=request.POST.get("command_action_id"), profile__user=request.user, ) row.enabled = bool(request.POST.get("enabled")) if request.POST.get("position") not in (None, ""): row.position = int(request.POST.get("position") or 0) row.save(update_fields=["enabled", "position", "updated_at"]) else: row.save(update_fields=["enabled", "updated_at"]) return redirect("command_routing") if action == "action_move": row = get_object_or_404( CommandAction, id=request.POST.get("command_action_id"), profile__user=request.user, ) direction = str(request.POST.get("direction") or "").strip().lower() if direction not in {"up", "down"}: return redirect("command_routing") with transaction.atomic(): ordered = self._normalize_action_positions(row.profile) action_ids = [entry.id for entry in ordered] try: idx = action_ids.index(row.id) except ValueError: return redirect("command_routing") target_idx = idx - 1 if direction == "up" else idx + 1 if target_idx < 0 or target_idx >= len(ordered): return redirect("command_routing") other = ordered[target_idx] current_pos = ordered[idx].position ordered[idx].position = other.position other.position = current_pos ordered[idx].save(update_fields=["position", "updated_at"]) other.save(update_fields=["position", "updated_at"]) return redirect("command_routing") return redirect("command_routing") class TranslationSettings(LoginRequiredMixin, View): template_name = "pages/translation-settings.html" def _context(self, request): bridges = TranslationBridge.objects.filter(user=request.user).order_by("-id") events = ( TranslationEventLog.objects.filter(bridge__user=request.user) .select_related("bridge") .order_by("-created_at")[:50] ) return { "bridges": bridges, "events": events, "channel_services": ("web", "xmpp", "signal", "whatsapp"), "bridge_directions": ("a_to_b", "b_to_a", "bidirectional"), } def get(self, request): return render(request, self.template_name, self._context(request)) def post(self, request): action = str(request.POST.get("action") or "").strip() if action == "bridge_create": quick_title = str(request.POST.get("quick_mode_title") or "").strip() inferred = parse_quick_mode_title(quick_title) TranslationBridge.objects.create( user=request.user, name=str(request.POST.get("name") or "Translation Bridge").strip() or "Translation Bridge", enabled=bool(request.POST.get("enabled") or "1"), a_service=str(request.POST.get("a_service") or "web").strip(), a_channel_identifier=str( request.POST.get("a_channel_identifier") or "" ).strip(), a_language=str( request.POST.get("a_language") or inferred.get("a_language") or "en" ).strip(), b_service=str(request.POST.get("b_service") or "web").strip(), b_channel_identifier=str( request.POST.get("b_channel_identifier") or "" ).strip(), b_language=str( request.POST.get("b_language") or inferred.get("b_language") or "en" ).strip(), direction=str(request.POST.get("direction") or "bidirectional").strip(), quick_mode_title=quick_title, settings={}, ) return redirect("translation_settings") if action == "bridge_delete": bridge = get_object_or_404( TranslationBridge, id=request.POST.get("bridge_id"), user=request.user ) bridge.delete() return redirect("translation_settings") return redirect("translation_settings") class AIExecutionLogSettings(LoginRequiredMixin, View): template_name = "pages/ai-execution-log.html" def _context(self, request): now = timezone.now() runs_qs = AIRunLog.objects.filter(user=request.user) runs = runs_qs.order_by("-started_at")[:300] last_24h = runs_qs.filter(started_at__gte=now - timedelta(hours=24)) last_7d = runs_qs.filter(started_at__gte=now - timedelta(days=7)) total_runs = runs_qs.count() total_ok = runs_qs.filter(status="ok").count() total_failed = runs_qs.filter(status="failed").count() avg_ms = runs_qs.aggregate(v=Avg("duration_ms")).get("v") or 0 success_rate = (float(total_ok) / float(total_runs) * 100.0) if total_runs else 0.0 usage_totals = runs_qs.aggregate( prompt_chars_total=Sum("prompt_chars"), response_chars_total=Sum("response_chars"), avg_prompt_chars=Avg("prompt_chars"), avg_response_chars=Avg("response_chars"), ) stats = { "total_runs": total_runs, "total_ok": total_ok, "total_failed": total_failed, "last_24h_runs": last_24h.count(), "last_24h_failed": last_24h.filter(status="failed").count(), "last_7d_runs": last_7d.count(), "avg_duration_ms": int(avg_ms), "success_rate": round(success_rate, 1), "total_prompt_chars": int(usage_totals.get("prompt_chars_total") or 0), "total_response_chars": int(usage_totals.get("response_chars_total") or 0), "avg_prompt_chars": int(usage_totals.get("avg_prompt_chars") or 0), "avg_response_chars": int(usage_totals.get("avg_response_chars") or 0), } operation_breakdown = ( runs_qs.values("operation") .annotate( total=Count("id"), failed=Count("id", filter=Q(status="failed")), ok=Count("id", filter=Q(status="ok")), ) .order_by("-total", "operation")[:20] ) model_breakdown = ( runs_qs.values("model") .annotate( total=Count("id"), failed=Count("id", filter=Q(status="failed")), ok=Count("id", filter=Q(status="ok")), ) .order_by("-total", "model")[:20] ) return { "stats": stats, "runs": runs, "operation_breakdown": operation_breakdown, "model_breakdown": model_breakdown, } def get(self, request): return render(request, self.template_name, self._context(request)) class BusinessPlanEditor(LoginRequiredMixin, View): template_name = "pages/business-plan-editor.html" def get(self, request, doc_id): document = get_object_or_404(BusinessPlanDocument, id=doc_id, user=request.user) revisions = document.revisions.order_by("-created_at")[:100] return render( request, self.template_name, { "document": document, "revisions": revisions, }, ) def post(self, request, doc_id): document = get_object_or_404(BusinessPlanDocument, id=doc_id, user=request.user) document.title = str(request.POST.get("title") or document.title).strip() document.status = str(request.POST.get("status") or document.status).strip() document.content_markdown = str(request.POST.get("content_markdown") or "") document.save() BusinessPlanRevision.objects.create( document=document, editor_user=request.user, content_markdown=document.content_markdown, structured_payload=document.structured_payload or {}, ) return redirect("business_plan_editor", doc_id=str(document.id)) class TranslationPreview(LoginRequiredMixin, View): def post(self, request): bridge = get_object_or_404( TranslationBridge, id=request.POST.get("bridge_id"), user=request.user, ) source = str(request.POST.get("source") or "").strip().lower() text = str(request.POST.get("text") or "") if source == "a": target_language = bridge.b_language else: target_language = bridge.a_language return JsonResponse( { "ok": True, "bridge_id": str(bridge.id), "target_language": target_language, "preview": text, "note": "Preview endpoint is non-mutating; final translation occurs in runtime sync.", } )