382 lines
14 KiB
Python
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(),
|
|
})
|