final plolish phase 2
This commit is contained in:
381
backend/igny8_core/api/dashboard_views.py
Normal file
381
backend/igny8_core/api/dashboard_views.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
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(),
|
||||
})
|
||||
@@ -8,6 +8,7 @@ from .account_views import (
|
||||
TeamManagementViewSet,
|
||||
UsageAnalyticsViewSet
|
||||
)
|
||||
from .dashboard_views import DashboardSummaryViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
@@ -22,5 +23,8 @@ urlpatterns = [
|
||||
# Usage analytics
|
||||
path('usage/analytics/', UsageAnalyticsViewSet.as_view({'get': 'overview'}), name='usage-analytics'),
|
||||
|
||||
# Dashboard summary (aggregated data for main dashboard)
|
||||
path('dashboard/summary/', DashboardSummaryViewSet.as_view({'get': 'summary'}), name='dashboard-summary'),
|
||||
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user