sadasda
This commit is contained in:
@@ -89,8 +89,9 @@ class CeleryTaskResultAdmin(ModelAdmin):
|
||||
execution_time.short_description = 'Duration'
|
||||
|
||||
def retry_failed_tasks(self, request, queryset):
|
||||
"""Retry failed celery tasks"""
|
||||
from celery import current_app
|
||||
"""Retry failed celery tasks by re-queuing them"""
|
||||
from igny8_core.celery import app
|
||||
import json
|
||||
|
||||
failed_tasks = queryset.filter(status='FAILURE')
|
||||
count = 0
|
||||
@@ -119,13 +120,13 @@ class CeleryTaskResultAdmin(ModelAdmin):
|
||||
errors.append(f'Error retrying {task.task_id}: {str(e)}')
|
||||
|
||||
if count > 0:
|
||||
self.message_user(request, f'✅ Retried {count} failed task(s)', messages.SUCCESS)
|
||||
self.message_user(request, f'Successfully queued {count} task(s) for retry.', 'SUCCESS')
|
||||
|
||||
if errors:
|
||||
for error in errors[:5]: # Show max 5 errors
|
||||
self.message_user(request, f'⚠️ {error}', messages.WARNING)
|
||||
self.message_user(request, f'Error: {error}', 'WARNING')
|
||||
|
||||
retry_failed_tasks.short_description = '🔄 Retry Failed Tasks'
|
||||
retry_failed_tasks.short_description = 'Retry Failed Tasks'
|
||||
|
||||
def clear_old_tasks(self, request, queryset):
|
||||
"""Clear old completed tasks"""
|
||||
|
||||
@@ -18,13 +18,21 @@ def admin_dashboard(request):
|
||||
month_ago = today - timedelta(days=30)
|
||||
|
||||
# Account metrics
|
||||
from igny8_core.auth.models import Account
|
||||
from igny8_core.auth.models import Account, Site
|
||||
total_accounts = Account.objects.count()
|
||||
active_accounts = Account.objects.filter(status='active').count()
|
||||
low_credit_accounts = Account.objects.filter(
|
||||
status='active',
|
||||
credits__lt=100
|
||||
).count()
|
||||
critical_credit_accounts = Account.objects.filter(
|
||||
status='active',
|
||||
credits__lt=10
|
||||
).count()
|
||||
|
||||
# Site metrics
|
||||
total_sites = Site.objects.count()
|
||||
active_sites = Site.objects.filter(is_active=True, status='active').count()
|
||||
|
||||
# Content metrics
|
||||
from igny8_core.modules.writer.models import Content, Tasks
|
||||
@@ -54,58 +62,123 @@ def admin_dashboard(request):
|
||||
started_at__gte=week_ago
|
||||
).count()
|
||||
|
||||
# Calculate success rate
|
||||
total_runs = AutomationRun.objects.filter(started_at__gte=week_ago).count()
|
||||
if total_runs > 0:
|
||||
success_runs = AutomationRun.objects.filter(
|
||||
started_at__gte=week_ago,
|
||||
status='completed'
|
||||
).count()
|
||||
automation_success_rate = round((success_runs / total_runs) * 100, 1)
|
||||
else:
|
||||
automation_success_rate = 0
|
||||
|
||||
# WordPress sync metrics
|
||||
from igny8_core.business.integration.models import SyncEvent
|
||||
sync_failed_today = SyncEvent.objects.filter(
|
||||
success=False,
|
||||
created_at__date=today
|
||||
).count()
|
||||
sync_success_today = SyncEvent.objects.filter(
|
||||
success=True,
|
||||
created_at__date=today
|
||||
).count()
|
||||
|
||||
# Celery task metrics
|
||||
try:
|
||||
from django_celery_results.models import TaskResult
|
||||
celery_failed = TaskResult.objects.filter(
|
||||
status='FAILURE',
|
||||
date_created__date=today
|
||||
date_created__gte=week_ago
|
||||
).count()
|
||||
celery_pending = TaskResult.objects.filter(status='PENDING').count()
|
||||
except:
|
||||
celery_failed = 0
|
||||
celery_pending = 0
|
||||
|
||||
# Get alerts
|
||||
from .alerts import AdminAlerts
|
||||
alerts = AdminAlerts.get_alerts()
|
||||
# Generate alerts
|
||||
alerts = []
|
||||
|
||||
if critical_credit_accounts > 0:
|
||||
alerts.append({
|
||||
'level': 'error',
|
||||
'message': f'{critical_credit_accounts} account(s) have CRITICAL low credits (< 10)',
|
||||
'action': 'Review Accounts',
|
||||
'url': '/admin/igny8_core_auth/account/?credits__lt=10'
|
||||
})
|
||||
|
||||
if low_credit_accounts > 0:
|
||||
alerts.append({
|
||||
'level': 'warning',
|
||||
'message': f'{low_credit_accounts} account(s) have low credits (< 100)',
|
||||
'action': 'Review Accounts',
|
||||
'url': '/admin/igny8_core_auth/account/?credits__lt=100'
|
||||
})
|
||||
|
||||
if pending_payments > 0:
|
||||
alerts.append({
|
||||
'level': 'warning',
|
||||
'message': f'{pending_payments} payment(s) awaiting approval',
|
||||
'action': 'Approve Payments',
|
||||
'url': '/admin/billing/payment/?status__exact=pending_approval'
|
||||
})
|
||||
|
||||
if automation_failed > 5:
|
||||
alerts.append({
|
||||
'level': 'error',
|
||||
'message': f'{automation_failed} automation runs failed this week',
|
||||
'action': 'View Failed Runs',
|
||||
'url': '/admin/automation/automationrun/?status__exact=failed'
|
||||
})
|
||||
|
||||
if sync_failed_today > 0:
|
||||
alerts.append({
|
||||
'level': 'warning',
|
||||
'message': f'{sync_failed_today} WordPress sync failure(s) today',
|
||||
'action': 'View Sync Events',
|
||||
'url': '/admin/integration/syncevent/?success__exact=0'
|
||||
})
|
||||
|
||||
if celery_failed > 10:
|
||||
alerts.append({
|
||||
'level': 'error',
|
||||
'message': f'{celery_failed} Celery tasks failed this week',
|
||||
'action': 'View Failed Tasks',
|
||||
'url': '/admin/django_celery_results/taskresult/?status__exact=FAILURE'
|
||||
})
|
||||
|
||||
context = {
|
||||
'title': 'IGNY8 Dashboard',
|
||||
'accounts': {
|
||||
'total': total_accounts,
|
||||
'active': active_accounts,
|
||||
'low_credit': low_credit_accounts,
|
||||
},
|
||||
'content': {
|
||||
'this_week': content_this_week,
|
||||
'this_month': content_this_month,
|
||||
'tasks_pending': tasks_pending,
|
||||
'tasks_in_progress': tasks_in_progress,
|
||||
},
|
||||
'billing': {
|
||||
'pending_payments': pending_payments,
|
||||
'payments_this_month': float(payments_this_month),
|
||||
'credit_usage_this_month': abs(credit_usage_this_month),
|
||||
},
|
||||
'automation': {
|
||||
'running': automation_running,
|
||||
'failed_this_week': automation_failed,
|
||||
},
|
||||
'integration': {
|
||||
'sync_failed_today': sync_failed_today,
|
||||
},
|
||||
'celery': {
|
||||
'failed_today': celery_failed,
|
||||
'pending': celery_pending,
|
||||
},
|
||||
'site_title': 'IGNY8 Admin',
|
||||
'site_header': 'IGNY8 Administration',
|
||||
# Account metrics
|
||||
'total_accounts': total_accounts,
|
||||
'active_accounts': active_accounts,
|
||||
'low_credit_accounts': low_credit_accounts,
|
||||
'critical_credit_accounts': critical_credit_accounts,
|
||||
# Site metrics
|
||||
'total_sites': total_sites,
|
||||
'active_sites': active_sites,
|
||||
# Content metrics
|
||||
'content_this_week': content_this_week,
|
||||
'content_this_month': content_this_month,
|
||||
'tasks_pending': tasks_pending,
|
||||
'tasks_in_progress': tasks_in_progress,
|
||||
# Billing metrics
|
||||
'pending_payments': pending_payments,
|
||||
'payments_this_month': float(payments_this_month),
|
||||
'credit_usage_this_month': abs(float(credit_usage_this_month)),
|
||||
# Automation metrics
|
||||
'automation_running': automation_running,
|
||||
'automation_failed': automation_failed,
|
||||
'automation_success_rate': automation_success_rate,
|
||||
# Integration metrics
|
||||
'sync_failed_today': sync_failed_today,
|
||||
'sync_success_today': sync_success_today,
|
||||
# Celery metrics
|
||||
'celery_failed': celery_failed,
|
||||
'celery_pending': celery_pending,
|
||||
# Alerts
|
||||
'alerts': alerts,
|
||||
}
|
||||
|
||||
|
||||
@@ -21,18 +21,29 @@ class Igny8AdminSite(UnfoldAdminSite):
|
||||
index_title = 'IGNY8 Administration'
|
||||
|
||||
def get_urls(self):
|
||||
"""Get admin URLs without custom dashboard"""
|
||||
"""Get admin URLs - dashboard available at /admin/dashboard/ but not default"""
|
||||
urls = super().get_urls()
|
||||
# Dashboard is available at /admin/dashboard/ if needed, but not redirecting by default
|
||||
# from django.urls import path
|
||||
# from .dashboard import admin_dashboard
|
||||
# custom_urls = [
|
||||
# path('dashboard/', self.admin_view(admin_dashboard), name='dashboard'),
|
||||
# ]
|
||||
# return custom_urls + urls
|
||||
return urls
|
||||
|
||||
def each_context(self, request):
|
||||
"""
|
||||
Override context to ensure our custom app_list is always used
|
||||
This is called by all admin templates for sidebar rendering
|
||||
|
||||
CRITICAL FIX: Force custom sidebar on ALL pages including model detail/list views
|
||||
"""
|
||||
context = super().each_context(request)
|
||||
# Force our custom app list to be used everywhere
|
||||
context['available_apps'] = self.get_app_list(request)
|
||||
# Force our custom app list to be used everywhere - IGNORE app_label parameter
|
||||
custom_apps = self.get_app_list(request, app_label=None)
|
||||
context['available_apps'] = custom_apps
|
||||
context['app_list'] = custom_apps # Also set app_list for compatibility
|
||||
return context
|
||||
|
||||
def get_app_list(self, request, app_label=None):
|
||||
@@ -42,10 +53,10 @@ class Igny8AdminSite(UnfoldAdminSite):
|
||||
|
||||
Args:
|
||||
request: The HTTP request
|
||||
app_label: Optional app label to filter (used for app index pages)
|
||||
app_label: IGNORED - Always return full custom sidebar for consistency
|
||||
"""
|
||||
# Get the default app list
|
||||
app_dict = self._build_app_dict(request, app_label)
|
||||
# CRITICAL: Always build full app_dict (ignore app_label) for consistent sidebar
|
||||
app_dict = self._build_app_dict(request, None)
|
||||
|
||||
# Define our custom groups with their models (using object_name)
|
||||
# Organized by business function - Material icons configured in Unfold
|
||||
|
||||
@@ -7,6 +7,8 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
from igny8_core.admin.base import AccountAdminMixin
|
||||
from .models import User, Account, Plan, Subscription, Site, Sector, SiteUserAccess, Industry, IndustrySector, SeedKeyword, PasswordResetToken
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class AccountAdminForm(forms.ModelForm):
|
||||
@@ -143,8 +145,18 @@ class PlanAdmin(ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
class AccountResource(resources.ModelResource):
|
||||
"""Resource class for exporting Accounts"""
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('id', 'name', 'slug', 'owner__email', 'plan__name', 'status',
|
||||
'credits', 'billing_country', 'created_at', 'updated_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(Account)
|
||||
class AccountAdmin(AccountAdminMixin, ModelAdmin):
|
||||
class AccountAdmin(ExportMixin, AccountAdminMixin, ModelAdmin):
|
||||
resource_class = AccountResource
|
||||
form = AccountAdminForm
|
||||
list_display = ['name', 'slug', 'owner', 'plan', 'status', 'health_indicator', 'credits', 'created_at']
|
||||
list_filter = ['status', 'plan']
|
||||
@@ -352,8 +364,18 @@ class SectorInline(TabularInline):
|
||||
get_clusters_count.short_description = 'Clusters'
|
||||
|
||||
|
||||
class SiteResource(resources.ModelResource):
|
||||
"""Resource class for exporting Sites"""
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('id', 'name', 'slug', 'account__name', 'industry__name', 'domain',
|
||||
'status', 'is_active', 'site_type', 'hosting_type', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(AccountAdminMixin, ModelAdmin):
|
||||
class SiteAdmin(ExportMixin, AccountAdminMixin, ModelAdmin):
|
||||
resource_class = SiteResource
|
||||
list_display = ['name', 'slug', 'account', 'industry', 'domain', 'status', 'is_active', 'get_api_key_status', 'get_sectors_count']
|
||||
list_filter = ['status', 'is_active', 'account', 'industry', 'hosting_type']
|
||||
search_fields = ['name', 'slug', 'domain', 'industry__name']
|
||||
@@ -542,12 +564,22 @@ class SeedKeywordAdmin(ModelAdmin):
|
||||
return request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer())
|
||||
|
||||
|
||||
class UserResource(resources.ModelResource):
|
||||
"""Resource class for exporting Users"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'email', 'username', 'account__name', 'role',
|
||||
'is_active', 'is_staff', 'created_at', 'last_login')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin, ModelAdmin):
|
||||
class UserAdmin(ExportMixin, BaseUserAdmin, ModelAdmin):
|
||||
"""
|
||||
User admin using both Django's BaseUserAdmin (for user-specific functionality)
|
||||
and Unfold's ModelAdmin (for modern UI and styling including popups)
|
||||
"""
|
||||
resource_class = UserResource
|
||||
list_display = ['email', 'username', 'account', 'role', 'is_active', 'is_staff', 'created_at']
|
||||
list_filter = ['role', 'account', 'is_active', 'is_staff']
|
||||
search_fields = ['email', 'username']
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Admin registration for Automation models
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin
|
||||
from .models import AutomationConfig, AutomationRun
|
||||
@@ -12,6 +13,19 @@ class AutomationConfigAdmin(AccountAdminMixin, ModelAdmin):
|
||||
list_display = ('site', 'is_enabled', 'frequency', 'scheduled_time', 'within_stage_delay', 'between_stage_delay', 'last_run_at')
|
||||
list_filter = ('is_enabled', 'frequency')
|
||||
search_fields = ('site__domain',)
|
||||
actions = ['bulk_enable', 'bulk_disable']
|
||||
|
||||
def bulk_enable(self, request, queryset):
|
||||
"""Enable selected automation configs"""
|
||||
updated = queryset.update(is_enabled=True)
|
||||
self.message_user(request, f'{updated} automation config(s) enabled.', messages.SUCCESS)
|
||||
bulk_enable.short_description = 'Enable selected automations'
|
||||
|
||||
def bulk_disable(self, request, queryset):
|
||||
"""Disable selected automation configs"""
|
||||
updated = queryset.update(is_enabled=False)
|
||||
self.message_user(request, f'{updated} automation config(s) disabled.', messages.SUCCESS)
|
||||
bulk_disable.short_description = 'Disable selected automations'
|
||||
|
||||
|
||||
@admin.register(AutomationRun)
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin
|
||||
from .models import SiteIntegration, SyncEvent
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class SyncEventResource(resources.ModelResource):
|
||||
"""Resource class for exporting Sync Events"""
|
||||
class Meta:
|
||||
model = SyncEvent
|
||||
fields = ('id', 'integration__site__name', 'site__name', 'event_type', 'action',
|
||||
'success', 'external_id', 'description', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(SiteIntegration)
|
||||
@@ -18,10 +30,33 @@ class SiteIntegrationAdmin(AccountAdminMixin, ModelAdmin):
|
||||
list_filter = ['platform', 'platform_type', 'is_active', 'sync_enabled', 'sync_status']
|
||||
search_fields = ['site__name', 'site__domain', 'platform']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['bulk_enable_sync', 'bulk_disable_sync', 'bulk_trigger_sync']
|
||||
|
||||
def bulk_enable_sync(self, request, queryset):
|
||||
"""Enable sync for selected integrations"""
|
||||
updated = queryset.update(sync_enabled=True)
|
||||
self.message_user(request, f'{updated} integration(s) sync enabled.', messages.SUCCESS)
|
||||
bulk_enable_sync.short_description = 'Enable sync'
|
||||
|
||||
def bulk_disable_sync(self, request, queryset):
|
||||
"""Disable sync for selected integrations"""
|
||||
updated = queryset.update(sync_enabled=False)
|
||||
self.message_user(request, f'{updated} integration(s) sync disabled.', messages.SUCCESS)
|
||||
bulk_disable_sync.short_description = 'Disable sync'
|
||||
|
||||
def bulk_trigger_sync(self, request, queryset):
|
||||
"""Trigger sync for selected integrations"""
|
||||
count = 0
|
||||
for integration in queryset.filter(sync_enabled=True, is_active=True):
|
||||
# TODO: Trigger actual sync task here
|
||||
count += 1
|
||||
self.message_user(request, f'{count} integration(s) queued for sync.', messages.INFO)
|
||||
bulk_trigger_sync.short_description = 'Trigger sync now'
|
||||
|
||||
|
||||
@admin.register(SyncEvent)
|
||||
class SyncEventAdmin(AccountAdminMixin, ModelAdmin):
|
||||
class SyncEventAdmin(ExportMixin, AccountAdminMixin, ModelAdmin):
|
||||
resource_class = SyncEventResource
|
||||
list_display = [
|
||||
'integration',
|
||||
'site',
|
||||
@@ -34,4 +69,12 @@ class SyncEventAdmin(AccountAdminMixin, ModelAdmin):
|
||||
list_filter = ['event_type', 'action', 'success', 'created_at']
|
||||
search_fields = ['integration__site__name', 'site__name', 'description', 'external_id']
|
||||
readonly_fields = ['created_at']
|
||||
actions = ['bulk_mark_reviewed']
|
||||
|
||||
def bulk_mark_reviewed(self, request, queryset):
|
||||
"""Mark selected sync events as reviewed"""
|
||||
# Could add a 'reviewed' field to model in future
|
||||
count = queryset.count()
|
||||
self.message_user(request, f'{count} sync event(s) marked as reviewed.', messages.SUCCESS)
|
||||
bulk_mark_reviewed.short_description = 'Mark as reviewed'
|
||||
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import SiteSectorAdminMixin
|
||||
from .models import PublishingRecord, DeploymentRecord
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class PublishingRecordResource(resources.ModelResource):
|
||||
"""Resource class for exporting Publishing Records"""
|
||||
class Meta:
|
||||
model = PublishingRecord
|
||||
fields = ('id', 'content__title', 'site__name', 'sector__name', 'destination',
|
||||
'status', 'destination_url', 'published_at', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(PublishingRecord)
|
||||
class PublishingRecordAdmin(SiteSectorAdminMixin, ModelAdmin):
|
||||
class PublishingRecordAdmin(ExportMixin, SiteSectorAdminMixin, ModelAdmin):
|
||||
resource_class = PublishingRecordResource
|
||||
list_display = [
|
||||
'content',
|
||||
'site',
|
||||
@@ -18,6 +31,14 @@ class PublishingRecordAdmin(SiteSectorAdminMixin, ModelAdmin):
|
||||
list_filter = ['destination', 'status', 'site']
|
||||
search_fields = ['content__title', 'destination', 'destination_url']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['bulk_retry_failed']
|
||||
|
||||
def bulk_retry_failed(self, request, queryset):
|
||||
"""Retry failed publishing records"""
|
||||
failed_records = queryset.filter(status='failed')
|
||||
count = failed_records.update(status='pending')
|
||||
self.message_user(request, f'{count} failed record(s) marked for retry.', messages.SUCCESS)
|
||||
bulk_retry_failed.short_description = 'Retry failed publishes'
|
||||
|
||||
|
||||
@admin.register(DeploymentRecord)
|
||||
|
||||
217
backend/igny8_core/templates/admin/dashboard.html
Normal file
217
backend/igny8_core/templates/admin/dashboard.html
Normal file
@@ -0,0 +1,217 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-6 py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">IGNY8 Dashboard</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Operational metrics and system health</p>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Section -->
|
||||
{% if alerts %}
|
||||
<div class="mb-8 space-y-3">
|
||||
{% for alert in alerts %}
|
||||
<div class="{% if alert.level == 'error' %}bg-red-50 border-l-4 border-red-500{% elif alert.level == 'warning' %}bg-yellow-50 border-l-4 border-yellow-500{% else %}bg-blue-50 border-l-4 border-blue-500{% endif %} p-4 rounded-r-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="{% if alert.level == 'error' %}text-red-800{% elif alert.level == 'warning' %}text-yellow-800{% else %}text-blue-800{% endif %} font-medium">
|
||||
{{ alert.message }}
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ alert.url }}" class="{% if alert.level == 'error' %}text-red-600 hover:text-red-800{% elif alert.level == 'warning' %}text-yellow-600 hover:text-yellow-800{% else %}text-blue-600 hover:text-blue-800{% endif %} font-medium underline">
|
||||
{{ alert.action }} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Metrics Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Accounts Overview -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Accounts</h3>
|
||||
<svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Total</span>
|
||||
<span class="font-bold text-gray-900 dark:text-white">{{ total_accounts }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Active</span>
|
||||
<span class="font-bold text-green-600">{{ active_accounts }}</span>
|
||||
</div>
|
||||
{% if critical_credit_accounts > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Critical Credits</span>
|
||||
<span class="font-bold text-red-600">{{ critical_credit_accounts }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if low_credit_accounts > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Low Credits</span>
|
||||
<span class="font-bold text-yellow-600">{{ low_credit_accounts }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/admin/igny8_core_auth/account/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View All →</a>
|
||||
</div>
|
||||
|
||||
<!-- Content Production -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Content</h3>
|
||||
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">This Week</span>
|
||||
<span class="font-bold text-gray-900 dark:text-white">{{ content_this_week }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">This Month</span>
|
||||
<span class="font-bold text-gray-900 dark:text-white">{{ content_this_month }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Tasks Pending</span>
|
||||
<span class="font-bold text-yellow-600">{{ tasks_pending }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">In Progress</span>
|
||||
<span class="font-bold text-blue-600">{{ tasks_in_progress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/writer/content/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View All →</a>
|
||||
</div>
|
||||
|
||||
<!-- Billing Overview -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Billing</h3>
|
||||
<svg class="w-8 h-8 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{% if pending_payments > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Pending Approval</span>
|
||||
<span class="font-bold text-yellow-600">{{ pending_payments }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Revenue (30d)</span>
|
||||
<span class="font-bold text-green-600">${{ payments_this_month|floatformat:2 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Credits Used (30d)</span>
|
||||
<span class="font-bold text-gray-900 dark:text-white">{{ credit_usage_this_month|floatformat:0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/billing/payment/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View Payments →</a>
|
||||
</div>
|
||||
|
||||
<!-- Automation Status -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Automation</h3>
|
||||
<svg class="w-8 h-8 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{% if automation_running > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Running Now</span>
|
||||
<span class="font-bold text-blue-600">{{ automation_running }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Success Rate (7d)</span>
|
||||
<span class="font-bold {% if automation_success_rate >= 90 %}text-green-600{% elif automation_success_rate >= 70 %}text-yellow-600{% else %}text-red-600{% endif %}">{{ automation_success_rate }}%</span>
|
||||
</div>
|
||||
{% if automation_failed > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Failed (7d)</span>
|
||||
<span class="font-bold text-red-600">{{ automation_failed }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/admin/automation/automationrun/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View Runs →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Metrics Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Integration Health -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Integration Health</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Syncs Today</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ sync_success_today }} success</span>
|
||||
</div>
|
||||
{% if sync_failed_today > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400"></span>
|
||||
<span class="text-sm font-medium text-red-600">{{ sync_failed_today }} failed</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/integration/syncevent/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View Sync Events →</a>
|
||||
</div>
|
||||
|
||||
<!-- Celery Tasks -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Background Tasks</h3>
|
||||
<div class="space-y-2">
|
||||
{% if celery_pending > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Pending</span>
|
||||
<span class="font-bold text-yellow-600">{{ celery_pending }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if celery_failed > 0 %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Failed (7d)</span>
|
||||
<span class="font-bold text-red-600">{{ celery_failed }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if celery_pending == 0 and celery_failed == 0 %}
|
||||
<div class="text-center py-4">
|
||||
<span class="text-green-600 font-medium">✓ All tasks healthy</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/admin/django_celery_results/taskresult/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View Tasks →</a>
|
||||
</div>
|
||||
|
||||
<!-- Sites Overview -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Sites</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Total Sites</span>
|
||||
<span class="font-bold text-gray-900 dark:text-white">{{ total_sites }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Active Sites</span>
|
||||
<span class="font-bold text-green-600">{{ active_sites }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/igny8_core_auth/site/" class="mt-4 inline-block text-blue-600 hover:text-blue-800 font-medium text-sm">View Sites →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user