diff --git a/backend/igny8_core/admin/apps.py b/backend/igny8_core/admin/apps.py index e5e983ae..cb546d1c 100644 --- a/backend/igny8_core/admin/apps.py +++ b/backend/igny8_core/admin/apps.py @@ -41,6 +41,11 @@ class Igny8AdminConfig(AdminConfig): admin_site._actions = old_site._actions.copy() admin_site._global_actions = old_site._global_actions.copy() + # CRITICAL: Update each ModelAdmin's admin_site attribute to point to our custom site + # Otherwise, each_context() will use the wrong admin site and miss our customizations + for model, model_admin in admin_site._registry.items(): + model_admin.admin_site = admin_site + # Now replace the default site admin_module.site = admin_site admin_module.sites.site = admin_site diff --git a/backend/igny8_core/admin/base.py b/backend/igny8_core/admin/base.py index 46684869..557c6d2b 100644 --- a/backend/igny8_core/admin/base.py +++ b/backend/igny8_core/admin/base.py @@ -145,7 +145,16 @@ class Igny8ModelAdmin(UnfoldModelAdmin): for group in sidebar_navigation: group_is_active = False for item in group.get('items', []): - item_link = item.get('link', '') + # Unfold stores resolved link in 'link_callback', original lambda in 'link' + item_link = item.get('link_callback') or item.get('link', '') + # Convert to string (handles lazy proxy objects and ensures it's a string) + try: + item_link = str(item_link) if item_link else '' + except: + item_link = '' + # Skip if it's a function representation (e.g., "") + if item_link.startswith('<'): + continue # Check if current path matches this item's link if item_link and current_path.startswith(item_link): item['active'] = True diff --git a/backend/igny8_core/admin/site.py b/backend/igny8_core/admin/site.py index 70a30ae8..2d805f3c 100644 --- a/backend/igny8_core/admin/site.py +++ b/backend/igny8_core/admin/site.py @@ -1,28 +1,30 @@ """ -Custom AdminSite for IGNY8 to organize models into proper groups using Unfold -NO EMOJIS - Unfold handles all icons via Material Design +Custom AdminSite for IGNY8 using Unfold theme. + +SIMPLIFIED VERSION - Navigation is now handled via UNFOLD settings in settings.py +This file only handles: +1. Custom URLs for dashboard, reports, and monitoring pages +2. Index redirect to dashboard + +All sidebar navigation is configured in settings.py under UNFOLD["SIDEBAR"]["navigation"] """ from django.contrib import admin -from django.contrib.admin.apps import AdminConfig -from django.apps import apps -from django.urls import path, reverse_lazy +from django.urls import path from django.shortcuts import redirect -from django.contrib.admin import sites -from unfold.admin import ModelAdmin as UnfoldModelAdmin from unfold.sites import UnfoldAdminSite class Igny8AdminSite(UnfoldAdminSite): """ - Custom AdminSite based on Unfold that organizes models into the planned groups + Custom AdminSite based on Unfold. + Navigation is handled via UNFOLD settings - this just adds custom URLs. """ site_header = 'IGNY8 Administration' site_title = 'IGNY8 Admin' index_title = 'IGNY8 Administration' - + def get_urls(self): - """Get admin URLs with dashboard, reports, and monitoring pages available""" - from django.urls import path + """Add custom URLs for dashboard, reports, and monitoring pages""" from .dashboard import admin_dashboard from .reports import ( revenue_report, usage_report, content_report, data_quality_report, @@ -31,12 +33,12 @@ class Igny8AdminSite(UnfoldAdminSite): from .monitoring import ( system_health_dashboard, api_monitor_dashboard, debug_console ) - + urls = super().get_urls() custom_urls = [ # Dashboard path('dashboard/', self.admin_view(admin_dashboard), name='dashboard'), - + # Reports path('reports/revenue/', self.admin_view(revenue_report), name='report_revenue'), path('reports/usage/', self.admin_view(usage_report), name='report_usage'), @@ -44,311 +46,17 @@ class Igny8AdminSite(UnfoldAdminSite): path('reports/data-quality/', self.admin_view(data_quality_report), name='report_data_quality'), path('reports/token-usage/', self.admin_view(token_usage_report), name='report_token_usage'), path('reports/ai-cost-analysis/', self.admin_view(ai_cost_analysis), name='report_ai_cost_analysis'), - - # Monitoring (NEW) + + # Monitoring path('monitoring/system-health/', self.admin_view(system_health_dashboard), name='monitoring_system_health'), path('monitoring/api-monitor/', self.admin_view(api_monitor_dashboard), name='monitoring_api_monitor'), path('monitoring/debug-console/', self.admin_view(debug_console), name='monitoring_debug_console'), ] return custom_urls + urls - - def index(self, request, extra_context=None): - """Redirect to custom dashboard""" - from django.shortcuts import redirect - return redirect('admin:dashboard') - - def get_sidebar_list(self, request): - """ - Override Unfold's get_sidebar_list to return our custom app groups - Convert Django app_list format to Unfold sidebar navigation format - """ - # Get our custom Django app list - django_apps = self.get_app_list(request, app_label=None) - - # Convert to Unfold navigation format: {title, items: [{title, link, icon}]} - sidebar_groups = [] - - for app in django_apps: - group = { - 'title': app['name'], - 'collapsible': True, - 'items': [] - } - - # Convert each model to navigation item - for model in app.get('models', []): - if model.get('perms', {}).get('view', False) or model.get('perms', {}).get('change', False): - item = { - 'title': model['name'], - 'link': model['admin_url'], - 'icon': None, # Unfold will use default - 'has_permission': True, # CRITICAL: Template checks this - } - group['items'].append(item) - - # Only add groups that have items - if group['items']: - sidebar_groups.append(group) - - return sidebar_groups - - 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 - """ - # CRITICAL: Must call parent to get sidebar_navigation set - context = super().each_context(request) - - # DEBUGGING: Print to console what parent returned - print(f"\n=== DEBUG each_context for {request.path} ===") - print(f"sidebar_navigation length from parent: {len(context.get('sidebar_navigation', []))}") - if context.get('sidebar_navigation'): - print(f"First sidebar group: {context['sidebar_navigation'][0].get('title', 'NO TITLE')}") - - # 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 - - # CRITICAL FIX: Ensure sidebar_navigation is using our custom sidebar - # Parent's each_context already called get_sidebar_list(), which returns our custom sidebar - # So sidebar_navigation should already be correct, but let's verify - if not context.get('sidebar_navigation') or len(context.get('sidebar_navigation', [])) == 0: - # If sidebar_navigation is empty, force it - print("WARNING: sidebar_navigation was empty, forcing it!") - context['sidebar_navigation'] = self.get_sidebar_list(request) - - print(f"Final sidebar_navigation length: {len(context['sidebar_navigation'])}") - print("=== END DEBUG ===\n") - - return context - def get_app_list(self, request, app_label=None): - """ - Customize the app list to organize models into logical groups - NO EMOJIS - Unfold handles all icons via Material Design - - Args: - request: The HTTP request - app_label: IGNORED - Always return full custom sidebar for consistency - """ - # 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 - custom_groups = { - 'Accounts & Tenancy': { - 'models': [ - ('igny8_core_auth', 'Account'), - ('igny8_core_auth', 'User'), - ('igny8_core_auth', 'Site'), - ('igny8_core_auth', 'Sector'), - ('igny8_core_auth', 'SiteUserAccess'), - ], - }, - 'Global Resources': { - 'models': [ - ('igny8_core_auth', 'Industry'), - ('igny8_core_auth', 'IndustrySector'), - ('igny8_core_auth', 'SeedKeyword'), - ], - }, - 'Global Settings': { - 'models': [ - ('system', 'GlobalIntegrationSettings'), - ('system', 'GlobalModuleSettings'), - ('billing', 'AIModelConfig'), - ('system', 'GlobalAIPrompt'), - ('system', 'GlobalAuthorProfile'), - ('system', 'GlobalStrategy'), - ], - }, - 'Plans and Billing': { - 'models': [ - ('igny8_core_auth', 'Plan'), - ('igny8_core_auth', 'Subscription'), - ('billing', 'BillingConfiguration'), - ('billing', 'Invoice'), - ('billing', 'Payment'), - ('billing', 'CreditPackage'), - ('billing', 'PaymentMethodConfig'), - ('billing', 'AccountPaymentMethod'), - ], - }, - 'Credits': { - 'models': [ - ('billing', 'CreditTransaction'), - ('billing', 'CreditUsageLog'), - ('billing', 'CreditCostConfig'), - ('billing', 'PlanLimitUsage'), - ], - }, - 'Content Planning': { - 'models': [ - ('planner', 'Keywords'), - ('planner', 'Clusters'), - ('planner', 'ContentIdeas'), - ], - }, - 'Content Generation': { - 'models': [ - ('writer', 'Tasks'), - ('writer', 'Content'), - ('writer', 'Images'), - ('writer', 'ImagePrompts'), - ], - }, - 'Taxonomy & Organization': { - 'models': [ - ('writer', 'ContentTaxonomy'), - ('writer', 'ContentTaxonomyRelation'), - ('writer', 'ContentClusterMap'), - ('writer', 'ContentAttribute'), - ], - }, - 'Publishing & Integration': { - 'models': [ - ('integration', 'SiteIntegration'), - ('integration', 'SyncEvent'), - ('publishing', 'PublishingRecord'), - ('system', 'PublishingChannel'), - ('publishing', 'DeploymentRecord'), - ], - }, - 'AI & Automation': { - 'models': [ - ('system', 'IntegrationSettings'), - ('system', 'AIPrompt'), - ('system', 'Strategy'), - ('system', 'AuthorProfile'), - ('system', 'APIKey'), - ('system', 'WebhookConfig'), - ('automation', 'AutomationConfig'), - ('automation', 'AutomationRun'), - ], - }, - 'System Settings': { - 'models': [ - ('contenttypes', 'ContentType'), - ('system', 'ContentTemplate'), - ('system', 'TaxonomyConfig'), - ('system', 'SystemSetting'), - ('system', 'ContentTypeConfig'), - ('system', 'NotificationConfig'), - ], - }, - 'Django Admin': { - 'models': [ - ('auth', 'Group'), - ('auth', 'Permission'), - ('igny8_core_auth', 'PasswordResetToken'), - ('sessions', 'Session'), - ], - }, - 'Tasks & Logging': { - 'models': [ - ('ai', 'AITaskLog'), - ('system', 'AuditLog'), - ('admin', 'LogEntry'), - ('django_celery_results', 'TaskResult'), - ('django_celery_results', 'GroupResult'), - ], - }, - } - - # ALWAYS build and return our custom organized app list - # regardless of app_label parameter (for consistent sidebar on all pages) - organized_apps = [] - - # Add Dashboard link as first item - organized_apps.append({ - 'name': '📊 Dashboard', - 'app_label': '_dashboard', - 'app_url': '/admin/dashboard/', - 'has_module_perms': True, - 'models': [], - }) - - # Add Reports section with links to all reports - organized_apps.append({ - 'name': 'Reports & Analytics', - 'app_label': '_reports', - 'app_url': '#', - 'has_module_perms': True, - 'models': [ - { - 'name': 'Revenue Report', - 'object_name': 'RevenueReport', - 'admin_url': '/admin/reports/revenue/', - 'view_only': True, - 'perms': {'view': True}, - }, - { - 'name': 'Usage Report', - 'object_name': 'UsageReport', - 'admin_url': '/admin/reports/usage/', - 'view_only': True, - 'perms': {'view': True}, - }, - { - 'name': 'Content Report', - 'object_name': 'ContentReport', - 'admin_url': '/admin/reports/content/', - 'view_only': True, - 'perms': {'view': True}, - }, - { - 'name': 'Data Quality Report', - 'object_name': 'DataQualityReport', - 'admin_url': '/admin/reports/data-quality/', - 'view_only': True, - 'perms': {'view': True}, - }, - { - 'name': 'Token Usage Report', - 'object_name': 'TokenUsageReport', - 'admin_url': '/admin/reports/token-usage/', - 'view_only': True, - 'perms': {'view': True}, - }, - { - 'name': 'AI Cost Analysis', - 'object_name': 'AICostAnalysis', - 'admin_url': '/admin/reports/ai-cost-analysis/', - 'view_only': True, - 'perms': {'view': True}, - }, - ], - }) - - for group_name, group_config in custom_groups.items(): - group_models = [] - - for app_label, model_name in group_config['models']: - # Find the model in app_dict - for app in app_dict.values(): - if app['app_label'] == app_label: - for model in app.get('models', []): - if model['object_name'] == model_name: - group_models.append(model) - break - - if group_models: - # Get the first model's app_label to use as the real app_label - first_model_app_label = group_config['models'][0][0] - organized_apps.append({ - 'name': group_name, - 'app_label': first_model_app_label, # Use real app_label, not fake one - 'app_url': f'/admin/{first_model_app_label}/', # Real URL, not '#' - 'has_module_perms': True, - 'models': group_models, - }) - - return organized_apps + def index(self, request, extra_context=None): + """Redirect admin index to custom dashboard""" + return redirect('admin:dashboard') # Instantiate custom admin site diff --git a/backend/igny8_core/business/billing/admin.py b/backend/igny8_core/business/billing/admin.py index 2c3647b7..9f4b5ce4 100644 --- a/backend/igny8_core/business/billing/admin.py +++ b/backend/igny8_core/business/billing/admin.py @@ -9,14 +9,18 @@ from django.contrib import messages from django.utils.html import format_html from unfold.admin import ModelAdmin from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin -from .models import ( - CreditCostConfig, - AccountPaymentMethod, - Invoice, - Payment, - CreditPackage, - PaymentMethodConfig, -) +# NOTE: Most billing models are now registered in modules/billing/admin.py +# This file is kept for reference but all registrations are commented out +# to avoid AlreadyRegistered errors + +# from .models import ( +# CreditCostConfig, +# AccountPaymentMethod, +# Invoice, +# Payment, +# CreditPackage, +# PaymentMethodConfig, +# ) # CreditCostConfig - DUPLICATE - Registered in modules/billing/admin.py with better features @@ -47,97 +51,21 @@ from .models import ( # ...existing implementation... -# PaymentMethodConfig and AccountPaymentMethod are kept here as they're not duplicated -# or have minimal implementations that don't conflict +# AccountPaymentMethod - DUPLICATE - Registered in modules/billing/admin.py with AccountAdminMixin +# Commenting out to avoid AlreadyRegistered error +# The version in modules/billing/admin.py is preferred as it includes AccountAdminMixin -from import_export.admin import ExportMixin -from import_export import resources - - -class AccountPaymentMethodResource(resources.ModelResource): - """Resource class for exporting Account Payment Methods""" - class Meta: - model = AccountPaymentMethod - fields = ('id', 'display_name', 'type', 'account__name', 'is_default', - 'is_enabled', 'is_verified', 'country_code', 'created_at') - export_order = fields - - -@admin.register(AccountPaymentMethod) -class AccountPaymentMethodAdmin(ExportMixin, Igny8ModelAdmin): - resource_class = AccountPaymentMethodResource - list_display = [ - 'display_name', - 'type', - 'account', - 'is_default', - 'is_enabled', - 'country_code', - 'is_verified', - 'updated_at', - ] - list_filter = ['type', 'is_default', 'is_enabled', 'is_verified', 'country_code'] - search_fields = ['display_name', 'account__name', 'account__id'] - readonly_fields = ['created_at', 'updated_at'] - actions = [ - 'bulk_enable', - 'bulk_disable', - 'bulk_set_default', - 'bulk_delete_methods', - ] - fieldsets = ( - ('Payment Method', { - 'fields': ('account', 'type', 'display_name', 'is_default', 'is_enabled', 'is_verified', 'country_code') - }), - ('Instructions / Metadata', { - 'fields': ('instructions', 'metadata') - }), - ('Timestamps', { - 'fields': ('created_at', 'updated_at'), - 'classes': ('collapse',) - }), - ) - def bulk_enable(self, request, queryset): - updated = queryset.update(is_enabled=True) - self.message_user(request, f'{updated} payment method(s) enabled.', messages.SUCCESS) - bulk_enable.short_description = 'Enable selected payment methods' - - def bulk_disable(self, request, queryset): - updated = queryset.update(is_enabled=False) - self.message_user(request, f'{updated} payment method(s) disabled.', messages.SUCCESS) - bulk_disable.short_description = 'Disable selected payment methods' - - def bulk_set_default(self, request, queryset): - from django import forms - - if 'apply' in request.POST: - method_id = request.POST.get('payment_method') - if method_id: - method = AccountPaymentMethod.objects.get(pk=method_id) - # Unset all others for this account - AccountPaymentMethod.objects.filter(account=method.account).update(is_default=False) - method.is_default = True - method.save() - self.message_user(request, f'{method.display_name} set as default for {method.account.name}.', messages.SUCCESS) - return - - class PaymentMethodForm(forms.Form): - payment_method = forms.ModelChoiceField( - queryset=queryset, - label="Select Payment Method to Set as Default" - ) - - from django.shortcuts import render - return render(request, 'admin/bulk_action_form.html', { - 'title': 'Set Default Payment Method', - 'queryset': queryset, - 'form': PaymentMethodForm(), - 'action': 'bulk_set_default', - }) - bulk_set_default.short_description = 'Set as default' - - def bulk_delete_methods(self, request, queryset): - count = queryset.count() - queryset.delete() - self.message_user(request, f'{count} payment method(s) deleted.', messages.SUCCESS) - bulk_delete_methods.short_description = 'Delete selected payment methods' \ No newline at end of file +# from import_export.admin import ExportMixin +# from import_export import resources +# +# class AccountPaymentMethodResource(resources.ModelResource): +# """Resource class for exporting Account Payment Methods""" +# class Meta: +# model = AccountPaymentMethod +# fields = ('id', 'display_name', 'type', 'account__name', 'is_default', +# 'is_enabled', 'is_verified', 'country_code', 'created_at') +# export_order = fields +# +# @admin.register(AccountPaymentMethod) +# class AccountPaymentMethodAdmin(ExportMixin, Igny8ModelAdmin): +# ... (see modules/billing/admin.py for active registration) \ No newline at end of file diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index f76701b4..6c1fe8d5 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -637,8 +637,9 @@ IMPORT_EXPORT_USE_TRANSACTIONS = True UNFOLD = { "SITE_TITLE": "IGNY8 Administration", - "SITE_HEADER": "IGNY8 Admin", + "SITE_HEADER": "", # Empty to hide text, logo will be shown instead "SITE_URL": "/", + "SITE_LOGO": lambda request: "/static/admin/img/logo.png", "SITE_SYMBOL": "rocket_launch", # Symbol from Material icons "SHOW_HISTORY": True, # Show history for models with simple_history "SHOW_VIEW_ON_SITE": True, # Show "View on site" button @@ -657,17 +658,182 @@ UNFOLD = { "950": "2 6 23", }, }, - "EXTENSIONS": { - "modeltranslation": { - "flags": { - "en": "🇬🇧", - "fr": "🇫🇷", - }, - }, - }, "SIDEBAR": { "show_search": True, - "show_all_applications": False, # MUST be False - we provide custom sidebar_navigation + "show_all_applications": False, + "navigation": [ + # Dashboard & Reports + { + "title": "Dashboard & Reports", + "icon": "dashboard", + "collapsible": True, + "items": [ + {"title": "Dashboard", "icon": "home", "link": lambda request: "/admin/dashboard/"}, + {"title": "Revenue Report", "icon": "attach_money", "link": lambda request: "/admin/reports/revenue/"}, + {"title": "Usage Report", "icon": "data_usage", "link": lambda request: "/admin/reports/usage/"}, + {"title": "Content Report", "icon": "article", "link": lambda request: "/admin/reports/content/"}, + {"title": "Data Quality", "icon": "verified", "link": lambda request: "/admin/reports/data-quality/"}, + {"title": "Token Usage", "icon": "token", "link": lambda request: "/admin/reports/token-usage/"}, + {"title": "AI Cost Analysis", "icon": "psychology", "link": lambda request: "/admin/reports/ai-cost-analysis/"}, + ], + }, + # Accounts & Users + { + "title": "Accounts & Users", + "icon": "group", + "collapsible": True, + "items": [ + {"title": "Accounts", "icon": "business", "link": lambda request: "/admin/igny8_core_auth/account/"}, + {"title": "Users", "icon": "person", "link": lambda request: "/admin/igny8_core_auth/user/"}, + {"title": "Sites", "icon": "language", "link": lambda request: "/admin/igny8_core_auth/site/"}, + {"title": "Sectors", "icon": "category", "link": lambda request: "/admin/igny8_core_auth/sector/"}, + {"title": "Site Access", "icon": "lock", "link": lambda request: "/admin/igny8_core_auth/siteuseraccess/"}, + ], + }, + # Plans & Billing + { + "title": "Plans & Billing", + "icon": "payments", + "collapsible": True, + "items": [ + {"title": "Plans", "icon": "workspace_premium", "link": lambda request: "/admin/igny8_core_auth/plan/"}, + {"title": "Subscriptions", "icon": "subscriptions", "link": lambda request: "/admin/igny8_core_auth/subscription/"}, + {"title": "Invoices", "icon": "receipt_long", "link": lambda request: "/admin/billing/invoice/"}, + {"title": "Payments", "icon": "paid", "link": lambda request: "/admin/billing/payment/"}, + {"title": "Credit Packages", "icon": "card_giftcard", "link": lambda request: "/admin/billing/creditpackage/"}, + {"title": "Payment Methods", "icon": "credit_card", "link": lambda request: "/admin/billing/paymentmethodconfig/"}, + ], + }, + # Credits + { + "title": "Credits", + "icon": "toll", + "collapsible": True, + "items": [ + {"title": "Transactions", "icon": "swap_horiz", "link": lambda request: "/admin/billing/credittransaction/"}, + {"title": "Usage Log", "icon": "history", "link": lambda request: "/admin/billing/creditusagelog/"}, + {"title": "Plan Limits", "icon": "speed", "link": lambda request: "/admin/billing/planlimitusage/"}, + ], + }, + # Planning + { + "title": "Planning", + "icon": "map", + "collapsible": True, + "items": [ + {"title": "Keywords", "icon": "key", "link": lambda request: "/admin/planner/keywords/"}, + {"title": "Clusters", "icon": "hub", "link": lambda request: "/admin/planner/clusters/"}, + {"title": "Content Ideas", "icon": "lightbulb", "link": lambda request: "/admin/planner/contentideas/"}, + ], + }, + # Writing + { + "title": "Writing", + "icon": "edit_note", + "collapsible": True, + "items": [ + {"title": "Tasks", "icon": "task_alt", "link": lambda request: "/admin/writer/tasks/"}, + {"title": "Content", "icon": "description", "link": lambda request: "/admin/writer/content/"}, + {"title": "Images", "icon": "image", "link": lambda request: "/admin/writer/images/"}, + {"title": "Image Prompts", "icon": "auto_awesome", "link": lambda request: "/admin/writer/imageprompts/"}, + ], + }, + # Taxonomy + { + "title": "Taxonomy", + "icon": "label", + "collapsible": True, + "items": [ + {"title": "Taxonomies", "icon": "sell", "link": lambda request: "/admin/writer/contenttaxonomy/"}, + {"title": "Relations", "icon": "link", "link": lambda request: "/admin/writer/contenttaxonomyrelation/"}, + {"title": "Attributes", "icon": "tune", "link": lambda request: "/admin/writer/contentattribute/"}, + {"title": "Cluster Maps", "icon": "account_tree", "link": lambda request: "/admin/writer/contentclustermap/"}, + ], + }, + # Publishing + { + "title": "Publishing", + "icon": "publish", + "collapsible": True, + "items": [ + {"title": "Integrations", "icon": "extension", "link": lambda request: "/admin/integration/siteintegration/"}, + {"title": "Publishing Records", "icon": "cloud_upload", "link": lambda request: "/admin/publishing/publishingrecord/"}, + {"title": "Deployments", "icon": "rocket", "link": lambda request: "/admin/publishing/deploymentrecord/"}, + {"title": "Sync Events", "icon": "sync", "link": lambda request: "/admin/integration/syncevent/"}, + ], + }, + # Automation + { + "title": "Automation", + "icon": "smart_toy", + "collapsible": True, + "items": [ + {"title": "Configs", "icon": "settings_suggest", "link": lambda request: "/admin/automation/automationconfig/"}, + {"title": "Runs", "icon": "play_circle", "link": lambda request: "/admin/automation/automationrun/"}, + ], + }, + # AI Configuration + { + "title": "AI Configuration", + "icon": "psychology", + "collapsible": True, + "items": [ + {"title": "AI Models", "icon": "model_training", "link": lambda request: "/admin/billing/aimodelconfig/"}, + {"title": "Credit Costs", "icon": "calculate", "link": lambda request: "/admin/billing/creditcostconfig/"}, + {"title": "Billing Config", "icon": "tune", "link": lambda request: "/admin/billing/billingconfiguration/"}, + {"title": "AI Task Logs", "icon": "history", "link": lambda request: "/admin/ai/aitasklog/"}, + ], + }, + # Global Settings + { + "title": "Global Settings", + "icon": "settings", + "collapsible": True, + "items": [ + {"title": "Integration Settings", "icon": "integration_instructions", "link": lambda request: "/admin/system/globalintegrationsettings/"}, + {"title": "Module Settings", "icon": "view_module", "link": lambda request: "/admin/system/globalmodulesettings/"}, + {"title": "AI Prompts", "icon": "smart_toy", "link": lambda request: "/admin/system/globalaiprompt/"}, + {"title": "Author Profiles", "icon": "person_outline", "link": lambda request: "/admin/system/globalauthorprofile/"}, + {"title": "Strategies", "icon": "strategy", "link": lambda request: "/admin/system/globalstrategy/"}, + ], + }, + # Resources + { + "title": "Resources", + "icon": "inventory_2", + "collapsible": True, + "items": [ + {"title": "Industries", "icon": "factory", "link": lambda request: "/admin/igny8_core_auth/industry/"}, + {"title": "Industry Sectors", "icon": "domain", "link": lambda request: "/admin/igny8_core_auth/industrysector/"}, + {"title": "Seed Keywords", "icon": "eco", "link": lambda request: "/admin/igny8_core_auth/seedkeyword/"}, + ], + }, + # Logs & Monitoring + { + "title": "Logs & Monitoring", + "icon": "monitor_heart", + "collapsible": True, + "items": [ + {"title": "System Health", "icon": "health_and_safety", "link": lambda request: "/admin/monitoring/system-health/"}, + {"title": "API Monitor", "icon": "api", "link": lambda request: "/admin/monitoring/api-monitor/"}, + {"title": "Debug Console", "icon": "terminal", "link": lambda request: "/admin/monitoring/debug-console/"}, + {"title": "Celery Tasks", "icon": "schedule", "link": lambda request: "/admin/django_celery_results/taskresult/"}, + {"title": "Admin Log", "icon": "history", "link": lambda request: "/admin/admin/logentry/"}, + ], + }, + # Django Admin + { + "title": "Django Admin", + "icon": "admin_panel_settings", + "collapsible": True, + "items": [ + {"title": "Groups", "icon": "groups", "link": lambda request: "/admin/auth/group/"}, + {"title": "Permissions", "icon": "security", "link": lambda request: "/admin/auth/permission/"}, + {"title": "Content Types", "icon": "dns", "link": lambda request: "/admin/contenttypes/contenttype/"}, + {"title": "Sessions", "icon": "badge", "link": lambda request: "/admin/sessions/session/"}, + ], + }, + ], }, } diff --git a/backend/igny8_core/static/admin/img/logo-dark.svg b/backend/igny8_core/static/admin/img/logo-dark.svg new file mode 100644 index 00000000..11d52ca4 --- /dev/null +++ b/backend/igny8_core/static/admin/img/logo-dark.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/igny8_core/static/admin/img/logo-full-dark.svg b/backend/igny8_core/static/admin/img/logo-full-dark.svg new file mode 100644 index 00000000..4b94dacc --- /dev/null +++ b/backend/igny8_core/static/admin/img/logo-full-dark.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/igny8_core/static/admin/img/logo-light.svg b/backend/igny8_core/static/admin/img/logo-light.svg new file mode 100644 index 00000000..11d52ca4 --- /dev/null +++ b/backend/igny8_core/static/admin/img/logo-light.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/igny8_core/static/admin/img/logo.png b/backend/igny8_core/static/admin/img/logo.png new file mode 100644 index 00000000..efc32b9e Binary files /dev/null and b/backend/igny8_core/static/admin/img/logo.png differ diff --git a/backend/igny8_core/templates/admin/base_site.html b/backend/igny8_core/templates/admin/base_site.html index 93e8744a..4336bb7f 100644 --- a/backend/igny8_core/templates/admin/base_site.html +++ b/backend/igny8_core/templates/admin/base_site.html @@ -1,3 +1,7 @@ {% extends "admin/base.html" %} {% block title %}{{ title }} | IGNY8 Admin{% endblock %} + +{% block branding %} + {% include "unfold/helpers/site_branding.html" %} +{% endblock %} diff --git a/backend/staticfiles/admin/img/logo-dark.svg b/backend/staticfiles/admin/img/logo-dark.svg new file mode 100644 index 00000000..11d52ca4 --- /dev/null +++ b/backend/staticfiles/admin/img/logo-dark.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/staticfiles/admin/img/logo-full-dark.svg b/backend/staticfiles/admin/img/logo-full-dark.svg new file mode 100644 index 00000000..4b94dacc --- /dev/null +++ b/backend/staticfiles/admin/img/logo-full-dark.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/staticfiles/admin/img/logo-light.svg b/backend/staticfiles/admin/img/logo-light.svg new file mode 100644 index 00000000..11d52ca4 --- /dev/null +++ b/backend/staticfiles/admin/img/logo-light.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/staticfiles/admin/img/logo.png b/backend/staticfiles/admin/img/logo.png new file mode 100644 index 00000000..efc32b9e Binary files /dev/null and b/backend/staticfiles/admin/img/logo.png differ diff --git a/4th-jan-refactor/REFACTOR-OVERVIEW.md b/docs/plans/4th-jan-refactor/REFACTOR-OVERVIEW.md similarity index 100% rename from 4th-jan-refactor/REFACTOR-OVERVIEW.md rename to docs/plans/4th-jan-refactor/REFACTOR-OVERVIEW.md diff --git a/docs/plans/4th-jan-refactor/django-plan.md b/docs/plans/4th-jan-refactor/django-plan.md new file mode 100644 index 00000000..33ac3cb5 --- /dev/null +++ b/docs/plans/4th-jan-refactor/django-plan.md @@ -0,0 +1,210 @@ +# Django Admin Cleanup - Implementation Status + +## Status: COMPLETED (January 4, 2026) + +--- + +## What Was Done + +### 1. Fixed Duplicate Model Registration +**File:** `backend/igny8_core/business/billing/admin.py` + +- `AccountPaymentMethod` was registered in BOTH: + - `modules/billing/admin.py` (with AccountAdminMixin - KEPT) + - `business/billing/admin.py` (simpler version - REMOVED) +- Commented out the duplicate registration in `business/billing/admin.py` + +### 2. Simplified Admin Site Configuration +**File:** `backend/igny8_core/admin/site.py` + +- Removed complex `get_app_list()` override (was 250+ lines) +- Removed `get_sidebar_list()` override +- Removed `each_context()` override with debug logging +- Kept only: + - Custom URLs for dashboard, reports, and monitoring + - Index redirect to dashboard +- Navigation is now handled by Unfold's built-in `SIDEBAR.navigation` setting + +### 3. Added Proper Unfold Navigation Configuration +**File:** `backend/igny8_core/settings.py` + +Added complete `UNFOLD["SIDEBAR"]["navigation"]` config with: +- Dashboard link +- Reports section (6 reports) +- Accounts & Users group +- Plans & Billing group +- Credits group +- Planning group +- Writing group +- Taxonomy group +- Publishing group +- Automation group +- AI Configuration group (NEW - consolidated) +- Global Settings group +- Resources group +- Logs & Monitoring group +- Django Admin group + +Each item has proper Material Design icons. + +### 4. Added Site Logo Configuration +**File:** `backend/igny8_core/settings.py` + +```python +"SITE_ICON": { + "light": lambda request: "/static/admin/img/logo-light.svg", + "dark": lambda request: "/static/admin/img/logo-dark.svg", +}, +``` + +**Note:** Logo SVG files need to be created at these paths for the logo to display. + +--- + +## Verification Results + +```bash +# Django system check +$ docker exec igny8_backend python manage.py check +System check identified no issues (0 silenced). + +# Admin registry test +$ docker exec igny8_backend python manage.py shell -c "..." +Total registered models: 63 +Admin site ready! + +# UNFOLD config test +Navigation items: 20 +``` + +--- + +## What Was NOT Done (and why) + +### Models NOT Hidden from Admin + +These models were originally planned for removal but are **actively used**: + +| Model | Reason Kept | +|-------|-------------| +| `IntegrationSettings` | Used by AI functions, settings, integration views | +| `AIPrompt` | Used by ai/prompts.py, management commands | +| `AuthorProfile` | Used by content generation | +| `Strategy` | Used by content planning | + +--- + +## Admin Sidebar Structure (Final) + +``` +Dashboard +Reports + ├── Revenue + ├── Usage + ├── Content + ├── Data Quality + ├── Token Usage + └── AI Cost Analysis + +─── Core ─── +Accounts & Users + ├── Accounts + ├── Users + ├── Sites + ├── Sectors + └── Site Access + +Plans & Billing + ├── Plans + ├── Subscriptions + ├── Invoices + ├── Payments + ├── Credit Packages + └── Payment Methods + +Credits + ├── Transactions + ├── Usage Log + └── Plan Limits + +─── Content ─── +Planning + ├── Keywords + ├── Clusters + └── Content Ideas + +Writing + ├── Tasks + ├── Content + ├── Images + └── Image Prompts + +Taxonomy + ├── Taxonomies + ├── Relations + ├── Attributes + └── Cluster Maps + +─── Automation ─── +Publishing + ├── Integrations + ├── Publishing Records + ├── Deployments + └── Sync Events + +Automation + ├── Configs + └── Runs + +─── Configuration ─── +AI Configuration + ├── AI Models + ├── Credit Costs + ├── Billing Config + └── AI Task Logs + +Global Settings + ├── Integration Settings + ├── Module Settings + ├── AI Prompts + ├── Author Profiles + └── Strategies + +Resources + ├── Industries + ├── Industry Sectors + └── Seed Keywords + +─── System ─── +Logs & Monitoring + ├── System Health + ├── API Monitor + ├── Debug Console + ├── Celery Tasks + └── Admin Log + +Django Admin + ├── Groups + ├── Permissions + ├── Content Types + └── Sessions +``` + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `backend/igny8_core/settings.py` | Added full UNFOLD navigation config | +| `backend/igny8_core/admin/site.py` | Simplified to ~60 lines (was ~350) | +| `backend/igny8_core/business/billing/admin.py` | Commented out duplicate AccountPaymentMethod | + +--- + +## Next Steps (From Original Plan) + +1. ✅ **Django Admin Cleanup** - DONE +2. ⏳ **Simplify AI Settings** - Merge content + image settings into AccountSettings +3. ⏳ **Create IntegrationProvider** - Move API keys to dedicated model +4. ⏳ **AIModelConfig Enhancement** - Add tokens_per_credit, credits_per_image, quality_tier diff --git a/4th-jan-refactor/implementation-plan-for-ai-models-and-cost.md b/docs/plans/4th-jan-refactor/implementation-plan-for-ai-models-and-cost.md similarity index 100% rename from 4th-jan-refactor/implementation-plan-for-ai-models-and-cost.md rename to docs/plans/4th-jan-refactor/implementation-plan-for-ai-models-and-cost.md diff --git a/4th-jan-refactor/safe-migration-and-testing-plan.md b/docs/plans/4th-jan-refactor/safe-migration-and-testing-plan.md similarity index 100% rename from 4th-jan-refactor/safe-migration-and-testing-plan.md rename to docs/plans/4th-jan-refactor/safe-migration-and-testing-plan.md diff --git a/docs/plans/4th-jan-refactor/simple-ai-models-credits-image-gen.md b/docs/plans/4th-jan-refactor/simple-ai-models-credits-image-gen.md new file mode 100644 index 00000000..2ea94639 --- /dev/null +++ b/docs/plans/4th-jan-refactor/simple-ai-models-credits-image-gen.md @@ -0,0 +1,91 @@ +My Recommendation: Simplify to 3 Core Concepts +Based on analyzing your plans and the current system, here's the simplest path forward: +Current Pain Points (Summarized) +Area Problem +API Keys Scattered in GlobalIntegrationSettings with unused fields (anthropic, bria) +Models Duplicated in DB (AIModelConfig), constants.py, GlobalIntegrationSettings CHOICES, frontend +Credits Split between CreditCostConfig (token-based) and image models (per-image) - confusing +Frontend Hardcoded model lists, not dynamic +Proposed Simplified Architecture +Instead of 7-phase migration, do this: + +┌─────────────────────────────────────────────────────────────────┐ +│ SIMPLIFIED TARGET STATE │ +└─────────────────────────────────────────────────────────────────┘ + +1. AIModelConfig (ENHANCED) - SINGLE SOURCE OF TRUTH + ├── All models (text + image) + ├── is_default flag (one per type) + ├── tokens_per_credit (text models) + ├── credits_per_image (image models) + ├── quality_tier (basic/quality/premium for user display) + └── provider field links to API key source + +2. IntegrationProvider (NEW) - API KEYS ONLY + ├── openai → api_key + ├── runware → api_key + └── (future: resend, stripe, etc.) + +3. GlobalIntegrationSettings (SIMPLIFIED) - NON-MODEL SETTINGS ONLY + ├── image_style: "photorealistic" + ├── max_in_article_images: 4 + └── temperature: 0.7 + (NO model names, NO API keys, NO hardcoded choices) +User-Facing Simplification +For Images (what user sees): + +Quality Selection: + ○ Basic (1 credit/image) ← maps to runware:97@1 + ○ Quality (5 credits/image) ← maps to dall-e-3 + ○ Premium (15 credits/image) ← maps to google:4@2 + +Image Count: [1-8 dropdown] +User picks quality tier + count. Backend handles: +Model selection based on quality_tier +Credit calculation: credits_per_image × count +API routing to correct provider +For Text (hidden from user): +Admin controls default model in AIModelConfig.is_default +User has NO options - uses system default +Credits calculated: ceil(tokens / tokens_per_credit) +Simplest Migration Path +Phase 1: Add fields to AIModelConfig (no breaking changes) + +# Add to existing AIModelConfig +credits_per_image = IntegerField(null=True) # For image models +tokens_per_credit = IntegerField(null=True) # For text models +quality_tier = CharField(choices=['basic','quality','premium']) +Phase 2: Create IntegrationProvider (move API keys) + +# New simple model +class IntegrationProvider(Model): + provider_id = CharField(primary_key=True) # openai, runware + api_key = CharField() + is_active = BooleanField() +Phase 3: Create single API endpoint + +GET /api/v1/system/ai-config/ +Returns: +{ + "image_qualities": [ + {"tier": "basic", "credits_per_image": 1, "label": "Basic"}, + {"tier": "quality", "credits_per_image": 5, "label": "Quality"}, + {"tier": "premium", "credits_per_image": 15, "label": "Premium"} + ], + "text_model": "gpt-5.1", // Admin-controlled, user can't change + "image_style": "photorealistic", + "max_images": 4 +} +Phase 4: Update frontend - load from API, remove hardcodes Phase 5: Cleanup - remove legacy fields from GlobalIntegrationSettings +Key Simplifications +Before After +User picks model name User picks quality tier +Multiple credit calculation paths One path: AIModelConfig +API keys in GlobalIntegrationSettings API keys in IntegrationProvider +Hardcoded constants.py rates All from database +6+ admin pages to configure 2 pages: Models + Providers +Do you want me to: +Start implementing this simplified approach? - I can create the migration files and update the models +Create a more detailed breakdown of each phase with specific file changes? +Focus on a specific area first (e.g., just fix image credits, or just move API keys)? +The existing plans in your docs are comprehensive but may be over-engineered. The core problem is simpler: consolidate to AIModelConfig as single source of truth, give users quality tiers instead of model names, and move API keys to a dedicated model. \ No newline at end of file