Files
igny8/backend/igny8_core/api/dashboard_views.py
IGNY8 VPS (Salman) b9e4b6f7e2 final plolish phase 2
2025-12-27 15:25:05 +00:00

382 lines
14 KiB
Python

"""
Dashboard API Views
Provides aggregated data for the frontend dashboard in a single call.
Replaces multiple sequential API calls for better performance.
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.db.models import Count, Sum, Q, F
from django.utils import timezone
from datetime import timedelta
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
from igny8_core.auth.models import Site, Sector
from igny8_core.business.planning.models import Keywords, Clusters, ContentIdeas
from igny8_core.business.content.models import Tasks, Content
from igny8_core.business.billing.models import CreditUsageLog
from igny8_core.ai.models import AITaskLog
@extend_schema_view(
summary=extend_schema(
tags=['Dashboard'],
summary='Get dashboard summary',
description='Returns aggregated dashboard data including pipeline counts, AI operations, recent activity, and items needing attention.',
parameters=[
OpenApiParameter(
name='site_id',
description='Filter by specific site ID',
required=False,
type=int
),
OpenApiParameter(
name='days',
description='Number of days for recent activity and AI operations (default: 7)',
required=False,
type=int
),
]
),
)
class DashboardSummaryViewSet(viewsets.ViewSet):
"""Dashboard summary providing aggregated data for the main dashboard."""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def summary(self, request):
"""
Get comprehensive dashboard summary in a single API call.
Returns:
- needs_attention: Items requiring user action
- pipeline: Workflow pipeline counts (keywords → published)
- ai_operations: Recent AI usage stats
- recent_activity: Latest activity log
- content_velocity: Content creation trends
- automation: Automation status summary
"""
account = request.user.account
site_id = request.query_params.get('site_id')
days = int(request.query_params.get('days', 7))
start_date = timezone.now() - timedelta(days=days)
# Build base filters
site_filter = Q(site__account=account)
if site_id:
site_filter &= Q(site_id=site_id)
# ==========================================
# 1. PIPELINE COUNTS
# ==========================================
keywords_count = Keywords.objects.filter(site_filter).count()
clusters_count = Clusters.objects.filter(site_filter).count()
ideas_count = ContentIdeas.objects.filter(site_filter).count()
tasks_count = Tasks.objects.filter(site_filter).count()
content_filter = site_filter
drafts_count = Content.objects.filter(content_filter, status='draft').count()
review_count = Content.objects.filter(content_filter, status='review').count()
published_count = Content.objects.filter(content_filter, status='published').count()
total_content = drafts_count + review_count + published_count
# Calculate completion percentage based on workflow milestones
milestones = [
keywords_count > 0,
clusters_count > 0,
ideas_count > 0,
tasks_count > 0,
total_content > 0,
published_count > 0,
]
completion_percentage = int((sum(milestones) / len(milestones)) * 100) if milestones else 0
pipeline = {
'keywords': keywords_count,
'clusters': clusters_count,
'ideas': ideas_count,
'tasks': tasks_count,
'drafts': drafts_count,
'review': review_count,
'published': published_count,
'total_content': total_content,
'completion_percentage': completion_percentage,
}
# ==========================================
# 2. NEEDS ATTENTION
# ==========================================
needs_attention = []
# Content pending review
if review_count > 0:
needs_attention.append({
'id': 'pending-review',
'type': 'pending_review',
'title': 'pending review',
'count': review_count,
'action_label': 'Review',
'action_url': '/writer/review',
'severity': 'warning',
})
# Sites without keywords (incomplete setup)
sites = Site.objects.filter(account=account, is_active=True)
sites_without_keywords = []
for site in sites:
kw_count = Keywords.objects.filter(site=site).count()
if kw_count == 0:
sites_without_keywords.append(site)
if sites_without_keywords:
if len(sites_without_keywords) == 1:
needs_attention.append({
'id': 'setup-incomplete',
'type': 'setup_incomplete',
'title': f'{sites_without_keywords[0].name} needs setup',
'action_label': 'Complete',
'action_url': f'/sites/{sites_without_keywords[0].id}',
'severity': 'info',
})
else:
needs_attention.append({
'id': 'setup-incomplete',
'type': 'setup_incomplete',
'title': f'{len(sites_without_keywords)} sites need setup',
'action_label': 'Complete',
'action_url': '/sites',
'severity': 'info',
})
# Sites without integrations
sites_without_integration = sites.filter(has_integration=False).count()
if sites_without_integration > 0:
needs_attention.append({
'id': 'no-integration',
'type': 'no_integration',
'title': f'{sites_without_integration} site{"s" if sites_without_integration > 1 else ""} without WordPress',
'action_label': 'Connect',
'action_url': '/integrations',
'severity': 'info',
})
# Low credits warning
if account.credits < 100:
needs_attention.append({
'id': 'credits-low',
'type': 'credits_low',
'title': f'Credits running low ({account.credits} remaining)',
'action_label': 'Upgrade',
'action_url': '/billing/plans',
'severity': 'warning' if account.credits > 20 else 'error',
})
# Queued tasks not processed
queued_tasks = Tasks.objects.filter(site_filter, status='queued').count()
if queued_tasks > 10:
needs_attention.append({
'id': 'queued-tasks',
'type': 'queued_tasks',
'title': f'{queued_tasks} tasks waiting to be generated',
'action_label': 'Generate',
'action_url': '/writer/tasks',
'severity': 'info',
})
# ==========================================
# 3. AI OPERATIONS (last N days)
# ==========================================
ai_usage = CreditUsageLog.objects.filter(
account=account,
created_at__gte=start_date
)
# Group by operation type
operations_by_type = ai_usage.values('operation_type').annotate(
count=Count('id'),
credits=Sum('credits_used'),
tokens=Sum('tokens_input') + Sum('tokens_output')
).order_by('-count')
# Format operation names
operation_display = {
'clustering': 'Clustering',
'idea_generation': 'Ideas',
'content_generation': 'Content',
'image_generation': 'Images',
'image_prompt_extraction': 'Image Prompts',
'linking': 'Linking',
'optimization': 'Optimization',
'reparse': 'Reparse',
'site_page_generation': 'Site Pages',
'site_structure_generation': 'Site Structure',
'ideas': 'Ideas',
'content': 'Content',
'images': 'Images',
}
operations = []
for op in operations_by_type[:5]: # Top 5 operations
operations.append({
'type': op['operation_type'],
'label': operation_display.get(op['operation_type'], op['operation_type'].replace('_', ' ').title()),
'count': op['count'],
'credits': op['credits'] or 0,
'tokens': op['tokens'] or 0,
})
total_credits_used = ai_usage.aggregate(total=Sum('credits_used'))['total'] or 0
total_operations = ai_usage.count()
ai_operations = {
'period_days': days,
'operations': operations,
'totals': {
'credits': total_credits_used,
'operations': total_operations,
}
}
# ==========================================
# 4. RECENT ACTIVITY
# ==========================================
recent_logs = AITaskLog.objects.filter(
account=account,
status='success',
created_at__gte=start_date
).order_by('-created_at')[:10]
activity_icons = {
'run_clustering': 'group',
'generate_content_ideas': 'bolt',
'generate_content': 'file-text',
'generate_images': 'image',
'publish_content': 'paper-plane',
'optimize_content': 'sparkles',
'link_content': 'link',
}
activity_colors = {
'run_clustering': 'purple',
'generate_content_ideas': 'orange',
'generate_content': 'blue',
'generate_images': 'pink',
'publish_content': 'green',
'optimize_content': 'cyan',
'link_content': 'indigo',
}
recent_activity = []
for log in recent_logs:
# Parse friendly message from the log
message = log.message or f'{log.function_name} completed'
recent_activity.append({
'id': log.id,
'type': log.function_name,
'description': message,
'timestamp': log.created_at.isoformat(),
'icon': activity_icons.get(log.function_name, 'bolt'),
'color': activity_colors.get(log.function_name, 'gray'),
'credits': float(log.cost) if log.cost else 0,
})
# ==========================================
# 5. CONTENT VELOCITY
# ==========================================
# Content created in different periods
now = timezone.now()
content_today = Content.objects.filter(
content_filter,
created_at__date=now.date()
).count()
content_this_week = Content.objects.filter(
content_filter,
created_at__gte=now - timedelta(days=7)
).count()
content_this_month = Content.objects.filter(
content_filter,
created_at__gte=now - timedelta(days=30)
).count()
# Daily breakdown for last 7 days
daily_content = []
for i in range(7):
day = now - timedelta(days=6-i)
count = Content.objects.filter(
content_filter,
created_at__date=day.date()
).count()
daily_content.append({
'date': day.date().isoformat(),
'count': count,
})
content_velocity = {
'today': content_today,
'this_week': content_this_week,
'this_month': content_this_month,
'daily': daily_content,
'average_per_day': round(content_this_week / 7, 1) if content_this_week else 0,
}
# ==========================================
# 6. AUTOMATION STATUS
# ==========================================
# Check automation settings
from igny8_core.business.automation.models import AutomationSettings
automation_enabled = AutomationSettings.objects.filter(
account=account,
enabled=True
).exists()
active_automations = AutomationSettings.objects.filter(
account=account,
enabled=True
).count()
automation = {
'enabled': automation_enabled,
'active_count': active_automations,
'status': 'active' if automation_enabled else 'inactive',
}
# ==========================================
# 7. SITES SUMMARY
# ==========================================
sites_data = []
for site in sites[:5]: # Top 5 sites
site_keywords = Keywords.objects.filter(site=site).count()
site_content = Content.objects.filter(site=site).count()
site_published = Content.objects.filter(site=site, status='published').count()
sites_data.append({
'id': site.id,
'name': site.name,
'domain': site.url,
'keywords': site_keywords,
'content': site_content,
'published': site_published,
'has_integration': site.has_integration,
'sectors_count': site.sectors.filter(is_active=True).count(),
})
return Response({
'needs_attention': needs_attention,
'pipeline': pipeline,
'ai_operations': ai_operations,
'recent_activity': recent_activity,
'content_velocity': content_velocity,
'automation': automation,
'sites': sites_data,
'account': {
'credits': account.credits,
'name': account.name,
},
'generated_at': timezone.now().isoformat(),
})