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.urls import reverse from django.utils import timezone from django.views import View from core.commands.policies import BP_VARIANT_KEYS, BP_VARIANT_META, ensure_variant_policies_for_profile from core.models import ( AIRunLog, BusinessPlanDocument, BusinessPlanRevision, CommandAction, CommandChannelBinding, CommandProfile, CommandVariantPolicy, TranslationBridge, TranslationEventLog, ) from core.translation.engine import parse_quick_mode_title def _channel_variants(service: str, identifier: str) -> list[str]: value = str(identifier or "").strip() if not value: return [] variants = [value] svc = str(service or "").strip().lower() if svc == "whatsapp": bare = value.split("@", 1)[0].strip() if bare and bare not in variants: variants.append(bare) group = f"{bare}@g.us" if bare else "" if group and group not in variants: variants.append(group) return variants 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 @staticmethod def _redirect_with_scope(request): service = str(request.GET.get("service") or request.POST.get("service") or "").strip() identifier = str( request.GET.get("identifier") or request.POST.get("identifier") or "" ).strip() if service and identifier: return redirect( f"{reverse('command_routing')}?service={service}&identifier={identifier}" ) return redirect("command_routing") def _context(self, request): profiles_qs = ( CommandProfile.objects.filter(user=request.user) .prefetch_related("channel_bindings", "actions", "variant_policies") .order_by("slug") ) scope_service = str(request.GET.get("service") or "").strip().lower() scope_identifier = str(request.GET.get("identifier") or "").strip() scope_variants = _channel_variants(scope_service, scope_identifier) profiles = list(profiles_qs) preview_profile_id = str(request.GET.get("preview_profile_id") or "").strip() for profile in profiles: policies = ensure_variant_policies_for_profile(profile) profile.show_actions_editor = str(profile.slug or "").strip() != "bp" if str(profile.slug or "").strip() == "bp": keys = BP_VARIANT_KEYS else: keys = ("default",) profile.variant_rows = [] for key in keys: row = policies.get(key) if row is None: continue meta = BP_VARIANT_META.get(key, {}) profile.variant_rows.append( { "variant_key": key, "variant_label": str(meta.get("name") or key), "trigger_token": str(meta.get("trigger_token") or profile.trigger_token or ""), "template_supported": bool(meta.get("template_supported")), "warn_verbatim_plan": bool( key in {"bp", "bp_set_range"} and str(getattr(row, "generation_mode", "") or "") == "verbatim" and bool(getattr(row, "send_plan_to_egress", False)) ), "row": row, } ) bindings = list(profile.channel_bindings.all()) if scope_service and scope_variants: profile.visible_bindings = [ row for row in bindings if str(row.service or "").strip().lower() == scope_service and str(row.channel_identifier or "").strip() in scope_variants ] else: profile.visible_bindings = bindings profile.enabled_egress_bindings = [ row for row in bindings if str(row.direction or "").strip() == "egress" and bool(row.enabled) ] profile.preview_mode = preview_profile_id and str(profile.id) == preview_profile_id 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"), "command_choices": ( ("bp", "Business Plan (bp)"), ("codex", "Codex (codex)"), ), "scope_service": scope_service, "scope_identifier": scope_identifier, "scope_variants": scope_variants, "preview_profile_id": preview_profile_id, } 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("command_slug") or 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 ("Codex" if slug == "codex" else "Business Plan")).strip() or ("Codex" if slug == "codex" else "Business Plan"), "enabled": True, "trigger_token": str( request.POST.get("trigger_token") or (".codex" if slug == "codex" else ".bp") ).strip() or (".codex" if slug == "codex" else ".bp"), "template_text": str(request.POST.get("template_text") or ""), }, ) profile.name = str(request.POST.get("name") or profile.name).strip() or profile.name if slug == "bp": profile.trigger_token = ".bp" profile.template_text = str(request.POST.get("template_text") or profile.template_text or "") if slug == "codex": profile.trigger_token = ".codex" profile.reply_required = False profile.exact_match_only = False profile.save( update_fields=[ "name", "trigger_token", "template_text", "reply_required", "exact_match_only", "updated_at", ] ) if slug == "bp": CommandAction.objects.get_or_create( profile=profile, action_type="extract_bp", defaults={"enabled": True, "position": 0}, ) # Keep legacy action rows in storage for compatibility and for # potential reuse by non-bp commands; bp UI now relies on # variant policies instead of exposing the generic action matrix. 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}, ) ensure_variant_policies_for_profile(profile) return self._redirect_with_scope(request) 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 "") # Legacy field retained for compatibility only. profile.visibility_mode = profile.visibility_mode or "status_in_source" profile.save() ensure_variant_policies_for_profile(profile) return self._redirect_with_scope(request) if action == "profile_delete": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) profile.delete() return self._redirect_with_scope(request) 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 self._redirect_with_scope(request) if action == "binding_delete": binding = get_object_or_404( CommandChannelBinding, id=request.POST.get("binding_id"), profile__user=request.user, ) binding.delete() return self._redirect_with_scope(request) 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 self._redirect_with_scope(request) 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 self._redirect_with_scope(request) 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 self._redirect_with_scope(request) target_idx = idx - 1 if direction == "up" else idx + 1 if target_idx < 0 or target_idx >= len(ordered): return self._redirect_with_scope(request) 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 self._redirect_with_scope(request) if action == "variant_policy_update": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) variant_key = str(request.POST.get("variant_key") or "").strip() policy = get_object_or_404( CommandVariantPolicy, profile=profile, variant_key=variant_key, ) policy.enabled = bool(request.POST.get("enabled")) mode = str(request.POST.get("generation_mode") or "verbatim").strip().lower() policy.generation_mode = mode if mode in {"ai", "verbatim"} else "verbatim" policy.send_plan_to_egress = bool(request.POST.get("send_plan_to_egress")) policy.send_status_to_source = bool(request.POST.get("send_status_to_source")) policy.send_status_to_egress = bool(request.POST.get("send_status_to_egress")) policy.store_document = bool(request.POST.get("store_document")) policy.save() return self._redirect_with_scope(request) if action == "variant_policy_reset_defaults": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) profile.variant_policies.all().delete() ensure_variant_policies_for_profile(profile) return self._redirect_with_scope(request) if action == "variant_preview": profile = get_object_or_404( CommandProfile, id=request.POST.get("profile_id"), user=request.user, ) ensure_variant_policies_for_profile(profile) service = str(request.GET.get("service") or request.POST.get("service") or "").strip() identifier = str( request.GET.get("identifier") or request.POST.get("identifier") or "" ).strip() query = f"?preview_profile_id={profile.id}" if service and identifier: query += f"&service={service}&identifier={identifier}" return redirect(f"{reverse('command_routing')}{query}") return self._redirect_with_scope(request) 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.", } )