feat(admin): Add API monitoring, debug console, and system health templates for enhanced admin interface docs: Add AI system cleanup summary and audit report detailing architecture, token management, and recommendations docs: Introduce credits and tokens system guide outlining configuration, data flow, and monitoring strategies
444 lines
16 KiB
Python
444 lines
16 KiB
Python
"""
|
|
System Module Admin
|
|
"""
|
|
from django.contrib import admin
|
|
from unfold.admin import ModelAdmin
|
|
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
|
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
|
|
from .global_settings_models import (
|
|
GlobalIntegrationSettings,
|
|
GlobalAIPrompt,
|
|
GlobalAuthorProfile,
|
|
GlobalStrategy,
|
|
)
|
|
|
|
from django.contrib import messages
|
|
from import_export.admin import ExportMixin, ImportExportMixin
|
|
from import_export import resources
|
|
|
|
|
|
class AIPromptResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting AI Prompts"""
|
|
class Meta:
|
|
model = AIPrompt
|
|
fields = ('id', 'account__name', 'prompt_type', 'prompt_value', 'is_active', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
# Import settings admin
|
|
from .settings_admin import (
|
|
SystemSettingsAdmin, AccountSettingsAdmin, UserSettingsAdmin,
|
|
ModuleSettingsAdmin, AISettingsAdmin
|
|
)
|
|
|
|
try:
|
|
from .models import SystemLog, SystemStatus
|
|
|
|
@admin.register(SystemLog)
|
|
class SystemLogAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
|
list_display = ['id', 'account', 'module', 'level', 'action', 'message', 'created_at']
|
|
list_filter = ['module', 'level', 'created_at', 'account']
|
|
search_fields = ['message', 'action']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
date_hierarchy = 'created_at'
|
|
|
|
|
|
@admin.register(SystemStatus)
|
|
class SystemStatusAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
|
list_display = ['component', 'account', 'status', 'message', 'last_check']
|
|
list_filter = ['status', 'component', 'account']
|
|
search_fields = ['component', 'message']
|
|
readonly_fields = ['last_check']
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
@admin.register(AIPrompt)
|
|
class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = AIPromptResource
|
|
list_display = ['id', 'prompt_type', 'account', 'is_customized', 'is_active', 'updated_at']
|
|
list_filter = ['prompt_type', 'is_active', 'is_customized', 'account']
|
|
search_fields = ['prompt_type']
|
|
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
'bulk_reset_to_default',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'prompt_type', 'is_active', 'is_customized')
|
|
}),
|
|
('Prompt Content', {
|
|
'fields': ('prompt_value', 'default_prompt'),
|
|
'description': 'Customize prompt_value or reset to default_prompt'
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} AI prompt(s) activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected prompts'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} AI prompt(s) deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected prompts'
|
|
|
|
def bulk_reset_to_default(self, request, queryset):
|
|
"""Reset selected prompts to their global defaults"""
|
|
count = 0
|
|
for prompt in queryset:
|
|
if prompt.default_prompt:
|
|
prompt.reset_to_default()
|
|
count += 1
|
|
self.message_user(request, f'{count} prompt(s) reset to default.', messages.SUCCESS)
|
|
bulk_reset_to_default.short_description = 'Reset selected prompts to global default'
|
|
|
|
|
|
class IntegrationSettingsResource(resources.ModelResource):
|
|
"""Resource class for exporting Integration Settings (config masked)"""
|
|
class Meta:
|
|
model = IntegrationSettings
|
|
fields = ('id', 'account__name', 'integration_type', 'is_active', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(IntegrationSettings)
|
|
class IntegrationSettingsAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
"""
|
|
Admin for per-account integration setting overrides.
|
|
|
|
IMPORTANT: This stores ONLY model selection and parameter overrides.
|
|
API keys come from GlobalIntegrationSettings and cannot be overridden.
|
|
Free plan users cannot create these - they must use global defaults.
|
|
"""
|
|
resource_class = IntegrationSettingsResource
|
|
list_display = ['id', 'integration_type', 'account', 'is_active', 'updated_at']
|
|
list_filter = ['integration_type', 'is_active', 'account']
|
|
search_fields = ['integration_type', 'account__name']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'integration_type', 'is_active')
|
|
}),
|
|
('Configuration Overrides', {
|
|
'fields': ('config',),
|
|
'description': (
|
|
'JSON overrides for model/parameter selection. '
|
|
'Fields: model, temperature, max_tokens, image_size, image_quality, etc. '
|
|
'Leave null to use global defaults. '
|
|
'Example: {"model": "gpt-4", "temperature": 0.8, "max_tokens": 4000} '
|
|
'WARNING: NEVER store API keys here - they come from GlobalIntegrationSettings'
|
|
)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} integration setting(s) activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected integrations'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} integration setting(s) deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected integrations'
|
|
|
|
def bulk_test_connection(self, request, queryset):
|
|
"""Test connection for selected integration settings"""
|
|
count = queryset.filter(is_active=True).count()
|
|
self.message_user(
|
|
request,
|
|
f'{count} integration(s) queued for connection test. (Test logic to be implemented)',
|
|
messages.INFO
|
|
)
|
|
bulk_test_connection.short_description = 'Test connections'
|
|
|
|
|
|
class AuthorProfileResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Author Profiles"""
|
|
class Meta:
|
|
model = AuthorProfile
|
|
fields = ('id', 'name', 'account__name', 'tone', 'language', 'is_active', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(AuthorProfile)
|
|
class AuthorProfileAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = AuthorProfileResource
|
|
list_display = ['name', 'account', 'tone', 'language', 'is_active', 'created_at']
|
|
list_filter = ['is_active', 'tone', 'language', 'account']
|
|
search_fields = ['name', 'description', 'tone']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
'bulk_clone',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'name', 'description', 'is_active')
|
|
}),
|
|
('Writing Style', {
|
|
'fields': ('tone', 'language', 'structure_template')
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} author profile(s) activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected profiles'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} author profile(s) deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected profiles'
|
|
|
|
def bulk_clone(self, request, queryset):
|
|
count = 0
|
|
for profile in queryset:
|
|
profile_copy = AuthorProfile.objects.get(pk=profile.pk)
|
|
profile_copy.pk = None
|
|
profile_copy.name = f"{profile.name} (Copy)"
|
|
profile_copy.is_active = False
|
|
profile_copy.save()
|
|
count += 1
|
|
self.message_user(request, f'{count} author profile(s) cloned.', messages.SUCCESS)
|
|
bulk_clone.short_description = 'Clone selected profiles'
|
|
|
|
|
|
class StrategyResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Strategies"""
|
|
class Meta:
|
|
model = Strategy
|
|
fields = ('id', 'name', 'account__name', 'sector__name', 'is_active', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(Strategy)
|
|
class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = StrategyResource
|
|
list_display = ['name', 'account', 'sector', 'is_active', 'created_at']
|
|
list_filter = ['is_active', 'account']
|
|
search_fields = ['name', 'description']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
'bulk_clone',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'name', 'description', 'sector', 'is_active')
|
|
}),
|
|
('Strategy Configuration', {
|
|
'fields': ('prompt_types', 'section_logic')
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def get_sector_display(self, obj):
|
|
"""Safely get sector name"""
|
|
try:
|
|
return obj.sector.name if obj.sector else 'Global'
|
|
except:
|
|
return 'Global'
|
|
get_sector_display.short_description = 'Sector'
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} strategy/strategies activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected strategies'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} strategy/strategies deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected strategies'
|
|
|
|
def bulk_clone(self, request, queryset):
|
|
count = 0
|
|
for strategy in queryset:
|
|
strategy_copy = Strategy.objects.get(pk=strategy.pk)
|
|
strategy_copy.pk = None
|
|
strategy_copy.name = f"{strategy.name} (Copy)"
|
|
strategy_copy.is_active = False
|
|
strategy_copy.save()
|
|
count += 1
|
|
self.message_user(request, f'{count} strategy/strategies cloned.', messages.SUCCESS)
|
|
bulk_clone.short_description = 'Clone selected strategies'
|
|
|
|
|
|
# =============================================================================
|
|
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
|
|
# =============================================================================
|
|
|
|
@admin.register(GlobalIntegrationSettings)
|
|
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
|
"""Admin for global integration settings (singleton)"""
|
|
list_display = ["id", "is_active", "last_updated", "updated_by"]
|
|
readonly_fields = ["last_updated"]
|
|
|
|
fieldsets = (
|
|
("OpenAI Settings", {
|
|
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
|
|
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
|
|
}),
|
|
("DALL-E Settings", {
|
|
"fields": ("dalle_api_key", "dalle_model", "dalle_size", "dalle_quality", "dalle_style"),
|
|
"description": "Global DALL-E image generation configuration"
|
|
}),
|
|
("Anthropic Settings", {
|
|
"fields": ("anthropic_api_key", "anthropic_model"),
|
|
"description": "Global Anthropic Claude configuration"
|
|
}),
|
|
("Runware Settings", {
|
|
"fields": ("runware_api_key",),
|
|
"description": "Global Runware image generation configuration"
|
|
}),
|
|
("Status", {
|
|
"fields": ("is_active", "last_updated", "updated_by")
|
|
}),
|
|
)
|
|
|
|
def has_add_permission(self, request):
|
|
"""Only allow one instance (singleton pattern)"""
|
|
return not GlobalIntegrationSettings.objects.exists()
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
"""Dont allow deletion of singleton"""
|
|
return False
|
|
|
|
@admin.register(GlobalAIPrompt)
|
|
class GlobalAIPromptAdmin(ExportMixin, Igny8ModelAdmin):
|
|
"""Admin for global AI prompt templates"""
|
|
list_display = ["prompt_type", "version", "is_active", "last_updated"]
|
|
list_filter = ["is_active", "prompt_type", "version"]
|
|
search_fields = ["prompt_type", "description"]
|
|
readonly_fields = ["last_updated", "created_at"]
|
|
|
|
fieldsets = (
|
|
("Basic Info", {
|
|
"fields": ("prompt_type", "description", "is_active", "version")
|
|
}),
|
|
("Prompt Content", {
|
|
"fields": ("prompt_value", "variables"),
|
|
"description": "Variables should be a list of variable names used in the prompt"
|
|
}),
|
|
("Timestamps", {
|
|
"fields": ("created_at", "last_updated")
|
|
}),
|
|
)
|
|
|
|
actions = ["increment_version"]
|
|
|
|
def increment_version(self, request, queryset):
|
|
"""Increment version for selected prompts"""
|
|
for prompt in queryset:
|
|
prompt.version += 1
|
|
prompt.save()
|
|
self.message_user(request, f"{queryset.count()} prompt(s) version incremented.", messages.SUCCESS)
|
|
increment_version.short_description = "Increment version"
|
|
|
|
|
|
@admin.register(GlobalAuthorProfile)
|
|
class GlobalAuthorProfileAdmin(ImportExportMixin, Igny8ModelAdmin):
|
|
"""Admin for global author profile templates"""
|
|
list_display = ["name", "category", "tone", "language", "is_active", "created_at"]
|
|
list_filter = ["is_active", "category", "tone", "language"]
|
|
search_fields = ["name", "description"]
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
fieldsets = (
|
|
("Basic Info", {
|
|
"fields": ("name", "description", "category", "is_active")
|
|
}),
|
|
("Writing Style", {
|
|
"fields": ("tone", "language", "structure_template")
|
|
}),
|
|
("Timestamps", {
|
|
"fields": ("created_at", "updated_at")
|
|
}),
|
|
)
|
|
|
|
|
|
@admin.register(GlobalStrategy)
|
|
class GlobalStrategyAdmin(ImportExportMixin, Igny8ModelAdmin):
|
|
"""Admin for global strategy templates"""
|
|
list_display = ["name", "category", "is_active", "created_at"]
|
|
list_filter = ["is_active", "category"]
|
|
search_fields = ["name", "description"]
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
fieldsets = (
|
|
("Basic Info", {
|
|
"fields": ("name", "description", "category", "is_active")
|
|
}),
|
|
("Strategy Configuration", {
|
|
"fields": ("prompt_types", "section_logic")
|
|
}),
|
|
("Timestamps", {
|
|
"fields": ("created_at", "updated_at")
|
|
}),
|
|
)
|
|
|