from django.http import JsonResponse from django.shortcuts import render from django.views import View from core.models import ( AdapterHealthEvent, AIRequest, AIResult, AIResultSignal, Chat, ChatSession, ConversationEvent, Group, MemoryItem, Message, MessageEvent, PatternArtifactExport, PatternMitigationAutoSettings, PatternMitigationCorrection, PatternMitigationGame, PatternMitigationMessage, PatternMitigationPlan, PatternMitigationRule, Person, Persona, PersonIdentifier, QueuedMessage, WorkspaceConversation, WorkspaceMetricSnapshot, ) from core.events.projection import shadow_compare_session from core.transports.capabilities import capability_snapshot from core.views.manage.permissions import SuperUserRequiredMixin class SystemSettings(SuperUserRequiredMixin, View): template_name = "pages/system-settings.html" def _counts(self, user): return { "chat_sessions": ChatSession.objects.filter(user=user).count(), "messages": Message.objects.filter(user=user).count(), "queued_messages": QueuedMessage.objects.filter(user=user).count(), "message_events": MessageEvent.objects.filter(user=user).count(), "conversation_events": ConversationEvent.objects.filter(user=user).count(), "adapter_health_events": AdapterHealthEvent.objects.filter(user=user).count(), "workspace_conversations": WorkspaceConversation.objects.filter( user=user ).count(), "workspace_snapshots": WorkspaceMetricSnapshot.objects.filter( conversation__user=user ).count(), "ai_requests": AIRequest.objects.filter(user=user).count(), "ai_results": AIResult.objects.filter(user=user).count(), "ai_result_signals": AIResultSignal.objects.filter(user=user).count(), "memory_items": MemoryItem.objects.filter(user=user).count(), "mitigation_plans": PatternMitigationPlan.objects.filter(user=user).count(), "mitigation_rules": PatternMitigationRule.objects.filter(user=user).count(), "mitigation_games": PatternMitigationGame.objects.filter(user=user).count(), "mitigation_corrections": PatternMitigationCorrection.objects.filter( user=user ).count(), "mitigation_messages": PatternMitigationMessage.objects.filter( user=user ).count(), "mitigation_auto_settings": PatternMitigationAutoSettings.objects.filter( user=user ).count(), "mitigation_exports": PatternArtifactExport.objects.filter( user=user ).count(), "osint_people": Person.objects.filter(user=user).count(), "osint_identifiers": PersonIdentifier.objects.filter(user=user).count(), "osint_groups": Group.objects.filter(user=user).count(), "osint_personas": Persona.objects.filter(user=user).count(), } def _purge_non_osint(self, user): deleted = 0 deleted += PatternArtifactExport.objects.filter(user=user).delete()[0] deleted += PatternMitigationMessage.objects.filter(user=user).delete()[0] deleted += PatternMitigationCorrection.objects.filter(user=user).delete()[0] deleted += PatternMitigationGame.objects.filter(user=user).delete()[0] deleted += PatternMitigationRule.objects.filter(user=user).delete()[0] deleted += PatternMitigationAutoSettings.objects.filter(user=user).delete()[0] deleted += PatternMitigationPlan.objects.filter(user=user).delete()[0] deleted += AIResultSignal.objects.filter(user=user).delete()[0] deleted += AIResult.objects.filter(user=user).delete()[0] deleted += AIRequest.objects.filter(user=user).delete()[0] deleted += MemoryItem.objects.filter(user=user).delete()[0] deleted += WorkspaceMetricSnapshot.objects.filter( conversation__user=user ).delete()[0] deleted += MessageEvent.objects.filter(user=user).delete()[0] deleted += ConversationEvent.objects.filter(user=user).delete()[0] deleted += AdapterHealthEvent.objects.filter(user=user).delete()[0] deleted += Message.objects.filter(user=user).delete()[0] deleted += QueuedMessage.objects.filter(user=user).delete()[0] deleted += WorkspaceConversation.objects.filter(user=user).delete()[0] deleted += ChatSession.objects.filter(user=user).delete()[0] # Chat rows are legacy Signal cache rows and are not user-scoped. deleted += Chat.objects.all().delete()[0] return deleted def _purge_osint_people(self, user): return Person.objects.filter(user=user).delete()[0] def _purge_osint_identifiers(self, user): return PersonIdentifier.objects.filter(user=user).delete()[0] def _purge_osint_groups(self, user): return Group.objects.filter(user=user).delete()[0] def _purge_osint_personas(self, user): return Persona.objects.filter(user=user).delete()[0] def _handle_action(self, request): action = str(request.POST.get("action") or "").strip().lower() if action == "purge_non_osint": return ( "success", f"Purged {self._purge_non_osint(request.user)} non-OSINT row(s).", ) if action == "purge_osint_people": return ( "warning", f"Purged {self._purge_osint_people(request.user)} OSINT people row(s).", ) if action == "purge_osint_identifiers": return ( "warning", f"Purged {self._purge_osint_identifiers(request.user)} OSINT identifier row(s).", ) if action == "purge_osint_groups": return ( "warning", f"Purged {self._purge_osint_groups(request.user)} OSINT group row(s).", ) if action == "purge_osint_personas": return ( "warning", f"Purged {self._purge_osint_personas(request.user)} OSINT persona row(s).", ) return ("danger", "Unknown action.") def get(self, request): return render( request, self.template_name, { "counts": self._counts(request.user), "notice_level": "", "notice_message": "", }, ) def post(self, request): notice_level, notice_message = self._handle_action(request) return render( request, self.template_name, { "counts": self._counts(request.user), "notice_level": notice_level, "notice_message": notice_message, }, ) class ServiceCapabilitySnapshotAPI(SuperUserRequiredMixin, View): def get(self, request): service = str(request.GET.get("service") or "").strip().lower() return JsonResponse( { "ok": True, "data": capability_snapshot(service), } ) class AdapterHealthSummaryAPI(SuperUserRequiredMixin, View): def get(self, request): latest_by_service = {} rows = AdapterHealthEvent.objects.order_by("service", "-ts")[:200] for row in rows: key = str(row.service or "").strip().lower() if key in latest_by_service: continue latest_by_service[key] = { "status": str(row.status or ""), "reason": str(row.reason or ""), "ts": int(row.ts or 0), "created_at": row.created_at.isoformat(), } return JsonResponse({"ok": True, "services": latest_by_service}) class TraceDiagnosticsAPI(SuperUserRequiredMixin, View): def get(self, request): trace_id = str(request.GET.get("trace_id") or "").strip() if not trace_id: return JsonResponse( {"ok": False, "error": "trace_id_required"}, status=400, ) rows = list( ConversationEvent.objects.filter( user=request.user, trace_id=trace_id, ) .select_related("session") .order_by("ts", "created_at")[:500] ) return JsonResponse( { "ok": True, "trace_id": trace_id, "count": len(rows), "events": [ { "id": str(row.id), "ts": int(row.ts or 0), "event_type": str(row.event_type or ""), "direction": str(row.direction or ""), "session_id": str(row.session_id or ""), "origin_transport": str(row.origin_transport or ""), "origin_message_id": str(row.origin_message_id or ""), "payload": dict(row.payload or {}), } for row in rows ], } ) class EventProjectionShadowAPI(SuperUserRequiredMixin, View): def get(self, request): session_id = str(request.GET.get("session_id") or "").strip() if not session_id: return JsonResponse( {"ok": False, "error": "session_id_required"}, status=400, ) detail_limit = int(request.GET.get("detail_limit") or 25) session = ChatSession.objects.filter( id=session_id, user=request.user, ).first() if session is None: return JsonResponse( {"ok": False, "error": "session_not_found"}, status=404, ) compared = shadow_compare_session(session, detail_limit=max(0, detail_limit)) return JsonResponse( { "ok": True, "result": compared, "cause_summary": dict(compared.get("cause_counts") or {}), } )