From 6e30d2d4e873a7b08891adb7be2c5c730aed5585 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sun, 4 Jan 2026 06:04:37 +0000 Subject: [PATCH] Django admin cleanup --- backend/igny8_core/admin/apps.py | 5 + backend/igny8_core/admin/base.py | 11 +- backend/igny8_core/admin/site.py | 332 ++---------------- backend/igny8_core/business/billing/admin.py | 130 ++----- backend/igny8_core/settings.py | 186 +++++++++- .../igny8_core/static/admin/img/logo-dark.svg | 44 +++ .../static/admin/img/logo-full-dark.svg | 53 +++ .../static/admin/img/logo-light.svg | 44 +++ backend/igny8_core/static/admin/img/logo.png | Bin 0 -> 14538 bytes .../igny8_core/templates/admin/base_site.html | 4 + backend/staticfiles/admin/img/logo-dark.svg | 44 +++ .../staticfiles/admin/img/logo-full-dark.svg | 53 +++ backend/staticfiles/admin/img/logo-light.svg | 44 +++ backend/staticfiles/admin/img/logo.png | Bin 0 -> 14538 bytes .../4th-jan-refactor}/REFACTOR-OVERVIEW.md | 0 docs/plans/4th-jan-refactor/django-plan.md | 210 +++++++++++ ...lementation-plan-for-ai-models-and-cost.md | 0 .../safe-migration-and-testing-plan.md | 0 .../simple-ai-models-credits-image-gen.md | 91 +++++ 19 files changed, 827 insertions(+), 424 deletions(-) create mode 100644 backend/igny8_core/static/admin/img/logo-dark.svg create mode 100644 backend/igny8_core/static/admin/img/logo-full-dark.svg create mode 100644 backend/igny8_core/static/admin/img/logo-light.svg create mode 100644 backend/igny8_core/static/admin/img/logo.png create mode 100644 backend/staticfiles/admin/img/logo-dark.svg create mode 100644 backend/staticfiles/admin/img/logo-full-dark.svg create mode 100644 backend/staticfiles/admin/img/logo-light.svg create mode 100644 backend/staticfiles/admin/img/logo.png rename {4th-jan-refactor => docs/plans/4th-jan-refactor}/REFACTOR-OVERVIEW.md (100%) create mode 100644 docs/plans/4th-jan-refactor/django-plan.md rename {4th-jan-refactor => docs/plans/4th-jan-refactor}/implementation-plan-for-ai-models-and-cost.md (100%) rename {4th-jan-refactor => docs/plans/4th-jan-refactor}/safe-migration-and-testing-plan.md (100%) create mode 100644 docs/plans/4th-jan-refactor/simple-ai-models-credits-image-gen.md 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 0000000000000000000000000000000000000000..efc32b9e59c24b1a30dc493f18e38112c3db54c3 GIT binary patch literal 14538 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..efc32b9e59c24b1a30dc493f18e38112c3db54c3 GIT binary patch literal 14538 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