django phase2.3.4.
This commit is contained in:
122
backend/igny8_core/admin/alerts.py
Normal file
122
backend/igny8_core/admin/alerts.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Admin Alert System
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class AdminAlerts:
|
||||
"""System for admin alerts and notifications"""
|
||||
|
||||
@staticmethod
|
||||
def get_alerts():
|
||||
"""Get all active alerts"""
|
||||
alerts = []
|
||||
today = timezone.now().date()
|
||||
|
||||
# Check for pending payments
|
||||
from igny8_core.business.billing.models import Payment
|
||||
pending_payments = Payment.objects.filter(status='pending_approval').count()
|
||||
if pending_payments > 0:
|
||||
alerts.append({
|
||||
'level': 'warning',
|
||||
'icon': '⚠️',
|
||||
'message': f'{pending_payments} payment(s) awaiting approval',
|
||||
'url': '/admin/billing/payment/?status=pending_approval',
|
||||
'action': 'Review Payments'
|
||||
})
|
||||
|
||||
# Check for low credit accounts
|
||||
from igny8_core.auth.models import Account
|
||||
low_credit_accounts = Account.objects.filter(
|
||||
status='active',
|
||||
credits__lt=100
|
||||
).count()
|
||||
if low_credit_accounts > 0:
|
||||
alerts.append({
|
||||
'level': 'info',
|
||||
'icon': 'ℹ️',
|
||||
'message': f'{low_credit_accounts} account(s) with low credits',
|
||||
'url': '/admin/igny8_core_auth/account/?credits__lt=100',
|
||||
'action': 'View Accounts'
|
||||
})
|
||||
|
||||
# Check for very low credits (critical)
|
||||
critical_credit_accounts = Account.objects.filter(
|
||||
status='active',
|
||||
credits__lt=10
|
||||
).count()
|
||||
if critical_credit_accounts > 0:
|
||||
alerts.append({
|
||||
'level': 'error',
|
||||
'icon': '🔴',
|
||||
'message': f'{critical_credit_accounts} account(s) with critical low credits (< 10)',
|
||||
'url': '/admin/igny8_core_auth/account/?credits__lt=10',
|
||||
'action': 'Urgent Review'
|
||||
})
|
||||
|
||||
# Check for failed automations
|
||||
from igny8_core.business.automation.models import AutomationRun
|
||||
failed_today = AutomationRun.objects.filter(
|
||||
status='failed',
|
||||
started_at__date=today
|
||||
).count()
|
||||
if failed_today > 0:
|
||||
alerts.append({
|
||||
'level': 'error',
|
||||
'icon': '🔴',
|
||||
'message': f'{failed_today} automation(s) failed today',
|
||||
'url': '/admin/automation/automationrun/?status=failed',
|
||||
'action': 'Review Failures'
|
||||
})
|
||||
|
||||
# Check for failed syncs
|
||||
from igny8_core.business.integration.models import SyncEvent
|
||||
failed_syncs = SyncEvent.objects.filter(
|
||||
success=False,
|
||||
created_at__date=today
|
||||
).count()
|
||||
if failed_syncs > 5: # Only alert if more than 5
|
||||
alerts.append({
|
||||
'level': 'warning',
|
||||
'icon': '⚠️',
|
||||
'message': f'{failed_syncs} WordPress sync failures today',
|
||||
'url': '/admin/integration/syncevent/?success=False',
|
||||
'action': 'Review Syncs'
|
||||
})
|
||||
|
||||
# Check for failed Celery tasks
|
||||
try:
|
||||
from django_celery_results.models import TaskResult
|
||||
celery_failed = TaskResult.objects.filter(
|
||||
status='FAILURE',
|
||||
date_created__date=today
|
||||
).count()
|
||||
if celery_failed > 0:
|
||||
alerts.append({
|
||||
'level': 'error',
|
||||
'icon': '🔴',
|
||||
'message': f'{celery_failed} Celery task(s) failed today',
|
||||
'url': '/admin/django_celery_results/taskresult/?status=FAILURE',
|
||||
'action': 'Review Tasks'
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check for stale pending tasks (older than 24 hours)
|
||||
from igny8_core.modules.writer.models import Tasks
|
||||
yesterday = today - timedelta(days=1)
|
||||
stale_tasks = Tasks.objects.filter(
|
||||
status='pending',
|
||||
created_at__date__lte=yesterday
|
||||
).count()
|
||||
if stale_tasks > 10:
|
||||
alerts.append({
|
||||
'level': 'info',
|
||||
'icon': 'ℹ️',
|
||||
'message': f'{stale_tasks} tasks pending for more than 24 hours',
|
||||
'url': '/admin/writer/tasks/?status=pending',
|
||||
'action': 'Review Tasks'
|
||||
})
|
||||
|
||||
return alerts
|
||||
@@ -39,5 +39,28 @@ class Igny8AdminConfig(AdminConfig):
|
||||
_safe_register(Group, admin.ModelAdmin)
|
||||
_safe_register(ContentType, ReadOnlyAdmin)
|
||||
_safe_register(Session, ReadOnlyAdmin)
|
||||
|
||||
# Import and setup enhanced Celery task monitoring
|
||||
self._setup_celery_admin()
|
||||
|
||||
def _setup_celery_admin(self):
|
||||
"""Setup enhanced Celery admin with proper unregister/register"""
|
||||
try:
|
||||
from django_celery_results.models import TaskResult
|
||||
from igny8_core.admin.celery_admin import CeleryTaskResultAdmin
|
||||
|
||||
# Unregister the default TaskResult admin
|
||||
try:
|
||||
admin.site.unregister(TaskResult)
|
||||
except admin.sites.NotRegistered:
|
||||
pass
|
||||
|
||||
# Register our enhanced version
|
||||
admin.site.register(TaskResult, CeleryTaskResultAdmin)
|
||||
except Exception as e:
|
||||
# Log the error but don't crash the app
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Could not setup enhanced Celery admin: {e}")
|
||||
|
||||
|
||||
|
||||
154
backend/igny8_core/admin/celery_admin.py
Normal file
154
backend/igny8_core/admin/celery_admin.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Celery Task Monitoring Admin
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.contrib import messages
|
||||
from django_celery_results.models import TaskResult
|
||||
from rangefilter.filters import DateRangeFilter
|
||||
|
||||
|
||||
class CeleryTaskResultAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for monitoring Celery tasks"""
|
||||
|
||||
list_display = [
|
||||
'task_id',
|
||||
'task_name',
|
||||
'colored_status',
|
||||
'date_created',
|
||||
'date_done',
|
||||
'execution_time',
|
||||
]
|
||||
list_filter = [
|
||||
'status',
|
||||
'task_name',
|
||||
('date_created', DateRangeFilter),
|
||||
('date_done', DateRangeFilter),
|
||||
]
|
||||
search_fields = ['task_id', 'task_name', 'task_args']
|
||||
readonly_fields = [
|
||||
'task_id', 'task_name', 'task_args', 'task_kwargs',
|
||||
'result', 'traceback', 'date_created', 'date_done',
|
||||
'colored_status', 'execution_time'
|
||||
]
|
||||
date_hierarchy = 'date_created'
|
||||
ordering = ['-date_created']
|
||||
|
||||
actions = ['retry_failed_tasks', 'clear_old_tasks']
|
||||
|
||||
fieldsets = (
|
||||
('Task Information', {
|
||||
'fields': ('task_id', 'task_name', 'colored_status')
|
||||
}),
|
||||
('Execution Details', {
|
||||
'fields': ('date_created', 'date_done', 'execution_time')
|
||||
}),
|
||||
('Task Arguments', {
|
||||
'fields': ('task_args', 'task_kwargs'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Result & Errors', {
|
||||
'fields': ('result', 'traceback'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def colored_status(self, obj):
|
||||
"""Display status with color coding"""
|
||||
colors = {
|
||||
'SUCCESS': '#0bbf87', # IGNY8 success green
|
||||
'FAILURE': '#ef4444', # IGNY8 danger red
|
||||
'PENDING': '#ff7a00', # IGNY8 warning orange
|
||||
'STARTED': '#0693e3', # IGNY8 primary blue
|
||||
'RETRY': '#5d4ae3', # IGNY8 purple
|
||||
}
|
||||
color = colors.get(obj.status, '#64748b') # Default gray
|
||||
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold; font-size: 14px;">{}</span>',
|
||||
color,
|
||||
obj.status
|
||||
)
|
||||
colored_status.short_description = 'Status'
|
||||
|
||||
def execution_time(self, obj):
|
||||
"""Calculate and display execution time"""
|
||||
if obj.date_done and obj.date_created:
|
||||
duration = obj.date_done - obj.date_created
|
||||
seconds = duration.total_seconds()
|
||||
|
||||
if seconds < 1:
|
||||
return format_html('<span style="color: #0bbf87;">{:.2f}ms</span>', seconds * 1000)
|
||||
elif seconds < 60:
|
||||
return format_html('<span style="color: #0693e3;">{:.2f}s</span>', seconds)
|
||||
else:
|
||||
minutes = seconds / 60
|
||||
return format_html('<span style="color: #ff7a00;">{:.1f}m</span>', minutes)
|
||||
return '-'
|
||||
execution_time.short_description = 'Duration'
|
||||
|
||||
def retry_failed_tasks(self, request, queryset):
|
||||
"""Retry failed celery tasks"""
|
||||
from celery import current_app
|
||||
|
||||
failed_tasks = queryset.filter(status='FAILURE')
|
||||
count = 0
|
||||
errors = []
|
||||
|
||||
for task in failed_tasks:
|
||||
try:
|
||||
# Get task function
|
||||
task_func = current_app.tasks.get(task.task_name)
|
||||
if task_func:
|
||||
# Parse task args and kwargs
|
||||
import ast
|
||||
try:
|
||||
args = ast.literal_eval(task.task_args) if task.task_args else []
|
||||
kwargs = ast.literal_eval(task.task_kwargs) if task.task_kwargs else {}
|
||||
except:
|
||||
args = []
|
||||
kwargs = {}
|
||||
|
||||
# Retry the task
|
||||
task_func.apply_async(args=args, kwargs=kwargs)
|
||||
count += 1
|
||||
else:
|
||||
errors.append(f'Task function not found: {task.task_name}')
|
||||
except Exception as e:
|
||||
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)
|
||||
|
||||
if errors:
|
||||
for error in errors[:5]: # Show max 5 errors
|
||||
self.message_user(request, f'⚠️ {error}', messages.WARNING)
|
||||
|
||||
retry_failed_tasks.short_description = '🔄 Retry Failed Tasks'
|
||||
|
||||
def clear_old_tasks(self, request, queryset):
|
||||
"""Clear old completed tasks"""
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
# Delete tasks older than 30 days
|
||||
cutoff_date = timezone.now() - timedelta(days=30)
|
||||
old_tasks = queryset.filter(
|
||||
date_created__lt=cutoff_date,
|
||||
status__in=['SUCCESS', 'FAILURE']
|
||||
)
|
||||
|
||||
count = old_tasks.count()
|
||||
old_tasks.delete()
|
||||
|
||||
self.message_user(request, f'🗑️ Cleared {count} old task(s)', messages.SUCCESS)
|
||||
|
||||
clear_old_tasks.short_description = '🗑️ Clear Old Tasks (30+ days)'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable manual task creation"""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Make read-only"""
|
||||
return False
|
||||
112
backend/igny8_core/admin/dashboard.py
Normal file
112
backend/igny8_core/admin/dashboard.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Custom Admin Dashboard with Key Metrics
|
||||
"""
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.shortcuts import render
|
||||
from django.db.models import Count, Sum, Q
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def admin_dashboard(request):
|
||||
"""Custom admin dashboard with operational metrics"""
|
||||
|
||||
# Date ranges
|
||||
today = timezone.now().date()
|
||||
week_ago = today - timedelta(days=7)
|
||||
month_ago = today - timedelta(days=30)
|
||||
|
||||
# Account metrics
|
||||
from igny8_core.auth.models import Account
|
||||
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()
|
||||
|
||||
# Content metrics
|
||||
from igny8_core.modules.writer.models import Content, Tasks
|
||||
content_this_week = Content.objects.filter(created_at__gte=week_ago).count()
|
||||
content_this_month = Content.objects.filter(created_at__gte=month_ago).count()
|
||||
tasks_pending = Tasks.objects.filter(status='pending').count()
|
||||
tasks_in_progress = Tasks.objects.filter(status='in_progress').count()
|
||||
|
||||
# Billing metrics
|
||||
from igny8_core.business.billing.models import Payment, CreditTransaction
|
||||
pending_payments = Payment.objects.filter(status='pending_approval').count()
|
||||
payments_this_month = Payment.objects.filter(
|
||||
created_at__gte=month_ago,
|
||||
status='succeeded'
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
credit_usage_this_month = CreditTransaction.objects.filter(
|
||||
created_at__gte=month_ago,
|
||||
transaction_type='deduction'
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
# Automation metrics
|
||||
from igny8_core.business.automation.models import AutomationRun
|
||||
automation_running = AutomationRun.objects.filter(status='running').count()
|
||||
automation_failed = AutomationRun.objects.filter(
|
||||
status='failed',
|
||||
started_at__gte=week_ago
|
||||
).count()
|
||||
|
||||
# WordPress sync metrics
|
||||
from igny8_core.business.integration.models import SyncEvent
|
||||
sync_failed_today = SyncEvent.objects.filter(
|
||||
success=False,
|
||||
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
|
||||
).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()
|
||||
|
||||
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,
|
||||
},
|
||||
'alerts': alerts,
|
||||
}
|
||||
|
||||
return render(request, 'admin/dashboard.html', context)
|
||||
@@ -4,6 +4,8 @@ Custom AdminSite for IGNY8 to organize models into proper groups
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.apps import AdminConfig
|
||||
from django.apps import apps
|
||||
from django.urls import path
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
class Igny8AdminSite(admin.AdminSite):
|
||||
@@ -20,6 +22,19 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
site_header = 'IGNY8 Administration'
|
||||
site_title = 'IGNY8 Admin'
|
||||
index_title = 'IGNY8 Administration'
|
||||
|
||||
def get_urls(self):
|
||||
"""Add dashboard URL"""
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path('dashboard/', self.dashboard_view, name='dashboard'),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
def dashboard_view(self, request):
|
||||
"""Dashboard view wrapper"""
|
||||
from igny8_core.admin.dashboard import admin_dashboard
|
||||
return admin_dashboard(request)
|
||||
|
||||
def get_app_list(self, request):
|
||||
"""
|
||||
@@ -113,7 +128,13 @@ class Igny8AdminSite(admin.AdminSite):
|
||||
('system', 'SystemStatus'),
|
||||
],
|
||||
},
|
||||
'🔧 Django System': {
|
||||
'<EFBFBD> Monitoring & Tasks': {
|
||||
'models': [
|
||||
('django_celery_results', 'TaskResult'),
|
||||
('django_celery_results', 'GroupResult'),
|
||||
],
|
||||
},
|
||||
'<EFBFBD>🔧 Django System': {
|
||||
'models': [
|
||||
('admin', 'LogEntry'),
|
||||
('auth', 'Group'),
|
||||
|
||||
Reference in New Issue
Block a user