""" Analytics & Reporting Views for IGNY8 Admin """ from django.contrib.admin.views.decorators import staff_member_required from django.shortcuts import render from django.db.models import Count, Sum, Avg, Q from django.utils import timezone from datetime import timedelta import json @staff_member_required def revenue_report(request): """Revenue and billing analytics""" from igny8_core.business.billing.models import Payment from igny8_core.auth.models import Plan # Date ranges today = timezone.now() months = [] monthly_revenue = [] for i in range(6): month_start = today.replace(day=1) - timedelta(days=30*i) month_end = month_start.replace(day=28) + timedelta(days=4) revenue = Payment.objects.filter( status='succeeded', processed_at__gte=month_start, processed_at__lt=month_end ).aggregate(total=Sum('amount'))['total'] or 0 months.insert(0, month_start.strftime('%b %Y')) monthly_revenue.insert(0, float(revenue)) # Plan distribution plan_distribution = Plan.objects.annotate( account_count=Count('accounts') ).values('name', 'account_count') # Payment method breakdown payment_methods = Payment.objects.filter( status='succeeded' ).values('payment_method').annotate( count=Count('id'), total=Sum('amount') ).order_by('-total') # Total revenue all time total_revenue = Payment.objects.filter( status='succeeded' ).aggregate(total=Sum('amount'))['total'] or 0 context = { 'title': 'Revenue Report', 'months': json.dumps(months), 'monthly_revenue': json.dumps(monthly_revenue), 'plan_distribution': list(plan_distribution), 'payment_methods': list(payment_methods), 'total_revenue': float(total_revenue), } # Merge with admin context from igny8_core.admin.site import admin_site admin_context = admin_site.each_context(request) context.update(admin_context) return render(request, 'admin/reports/revenue.html', context) @staff_member_required def usage_report(request): """Credit usage and AI operations analytics""" from igny8_core.business.billing.models import CreditUsageLog # Usage by operation type usage_by_operation = CreditUsageLog.objects.values( 'operation_type' ).annotate( total_credits=Sum('credits_used'), total_cost=Sum('cost_usd'), operation_count=Count('id') ).order_by('-total_credits') # Format operation types as Title Case for usage in usage_by_operation: usage['operation_type'] = usage['operation_type'].replace('_', ' ').title() if usage['operation_type'] else 'Unknown' # Top credit consumers top_consumers = CreditUsageLog.objects.values( 'account__name' ).annotate( total_credits=Sum('credits_used'), operation_count=Count('id') ).order_by('-total_credits')[:10] # Model usage distribution model_usage = CreditUsageLog.objects.values( 'model_used' ).annotate( usage_count=Count('id') ).order_by('-usage_count') # Total credits used total_credits = CreditUsageLog.objects.aggregate( total=Sum('credits_used') )['total'] or 0 context = { 'title': 'Usage Report', 'usage_by_operation': list(usage_by_operation), 'top_consumers': list(top_consumers), 'model_usage': list(model_usage), 'total_credits': int(total_credits), } # Merge with admin context from igny8_core.admin.site import admin_site admin_context = admin_site.each_context(request) context.update(admin_context) return render(request, 'admin/reports/usage.html', context) @staff_member_required def content_report(request): """Content production analytics""" from igny8_core.modules.writer.models import Content, Tasks # Content by type content_by_type = Content.objects.values( 'content_type' ).annotate(count=Count('id')).order_by('-count') # Production timeline (last 30 days) days = [] daily_counts = [] for i in range(30): day = timezone.now().date() - timedelta(days=i) count = Content.objects.filter(created_at__date=day).count() days.insert(0, day.strftime('%m/%d')) daily_counts.insert(0, count) # Average word count by content type avg_words = Content.objects.values('content_type').annotate( avg_words=Avg('word_count') ).order_by('-avg_words') # Task completion rate total_tasks = Tasks.objects.count() completed_tasks = Tasks.objects.filter(status='completed').count() completion_rate = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0 # Total content produced total_content = Content.objects.count() context = { 'title': 'Content Production Report', 'content_by_type': list(content_by_type), 'days': json.dumps(days), 'daily_counts': json.dumps(daily_counts), 'avg_words': list(avg_words), 'completion_rate': round(completion_rate, 1), 'total_content': total_content, 'total_tasks': total_tasks, 'completed_tasks': completed_tasks, } # Merge with admin context from igny8_core.admin.site import admin_site admin_context = admin_site.each_context(request) context.update(admin_context) return render(request, 'admin/reports/content.html', context) @staff_member_required def data_quality_report(request): """Check data quality and integrity""" issues = [] # Orphaned content (no site) from igny8_core.modules.writer.models import Content orphaned_content = Content.objects.filter(site__isnull=True).count() if orphaned_content > 0: issues.append({ 'severity': 'warning', 'type': 'Orphaned Records', 'count': orphaned_content, 'description': 'Content items without assigned site', 'action_url': '/admin/writer/content/?site__isnull=True' }) # Tasks without clusters from igny8_core.modules.writer.models import Tasks tasks_no_cluster = Tasks.objects.filter(cluster__isnull=True).count() if tasks_no_cluster > 0: issues.append({ 'severity': 'info', 'type': 'Missing Relationships', 'count': tasks_no_cluster, 'description': 'Tasks without assigned cluster', 'action_url': '/admin/writer/tasks/?cluster__isnull=True' }) # Accounts with negative credits from igny8_core.auth.models import Account negative_credits = Account.objects.filter(credits__lt=0).count() if negative_credits > 0: issues.append({ 'severity': 'error', 'type': 'Data Integrity', 'count': negative_credits, 'description': 'Accounts with negative credit balance', 'action_url': '/admin/igny8_core_auth/account/?credits__lt=0' }) # Duplicate keywords from igny8_core.modules.planner.models import Keywords duplicates = Keywords.objects.values('seed_keyword', 'site', 'sector').annotate( count=Count('id') ).filter(count__gt=1).count() if duplicates > 0: issues.append({ 'severity': 'warning', 'type': 'Duplicates', 'count': duplicates, 'description': 'Duplicate keywords for same site/sector', 'action_url': '/admin/planner/keywords/' }) # Content without SEO data no_seo = Content.objects.filter( Q(meta_title__isnull=True) | Q(meta_title='') | Q(meta_description__isnull=True) | Q(meta_description='') ).count() if no_seo > 0: issues.append({ 'severity': 'info', 'type': 'Incomplete Data', 'count': no_seo, 'description': 'Content missing SEO metadata', 'action_url': '/admin/writer/content/' }) context = { 'title': 'Data Quality Report', 'issues': issues, 'total_issues': len(issues), } # Merge with admin context from igny8_core.admin.site import admin_site admin_context = admin_site.each_context(request) context.update(admin_context) return render(request, 'admin/reports/data_quality.html', context) @staff_member_required def token_usage_report(request): """Comprehensive token usage analytics with multi-dimensional insights""" from igny8_core.business.billing.models import CreditUsageLog from igny8_core.auth.models import Account from decimal import Decimal # Date filter setup days_filter = request.GET.get('days', '30') try: days = int(days_filter) except ValueError: days = 30 start_date = timezone.now() - timedelta(days=days) # Base queryset - include all records (tokens may be 0 for historical data) logs = CreditUsageLog.objects.filter( created_at__gte=start_date ) # Total statistics total_tokens_input = logs.aggregate(total=Sum('tokens_input'))['total'] or 0 total_tokens_output = logs.aggregate(total=Sum('tokens_output'))['total'] or 0 total_tokens = total_tokens_input + total_tokens_output total_calls = logs.count() avg_tokens_per_call = total_tokens / total_calls if total_calls > 0 else 0 # Token usage by model token_by_model = logs.values('model_used').annotate( total_tokens_input=Sum('tokens_input'), total_tokens_output=Sum('tokens_output'), call_count=Count('id'), total_cost=Sum('cost_usd') ).order_by('-total_tokens_input')[:10] # Add total_tokens to each model and sort by total for model in token_by_model: model['total_tokens'] = (model['total_tokens_input'] or 0) + (model['total_tokens_output'] or 0) model['avg_tokens'] = model['total_tokens'] / model['call_count'] if model['call_count'] > 0 else 0 model['model'] = model['model_used'] # Add alias for template token_by_model = sorted(token_by_model, key=lambda x: x['total_tokens'], reverse=True) # Token usage by function/operation token_by_function = logs.values('operation_type').annotate( total_tokens_input=Sum('tokens_input'), total_tokens_output=Sum('tokens_output'), call_count=Count('id'), total_cost=Sum('cost_usd') ).order_by('-total_tokens_input')[:10] # Add total_tokens to each function and sort by total for func in token_by_function: func['total_tokens'] = (func['total_tokens_input'] or 0) + (func['total_tokens_output'] or 0) func['avg_tokens'] = func['total_tokens'] / func['call_count'] if func['call_count'] > 0 else 0 # Format operation_type as Title Case func['function'] = func['operation_type'].replace('_', ' ').title() if func['operation_type'] else 'Unknown' token_by_function = sorted(token_by_function, key=lambda x: x['total_tokens'], reverse=True) # Token usage by account (top consumers) token_by_account = logs.values('account__name', 'account_id').annotate( total_tokens_input=Sum('tokens_input'), total_tokens_output=Sum('tokens_output'), call_count=Count('id'), total_cost=Sum('cost_usd') ).order_by('-total_tokens_input')[:15] # Add total_tokens to each account and sort by total for account in token_by_account: account['total_tokens'] = (account['total_tokens_input'] or 0) + (account['total_tokens_output'] or 0) token_by_account = sorted(token_by_account, key=lambda x: x['total_tokens'], reverse=True)[:15] # Daily token trends (time series) daily_data = [] daily_labels = [] for i in range(days): day = timezone.now().date() - timedelta(days=days-i-1) day_logs = logs.filter(created_at__date=day) day_tokens_input = day_logs.aggregate(total=Sum('tokens_input'))['total'] or 0 day_tokens_output = day_logs.aggregate(total=Sum('tokens_output'))['total'] or 0 day_tokens = day_tokens_input + day_tokens_output daily_labels.append(day.strftime('%m/%d')) daily_data.append(int(day_tokens)) # Token efficiency metrics (CreditUsageLog doesn't have error field, so assume all successful) success_rate = 100.0 successful_tokens = total_tokens wasted_tokens = 0 # Create tokens_by_status for template compatibility tokens_by_status = [{ 'error': None, 'total_tokens': total_tokens, 'call_count': total_calls, 'avg_tokens': avg_tokens_per_call }] # Peak usage times (hour of day) hourly_usage = logs.extra( select={'hour': "EXTRACT(hour FROM created_at)"} ).values('hour').annotate( token_input=Sum('tokens_input'), token_output=Sum('tokens_output'), call_count=Count('id') ).order_by('hour') # Add total token_count for each hour for hour_data in hourly_usage: hour_data['token_count'] = (hour_data['token_input'] or 0) + (hour_data['token_output'] or 0) # Cost efficiency total_cost = logs.aggregate(total=Sum('cost_usd'))['total'] or Decimal('0.00') cost_per_1k_tokens = float(total_cost) / (total_tokens / 1000) if total_tokens > 0 else 0.0 context = { 'title': 'Token Usage Report', 'days_filter': days, 'total_tokens': int(total_tokens), 'total_calls': total_calls, 'avg_tokens_per_call': round(avg_tokens_per_call, 2), 'token_by_model': list(token_by_model), 'token_by_function': list(token_by_function), 'token_by_account': list(token_by_account), 'daily_labels': json.dumps(daily_labels), 'daily_data': json.dumps(daily_data), 'tokens_by_status': list(tokens_by_status), 'success_rate': round(success_rate, 2), 'successful_tokens': int(successful_tokens), 'wasted_tokens': int(wasted_tokens), 'hourly_usage': list(hourly_usage), 'total_cost': float(total_cost), 'cost_per_1k_tokens': float(cost_per_1k_tokens), 'current_app': '_reports', # For active menu state } # Merge with admin context from igny8_core.admin.site import admin_site admin_context = admin_site.each_context(request) context.update(admin_context) return render(request, 'admin/reports/token_usage.html', context) @staff_member_required def ai_cost_analysis(request): """Multi-dimensional AI cost analysis with model pricing, trends, and predictions""" from igny8_core.business.billing.models import CreditUsageLog from igny8_core.auth.models import Account from decimal import Decimal # Date filter setup days_filter = request.GET.get('days', '30') try: days = int(days_filter) except ValueError: days = 30 start_date = timezone.now() - timedelta(days=days) # Base queryset - filter for records with cost data logs = CreditUsageLog.objects.filter( created_at__gte=start_date, cost_usd__isnull=False ) # Overall cost metrics total_cost = logs.aggregate(total=Sum('cost_usd'))['total'] or Decimal('0.00') total_calls = logs.count() avg_cost_per_call = logs.aggregate(avg=Avg('cost_usd'))['avg'] or Decimal('0.00') total_tokens_input = logs.aggregate(total=Sum('tokens_input'))['total'] or 0 total_tokens_output = logs.aggregate(total=Sum('tokens_output'))['total'] or 0 total_tokens = total_tokens_input + total_tokens_output # Revenue & Margin calculation from igny8_core.business.billing.models import BillingConfiguration billing_config = BillingConfiguration.get_config() total_credits_charged = logs.aggregate(total=Sum('credits_used'))['total'] or 0 total_revenue = Decimal(total_credits_charged) * billing_config.default_credit_price_usd total_margin = total_revenue - total_cost margin_percentage = float((total_margin / total_revenue * 100) if total_revenue > 0 else 0) # Per-unit margins # Calculate per 1M tokens (margin per million tokens) margin_per_1m_tokens = float(total_margin) / (total_tokens / 1_000_000) if total_tokens > 0 else 0 # Calculate per 1K credits (margin per thousand credits) margin_per_1k_credits = float(total_margin) / (total_credits_charged / 1000) if total_credits_charged > 0 else 0 # Cost by model with efficiency metrics cost_by_model = logs.values('model_used').annotate( total_cost=Sum('cost_usd'), call_count=Count('id'), avg_cost=Avg('cost_usd'), total_tokens_input=Sum('tokens_input'), total_tokens_output=Sum('tokens_output') ).order_by('-total_cost') # Add cost efficiency and margin for each model for model in cost_by_model: model['total_tokens'] = (model['total_tokens_input'] or 0) + (model['total_tokens_output'] or 0) model['avg_tokens'] = model['total_tokens'] / model['call_count'] if model['call_count'] > 0 else 0 model['model'] = model['model_used'] # Add alias for template if model['total_tokens'] and model['total_tokens'] > 0: model['cost_per_1k_tokens'] = float(model['total_cost']) / (model['total_tokens'] / 1000) else: model['cost_per_1k_tokens'] = 0 # Calculate margin for this model model_credits = logs.filter(model_used=model['model_used']).aggregate(total=Sum('credits_used'))['total'] or 0 model_revenue = Decimal(model_credits) * billing_config.default_credit_price_usd model_margin = model_revenue - model['total_cost'] model['revenue'] = float(model_revenue) model['margin'] = float(model_margin) model['margin_percentage'] = float((model_margin / model_revenue * 100) if model_revenue > 0 else 0) # Cost by account (top spenders) cost_by_account = logs.values('account__name', 'account_id').annotate( total_cost=Sum('cost_usd'), call_count=Count('id'), total_tokens_input=Sum('tokens_input'), total_tokens_output=Sum('tokens_output'), avg_cost=Avg('cost_usd') ).order_by('-total_cost')[:15] # Add total_tokens to each account for account in cost_by_account: account['total_tokens'] = (account['total_tokens_input'] or 0) + (account['total_tokens_output'] or 0) # Cost by function/operation cost_by_function = logs.values('operation_type').annotate( total_cost=Sum('cost_usd'), call_count=Count('id'), avg_cost=Avg('cost_usd'), total_tokens_input=Sum('tokens_input'), total_tokens_output=Sum('tokens_output') ).order_by('-total_cost')[:10] # Add total_tokens, function alias, and margin for func in cost_by_function: func['total_tokens'] = (func['total_tokens_input'] or 0) + (func['total_tokens_output'] or 0) # Format operation_type as Title Case func['function'] = func['operation_type'].replace('_', ' ').title() if func['operation_type'] else 'Unknown' # Calculate margin for this operation func_credits = logs.filter(operation_type=func['operation_type']).aggregate(total=Sum('credits_used'))['total'] or 0 func_revenue = Decimal(func_credits) * billing_config.default_credit_price_usd func_margin = func_revenue - func['total_cost'] func['revenue'] = float(func_revenue) func['margin'] = float(func_margin) func['margin_percentage'] = float((func_margin / func_revenue * 100) if func_revenue > 0 else 0) # Daily cost trends (time series) daily_cost_data = [] daily_cost_labels = [] daily_call_data = [] for i in range(days): day = timezone.now().date() - timedelta(days=days-i-1) day_logs = logs.filter(created_at__date=day) day_cost = day_logs.aggregate(total=Sum('cost_usd'))['total'] or Decimal('0.00') day_calls = day_logs.count() daily_cost_labels.append(day.strftime('%m/%d')) daily_cost_data.append(float(day_cost)) daily_call_data.append(day_calls) # Cost prediction (simple linear extrapolation) if len(daily_cost_data) > 7: recent_avg_daily = sum(daily_cost_data[-7:]) / 7 projected_monthly = recent_avg_daily * 30 else: projected_monthly = 0 # Failed requests cost (CreditUsageLog doesn't track errors, so no failed cost) failed_cost = Decimal('0.00') # Cost anomalies (calls costing > 3x average) if avg_cost_per_call > 0: anomaly_threshold = float(avg_cost_per_call) * 3 anomalies = logs.filter(cost_usd__gt=anomaly_threshold).values( 'model_used', 'operation_type', 'account__name', 'cost_usd', 'tokens_input', 'tokens_output', 'created_at' ).order_by('-cost_usd')[:10] # Add aliases and calculate total tokens for each anomaly for anomaly in anomalies: anomaly['model'] = anomaly['model_used'] # Format operation_type as Title Case anomaly['function'] = anomaly['operation_type'].replace('_', ' ').title() if anomaly['operation_type'] else 'Unknown' anomaly['cost'] = anomaly['cost_usd'] anomaly['tokens'] = (anomaly['tokens_input'] or 0) + (anomaly['tokens_output'] or 0) else: anomalies = [] # Model comparison matrix model_comparison = [] for model_data in cost_by_model: model_name = model_data['model'] model_comparison.append({ 'model': model_name, 'total_cost': float(model_data['total_cost']), 'calls': model_data['call_count'], 'avg_cost': float(model_data['avg_cost']), 'total_tokens': model_data['total_tokens'], 'cost_per_1k': model_data['cost_per_1k_tokens'], }) # Cost distribution percentages if total_cost > 0: for item in cost_by_model: item['cost_percentage'] = float((item['total_cost'] / total_cost) * 100) # Peak cost hours hourly_cost = logs.extra( select={'hour': "EXTRACT(hour FROM created_at)"} ).values('hour').annotate( total_cost=Sum('cost_usd'), call_count=Count('id') ).order_by('hour') # Cost efficiency score (CreditUsageLog doesn't track errors, assume all successful) successful_cost = total_cost efficiency_score = 100.0 context = { 'title': 'AI Cost & Margin Analysis', 'days_filter': days, 'total_cost': float(total_cost), 'total_revenue': float(total_revenue), 'total_margin': float(total_margin), 'margin_percentage': round(margin_percentage, 2), 'margin_per_1m_tokens': round(margin_per_1m_tokens, 4), 'margin_per_1k_credits': round(margin_per_1k_credits, 4), 'total_credits_charged': total_credits_charged, 'credit_price': float(billing_config.default_credit_price_usd), 'total_calls': total_calls, 'avg_cost_per_call': float(avg_cost_per_call), 'total_tokens': int(total_tokens), 'cost_by_model': list(cost_by_model), 'cost_by_account': list(cost_by_account), 'cost_by_function': list(cost_by_function), 'daily_cost_labels': json.dumps(daily_cost_labels), 'daily_cost_data': json.dumps(daily_cost_data), 'daily_call_data': json.dumps(daily_call_data), 'projected_monthly': round(projected_monthly, 2), 'failed_cost': float(failed_cost), 'wasted_percentage': float((failed_cost / total_cost * 100) if total_cost > 0 else 0), 'anomalies': list(anomalies), 'model_comparison': model_comparison, 'hourly_cost': list(hourly_cost), 'efficiency_score': round(efficiency_score, 2), 'successful_cost': float(successful_cost), 'current_app': '_reports', # For active menu state } # Merge with admin context from igny8_core.admin.site import admin_site admin_context = admin_site.each_context(request) context.update(admin_context) return render(request, 'admin/reports/ai_cost_analysis.html', context)