from datetime import timedelta from django.contrib import admin from django.db.models import Avg, Count, DurationField, ExpressionWrapper, F, Max, Q, Sum from django.template.response import TemplateResponse from django.utils import timezone from unfold.admin import ModelAdmin from apps.account.models import User from .models import ApiCallLog, AppSession, ClickEvent, ClientErrorLog, PageView # مانیتورینگِ کلیِ اپ توسط پنل ادمین: صفحه‌ی changelist مدلِ AppSession # جایگزینِ یک داشبوردِ تجمیعی می‌شود (نه لیستِ خامِ ردیف‌ها) — برای دیدنِ # لیستِ خام هر رکورد، لینکِ «نمای خام» بالای همان صفحه با ?raw=1 به همان # changelist معمولیِ جنگو می‌رود. _WINDOW_DAYS = 30 @admin.register(AppSession) class AppSessionAdmin(ModelAdmin): list_display = ("user", "platform", "started_at", "ended_at", "ip_address") list_filter = ("platform",) search_fields = ("user__phone", "ip_address") date_hierarchy = "started_at" def changelist_view(self, request, extra_context=None): if request.GET.get("raw") == "1": return super().changelist_view(request, extra_context) return self._dashboard_view(request) def _dashboard_view(self, request): context = { **self.admin_site.each_context(request), "title": "مانیتورینگ اپ", "opts": self.model._meta, **self._build_dashboard_context(), } return TemplateResponse(request, "admin/telemetry/dashboard.html", context) def _build_dashboard_context(self): now = timezone.now() cutoff = now - timedelta(days=_WINDOW_DAYS) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) sessions_qs = AppSession.objects.filter(started_at__gte=cutoff) api_qs = ApiCallLog.objects.filter(created_at__gte=cutoff) pageviews_qs = PageView.objects.filter(entered_at__gte=cutoff) errors_qs = ClientErrorLog.objects.filter(created_at__gte=cutoff) overview = { "active_users_today": sessions_qs.filter(started_at__gte=today_start) .values("user") .distinct() .count(), "sessions_today": sessions_qs.filter(started_at__gte=today_start).count(), "sessions_window": sessions_qs.count(), "page_views_today": pageviews_qs.filter(entered_at__gte=today_start).count(), "api_calls_today": api_qs.filter(created_at__gte=today_start).count(), "errors_today": errors_qs.filter(created_at__gte=today_start).count(), "avg_response_ms": round(api_qs.aggregate(v=Avg("duration_ms"))["v"] or 0, 1), } ended = sessions_qs.exclude(ended_at__isnull=True).annotate( dur=ExpressionWrapper(F("ended_at") - F("started_at"), output_field=DurationField()) ) avg_duration = ended.aggregate(v=Avg("dur"))["v"] overview["avg_session_minutes"] = round(avg_duration.total_seconds() / 60, 1) if avg_duration else 0 top_screens = list( pageviews_qs.values("screen_name") .annotate(count=Count("id"), avg_duration=Avg("duration_seconds")) .order_by("-count")[:10] ) max_screen_count = max((s["count"] for s in top_screens), default=1) or 1 for s in top_screens: s["bar_percent"] = round(100 * s["count"] / max_screen_count) s["avg_duration"] = round(s["avg_duration"], 1) if s["avg_duration"] else None endpoints = list( api_qs.values("path", "method") .annotate( count=Count("id"), avg_ms=Avg("duration_ms"), max_ms=Max("duration_ms"), error_count=Count("id", filter=Q(status_code__gte=400)), ) .order_by("-count")[:20] ) for e in endpoints: e["avg_ms"] = round(e["avg_ms"], 1) if e["avg_ms"] else 0 recent_errors = list(errors_qs.select_related("session__user")[:20]) active_user_ids = sessions_qs.values_list("user_id", flat=True).distinct() user_rows = [] for user in User.objects.filter(id__in=active_user_ids): user_sessions = sessions_qs.filter(user=user) ended_user = user_sessions.exclude(ended_at__isnull=True).annotate( dur=ExpressionWrapper(F("ended_at") - F("started_at"), output_field=DurationField()) ) total_dur = ended_user.aggregate(v=Sum("dur"))["v"] ips = list( user_sessions.exclude(ip_address__isnull=True) .values_list("ip_address", flat=True) .distinct() ) top_screen = ( pageviews_qs.filter(session__user=user) .values("screen_name") .annotate(c=Count("id")) .order_by("-c") .first() ) last_session = user_sessions.order_by("-started_at").first() user_rows.append( { "user": user, "session_count": user_sessions.count(), "total_minutes": round(total_dur.total_seconds() / 60, 1) if total_dur else 0, "page_view_count": pageviews_qs.filter(session__user=user).count(), "api_call_count": api_qs.filter(user=user).count(), "error_count": errors_qs.filter(session__user=user).count(), "ips": ips, "referral_count": User.objects.filter( referred_by=user, referral_bonus_granted=True ).count(), "top_screen": top_screen["screen_name"] if top_screen else "—", "last_seen": last_session.started_at if last_session else None, } ) user_rows.sort(key=lambda r: r["total_minutes"], reverse=True) overview_cards = [ ("کاربران فعال امروز", overview["active_users_today"]), ("جلسه‌های امروز", overview["sessions_today"]), (f"جلسه‌ها ({_WINDOW_DAYS} روز اخیر)", overview["sessions_window"]), ("میانگین طول جلسه (دقیقه)", overview["avg_session_minutes"]), ("صفحه‌بازدید امروز", overview["page_views_today"]), ("فراخوانی API امروز", overview["api_calls_today"]), ("میانگین زمان پاسخ (ms)", overview["avg_response_ms"]), ("خطای امروز", overview["errors_today"]), ] return { "overview": overview, "overview_cards": overview_cards, "top_screens": top_screens, "endpoints": endpoints, "recent_errors": recent_errors, "user_rows": user_rows[:50], "window_days": _WINDOW_DAYS, } @admin.register(ApiCallLog) class ApiCallLogAdmin(ModelAdmin): list_display = ("method", "path", "status_code", "duration_ms", "ip_address", "user", "created_at") list_filter = ("method", "status_code") search_fields = ("path", "user__phone", "ip_address") date_hierarchy = "created_at" @admin.register(PageView) class PageViewAdmin(ModelAdmin): list_display = ("screen_name", "session", "entered_at", "duration_seconds") list_filter = ("screen_name",) search_fields = ("screen_name",) @admin.register(ClickEvent) class ClickEventAdmin(ModelAdmin): list_display = ("screen_name", "element", "session", "created_at") list_filter = ("screen_name",) search_fields = ("screen_name", "element") @admin.register(ClientErrorLog) class ClientErrorLogAdmin(ModelAdmin): list_display = ("screen_name", "message", "session", "created_at") list_filter = ("screen_name",) search_fields = ("screen_name", "message")