metricsa dn backedn fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-29 04:33:22 +00:00
parent 53fdebf733
commit 0ffd21b9bf
17 changed files with 929 additions and 266 deletions

View File

@@ -170,6 +170,7 @@ class Igny8AdminSite(UnfoldAdminSite):
'models': [
('igny8_core_auth', 'Plan'),
('igny8_core_auth', 'Subscription'),
('billing', 'BillingConfiguration'),
('billing', 'Invoice'),
('billing', 'Payment'),
('billing', 'CreditPackage'),

View File

@@ -5,7 +5,8 @@ from django.urls import path
from igny8_core.api.account_views import (
AccountSettingsViewSet,
TeamManagementViewSet,
UsageAnalyticsViewSet
UsageAnalyticsViewSet,
DashboardStatsViewSet
)
urlpatterns = [
@@ -28,4 +29,9 @@ urlpatterns = [
path('usage/analytics/', UsageAnalyticsViewSet.as_view({
'get': 'overview'
}), name='usage-analytics'),
# Dashboard Stats (real data for home page)
path('dashboard/stats/', DashboardStatsViewSet.as_view({
'get': 'stats'
}), name='dashboard-stats'),
]

View File

@@ -10,6 +10,7 @@ from django.contrib.auth import get_user_model
from django.db.models import Q, Count, Sum
from django.utils import timezone
from datetime import timedelta
from decimal import Decimal
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.auth.models import Account
@@ -242,3 +243,216 @@ class UsageAnalyticsViewSet(viewsets.ViewSet):
'total_usage': abs(transactions.filter(amount__lt=0).aggregate(Sum('amount'))['amount__sum'] or 0),
'total_purchases': transactions.filter(amount__gt=0).aggregate(Sum('amount'))['amount__sum'] or 0,
})
@extend_schema_view(
stats=extend_schema(tags=['Account']),
)
class DashboardStatsViewSet(viewsets.ViewSet):
"""Dashboard statistics - real data for home page widgets"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def stats(self, request):
"""
Get dashboard statistics for the home page.
Query params:
- site_id: Filter by site (optional, defaults to all sites)
- days: Number of days for AI operations (default: 7)
Returns:
- ai_operations: Real credit usage by operation type
- recent_activity: Recent notifications
- content_velocity: Content created this week/month
- images_count: Actual total images count
- published_count: Actual published content count
"""
account = request.user.account
site_id = request.query_params.get('site_id')
days = int(request.query_params.get('days', 7))
# Import models here to avoid circular imports
from igny8_core.modules.writer.models import Images, Content
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
from igny8_core.business.notifications.models import Notification
from igny8_core.business.billing.models import CreditUsageLog
from igny8_core.auth.models import Site
# Build base filter for site
site_filter = {}
if site_id:
try:
site_filter['site_id'] = int(site_id)
except (ValueError, TypeError):
pass
# ========== AI OPERATIONS (from CreditUsageLog) ==========
start_date = timezone.now() - timedelta(days=days)
usage_query = CreditUsageLog.objects.filter(
account=account,
created_at__gte=start_date
)
# Get operations grouped by type
operations_data = usage_query.values('operation_type').annotate(
count=Count('id'),
credits=Sum('credits_used')
).order_by('-credits')
# Calculate totals
total_ops = usage_query.count()
total_credits = usage_query.aggregate(total=Sum('credits_used'))['total'] or 0
# Format operations for frontend
operations = []
for op in operations_data:
op_type = op['operation_type'] or 'other'
operations.append({
'type': op_type,
'count': op['count'] or 0,
'credits': op['credits'] or 0,
})
ai_operations = {
'period': f'{days}d',
'operations': operations,
'totals': {
'count': total_ops,
'credits': total_credits,
'successRate': 98.5, # TODO: calculate from actual success/failure
'avgCreditsPerOp': round(total_credits / total_ops, 1) if total_ops > 0 else 0,
}
}
# ========== RECENT ACTIVITY (from Notifications) ==========
recent_notifications = Notification.objects.filter(
account=account
).order_by('-created_at')[:10]
recent_activity = []
for notif in recent_notifications:
# Map notification type to activity type
activity_type_map = {
'ai_clustering_complete': 'clustering',
'ai_ideas_complete': 'ideas',
'ai_content_complete': 'content',
'ai_images_complete': 'images',
'ai_prompts_complete': 'images',
'content_published': 'published',
'wp_sync_success': 'published',
}
activity_type = activity_type_map.get(notif.notification_type, 'system')
# Map notification type to href
href_map = {
'clustering': '/planner/clusters',
'ideas': '/planner/ideas',
'content': '/writer/content',
'images': '/writer/images',
'published': '/writer/published',
}
recent_activity.append({
'id': str(notif.id),
'type': activity_type,
'title': notif.title,
'description': notif.message[:100] if notif.message else '',
'timestamp': notif.created_at.isoformat(),
'href': href_map.get(activity_type, '/dashboard'),
})
# ========== CONTENT COUNTS ==========
content_base = Content.objects.filter(account=account)
if site_filter:
content_base = content_base.filter(**site_filter)
total_content = content_base.count()
draft_content = content_base.filter(status='draft').count()
review_content = content_base.filter(status='review').count()
published_content = content_base.filter(status='published').count()
# ========== IMAGES COUNT (actual images, not content with images) ==========
images_base = Images.objects.filter(account=account)
if site_filter:
images_base = images_base.filter(**site_filter)
total_images = images_base.count()
generated_images = images_base.filter(status='generated').count()
pending_images = images_base.filter(status='pending').count()
# ========== CONTENT VELOCITY ==========
now = timezone.now()
week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30)
# This week's content
week_content = content_base.filter(created_at__gte=week_ago).count()
week_images = images_base.filter(created_at__gte=week_ago).count()
# This month's content
month_content = content_base.filter(created_at__gte=month_ago).count()
month_images = images_base.filter(created_at__gte=month_ago).count()
# Estimate words (avg 1500 per article)
content_velocity = {
'thisWeek': {
'articles': week_content,
'words': week_content * 1500,
'images': week_images,
},
'thisMonth': {
'articles': month_content,
'words': month_content * 1500,
'images': month_images,
},
'total': {
'articles': total_content,
'words': total_content * 1500,
'images': total_images,
},
'trend': 0, # TODO: calculate actual trend
}
# ========== PIPELINE COUNTS ==========
keywords_base = Keywords.objects.filter(account=account)
clusters_base = Clusters.objects.filter(account=account)
ideas_base = ContentIdeas.objects.filter(account=account)
if site_filter:
keywords_base = keywords_base.filter(**site_filter)
clusters_base = clusters_base.filter(**site_filter)
ideas_base = ideas_base.filter(**site_filter)
# Get site count
sites_count = Site.objects.filter(account=account, is_active=True).count()
pipeline = {
'sites': sites_count,
'keywords': keywords_base.count(),
'clusters': clusters_base.count(),
'ideas': ideas_base.count(),
'tasks': ideas_base.filter(status='queued').count() + ideas_base.filter(status='completed').count(),
'drafts': draft_content + review_content,
'published': published_content,
}
return Response({
'ai_operations': ai_operations,
'recent_activity': recent_activity,
'content_velocity': content_velocity,
'pipeline': pipeline,
'counts': {
'content': {
'total': total_content,
'draft': draft_content,
'review': review_content,
'published': published_content,
},
'images': {
'total': total_images,
'generated': generated_images,
'pending': pending_images,
},
}
})

View File

@@ -6,7 +6,8 @@ from rest_framework.routers import DefaultRouter
from .account_views import (
AccountSettingsViewSet,
TeamManagementViewSet,
UsageAnalyticsViewSet
UsageAnalyticsViewSet,
DashboardStatsViewSet
)
router = DefaultRouter()
@@ -22,5 +23,8 @@ urlpatterns = [
# Usage analytics
path('usage/analytics/', UsageAnalyticsViewSet.as_view({'get': 'overview'}), name='usage-analytics'),
# Dashboard stats (real data for home page)
path('dashboard/stats/', DashboardStatsViewSet.as_view({'get': 'stats'}), name='dashboard-stats'),
path('', include(router.urls)),
]

View File

@@ -387,16 +387,17 @@ class AutomationViewSet(viewsets.ViewSet):
return counts, total
# Stage 1: Keywords pending clustering (keep previous "pending" semantics but also return status breakdown)
# Stage 1: Keywords pending clustering
stage_1_counts, stage_1_total = _counts_by_status(
Keywords,
extra_filter={'disabled': False}
)
# pending definition used by the UI previously (new & not clustered)
# FIXED: Stage 1 pending = all keywords with status='new' (ready for clustering)
# This should match the "New" count shown in Keywords metric card
# Previously filtered by cluster__isnull=True which caused mismatch
stage_1_pending = Keywords.objects.filter(
site=site,
status='new',
cluster__isnull=True,
disabled=False
).count()