wokring models and image genration model and admin apges

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-03 17:28:18 +00:00
parent 52600c9dca
commit a1016ec1c2
15 changed files with 1119 additions and 25 deletions

View File

@@ -161,6 +161,7 @@ class Igny8AdminSite(UnfoldAdminSite):
'models': [
('system', 'GlobalIntegrationSettings'),
('system', 'GlobalModuleSettings'),
('billing', 'AIModelConfig'),
('system', 'GlobalAIPrompt'),
('system', 'GlobalAuthorProfile'),
('system', 'GlobalStrategy'),

View File

@@ -59,9 +59,21 @@ def get_model_config(function_name: str, account) -> Dict[str, Any]:
# Start with global defaults
model = global_settings.openai_model
temperature = global_settings.openai_temperature
max_tokens = global_settings.openai_max_tokens
api_key = global_settings.openai_api_key # ALWAYS from global
# Get max_tokens from AIModelConfig for the selected model
max_tokens = global_settings.openai_max_tokens # Fallback
try:
from igny8_core.business.billing.models import AIModelConfig
model_config = AIModelConfig.objects.filter(
model_name=model,
is_active=True
).first()
if model_config and model_config.max_output_tokens:
max_tokens = model_config.max_output_tokens
except Exception as e:
logger.warning(f"Could not load max_tokens from AIModelConfig for {model}: {e}")
# Check if account has overrides (only for Starter/Growth/Scale plans)
# Free plan users cannot create IntegrationSettings records
try:
@@ -76,12 +88,23 @@ def get_model_config(function_name: str, account) -> Dict[str, Any]:
# Override model if specified (NULL = use global)
if config.get('model'):
model = config['model']
# Also update max_tokens for the overridden model
try:
from igny8_core.business.billing.models import AIModelConfig
override_config = AIModelConfig.objects.filter(
model_name=model,
is_active=True
).first()
if override_config and override_config.max_output_tokens:
max_tokens = override_config.max_output_tokens
except Exception:
pass
# Override temperature if specified
if config.get('temperature') is not None:
temperature = config['temperature']
# Override max_tokens if specified
# Override max_tokens if explicitly specified (rare case)
if config.get('max_tokens'):
max_tokens = config['max_tokens']

View File

@@ -124,12 +124,22 @@ class IsEditorOrAbove(permissions.BasePermission):
class IsAdminOrOwner(permissions.BasePermission):
"""
Permission class that requires admin or owner role only
OR user belongs to aws-admin account
For settings, keys, billing operations
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Check if user belongs to aws-admin account (case-insensitive)
if hasattr(request.user, 'account') and request.user.account:
account_name = getattr(request.user.account, 'name', None)
account_slug = getattr(request.user.account, 'slug', None)
if account_name and account_name.lower() == 'aws admin':
return True
if account_slug == 'aws-admin':
return True
# Check user role
if hasattr(request.user, 'role'):
role = request.user.role

View File

@@ -0,0 +1,87 @@
"""
Migration: Update Runware model configurations in AIModelConfig
This migration:
1. Updates runware:97@1 to have display_name "Hi Dream Full - Standard"
2. Adds Bria 3.2 model as civitai:618692@691639
"""
from decimal import Decimal
from django.db import migrations
def update_runware_models(apps, schema_editor):
"""Update Runware models in AIModelConfig"""
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
# Update existing runware:97@1 model
AIModelConfig.objects.update_or_create(
model_name='runware:97@1',
defaults={
'display_name': 'Hi Dream Full - Standard',
'model_type': 'image',
'provider': 'runware',
'cost_per_image': Decimal('0.008'),
'valid_sizes': ['512x512', '768x768', '1024x1024', '1024x1792', '1792x1024'],
'supports_json_mode': False,
'supports_vision': False,
'supports_function_calling': False,
'is_active': True,
'is_default': True, # Make this the default Runware model
'sort_order': 10,
'description': 'Hi Dream Full - Standard quality image generation via Runware',
}
)
# Add Bria 3.2 Premium model
AIModelConfig.objects.update_or_create(
model_name='civitai:618692@691639',
defaults={
'display_name': 'Bria 3.2 - Premium',
'model_type': 'image',
'provider': 'runware',
'cost_per_image': Decimal('0.012'),
'valid_sizes': ['512x512', '768x768', '1024x1024', '1024x1792', '1792x1024'],
'supports_json_mode': False,
'supports_vision': False,
'supports_function_calling': False,
'is_active': True,
'is_default': False,
'sort_order': 11,
'description': 'Bria 3.2 - Premium quality image generation via Runware/Civitai',
}
)
# Optionally remove the old runware:100@1 and runware:101@1 models if they exist
AIModelConfig.objects.filter(
model_name__in=['runware:100@1', 'runware:101@1']
).update(is_active=False)
def reverse_migration(apps, schema_editor):
"""Reverse the migration"""
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
# Restore old display name
AIModelConfig.objects.filter(model_name='runware:97@1').update(
display_name='Runware Standard',
is_default=False,
)
# Remove Bria 3.2 model
AIModelConfig.objects.filter(model_name='civitai:618692@691639').delete()
# Re-activate old models
AIModelConfig.objects.filter(
model_name__in=['runware:100@1', 'runware:101@1']
).update(is_active=True)
class Migration(migrations.Migration):
dependencies = [
('billing', '0022_fix_historical_calculation_mode_null'),
]
operations = [
migrations.RunPython(update_runware_models, reverse_migration),
]

View File

@@ -2,6 +2,7 @@
System Module Admin
"""
from django.contrib import admin
from django import forms
from unfold.admin import ModelAdmin
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
@@ -333,16 +334,61 @@ class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
# =============================================================================
class GlobalIntegrationSettingsForm(forms.ModelForm):
"""Custom form for GlobalIntegrationSettings with dynamic choices from AIModelConfig"""
class Meta:
model = GlobalIntegrationSettings
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Load choices dynamically from AIModelConfig
from igny8_core.modules.system.global_settings_models import (
get_text_model_choices,
get_image_model_choices,
get_provider_choices,
)
# OpenAI text model choices
openai_choices = get_text_model_choices()
openai_text_choices = [(m, d) for m, d in openai_choices if 'gpt' in m.lower() or 'openai' in m.lower()]
if openai_text_choices:
self.fields['openai_model'].choices = openai_text_choices
# DALL-E image model choices
dalle_choices = get_image_model_choices(provider='openai')
if dalle_choices:
self.fields['dalle_model'].choices = dalle_choices
# Runware image model choices
runware_choices = get_image_model_choices(provider='runware')
if runware_choices:
self.fields['runware_model'].choices = runware_choices
# Image service provider choices (only OpenAI and Runware for now)
image_providers = get_provider_choices(model_type='image')
# Filter to only OpenAI and Runware
allowed_image_providers = [
(p, d) for p, d in image_providers
if p in ('openai', 'runware')
]
if allowed_image_providers:
self.fields['default_image_service'].choices = allowed_image_providers
@admin.register(GlobalIntegrationSettings)
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
"""Admin for global integration settings (singleton)"""
form = GlobalIntegrationSettingsForm
list_display = ["id", "is_active", "last_updated", "updated_by"]
readonly_fields = ["last_updated"]
readonly_fields = ["last_updated", "openai_max_tokens", "anthropic_max_tokens"]
fieldsets = (
("OpenAI Settings", {
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
"description": "Global OpenAI configuration used by all accounts (unless overridden). Max tokens is loaded from AI Model Configuration."
}),
("Image Generation - Default Service", {
"fields": ("default_image_service",),
@@ -365,6 +411,49 @@ class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
}),
)
def get_readonly_fields(self, request, obj=None):
"""Make max_tokens fields readonly - they are populated from AI Model Configuration"""
readonly = list(super().get_readonly_fields(request, obj))
if 'openai_max_tokens' not in readonly:
readonly.append('openai_max_tokens')
if 'anthropic_max_tokens' not in readonly:
readonly.append('anthropic_max_tokens')
return readonly
def openai_max_tokens(self, obj):
"""Display max tokens from the selected OpenAI model's configuration"""
from igny8_core.modules.system.global_settings_models import get_model_max_tokens
max_tokens = get_model_max_tokens(obj.openai_model) if obj else None
if max_tokens:
return f"{max_tokens:,} (from AI Model Configuration)"
return obj.openai_max_tokens if obj else "8192 (default)"
openai_max_tokens.short_description = "Max Output Tokens"
def anthropic_max_tokens(self, obj):
"""Display max tokens from the selected Anthropic model's configuration"""
from igny8_core.modules.system.global_settings_models import get_model_max_tokens
max_tokens = get_model_max_tokens(obj.anthropic_model) if obj else None
if max_tokens:
return f"{max_tokens:,} (from AI Model Configuration)"
return obj.anthropic_max_tokens if obj else "8192 (default)"
anthropic_max_tokens.short_description = "Max Output Tokens"
def save_model(self, request, obj, form, change):
"""Update max_tokens from model config on save"""
from igny8_core.modules.system.global_settings_models import get_model_max_tokens
# Update OpenAI max tokens from model config
openai_max = get_model_max_tokens(obj.openai_model)
if openai_max:
obj.openai_max_tokens = openai_max
# Update Anthropic max tokens from model config
anthropic_max = get_model_max_tokens(obj.anthropic_model)
if anthropic_max:
obj.anthropic_max_tokens = anthropic_max
super().save_model(request, obj, form, change)
def has_add_permission(self, request):
"""Only allow one instance (singleton pattern)"""
return not GlobalIntegrationSettings.objects.exists()

View File

@@ -5,6 +5,125 @@ Accounts can override model selection and parameters (but NOT API keys).
"""
from django.db import models
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
def get_text_model_choices():
"""
Get text model choices from AIModelConfig database.
Returns list of tuples (model_name, display_name) for active text models.
Falls back to hardcoded defaults if database unavailable.
"""
try:
from igny8_core.business.billing.models import AIModelConfig
models = AIModelConfig.objects.filter(
model_type='text',
is_active=True
).order_by('sort_order', 'model_name')
if models.exists():
return [(m.model_name, m.display_name) for m in models]
except Exception as e:
logger.warning(f"Could not load text models from database: {e}")
# Fallback to hardcoded defaults
return [
('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'),
('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'),
]
def get_image_model_choices(provider=None):
"""
Get image model choices from AIModelConfig database.
Optionally filter by provider (openai, runware, etc.)
"""
try:
from igny8_core.business.billing.models import AIModelConfig
qs = AIModelConfig.objects.filter(
model_type='image',
is_active=True
)
if provider:
qs = qs.filter(provider=provider)
qs = qs.order_by('sort_order', 'model_name')
if qs.exists():
return [(m.model_name, m.display_name) for m in qs]
except Exception as e:
logger.warning(f"Could not load image models from database: {e}")
# Fallback based on provider
if provider == 'openai':
return [
('dall-e-3', 'DALL·E 3 - $0.040 per image'),
('dall-e-2', 'DALL·E 2 - $0.020 per image'),
]
elif provider == 'runware':
return [
('runware:97@1', 'Hi Dream Full - Standard'),
('civitai:618692@691639', 'Bria 3.2 - Premium'),
]
return []
def get_provider_choices(model_type='text'):
"""
Get provider choices from AIModelConfig database.
Returns unique providers for the given model type.
"""
try:
from igny8_core.business.billing.models import AIModelConfig
providers = list(AIModelConfig.objects.filter(
model_type=model_type,
is_active=True
).values_list('provider', flat=True).distinct())
provider_display = {
'openai': 'OpenAI DALL-E' if model_type == 'image' else 'OpenAI',
'anthropic': 'Anthropic (Claude)',
'runware': 'Runware',
'google': 'Google',
}
if providers:
# Use dict to ensure unique entries
unique_providers = {}
for p in providers:
if p not in unique_providers:
unique_providers[p] = provider_display.get(p, p.title())
return [(p, d) for p, d in unique_providers.items()]
except Exception as e:
logger.warning(f"Could not load providers from database: {e}")
# Fallback
if model_type == 'text':
return [('openai', 'OpenAI (GPT)'), ('anthropic', 'Anthropic (Claude)')]
elif model_type == 'image':
return [('openai', 'OpenAI DALL-E'), ('runware', 'Runware')]
return []
def get_model_max_tokens(model_name):
"""
Get max_output_tokens for a specific model from AIModelConfig.
Returns None if model not found or doesn't have max_output_tokens.
"""
try:
from igny8_core.business.billing.models import AIModelConfig
model_config = AIModelConfig.objects.filter(
model_name=model_name,
is_active=True
).first()
if model_config and model_config.max_output_tokens:
return model_config.max_output_tokens
except Exception as e:
logger.warning(f"Could not get max tokens for model {model_name}: {e}")
return None
class GlobalIntegrationSettings(models.Model):
@@ -52,9 +171,8 @@ class GlobalIntegrationSettings(models.Model):
]
RUNWARE_MODEL_CHOICES = [
('runware:97@1', 'Runware 97@1 - Versatile Model'),
('runware:100@1', 'Runware 100@1 - High Quality'),
('runware:101@1', 'Runware 101@1 - Fast Generation'),
('runware:97@1', 'Hi Dream Full - Standard'),
('civitai:618692@691639', 'Bria 3.2 - Premium'),
]
BRIA_MODEL_CHOICES = [
@@ -79,7 +197,6 @@ class GlobalIntegrationSettings(models.Model):
IMAGE_SERVICE_CHOICES = [
('openai', 'OpenAI DALL-E'),
('runware', 'Runware'),
('bria', 'Bria AI'),
]
ANTHROPIC_MODEL_CHOICES = [

View File

@@ -795,12 +795,25 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
# Build response with global defaults
if integration_type == 'openai':
# Get max_tokens from AIModelConfig for the selected model
max_tokens = global_settings.openai_max_tokens # Fallback
try:
from igny8_core.business.billing.models import AIModelConfig
model_config = AIModelConfig.objects.filter(
model_name=global_settings.openai_model,
is_active=True
).first()
if model_config and model_config.max_output_tokens:
max_tokens = model_config.max_output_tokens
except Exception:
pass
response_data = {
'id': 'openai',
'enabled': True, # Always enabled (platform-wide)
'model': global_settings.openai_model,
'temperature': global_settings.openai_temperature,
'max_tokens': global_settings.openai_max_tokens,
'max_tokens': max_tokens,
'using_global': True, # Flag to show it's using global
}

View File

@@ -0,0 +1,360 @@
# IGNY8 Application Repository
**Repository Name:** `igny8-app`
**Purpose:** Custom application code for the IGNY8 Content AI Platform
---
## Overview
This repository contains ONLY the custom business logic for IGNY8. It does not contain:
- Dockerfiles (in stack repo)
- package.json / requirements.txt (in stack repo)
- Vite/TypeScript configs (in stack repo)
- Docker compose files (in stack repo)
---
## Complete Folder Structure
```
igny8-app/
├── README.md # App overview
├── CHANGELOG.md # Version history
├── .gitignore # Git ignore rules
├── .env.example # Environment template
├── backend/
│ └── igny8_core/ # Django application
│ ├── __init__.py
│ ├── settings.py # Django settings
│ ├── urls.py # Root URL routing
│ ├── celery.py # Celery configuration
│ ├── wsgi.py # WSGI entry point
│ ├── asgi.py # ASGI entry point
│ ├── tasks.py # Root Celery tasks
│ │
│ ├── auth/ # Authentication module
│ │ ├── __init__.py
│ │ ├── models.py # User, Account, Site, Sector, Plan
│ │ ├── views.py # Login, register, password reset
│ │ ├── serializers.py # DRF serializers
│ │ ├── middleware.py # AccountContextMiddleware
│ │ ├── urls.py # Auth URL routes
│ │ └── migrations/ # Database migrations
│ │
│ ├── api/ # API infrastructure
│ │ ├── __init__.py
│ │ ├── base.py # Base ViewSets (AccountModelViewSet)
│ │ ├── authentication.py # JWT, API key auth
│ │ ├── pagination.py # Unified pagination
│ │ └── tests/ # API tests
│ │
│ ├── ai/ # AI engine
│ │ ├── __init__.py
│ │ ├── engine.py # AIEngine orchestrator
│ │ ├── registry.py # Function registry
│ │ ├── model_registry.py # AI model configuration
│ │ ├── progress.py # Progress tracking
│ │ └── functions/ # AI function implementations
│ │ ├── __init__.py
│ │ ├── auto_cluster.py
│ │ ├── generate_ideas.py
│ │ ├── generate_content.py
│ │ └── ...
│ │
│ ├── modules/ # Feature modules (API layer)
│ │ ├── __init__.py
│ │ ├── planner/ # Keywords, Clusters, Ideas
│ │ │ ├── models.py
│ │ │ ├── views.py
│ │ │ ├── serializers.py
│ │ │ ├── urls.py
│ │ │ └── migrations/
│ │ ├── writer/ # Tasks, Content, Images
│ │ ├── billing/ # Credits, usage, transactions
│ │ ├── integration/ # WordPress integration
│ │ ├── system/ # Settings, prompts, AI config
│ │ ├── linker/ # Internal linking
│ │ ├── optimizer/ # Content optimization
│ │ └── publisher/ # Publishing pipeline
│ │
│ ├── business/ # Business logic (services)
│ │ ├── __init__.py
│ │ ├── automation/ # 7-stage automation pipeline
│ │ ├── billing/ # Credit service
│ │ ├── content/ # Content generation
│ │ ├── integration/ # Sync services
│ │ ├── linking/ # Link processing
│ │ ├── notifications/ # Notification system
│ │ ├── optimization/ # Content optimization
│ │ ├── planning/ # Clustering, ideas
│ │ └── publishing/ # Publishing orchestration
│ │
│ ├── middleware/ # Custom middleware
│ │ ├── __init__.py
│ │ ├── request_id.py # X-Request-ID header
│ │ └── resource_tracker.py # Resource tracking
│ │
│ ├── tasks/ # Celery background tasks
│ │ ├── __init__.py
│ │ └── *.py
│ │
│ ├── admin/ # Django admin customization
│ ├── common/ # Shared utilities
│ ├── management/ # Django management commands
│ │ └── commands/
│ ├── utils/ # Helper functions
│ ├── urls/ # URL routing modules
│ ├── static/ # App static files
│ └── templates/ # Django templates
├── frontend/
│ └── src/ # React application
│ ├── main.tsx # Entry point
│ ├── App.tsx # Root component, routing
│ ├── index.css # Global styles, Tailwind
│ ├── vite-env.d.ts # Vite type definitions
│ ├── svg.d.ts # SVG type definitions
│ │
│ ├── api/ # API client modules
│ │ ├── linker.api.ts
│ │ ├── optimizer.api.ts
│ │ └── ...
│ │
│ ├── services/ # Core services
│ │ ├── api.ts # Main API service
│ │ └── notifications.api.ts # Notification API
│ │
│ ├── store/ # Zustand state stores
│ │ ├── authStore.ts # Authentication state
│ │ ├── siteStore.ts # Active site
│ │ ├── sectorStore.ts # Active sector
│ │ ├── billingStore.ts # Billing state
│ │ ├── moduleStore.ts # Module enable/disable
│ │ ├── notificationStore.ts # Notifications
│ │ └── ...
│ │
│ ├── pages/ # Route pages
│ │ ├── Dashboard/
│ │ ├── Planner/
│ │ ├── Writer/
│ │ ├── Automation/
│ │ ├── Linker/
│ │ ├── Optimizer/
│ │ ├── Settings/
│ │ ├── Billing/
│ │ └── Auth/
│ │
│ ├── components/ # Reusable components
│ │ ├── common/ # Shared UI components
│ │ ├── dashboard/ # Dashboard widgets
│ │ ├── header/ # Header components
│ │ └── shared/ # Cross-feature components
│ │
│ ├── layout/ # Layout components
│ │ ├── AppLayout.tsx
│ │ ├── AppHeader.tsx
│ │ └── AppSidebar.tsx
│ │
│ ├── hooks/ # Custom React hooks
│ ├── context/ # React contexts
│ ├── config/ # App configuration
│ ├── icons/ # Icon components
│ ├── styles/ # CSS modules/styles
│ ├── utils/ # Utility functions
│ ├── types/ # TypeScript types
│ ├── templates/ # Content templates
│ ├── modules/ # Feature modules
│ ├── marketing/ # Marketing site
│ │ ├── index.tsx
│ │ ├── MarketingApp.tsx
│ │ ├── config/
│ │ ├── components/
│ │ ├── layout/
│ │ ├── pages/
│ │ ├── images/
│ │ └── styles/
│ │
│ └── __tests__/ # Test files
├── public/ # Public static assets
│ ├── favicon.ico
│ ├── logo.svg
│ └── images/
└── docs/ # Documentation
├── 00-SYSTEM/
│ └── ARCHITECTURE.md
├── 10-MODULES/
├── 20-API/
├── 30-FRONTEND/
├── 40-WORKFLOWS/
├── 50-DEPLOYMENT/
│ ├── TWO-REPO-ARCHITECTURE.md
│ ├── INFRASTRUCTURE-STACK.md
│ ├── IGNY8-APP-STRUCTURE.md # This file
│ ├── DOCKER-DEPLOYMENT.md
│ └── ENVIRONMENT-SETUP.md
├── 90-REFERENCE/
└── plans/
```
---
## Deployment to New Server
### Prerequisites
- Stack already installed (see INFRASTRUCTURE-STACK.md)
- Server has `/data/stack/igny8-stack/` ready
- Docker network `igny8_net` exists
### Step-by-Step Deployment
```
1. Copy app code to server
Option A: Git clone
cd /data/app
git clone https://github.com/yourorg/igny8-app.git igny8
Option B: Rsync from existing server
rsync -avz --exclude='.git' \
--exclude='node_modules' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='staticfiles' \
--exclude='dist' \
old-server:/data/app/igny8/ /data/app/igny8/
2. Create symlinks to stack
cd /data/stack/igny8-stack
./scripts/link-app.sh igny8
3. Configure environment
cd /data/app/igny8
cp .env.example .env
nano .env # Set your secrets
4. Install frontend dependencies
docker exec igny8_frontend npm install
5. Run database migrations
docker exec igny8_backend python manage.py migrate
6. Create superuser (first time only)
docker exec -it igny8_backend python manage.py createsuperuser
7. Collect static files
docker exec igny8_backend python manage.py collectstatic --noinput
8. Start all services
docker compose -f docker-compose.app.yml up -d
9. Verify
curl https://api.igny8.com/api/v1/system/status/
```
---
## What Gets Copied vs Symlinked
### Copied (Your Code)
| Path | Content |
|------|---------|
| `backend/igny8_core/` | All Django app code |
| `frontend/src/` | All React code |
| `public/` | Static assets |
| `docs/` | Documentation |
| `.env` | Environment config |
### Symlinked (From Stack)
| App Path | Stack Path |
|----------|------------|
| `backend/Dockerfile` | `stack/backend/Dockerfile` |
| `backend/requirements.txt` | `stack/backend/requirements.txt` |
| `backend/manage.py` | `stack/backend/manage.py` |
| `frontend/Dockerfile.dev` | `stack/frontend/Dockerfile.dev` |
| `frontend/package.json` | `stack/frontend/package.json` |
| `frontend/vite.config.ts` | `stack/frontend/vite.config.ts` |
| `frontend/tsconfig*.json` | `stack/frontend/tsconfig*.json` |
| `docker-compose.app.yml` | `stack/docker/docker-compose.app.yml` |
---
## Updating App on Existing Server
### Code Update (Most Common)
```
cd /data/app/igny8
git pull # Get latest code
# If migrations changed:
docker exec igny8_backend python manage.py migrate
# If frontend deps changed:
docker exec igny8_frontend npm install
# Restart to apply:
docker compose restart
```
### Full Rebuild (After Major Changes)
```
cd /data/app/igny8
docker compose down
docker compose build --no-cache
docker compose up -d
```
---
## Backup Before Migration
### What to Backup
| Item | Command/Method |
|------|----------------|
| Database | `pg_dump igny8_db > backup.sql` |
| App code | `git push` or `rsync` |
| Uploads | Copy `/data/app/igny8/backend/media/` |
| Environment | Copy `.env` file |
### Full Backup Script
```
# On old server
DATE=$(date +%Y%m%d)
mkdir -p /data/backups/$DATE
# Database
docker exec igny8_postgres pg_dump -U igny8 igny8_db > /data/backups/$DATE/db.sql
# App code (excluding generated files)
tar -czf /data/backups/$DATE/app.tar.gz \
--exclude='node_modules' \
--exclude='__pycache__' \
--exclude='staticfiles' \
--exclude='dist' \
--exclude='.git' \
/data/app/igny8/
# Environment
cp /data/app/igny8/.env /data/backups/$DATE/
echo "Backup complete: /data/backups/$DATE/"
```
---
## Related Documentation
- [TWO-REPO-ARCHITECTURE.md](TWO-REPO-ARCHITECTURE.md) - Why this structure
- [INFRASTRUCTURE-STACK.md](INFRASTRUCTURE-STACK.md) - Stack repo details
- [DOCKER-DEPLOYMENT.md](DOCKER-DEPLOYMENT.md) - Container details
- [ARCHITECTURE.md](../00-SYSTEM/ARCHITECTURE.md) - System architecture

View File

@@ -0,0 +1,219 @@
# Infrastructure Stack Repository
**Repository Name:** `igny8-stack`
**Purpose:** Reusable tech stack for Django + React applications
---
## Complete Folder Structure
```
igny8-stack/
├── README.md # Stack overview and quick start
├── install.sh # Main installation script
├── uninstall.sh # Cleanup script
├── config/
│ ├── .env.example # Environment variables template
│ │
│ └── caddy/
│ └── Caddyfile # Reverse proxy configuration
├── docker/
│ ├── docker-compose.infra.yml # Shared services (Postgres, Redis, Caddy)
│ └── docker-compose.app.yml # App services template
├── backend/
│ ├── Dockerfile # Python 3.11 slim + dependencies
│ ├── requirements.txt # Django, DRF, Celery, Gunicorn, etc.
│ ├── manage.py # Django CLI entry point
│ ├── container_startup.sh # Container entrypoint script
│ └── create_groups.py # Initial permission groups setup
├── frontend/
│ ├── Dockerfile.dev # Vite dev server with HMR
│ ├── Dockerfile.prod # Production build with Caddy
│ ├── Dockerfile.marketing.dev # Marketing site dev server
│ ├── package.json # React, Vite, Tailwind, Zustand, etc.
│ ├── package-lock.json # Locked dependency versions
│ ├── vite.config.ts # Vite configuration
│ ├── tsconfig.json # TypeScript base config
│ ├── tsconfig.app.json # App TypeScript config
│ ├── tsconfig.node.json # Node TypeScript config
│ ├── postcss.config.js # PostCSS for Tailwind
│ ├── eslint.config.js # ESLint rules
│ ├── index.html # Main app HTML template
│ └── marketing.html # Marketing site HTML template
└── scripts/
├── build-images.sh # Build all Docker images
├── migrate.sh # Run Django migrations
├── backup-db.sh # Database backup utility
├── restore-db.sh # Database restore utility
└── health-check.sh # System health check
```
---
## File Purposes
### Root Files
| File | Purpose |
|------|---------|
| `install.sh` | Creates directories, symlinks, builds images, sets up network |
| `uninstall.sh` | Removes symlinks and cleans up (keeps data) |
### Config Folder
| File | Purpose |
|------|---------|
| `.env.example` | Template for environment variables (DB passwords, secrets, domains) |
| `caddy/Caddyfile` | Routes domains to containers, handles SSL, WebSocket proxying |
### Docker Folder
| File | Purpose |
|------|---------|
| `docker-compose.infra.yml` | Postgres, Redis, Caddy, PgAdmin, FileBrowser, Portainer |
| `docker-compose.app.yml` | Backend, Frontend, Celery Worker, Celery Beat, Flower |
### Backend Folder
| File | Purpose |
|------|---------|
| `Dockerfile` | Python 3.11 slim, installs requirements, runs Gunicorn |
| `requirements.txt` | All Python dependencies (Django 5.x, DRF, Celery, etc.) |
| `manage.py` | Django management command entry point |
| `container_startup.sh` | Logs startup, runs migrations if needed |
| `create_groups.py` | Creates initial Django permission groups |
### Frontend Folder
| File | Purpose |
|------|---------|
| `Dockerfile.dev` | Node 18, Vite dev server with hot reload |
| `Dockerfile.prod` | Multi-stage build, Caddy serves static files |
| `Dockerfile.marketing.dev` | Marketing site dev server (port 5174) |
| `package.json` | All npm dependencies |
| `vite.config.ts` | Build config, HMR settings, code splitting |
| `tsconfig*.json` | TypeScript compiler settings |
| `postcss.config.js` | Tailwind CSS processing |
| `eslint.config.js` | Code linting rules |
| `index.html` | Main app entry HTML |
| `marketing.html` | Marketing site entry HTML |
### Scripts Folder
| Script | Purpose |
|--------|---------|
| `build-images.sh` | Builds all Docker images with proper tags |
| `migrate.sh` | Runs Django migrations inside container |
| `backup-db.sh` | Dumps PostgreSQL database to backup file |
| `restore-db.sh` | Restores database from backup file |
| `health-check.sh` | Checks all services are running |
---
## New Server Installation
### Prerequisites
- Ubuntu 22.04+ or Debian 12+
- Docker and Docker Compose installed
- Git installed
- Domain DNS pointing to server IP
### Step-by-Step Installation
```
1. Create directories
mkdir -p /data/stack /data/app /data/logs
2. Clone stack repository
cd /data/stack
git clone https://github.com/yourorg/igny8-stack.git
3. Run installation script
cd igny8-stack
chmod +x install.sh
./install.sh
4. Verify installation
docker images # Should show app-backend, app-frontend images
docker network ls # Should show igny8_net
ls -la /data/app/ # Should show prepared structure
```
### What install.sh Does
1. Creates Docker network (`igny8_net`)
2. Creates directory structure under `/data/app/`
3. Builds Docker images:
- `app-backend:latest`
- `app-frontend-dev:latest`
- `app-marketing-dev:latest`
4. Starts infrastructure services (Postgres, Redis, Caddy)
5. Creates symlinks for app folder structure
6. Prints next steps
---
## Updating Stack on Existing Server
```
1. Pull latest changes
cd /data/stack/igny8-stack
git pull
2. Rebuild images if Dockerfile changed
./scripts/build-images.sh
3. Restart containers to use new images
cd /data/app/igny8
docker compose -f docker-compose.app.yml down
docker compose -f docker-compose.app.yml up -d
4. Verify
docker ps
./scripts/health-check.sh
```
---
## Customizing for Different Apps
The stack is designed to run any Django + React application. To use it for a different app (not igny8):
1. Clone stack to new server
2. Run `install.sh`
3. Clone your app repo to `/data/app/yourapp/`
4. Update `.env` with your app's settings
5. Update Caddyfile with your domains
6. Start containers
The stack doesn't contain any igny8-specific logic - it's a generic Django+React runtime environment.
---
## Environment Variables
Key variables to set in `.env`:
| Variable | Purpose | Example |
|----------|---------|---------|
| `DB_HOST` | PostgreSQL host | `postgres` |
| `DB_NAME` | Database name | `igny8_db` |
| `DB_USER` | Database user | `igny8` |
| `DB_PASSWORD` | Database password | `secure_password` |
| `REDIS_HOST` | Redis host | `redis` |
| `SECRET_KEY` | Django secret key | `random_50_char_string` |
| `DOMAIN` | Primary domain | `igny8.com` |
| `DEBUG` | Debug mode | `False` |
---
## Related Documentation
- [TWO-REPO-ARCHITECTURE.md](TWO-REPO-ARCHITECTURE.md) - Why two repos
- [IGNY8-APP-STRUCTURE.md](IGNY8-APP-STRUCTURE.md) - App-specific code structure

View File

@@ -0,0 +1,150 @@
# Two-Repository Architecture
**Purpose:** Separate tech stack infrastructure from custom application code for easy server migration and deployment.
---
## Overview
Instead of one monolithic repository, split into two:
| Repository | Contains | Changes | Deployment |
|------------|----------|---------|------------|
| `igny8-stack` | Docker, configs, dependencies | Rarely | Clone once per server |
| `igny8-app` | Your custom business logic | Frequently | Copy to deploy |
---
## Why This Architecture?
### Current Problem
- Everything mixed together (Dockerfiles, configs, your code)
- To deploy to new server, must copy everything
- Hard to distinguish "what's mine" vs "what's framework"
### Solution Benefits
- **Clean separation**: Stack vs App code
- **Easy migration**: Just copy app folder after stack is installed
- **Version independence**: Update stack without touching app
- **Reusable stack**: Same stack can run different apps on different servers
---
## How It Works
### Server Structure After Setup
```
/data/
├── stack/
│ └── igny8-stack/ # Cloned from stack repo (read-only)
├── app/
│ └── igny8/ # Your app code + symlinks to stack
│ ├── backend/
│ │ ├── Dockerfile → symlink to stack
│ │ ├── requirements.txt → symlink to stack
│ │ ├── manage.py → symlink to stack
│ │ └── igny8_core/ # YOUR CODE
│ │
│ └── frontend/
│ ├── package.json → symlink to stack
│ ├── vite.config.ts → symlink to stack
│ └── src/ # YOUR CODE
└── logs/
```
### Symlinks Explained
The `install.sh` script creates symbolic links from your app folder to the stack folder. This means:
- Stack files (Dockerfile, package.json, etc.) live in one place
- Your app folder references them via symlinks
- Docker sees a complete project structure
- You only manage your custom code
---
## Migration Workflow
### On New Server
```
Step 1: Install Stack (one-time)
────────────────────────────────
cd /data/stack
git clone https://your-repo/igny8-stack.git
cd igny8-stack
./install.sh
Step 2: Deploy Your App
────────────────────────────────
cd /data/app
git clone https://your-repo/igny8-app.git igny8
# OR: rsync/scp from old server
Step 3: Configure
────────────────────────────────
cp .env.example .env
nano .env # Set secrets, domains
Step 4: Start
────────────────────────────────
docker compose -f docker-compose.app.yml up -d
```
### Updating Existing Server
**Update app code only:**
```
cd /data/app/igny8
git pull
docker compose restart
```
**Update stack (rare):**
```
cd /data/stack/igny8-stack
git pull
./scripts/build-images.sh
docker compose -f /data/app/igny8/docker-compose.app.yml up -d
```
---
## What Goes Where?
### Stack Repo (igny8-stack)
Things that are **same for any Django+React app**:
- Dockerfiles
- Base requirements.txt / package.json
- Vite config, TypeScript config
- Docker compose templates
- Caddy config templates
- Utility scripts
### App Repo (igny8-app)
Things that are **specific to your application**:
- Django app code (models, views, serializers, business logic)
- React components, pages, stores
- App-specific static assets
- Documentation
- Environment config templates
---
## Key Principle
> If you deleted your app code and replaced it with a different Django+React app,
> would this file still make sense?
> - YES → Stack repo
> - NO → App repo
---
## Related Documentation
- [INFRASTRUCTURE-STACK.md](INFRASTRUCTURE-STACK.md) - Complete stack repo structure
- [IGNY8-APP-STRUCTURE.md](IGNY8-APP-STRUCTURE.md) - App repo structure

View File

@@ -87,6 +87,12 @@ Combined Free Limits:
- Brevo: 9,000/mo marketing
- Total: 12,000 emails/month free
1. Configure both email services
2. integrate app with both providers for both types of iamges,
3. create a mail history log with small mail details, archived eery 30 days (in some compact format) want to keep it to minalml records which can eb acesed later, so nto to cluter the databse,
4. verification, that all signup adn alerts and notifications mails are beign sent successfully, so that has to be deifend also when emailas are supposed to be triggered and cofnigured, if nto present already in bacekdn /frotnedn
---
## 9. Pipeline Verification
@@ -132,3 +138,17 @@ Combined Free Limits:
----
Other items
1. SEtup wizard tobe updated to use page style like other app pages, (currently its converted form modal not a good design at all) nad also removal of unneccsary header and icon from the wizard main apage,
and it needs mroe clearner adn better intro cards of waht is beign provided through wizrd
2. adding 3 more widgets in home(dsahabrod)
i) sites and small sites data/status info with buttons to directly land to settings or action
ii) rmeaining credits uasge , ai runs (jsut liek site dahbarod)
iii) account realted info like x credits consumed, x remainig, reset day, last payment date, next payment due, plan package tyep these kind of info , trarack waht is the best accoutn realted info to add to this widget
3. many buttons in sytem are configured with icon on top, tits a kind of bug, and some buttons use type attribute while some not, sinc ewe agreed and plplnnaed for gloabl compoeent usage, iw ant to standrdize al lbutton in system, if it si for toogle, or button group or single button with icon button oor without icon, all should suse singe compoenetn for each type item or variant, idelly the one which doesnt need the type=button at the ened of boutton defiention showign in html
4. integrate Stripe & Paypal payment methods
5. exact same padding defeined in gloabl app layout, so below header and after sidebar, adn on all pages of app there is exact asmae mardign and padding 12px on top bottom, left right, but only defiend in lgoabl layout, and remoing of all inner margind and padding from internal page main wrappers

View File

@@ -8,7 +8,8 @@ interface AdminRouteProps {
/**
* AdminRoute component - guards routes requiring admin or staff privileges
* Redirects to dashboard if user is not admin/staff
* OR users belonging to the aws-admin account
* Redirects to dashboard if user doesn't meet requirements
*/
export default function AdminRoute({ children }: AdminRouteProps) {
const { user, isAuthenticated } = useAuthStore();
@@ -19,12 +20,14 @@ export default function AdminRoute({ children }: AdminRouteProps) {
return null;
}
// Check if user is admin or staff
// Check if user is admin/staff OR belongs to aws-admin account
const isAdmin = user?.role === 'admin' || user?.is_staff === true;
const isAwsAdminAccount = user?.account?.name === 'aws-admin' || user?.account?.slug === 'aws-admin';
const hasAccess = isAdmin || isAwsAdminAccount;
if (!isAdmin) {
// Redirect non-admin users to dashboard
console.log('AdminRoute: User is not admin/staff, redirecting to dashboard');
if (!hasAccess) {
// Redirect unauthorized users to dashboard
console.log('AdminRoute: User does not have admin access, redirecting to dashboard');
return <Navigate to="/" state={{ from: location }} replace />;
}

View File

@@ -91,9 +91,8 @@ export default function ImageServiceCard({
const model = imageSettings.runwareModel || 'runware:97@1';
// Map model ID to display name
const modelDisplayNames: Record<string, string> = {
'runware:97@1': 'HiDream-I1 Full',
'runware:gen3a_turbo': 'Gen3a Turbo',
'runware:gen3a': 'Gen3a',
'runware:97@1': 'Hi Dream Full - Standard',
'civitai:618692@691639': 'Bria 3.2 - Premium',
};
const displayName = modelDisplayNames[model] || model;
return `Runware ${displayName}`;

View File

@@ -332,9 +332,14 @@ const AppSidebar: React.FC = () => {
{items
.filter((nav) => {
// Filter out admin-only items for non-admin users
if (nav.adminOnly && user?.role !== 'admin' && !user?.is_staff) {
// Allow access for: admin role, staff users, or aws-admin account members
if (nav.adminOnly) {
const isAdmin = user?.role === 'admin' || user?.is_staff === true;
const isAwsAdminAccount = user?.account?.name === 'aws-admin' || user?.account?.slug === 'aws-admin';
if (!isAdmin && !isAwsAdminAccount) {
return false;
}
}
return true;
})
.map((nav, itemIndex) => {

View File

@@ -552,9 +552,8 @@ export default function Integration() {
});
},
options: [
{ value: 'runware:97@1', label: 'HiDream-I1 Full - $0.009 per image' },
{ value: 'runware:gen3a_turbo', label: 'Gen3a Turbo - $0.009 per image' },
{ value: 'runware:gen3a', label: 'Gen3a - $0.009 per image' },
{ value: 'runware:97@1', label: 'Hi Dream Full - Standard' },
{ value: 'civitai:618692@691639', label: 'Bria 3.2 - Premium' },
],
});
}
@@ -922,9 +921,8 @@ export default function Integration() {
? (() => {
// Map model ID to display name
const modelDisplayNames: Record<string, string> = {
'runware:97@1': 'HiDream-I1 Full',
'runware:gen3a_turbo': 'Gen3a Turbo',
'runware:gen3a': 'Gen3a',
'runware:97@1': 'Hi Dream Full - Standard',
'civitai:618692@691639': 'Bria 3.2 - Premium',
};
return modelDisplayNames[integrations.image_generation.runwareModel] || integrations.image_generation.runwareModel;
})()