263 lines
10 KiB
Python
263 lines
10 KiB
Python
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 {}),
|
|
"cause_samples": dict(compared.get("cause_samples") or {}),
|
|
}
|
|
)
|