diff --git a/=0.27.0 b/=0.27.0 deleted file mode 100644 index f5d18029..00000000 --- a/=0.27.0 +++ /dev/null @@ -1,36 +0,0 @@ -Collecting drf-spectacular - Downloading drf_spectacular-0.29.0-py3-none-any.whl.metadata (14 kB) -Requirement already satisfied: Django>=2.2 in /usr/local/lib/python3.11/site-packages (from drf-spectacular) (5.2.8) -Requirement already satisfied: djangorestframework>=3.10.3 in /usr/local/lib/python3.11/site-packages (from drf-spectacular) (3.16.1) -Collecting uritemplate>=2.0.0 (from drf-spectacular) - Downloading uritemplate-4.2.0-py3-none-any.whl.metadata (2.6 kB) -Collecting PyYAML>=5.1 (from drf-spectacular) - Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB) -Collecting jsonschema>=2.6.0 (from drf-spectacular) - Downloading jsonschema-4.25.1-py3-none-any.whl.metadata (7.6 kB) -Collecting inflection>=0.3.1 (from drf-spectacular) - Downloading inflection-0.5.1-py2.py3-none-any.whl.metadata (1.7 kB) -Requirement already satisfied: asgiref>=3.8.1 in /usr/local/lib/python3.11/site-packages (from Django>=2.2->drf-spectacular) (3.10.0) -Requirement already satisfied: sqlparse>=0.3.1 in /usr/local/lib/python3.11/site-packages (from Django>=2.2->drf-spectacular) (0.5.3) -Collecting attrs>=22.2.0 (from jsonschema>=2.6.0->drf-spectacular) - Downloading attrs-25.4.0-py3-none-any.whl.metadata (10 kB) -Collecting jsonschema-specifications>=2023.03.6 (from jsonschema>=2.6.0->drf-spectacular) - Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl.metadata (2.9 kB) -Collecting referencing>=0.28.4 (from jsonschema>=2.6.0->drf-spectacular) - Downloading referencing-0.37.0-py3-none-any.whl.metadata (2.8 kB) -Collecting rpds-py>=0.7.1 (from jsonschema>=2.6.0->drf-spectacular) - Downloading rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.1 kB) -Requirement already satisfied: typing-extensions>=4.4.0 in /usr/local/lib/python3.11/site-packages (from referencing>=0.28.4->jsonschema>=2.6.0->drf-spectacular) (4.15.0) -Downloading drf_spectacular-0.29.0-py3-none-any.whl (105 kB) -Downloading inflection-0.5.1-py2.py3-none-any.whl (9.5 kB) -Downloading jsonschema-4.25.1-py3-none-any.whl (90 kB) -Downloading attrs-25.4.0-py3-none-any.whl (67 kB) -Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB) -Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 7.8 MB/s 0:00:00 -Downloading referencing-0.37.0-py3-none-any.whl (26 kB) -Downloading rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (382 kB) -Downloading uritemplate-4.2.0-py3-none-any.whl (11 kB) -Installing collected packages: uritemplate, rpds-py, PyYAML, inflection, attrs, referencing, jsonschema-specifications, jsonschema, drf-spectacular - -Successfully installed PyYAML-6.0.3 attrs-25.4.0 drf-spectacular-0.29.0 inflection-0.5.1 jsonschema-4.25.1 jsonschema-specifications-2025.9.1 referencing-0.37.0 rpds-py-0.28.0 uritemplate-4.2.0 diff --git a/backup-api-standard-v1/backend/igny8_core/api/schema_extensions.py b/backup-api-standard-v1/backend/igny8_core/api/schema_extensions.py deleted file mode 100644 index 9589315a..00000000 --- a/backup-api-standard-v1/backend/igny8_core/api/schema_extensions.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -OpenAPI Schema Extensions for drf-spectacular -Custom extensions for JWT authentication and unified response format -""" -from drf_spectacular.extensions import OpenApiAuthenticationExtension -from drf_spectacular.plumbing import build_bearer_security_scheme_object -from drf_spectacular.utils import extend_schema, OpenApiResponse -from rest_framework import status - - -class JWTAuthenticationExtension(OpenApiAuthenticationExtension): - """ - OpenAPI extension for JWT Bearer Token authentication - """ - target_class = 'igny8_core.api.authentication.JWTAuthentication' - name = 'JWTAuthentication' - - def get_security_definition(self, auto_schema): - return build_bearer_security_scheme_object( - header_name='Authorization', - token_prefix='Bearer', - bearer_format='JWT' - ) - - -class CSRFExemptSessionAuthenticationExtension(OpenApiAuthenticationExtension): - """ - OpenAPI extension for CSRF-exempt session authentication - """ - target_class = 'igny8_core.api.authentication.CSRFExemptSessionAuthentication' - name = 'SessionAuthentication' - - def get_security_definition(self, auto_schema): - return { - 'type': 'apiKey', - 'in': 'cookie', - 'name': 'sessionid' - } - diff --git a/backup-api-standard-v1/backend/igny8_core/auth/migrations/0009_fix_admin_log_user_fk.py b/backup-api-standard-v1/backend/igny8_core/auth/migrations/0009_fix_admin_log_user_fk.py deleted file mode 100644 index d7ef10f4..00000000 --- a/backup-api-standard-v1/backend/igny8_core/auth/migrations/0009_fix_admin_log_user_fk.py +++ /dev/null @@ -1,88 +0,0 @@ -from django.db import migrations - - -def forward_fix_admin_log_fk(apps, schema_editor): - if schema_editor.connection.vendor != "postgresql": - return - schema_editor.execute( - """ - ALTER TABLE django_admin_log - DROP CONSTRAINT IF EXISTS django_admin_log_user_id_c564eba6_fk_auth_user_id; - """ - ) - schema_editor.execute( - """ - UPDATE django_admin_log - SET user_id = sub.new_user_id - FROM ( - SELECT id AS new_user_id - FROM igny8_users - ORDER BY id - LIMIT 1 - ) AS sub - WHERE django_admin_log.user_id NOT IN ( - SELECT id FROM igny8_users - ); - """ - ) - schema_editor.execute( - """ - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'django_admin_log_user_id_c564eba6_fk_igny8_users_id' - ) THEN - ALTER TABLE django_admin_log - ADD CONSTRAINT django_admin_log_user_id_c564eba6_fk_igny8_users_id - FOREIGN KEY (user_id) REFERENCES igny8_users(id) DEFERRABLE INITIALLY DEFERRED; - END IF; - END $$; - """ - ) - - -def reverse_fix_admin_log_fk(apps, schema_editor): - if schema_editor.connection.vendor != "postgresql": - return - schema_editor.execute( - """ - ALTER TABLE django_admin_log - DROP CONSTRAINT IF EXISTS django_admin_log_user_id_c564eba6_fk_igny8_users_id; - """ - ) - schema_editor.execute( - """ - UPDATE django_admin_log - SET user_id = sub.old_user_id - FROM ( - SELECT id AS old_user_id - FROM auth_user - ORDER BY id - LIMIT 1 - ) AS sub - WHERE django_admin_log.user_id NOT IN ( - SELECT id FROM auth_user - ); - """ - ) - schema_editor.execute( - """ - ALTER TABLE django_admin_log - ADD CONSTRAINT django_admin_log_user_id_c564eba6_fk_auth_user_id - FOREIGN KEY (user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED; - """ - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ("igny8_core_auth", "0008_passwordresettoken_alter_industry_options_and_more"), - ] - - operations = [ - migrations.RunPython(forward_fix_admin_log_fk, reverse_fix_admin_log_fk), - ] - - diff --git a/backup-api-standard-v1/backend/igny8_core/modules/system/migrations/0006_alter_systemstatus_unique_together_and_more.py b/backup-api-standard-v1/backend/igny8_core/modules/system/migrations/0006_alter_systemstatus_unique_together_and_more.py deleted file mode 100644 index 5a83fbe0..00000000 --- a/backup-api-standard-v1/backend/igny8_core/modules/system/migrations/0006_alter_systemstatus_unique_together_and_more.py +++ /dev/null @@ -1,71 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-07 14:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('system', '0005_add_author_profile_strategy'), - ] - - operations = [ - # Remove unique_together constraint if it exists and table exists - migrations.RunSQL( - """ - DO $$ - BEGIN - -- Drop unique constraint if table and constraint exist - IF EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_name = 'igny8_system_status' - ) AND EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname LIKE '%systemstatus%tenant_id%component%' - ) THEN - ALTER TABLE igny8_system_status DROP CONSTRAINT IF EXISTS igny8_system_status_tenant_id_component_key; - END IF; - END $$; - """, - reverse_sql=migrations.RunSQL.noop - ), - # Only remove field if table exists - migrations.RunSQL( - """ - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_name = 'igny8_system_status' - ) AND EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'igny8_system_status' AND column_name = 'tenant_id' - ) THEN - ALTER TABLE igny8_system_status DROP COLUMN IF EXISTS tenant_id; - END IF; - END $$; - """, - reverse_sql=migrations.RunSQL.noop - ), - # Delete models only if tables exist - migrations.RunSQL( - """ - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_name = 'igny8_system_logs' - ) THEN - DROP TABLE IF EXISTS igny8_system_logs CASCADE; - END IF; - IF EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_name = 'igny8_system_status' - ) THEN - DROP TABLE IF EXISTS igny8_system_status CASCADE; - END IF; - END $$; - """, - reverse_sql=migrations.RunSQL.noop - ), - ] diff --git a/backup-api-standard-v1/backend/igny8_core/settings.py b/backup-api-standard-v1/backend/igny8_core/settings.py deleted file mode 100644 index 2a5a0966..00000000 --- a/backup-api-standard-v1/backend/igny8_core/settings.py +++ /dev/null @@ -1,443 +0,0 @@ -""" -Django settings for igny8_core project. -""" - -from pathlib import Path -from datetime import timedelta -from urllib.parse import urlparse -import os - -BASE_DIR = Path(__file__).resolve().parent.parent - -# SECURITY: SECRET_KEY must be set via environment variable in production -# Generate a new key with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" -SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-)#i8!6+_&j97eb_4actu86=qtg)p+p#)vr48!ahjs8u=o5#5aw') - -# SECURITY: DEBUG should be False in production -# Set DEBUG=False via environment variable for production deployments -DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' - -# Unified API Standard v1.0 Feature Flags -# Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=True to enable unified exception handler -# Set IGNY8_DEBUG_THROTTLE=True to bypass rate limiting in development -IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', str(DEBUG)).lower() == 'true' - -ALLOWED_HOSTS = [ - '*', # Allow all hosts for flexibility - 'api.igny8.com', - 'app.igny8.com', - 'igny8.com', - 'www.igny8.com', - 'localhost', - '127.0.0.1', - # Note: Do NOT add static IP addresses here - they change on container restart - # Use container names or domain names instead -] - -INSTALLED_APPS = [ - 'igny8_core.admin.apps.Igny8AdminConfig', # Custom admin config - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'django_filters', - 'corsheaders', - 'drf_spectacular', # OpenAPI 3.0 schema generation - 'igny8_core.auth.apps.Igny8CoreAuthConfig', # Use app config with custom label - 'igny8_core.ai.apps.AIConfig', # AI Framework - 'igny8_core.modules.planner.apps.PlannerConfig', - 'igny8_core.modules.writer.apps.WriterConfig', - 'igny8_core.modules.system.apps.SystemConfig', - 'igny8_core.modules.billing.apps.BillingConfig', -] - -# System module needs explicit registration for admin - -AUTH_USER_MODEL = 'igny8_core_auth.User' - -CSRF_TRUSTED_ORIGINS = [ - 'https://api.igny8.com', - 'https://app.igny8.com', - 'http://localhost:8011', - 'http://127.0.0.1:8011', -] - -# Only use secure cookies in production (HTTPS) -# Default to False - set USE_SECURE_COOKIES=True in docker-compose for production -# This allows local development to work without HTTPS -USE_SECURE_COOKIES = os.getenv('USE_SECURE_COOKIES', 'False').lower() == 'true' -SESSION_COOKIE_SECURE = USE_SECURE_COOKIES -CSRF_COOKIE_SECURE = USE_SECURE_COOKIES - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'igny8_core.middleware.request_id.RequestIDMiddleware', # Request ID tracking (must be early) - 'igny8_core.auth.middleware.AccountContextMiddleware', # Multi-account support - # AccountContextMiddleware sets request.account from JWT - 'igny8_core.middleware.resource_tracker.ResourceTrackingMiddleware', # Resource tracking for admin debug - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'igny8_core.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'igny8_core.wsgi.application' - -DATABASES = {} - -database_url = os.getenv("DATABASE_URL") -db_engine = os.getenv("DB_ENGINE", "").lower() -force_postgres = os.getenv("DJANGO_FORCE_POSTGRES", "false").lower() == "true" - -if database_url: - parsed = urlparse(database_url) - scheme = (parsed.scheme or "").lower() - - if scheme in {"sqlite", "sqlite3"}: - # Support both absolute and project-relative SQLite paths - netloc_path = f"{parsed.netloc}{parsed.path}" if parsed.netloc else parsed.path - db_path = netloc_path.lstrip("/") or "db.sqlite3" - if os.path.isabs(netloc_path): - sqlite_name = netloc_path - else: - sqlite_name = Path(db_path) if os.path.isabs(db_path) else BASE_DIR / db_path - DATABASES["default"] = { - "ENGINE": "django.db.backends.sqlite3", - "NAME": str(sqlite_name), - } - else: - DATABASES["default"] = { - "ENGINE": "django.db.backends.postgresql", - "NAME": parsed.path.lstrip("/") or os.getenv("DB_NAME", "igny8_db"), - "USER": parsed.username or os.getenv("DB_USER", "igny8"), - "PASSWORD": parsed.password or os.getenv("DB_PASSWORD", "igny8pass"), - "HOST": parsed.hostname or os.getenv("DB_HOST", "postgres"), - "PORT": str(parsed.port or os.getenv("DB_PORT", "5432")), - } -elif db_engine in {"sqlite", "sqlite3"} or os.getenv("USE_SQLITE", "false").lower() == "true": - sqlite_name = os.getenv("SQLITE_NAME") - if not sqlite_name: - sqlite_name = BASE_DIR / "db.sqlite3" - DATABASES["default"] = { - "ENGINE": "django.db.backends.sqlite3", - "NAME": str(sqlite_name), - } -elif DEBUG and not force_postgres and not os.getenv("DB_HOST") and not os.getenv("DB_NAME") and not os.getenv("DB_USER"): - DATABASES["default"] = { - "ENGINE": "django.db.backends.sqlite3", - "NAME": str(BASE_DIR / "db.sqlite3"), - } -else: - DATABASES["default"] = { - "ENGINE": "django.db.backends.postgresql", - "NAME": os.getenv("DB_NAME", "igny8_db"), - "USER": os.getenv("DB_USER", "igny8"), - "PASSWORD": os.getenv("DB_PASSWORD", "igny8pass"), - "HOST": os.getenv("DB_HOST", "postgres"), - "PORT": os.getenv("DB_PORT", "5432"), - } - -AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, -] - -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_TZ = True - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# Only use SECURE_PROXY_SSL_HEADER in production behind reverse proxy -# Default to False - set USE_SECURE_PROXY_HEADER=True in docker-compose for production -# Caddy sets X-Forwarded-Proto header, so enable this when behind Caddy -USE_SECURE_PROXY = os.getenv('USE_SECURE_PROXY_HEADER', 'False').lower() == 'true' -if USE_SECURE_PROXY: - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -else: - SECURE_PROXY_SSL_HEADER = None - -# Admin login URL - use relative URL to avoid hardcoded domain -LOGIN_URL = '/admin/login/' -LOGIN_REDIRECT_URL = '/admin/' - -# Force Django to use request.get_host() instead of Sites framework -# This ensures redirects use the current request's host -USE_X_FORWARDED_HOST = False - -# REST Framework Configuration -REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'igny8_core.api.pagination.CustomPageNumberPagination', - 'PAGE_SIZE': 10, - 'DEFAULT_FILTER_BACKENDS': [ - 'django_filters.rest_framework.DjangoFilterBackend', - 'rest_framework.filters.SearchFilter', - 'rest_framework.filters.OrderingFilter', - ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.AllowAny', # Allow unauthenticated access for now - ], - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'igny8_core.api.authentication.JWTAuthentication', # JWT token authentication - 'igny8_core.api.authentication.CSRFExemptSessionAuthentication', # Session auth without CSRF for API - 'rest_framework.authentication.BasicAuthentication', # Enable basic auth as fallback - ], - # Unified API Standard v1.0 Configuration - # Exception handler - wraps all errors in unified format - # Unified API Standard v1.0: Exception handler enabled by default - # Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=False to disable - 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' if os.getenv('IGNY8_USE_UNIFIED_EXCEPTION_HANDLER', 'True').lower() == 'false' else 'igny8_core.api.exception_handlers.custom_exception_handler', - # Rate limiting - configured but bypassed in DEBUG mode - 'DEFAULT_THROTTLE_CLASSES': [ - 'igny8_core.api.throttles.DebugScopedRateThrottle', - ], - 'DEFAULT_THROTTLE_RATES': { - # AI Functions - Expensive operations - 'ai_function': '10/min', # AI content generation, clustering - 'image_gen': '15/min', # Image generation - # Content Operations - 'content_write': '30/min', # Content creation, updates - 'content_read': '100/min', # Content listing, retrieval - # Authentication - 'auth': '20/min', # Login, register, password reset - 'auth_strict': '5/min', # Sensitive auth operations - # Planner Operations - 'planner': '60/min', # Keyword, cluster, idea operations - 'planner_ai': '10/min', # AI-powered planner operations - # Writer Operations - 'writer': '60/min', # Task, content management - 'writer_ai': '10/min', # AI-powered writer operations - # System Operations - 'system': '100/min', # Settings, prompts, profiles - 'system_admin': '30/min', # Admin-only system operations - # Billing Operations - 'billing': '30/min', # Credit queries, usage logs - 'billing_admin': '10/min', # Credit management (admin) - # Default fallback - 'default': '100/min', # Default for endpoints without scope - }, - # OpenAPI Schema Generation (drf-spectacular) - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', -} - -# drf-spectacular Settings for OpenAPI 3.0 Schema Generation -SPECTACULAR_SETTINGS = { - 'TITLE': 'IGNY8 API v1.0', - 'DESCRIPTION': ''' - IGNY8 Unified API Standard v1.0 - - A comprehensive REST API for content planning, creation, and management. - - ## Features - - **Unified Response Format**: All endpoints return consistent JSON structure - - **Layered Authorization**: Authentication → Tenant Access → Role → Site/Sector - - **Centralized Error Handling**: All errors wrapped in unified format - - **Scoped Rate Limiting**: Different limits for different operation types - - **Tenant Isolation**: All resources scoped by account/site/sector - - **Request Tracking**: Every request has a unique ID for debugging - - ## Authentication - All endpoints require JWT Bearer token authentication except: - - `POST /api/v1/auth/login/` - User login - - `POST /api/v1/auth/register/` - User registration - - Include token in Authorization header: - ``` - Authorization: Bearer - ``` - - ## Response Format - All successful responses follow this format: - ```json - { - "success": true, - "data": {...}, - "message": "Optional success message", - "request_id": "uuid" - } - ``` - - All error responses follow this format: - ```json - { - "success": false, - "error": "Error message", - "errors": { - "field_name": ["Field-specific errors"] - }, - "request_id": "uuid" - } - ``` - - ## Rate Limiting - Rate limits are scoped by operation type. Check response headers: - - `X-Throttle-Limit`: Maximum requests allowed - - `X-Throttle-Remaining`: Remaining requests in current window - - `X-Throttle-Reset`: Time when limit resets (Unix timestamp) - - ## Pagination - List endpoints support pagination with query parameters: - - `page`: Page number (default: 1) - - `page_size`: Items per page (default: 10, max: 100) - - Paginated responses include: - ```json - { - "success": true, - "count": 100, - "next": "http://api.igny8.com/api/v1/endpoint/?page=2", - "previous": null, - "results": [...] - } - ``` - ''', - 'VERSION': '1.0.0', - 'SERVE_INCLUDE_SCHEMA': False, - 'SCHEMA_PATH_PREFIX': '/api/v1', - 'COMPONENT_SPLIT_REQUEST': True, - 'COMPONENT_NO_READ_ONLY_REQUIRED': True, - # Custom schema generator to include unified response format - 'SCHEMA_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator', - # Include request/response examples - 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], - 'SERVE_AUTHENTICATION': None, # Allow unauthenticated access to docs - # Tags for grouping endpoints - 'TAGS': [ - {'name': 'Authentication', 'description': 'User authentication and registration'}, - {'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'}, - {'name': 'Writer', 'description': 'Tasks, content, and images'}, - {'name': 'System', 'description': 'Settings, prompts, and integrations'}, - {'name': 'Billing', 'description': 'Credits, usage, and transactions'}, - ], - # Custom response format documentation - 'EXTENSIONS_INFO': { - 'x-code-samples': [ - { - 'lang': 'Python', - 'source': ''' -import requests - -headers = { - 'Authorization': 'Bearer ', - 'Content-Type': 'application/json' -} - -response = requests.get('https://api.igny8.com/api/v1/planner/keywords/', headers=headers) -data = response.json() - -if data['success']: - keywords = data['results'] # or data['data'] for single objects -else: - print(f"Error: {data['error']}") - ''' - }, - { - 'lang': 'JavaScript', - 'source': ''' -const response = await fetch('https://api.igny8.com/api/v1/planner/keywords/', { - headers: { - 'Authorization': 'Bearer ', - 'Content-Type': 'application/json' - } -}); - -const data = await response.json(); - -if (data.success) { - const keywords = data.results || data.data; -} else { - console.error('Error:', data.error); -} - ''' - } - ] - } -} - -# CORS Configuration -CORS_ALLOWED_ORIGINS = [ - "https://app.igny8.com", - "https://igny8.com", - "https://www.igny8.com", - "http://localhost:5173", - "http://localhost:5174", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:5174", -] - -CORS_ALLOW_CREDENTIALS = True - -# Allow custom headers for resource tracking -# Include default headers plus our custom debug header -CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', - 'x-debug-resource-tracking', # Allow debug tracking header -] - -# Note: django-cors-headers has default headers that include the above. -# If you want to extend defaults, you can import default_headers from corsheaders.defaults -# For now, we're explicitly listing all needed headers including our custom one. - -# Expose custom headers to frontend -CORS_EXPOSE_HEADERS = [ - 'x-resource-tracking-id', # Expose request tracking ID -] - -# JWT Configuration -JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY) -JWT_ALGORITHM = 'HS256' -JWT_ACCESS_TOKEN_EXPIRY = timedelta(minutes=15) -JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=7) - -# Celery Configuration -CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', f"redis://{os.getenv('REDIS_HOST', 'redis')}:{os.getenv('REDIS_PORT', '6379')}/0") -CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', f"redis://{os.getenv('REDIS_HOST', 'redis')}:{os.getenv('REDIS_PORT', '6379')}/0") -CELERY_ACCEPT_CONTENT = ['json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' -CELERY_TIMEZONE = TIME_ZONE -CELERY_ENABLE_UTC = True -CELERY_TASK_TRACK_STARTED = True -CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes -CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 minutes -CELERY_WORKER_PREFETCH_MULTIPLIER = 1 -CELERY_WORKER_MAX_TASKS_PER_CHILD = 1000 diff --git a/backup-api-standard-v1/backend/igny8_core/urls.py b/backup-api-standard-v1/backend/igny8_core/urls.py deleted file mode 100644 index 9c2908da..00000000 --- a/backup-api-standard-v1/backend/igny8_core/urls.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -URL configuration for igny8_core project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include -from drf_spectacular.views import ( - SpectacularAPIView, - SpectacularRedocView, - SpectacularSwaggerView, -) - -urlpatterns = [ - path('admin/', admin.site.urls), - path('api/v1/auth/', include('igny8_core.auth.urls')), # Auth endpoints - path('api/v1/planner/', include('igny8_core.modules.planner.urls')), - path('api/v1/writer/', include('igny8_core.modules.writer.urls')), - path('api/v1/system/', include('igny8_core.modules.system.urls')), - path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints - # OpenAPI Schema and Documentation - path('api/schema/', SpectacularAPIView.as_view(), name='schema'), - path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), - path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), -] diff --git a/backup-api-standard-v1/backend/requirements.txt b/backup-api-standard-v1/backend/requirements.txt deleted file mode 100644 index fc77e50c..00000000 --- a/backup-api-standard-v1/backend/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -Django>=5.2.7 -gunicorn -psycopg2-binary -redis -whitenoise -djangorestframework -django-filter -django-cors-headers -PyJWT>=2.8.0 -requests>=2.31.0 -celery>=5.3.0 -beautifulsoup4>=4.12.0 -psutil>=5.9.0 -docker>=7.0.0 -drf-spectacular>=0.27.0 diff --git a/backup-api-standard-v1/docs/API-DOCUMENTATION.md b/backup-api-standard-v1/docs/API-DOCUMENTATION.md deleted file mode 100644 index 2b724605..00000000 --- a/backup-api-standard-v1/docs/API-DOCUMENTATION.md +++ /dev/null @@ -1,545 +0,0 @@ -# IGNY8 API Documentation v1.0 - -**Base URL**: `https://api.igny8.com/api/v1/` -**Version**: 1.0.0 -**Last Updated**: 2025-11-16 - -## Quick Links - -- [Interactive API Documentation (Swagger UI)](#swagger-ui) -- [Authentication Guide](#authentication) -- [Response Format](#response-format) -- [Error Handling](#error-handling) -- [Rate Limiting](#rate-limiting) -- [Pagination](#pagination) -- [Endpoint Reference](#endpoint-reference) - ---- - -## Swagger UI - -Interactive API documentation is available at: -- **Swagger UI**: `https://api.igny8.com/api/docs/` -- **ReDoc**: `https://api.igny8.com/api/redoc/` -- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` - -The Swagger UI provides: -- Interactive endpoint testing -- Request/response examples -- Authentication testing -- Schema definitions -- Code samples in multiple languages - ---- - -## Authentication - -### JWT Bearer Token - -All endpoints require JWT Bearer token authentication except: -- `POST /api/v1/auth/login/` - User login -- `POST /api/v1/auth/register/` - User registration - -### Getting an Access Token - -**Login Endpoint:** -```http -POST /api/v1/auth/login/ -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "your_password" -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "user": { - "id": 1, - "email": "user@example.com", - "username": "user", - "role": "owner" - }, - "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "request_id": "uuid" -} -``` - -### Using the Token - -Include the token in the `Authorization` header: - -```http -GET /api/v1/planner/keywords/ -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -Content-Type: application/json -``` - -### Token Expiration - -- **Access Token**: 15 minutes -- **Refresh Token**: 7 days - -Use the refresh token to get a new access token: -```http -POST /api/v1/auth/refresh/ -Content-Type: application/json - -{ - "refresh": "your_refresh_token" -} -``` - ---- - -## Response Format - -### Success Response - -All successful responses follow this unified format: - -```json -{ - "success": true, - "data": { - "id": 1, - "name": "Example", - ... - }, - "message": "Optional success message", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Paginated Response - -List endpoints return paginated data: - -```json -{ - "success": true, - "count": 100, - "next": "https://api.igny8.com/api/v1/planner/keywords/?page=2", - "previous": null, - "results": [ - {"id": 1, "name": "Keyword 1"}, - {"id": 2, "name": "Keyword 2"}, - ... - ], - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Error Response - -All error responses follow this unified format: - -```json -{ - "success": false, - "error": "Validation failed", - "errors": { - "email": ["This field is required"], - "password": ["Password too short"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## Error Handling - -### HTTP Status Codes - -| Code | Meaning | Description | -|------|---------|-------------| -| 200 | OK | Request successful | -| 201 | Created | Resource created successfully | -| 204 | No Content | Resource deleted successfully | -| 400 | Bad Request | Validation error or invalid request | -| 401 | Unauthorized | Authentication required | -| 403 | Forbidden | Permission denied | -| 404 | Not Found | Resource not found | -| 409 | Conflict | Resource conflict (e.g., duplicate) | -| 422 | Unprocessable Entity | Validation failed | -| 429 | Too Many Requests | Rate limit exceeded | -| 500 | Internal Server Error | Server error | - -### Error Response Structure - -All errors include: -- `success`: Always `false` -- `error`: Top-level error message -- `errors`: Field-specific errors (for validation errors) -- `request_id`: Unique request ID for debugging - -### Example Error Responses - -**Validation Error (400):** -```json -{ - "success": false, - "error": "Validation failed", - "errors": { - "email": ["Invalid email format"], - "password": ["Password must be at least 8 characters"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Authentication Error (401):** -```json -{ - "success": false, - "error": "Authentication required", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Permission Error (403):** -```json -{ - "success": false, - "error": "Permission denied", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Not Found (404):** -```json -{ - "success": false, - "error": "Resource not found", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Rate Limit (429):** -```json -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## Rate Limiting - -Rate limits are scoped by operation type. Check response headers for limit information: - -- `X-Throttle-Limit`: Maximum requests allowed -- `X-Throttle-Remaining`: Remaining requests in current window -- `X-Throttle-Reset`: Time when limit resets (Unix timestamp) - -### Rate Limit Scopes - -| Scope | Limit | Description | -|-------|-------|-------------| -| `ai_function` | 10/min | AI content generation, clustering | -| `image_gen` | 15/min | Image generation | -| `content_write` | 30/min | Content creation, updates | -| `content_read` | 100/min | Content listing, retrieval | -| `auth` | 20/min | Login, register, password reset | -| `auth_strict` | 5/min | Sensitive auth operations | -| `planner` | 60/min | Keyword, cluster, idea operations | -| `planner_ai` | 10/min | AI-powered planner operations | -| `writer` | 60/min | Task, content management | -| `writer_ai` | 10/min | AI-powered writer operations | -| `system` | 100/min | Settings, prompts, profiles | -| `system_admin` | 30/min | Admin-only system operations | -| `billing` | 30/min | Credit queries, usage logs | -| `billing_admin` | 10/min | Credit management (admin) | -| `default` | 100/min | Default for endpoints without scope | - -### Handling Rate Limits - -When rate limited (429), the response includes: -- Error message: "Rate limit exceeded" -- Headers with reset time -- Wait until `X-Throttle-Reset` before retrying - -**Example:** -```http -HTTP/1.1 429 Too Many Requests -X-Throttle-Limit: 60 -X-Throttle-Remaining: 0 -X-Throttle-Reset: 1700123456 - -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## Pagination - -List endpoints support pagination with query parameters: - -- `page`: Page number (default: 1) -- `page_size`: Items per page (default: 10, max: 100) - -### Example Request - -```http -GET /api/v1/planner/keywords/?page=2&page_size=20 -``` - -### Paginated Response - -```json -{ - "success": true, - "count": 100, - "next": "https://api.igny8.com/api/v1/planner/keywords/?page=3&page_size=20", - "previous": "https://api.igny8.com/api/v1/planner/keywords/?page=1&page_size=20", - "results": [ - {"id": 21, "name": "Keyword 21"}, - {"id": 22, "name": "Keyword 22"}, - ... - ], - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Pagination Fields - -- `count`: Total number of items -- `next`: URL to next page (null if last page) -- `previous`: URL to previous page (null if first page) -- `results`: Array of items for current page - ---- - -## Endpoint Reference - -### Authentication Endpoints - -#### Login -```http -POST /api/v1/auth/login/ -``` - -#### Register -```http -POST /api/v1/auth/register/ -``` - -#### Refresh Token -```http -POST /api/v1/auth/refresh/ -``` - -### Planner Endpoints - -#### List Keywords -```http -GET /api/v1/planner/keywords/ -``` - -#### Create Keyword -```http -POST /api/v1/planner/keywords/ -``` - -#### Get Keyword -```http -GET /api/v1/planner/keywords/{id}/ -``` - -#### Update Keyword -```http -PUT /api/v1/planner/keywords/{id}/ -PATCH /api/v1/planner/keywords/{id}/ -``` - -#### Delete Keyword -```http -DELETE /api/v1/planner/keywords/{id}/ -``` - -#### Auto Cluster Keywords -```http -POST /api/v1/planner/keywords/auto_cluster/ -``` - -### Writer Endpoints - -#### List Tasks -```http -GET /api/v1/writer/tasks/ -``` - -#### Create Task -```http -POST /api/v1/writer/tasks/ -``` - -### System Endpoints - -#### System Status -```http -GET /api/v1/system/status/ -``` - -#### List Prompts -```http -GET /api/v1/system/prompts/ -``` - -### Billing Endpoints - -#### Credit Balance -```http -GET /api/v1/billing/credits/balance/balance/ -``` - -#### Usage Summary -```http -GET /api/v1/billing/credits/usage/summary/ -``` - ---- - -## Code Examples - -### Python - -```python -import requests - -BASE_URL = "https://api.igny8.com/api/v1" - -# Login -response = requests.post( - f"{BASE_URL}/auth/login/", - json={"email": "user@example.com", "password": "password"} -) -data = response.json() - -if data['success']: - token = data['data']['access'] - - # Use token for authenticated requests - headers = { - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json' - } - - # Get keywords - response = requests.get( - f"{BASE_URL}/planner/keywords/", - headers=headers - ) - keywords_data = response.json() - - if keywords_data['success']: - keywords = keywords_data['results'] - print(f"Found {keywords_data['count']} keywords") - else: - print(f"Error: {keywords_data['error']}") -else: - print(f"Login failed: {data['error']}") -``` - -### JavaScript - -```javascript -const BASE_URL = 'https://api.igny8.com/api/v1'; - -// Login -const loginResponse = await fetch(`${BASE_URL}/auth/login/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: 'user@example.com', - password: 'password' - }) -}); - -const loginData = await loginResponse.json(); - -if (loginData.success) { - const token = loginData.data.access; - - // Use token for authenticated requests - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - - // Get keywords - const keywordsResponse = await fetch( - `${BASE_URL}/planner/keywords/`, - { headers } - ); - - const keywordsData = await keywordsResponse.json(); - - if (keywordsData.success) { - const keywords = keywordsData.results; - console.log(`Found ${keywordsData.count} keywords`); - } else { - console.error('Error:', keywordsData.error); - } -} else { - console.error('Login failed:', loginData.error); -} -``` - -### cURL - -```bash -# Login -curl -X POST https://api.igny8.com/api/v1/auth/login/ \ - -H "Content-Type: application/json" \ - -d '{"email":"user@example.com","password":"password"}' - -# Get keywords (with token) -curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" -``` - ---- - -## Request ID - -Every API request includes a unique `request_id` in the response. Use this ID for: -- Debugging issues -- Log correlation -- Support requests - -The `request_id` is included in: -- All success responses -- All error responses -- Response headers (`X-Request-ID`) - ---- - -## Support - -For API support: -- Check the [Interactive Documentation](https://api.igny8.com/api/docs/) -- Review [Error Codes Reference](ERROR-CODES.md) -- Contact support with your `request_id` - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/docs/AUTHENTICATION-GUIDE.md b/backup-api-standard-v1/docs/AUTHENTICATION-GUIDE.md deleted file mode 100644 index db936030..00000000 --- a/backup-api-standard-v1/docs/AUTHENTICATION-GUIDE.md +++ /dev/null @@ -1,493 +0,0 @@ -# Authentication Guide - -**Version**: 1.0.0 -**Last Updated**: 2025-11-16 - -Complete guide for authenticating with the IGNY8 API v1.0. - ---- - -## Overview - -The IGNY8 API uses **JWT (JSON Web Token) Bearer Token** authentication. All endpoints require authentication except: -- `POST /api/v1/auth/login/` - User login -- `POST /api/v1/auth/register/` - User registration - ---- - -## Authentication Flow - -### 1. Register or Login - -**Register** (if new user): -```http -POST /api/v1/auth/register/ -Content-Type: application/json - -{ - "email": "user@example.com", - "username": "user", - "password": "secure_password123", - "first_name": "John", - "last_name": "Doe" -} -``` - -**Login** (existing user): -```http -POST /api/v1/auth/login/ -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "secure_password123" -} -``` - -### 2. Receive Tokens - -**Response**: -```json -{ - "success": true, - "data": { - "user": { - "id": 1, - "email": "user@example.com", - "username": "user", - "role": "owner", - "account": { - "id": 1, - "name": "My Account", - "slug": "my-account" - } - }, - "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDAxMjM0NTZ9...", - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDAxODk0NTZ9..." - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 3. Use Access Token - -Include the `access` token in all subsequent requests: - -```http -GET /api/v1/planner/keywords/ -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -Content-Type: application/json -``` - -### 4. Refresh Token (when expired) - -When the access token expires (15 minutes), use the refresh token: - -```http -POST /api/v1/auth/refresh/ -Content-Type: application/json - -{ - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." -} -``` - -**Response**: -```json -{ - "success": true, - "data": { - "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## Token Expiration - -- **Access Token**: 15 minutes -- **Refresh Token**: 7 days - -### Handling Token Expiration - -**Option 1: Automatic Refresh** -```python -def get_access_token(): - # Check if token is expired - if is_token_expired(current_token): - # Refresh token - response = requests.post( - f"{BASE_URL}/auth/refresh/", - json={"refresh": refresh_token} - ) - data = response.json() - if data['success']: - return data['data']['access'] - return current_token -``` - -**Option 2: Re-login** -```python -def login(): - response = requests.post( - f"{BASE_URL}/auth/login/", - json={"email": email, "password": password} - ) - data = response.json() - if data['success']: - return data['data']['access'] -``` - ---- - -## Code Examples - -### Python - -```python -import requests -import time -from datetime import datetime, timedelta - -class Igny8API: - def __init__(self, base_url="https://api.igny8.com/api/v1"): - self.base_url = base_url - self.access_token = None - self.refresh_token = None - self.token_expires_at = None - - def login(self, email, password): - """Login and store tokens""" - response = requests.post( - f"{self.base_url}/auth/login/", - json={"email": email, "password": password} - ) - data = response.json() - - if data['success']: - self.access_token = data['data']['access'] - self.refresh_token = data['data']['refresh'] - # Token expires in 15 minutes - self.token_expires_at = datetime.now() + timedelta(minutes=14) - return True - else: - print(f"Login failed: {data['error']}") - return False - - def refresh_access_token(self): - """Refresh access token using refresh token""" - if not self.refresh_token: - return False - - response = requests.post( - f"{self.base_url}/auth/refresh/", - json={"refresh": self.refresh_token} - ) - data = response.json() - - if data['success']: - self.access_token = data['data']['access'] - self.refresh_token = data['data']['refresh'] - self.token_expires_at = datetime.now() + timedelta(minutes=14) - return True - else: - print(f"Token refresh failed: {data['error']}") - return False - - def get_headers(self): - """Get headers with valid access token""" - # Check if token is expired or about to expire - if not self.token_expires_at or datetime.now() >= self.token_expires_at: - if not self.refresh_access_token(): - raise Exception("Token expired and refresh failed") - - return { - 'Authorization': f'Bearer {self.access_token}', - 'Content-Type': 'application/json' - } - - def get(self, endpoint): - """Make authenticated GET request""" - response = requests.get( - f"{self.base_url}{endpoint}", - headers=self.get_headers() - ) - return response.json() - - def post(self, endpoint, data): - """Make authenticated POST request""" - response = requests.post( - f"{self.base_url}{endpoint}", - headers=self.get_headers(), - json=data - ) - return response.json() - -# Usage -api = Igny8API() -api.login("user@example.com", "password") - -# Make authenticated requests -keywords = api.get("/planner/keywords/") -``` - -### JavaScript - -```javascript -class Igny8API { - constructor(baseUrl = 'https://api.igny8.com/api/v1') { - this.baseUrl = baseUrl; - this.accessToken = null; - this.refreshToken = null; - this.tokenExpiresAt = null; - } - - async login(email, password) { - const response = await fetch(`${this.baseUrl}/auth/login/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ email, password }) - }); - - const data = await response.json(); - - if (data.success) { - this.accessToken = data.data.access; - this.refreshToken = data.data.refresh; - // Token expires in 15 minutes - this.tokenExpiresAt = new Date(Date.now() + 14 * 60 * 1000); - return true; - } else { - console.error('Login failed:', data.error); - return false; - } - } - - async refreshAccessToken() { - if (!this.refreshToken) { - return false; - } - - const response = await fetch(`${this.baseUrl}/auth/refresh/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ refresh: this.refreshToken }) - }); - - const data = await response.json(); - - if (data.success) { - this.accessToken = data.data.access; - this.refreshToken = data.data.refresh; - this.tokenExpiresAt = new Date(Date.now() + 14 * 60 * 1000); - return true; - } else { - console.error('Token refresh failed:', data.error); - return false; - } - } - - async getHeaders() { - // Check if token is expired or about to expire - if (!this.tokenExpiresAt || new Date() >= this.tokenExpiresAt) { - if (!await this.refreshAccessToken()) { - throw new Error('Token expired and refresh failed'); - } - } - - return { - 'Authorization': `Bearer ${this.accessToken}`, - 'Content-Type': 'application/json' - }; - } - - async get(endpoint) { - const response = await fetch( - `${this.baseUrl}${endpoint}`, - { headers: await this.getHeaders() } - ); - return await response.json(); - } - - async post(endpoint, data) { - const response = await fetch( - `${this.baseUrl}${endpoint}`, - { - method: 'POST', - headers: await this.getHeaders(), - body: JSON.stringify(data) - } - ); - return await response.json(); - } -} - -// Usage -const api = new Igny8API(); -await api.login('user@example.com', 'password'); - -// Make authenticated requests -const keywords = await api.get('/planner/keywords/'); -``` - ---- - -## Security Best Practices - -### 1. Store Tokens Securely - -**❌ Don't:** -- Store tokens in localStorage (XSS risk) -- Commit tokens to version control -- Log tokens in console/logs -- Send tokens in URL parameters - -**✅ Do:** -- Store tokens in httpOnly cookies (server-side) -- Use secure storage (encrypted) for client-side -- Rotate tokens regularly -- Implement token revocation - -### 2. Handle Token Expiration - -Always check token expiration and refresh before making requests: - -```python -def is_token_valid(token_expires_at): - # Refresh 1 minute before expiration - return datetime.now() < (token_expires_at - timedelta(minutes=1)) -``` - -### 3. Implement Retry Logic - -```python -def make_request_with_retry(url, headers, max_retries=3): - for attempt in range(max_retries): - response = requests.get(url, headers=headers) - - if response.status_code == 401: - # Token expired, refresh and retry - refresh_token() - headers = get_headers() - continue - - return response.json() - - raise Exception("Max retries exceeded") -``` - -### 4. Validate Token Before Use - -```python -def validate_token(token): - try: - # Decode token (without verification for structure check) - import jwt - decoded = jwt.decode(token, options={"verify_signature": False}) - exp = decoded.get('exp') - - if exp and datetime.fromtimestamp(exp) < datetime.now(): - return False - return True - except: - return False -``` - ---- - -## Error Handling - -### Authentication Errors - -**401 Unauthorized**: -```json -{ - "success": false, - "error": "Authentication required", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Include valid `Authorization: Bearer ` header. - -**403 Forbidden**: -```json -{ - "success": false, - "error": "Permission denied", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: User lacks required permissions. Check user role and resource access. - ---- - -## Testing Authentication - -### Using Swagger UI - -1. Navigate to `https://api.igny8.com/api/docs/` -2. Click "Authorize" button -3. Enter: `Bearer ` -4. Click "Authorize" -5. All requests will include the token - -### Using cURL - -```bash -# Login -curl -X POST https://api.igny8.com/api/v1/auth/login/ \ - -H "Content-Type: application/json" \ - -d '{"email":"user@example.com","password":"password"}' - -# Use token -curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" -``` - ---- - -## Troubleshooting - -### Issue: "Authentication required" (401) - -**Causes**: -- Missing Authorization header -- Invalid token format -- Expired token - -**Solutions**: -1. Verify `Authorization: Bearer ` header is included -2. Check token is not expired -3. Refresh token or re-login - -### Issue: "Permission denied" (403) - -**Causes**: -- User lacks required role -- Resource belongs to different account -- Site/sector access denied - -**Solutions**: -1. Check user role has required permissions -2. Verify resource belongs to user's account -3. Check site/sector access permissions - -### Issue: Token expires frequently - -**Solution**: Implement automatic token refresh before expiration. - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/docs/DOCUMENTATION-SUMMARY.md b/backup-api-standard-v1/docs/DOCUMENTATION-SUMMARY.md deleted file mode 100644 index 30b3e263..00000000 --- a/backup-api-standard-v1/docs/DOCUMENTATION-SUMMARY.md +++ /dev/null @@ -1,207 +0,0 @@ -# Documentation Implementation Summary - -**Section 2: Documentation - COMPLETE** ✅ - -**Date Completed**: 2025-11-16 -**Status**: All Documentation Complete and Ready - ---- - -## Implementation Overview - -Complete documentation system for IGNY8 API v1.0 including: -- OpenAPI 3.0 schema generation -- Interactive Swagger UI -- Comprehensive documentation files -- Code examples and integration guides - ---- - -## OpenAPI/Swagger Integration ✅ - -### Configuration -- ✅ Installed `drf-spectacular>=0.27.0` -- ✅ Added to `INSTALLED_APPS` -- ✅ Configured `SPECTACULAR_SETTINGS` with comprehensive description -- ✅ Added URL endpoints for schema and documentation - -### Endpoints Created -- ✅ `/api/schema/` - OpenAPI 3.0 schema (JSON/YAML) -- ✅ `/api/docs/` - Swagger UI (interactive documentation) -- ✅ `/api/redoc/` - ReDoc (alternative documentation UI) - -### Features -- ✅ Comprehensive API description with features overview -- ✅ Authentication documentation (JWT Bearer tokens) -- ✅ Response format examples -- ✅ Rate limiting documentation -- ✅ Pagination documentation -- ✅ Endpoint tags (Authentication, Planner, Writer, System, Billing) -- ✅ Code samples in Python and JavaScript -- ✅ Custom authentication extensions - ---- - -## Documentation Files Created ✅ - -### 1. API-DOCUMENTATION.md -**Purpose**: Complete API reference -**Contents**: -- Quick start guide -- Authentication guide -- Response format details -- Error handling -- Rate limiting -- Pagination -- Endpoint reference -- Code examples (Python, JavaScript, cURL) - -### 2. AUTHENTICATION-GUIDE.md -**Purpose**: Authentication and authorization -**Contents**: -- JWT Bearer token authentication -- Token management and refresh -- Code examples (Python, JavaScript) -- Security best practices -- Token expiration handling -- Troubleshooting - -### 3. ERROR-CODES.md -**Purpose**: Complete error code reference -**Contents**: -- HTTP status codes (200, 201, 400, 401, 403, 404, 409, 422, 429, 500) -- Field-specific error messages -- Error handling best practices -- Common error scenarios -- Debugging tips - -### 4. RATE-LIMITING.md -**Purpose**: Rate limiting and throttling -**Contents**: -- Rate limit scopes and limits -- Handling rate limits (429 responses) -- Best practices -- Code examples with backoff strategies -- Request queuing and caching - -### 5. MIGRATION-GUIDE.md -**Purpose**: Migration guide for API consumers -**Contents**: -- What changed in v1.0 -- Step-by-step migration instructions -- Code examples (before/after) -- Breaking and non-breaking changes -- Migration checklist - -### 6. WORDPRESS-PLUGIN-INTEGRATION.md -**Purpose**: WordPress plugin integration -**Contents**: -- Complete PHP API client class -- Authentication implementation -- Error handling -- WordPress admin integration -- Best practices -- Testing examples - -### 7. README.md -**Purpose**: Documentation index -**Contents**: -- Documentation index -- Quick start guide -- Links to all documentation files -- Support information - ---- - -## Schema Extensions ✅ - -### Custom Authentication Extensions -- ✅ `JWTAuthenticationExtension` - JWT Bearer token authentication -- ✅ `CSRFExemptSessionAuthenticationExtension` - Session authentication -- ✅ Proper OpenAPI security scheme definitions - -**File**: `backend/igny8_core/api/schema_extensions.py` - ---- - -## Verification - -### Schema Generation -```bash -python manage.py spectacular --color -``` -**Status**: ✅ Schema generates successfully - -### Documentation Endpoints -- ✅ `/api/schema/` - OpenAPI schema -- ✅ `/api/docs/` - Swagger UI -- ✅ `/api/redoc/` - ReDoc - -### Documentation Files -- ✅ 7 comprehensive documentation files created -- ✅ All files include code examples -- ✅ All files include best practices -- ✅ All files properly formatted - ---- - -## Documentation Statistics - -- **Total Documentation Files**: 7 -- **Total Pages**: ~100+ pages of documentation -- **Code Examples**: Python, JavaScript, PHP, cURL -- **Coverage**: 100% of API features documented - ---- - -## What's Documented - -### ✅ API Features -- Unified response format -- Authentication and authorization -- Error handling -- Rate limiting -- Pagination -- Request ID tracking - -### ✅ Integration Guides -- Python integration -- JavaScript integration -- WordPress plugin integration -- Migration from legacy format - -### ✅ Reference Materials -- Error codes -- Rate limit scopes -- Endpoint reference -- Code examples - ---- - -## Access Points - -### Interactive Documentation -- **Swagger UI**: `https://api.igny8.com/api/docs/` -- **ReDoc**: `https://api.igny8.com/api/redoc/` -- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` - -### Documentation Files -- All files in `docs/` directory -- Index: `docs/README.md` - ---- - -## Next Steps - -1. ✅ Documentation complete -2. ✅ Swagger UI accessible -3. ✅ All guides created -4. ✅ Changelog updated - -**Section 2: Documentation is COMPLETE** ✅ - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/docs/ERROR-CODES.md b/backup-api-standard-v1/docs/ERROR-CODES.md deleted file mode 100644 index d5fcd9f5..00000000 --- a/backup-api-standard-v1/docs/ERROR-CODES.md +++ /dev/null @@ -1,407 +0,0 @@ -# API Error Codes Reference - -**Version**: 1.0.0 -**Last Updated**: 2025-11-16 - -This document provides a comprehensive reference for all error codes and error scenarios in the IGNY8 API v1.0. - ---- - -## Error Response Format - -All errors follow this unified format: - -```json -{ - "success": false, - "error": "Error message", - "errors": { - "field_name": ["Field-specific errors"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## HTTP Status Codes - -### 200 OK -**Meaning**: Request successful -**Response**: Success response with data - -### 201 Created -**Meaning**: Resource created successfully -**Response**: Success response with created resource data - -### 204 No Content -**Meaning**: Resource deleted successfully -**Response**: Empty response body - -### 400 Bad Request -**Meaning**: Validation error or invalid request -**Common Causes**: -- Missing required fields -- Invalid field values -- Invalid data format -- Business logic validation failures - -**Example**: -```json -{ - "success": false, - "error": "Validation failed", - "errors": { - "email": ["This field is required"], - "password": ["Password must be at least 8 characters"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 401 Unauthorized -**Meaning**: Authentication required -**Common Causes**: -- Missing Authorization header -- Invalid or expired token -- Token not provided - -**Example**: -```json -{ - "success": false, - "error": "Authentication required", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 403 Forbidden -**Meaning**: Permission denied -**Common Causes**: -- User lacks required role -- User doesn't have access to resource -- Account/site/sector access denied - -**Example**: -```json -{ - "success": false, - "error": "Permission denied", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 404 Not Found -**Meaning**: Resource not found -**Common Causes**: -- Invalid resource ID -- Resource doesn't exist -- Resource belongs to different account - -**Example**: -```json -{ - "success": false, - "error": "Resource not found", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 409 Conflict -**Meaning**: Resource conflict -**Common Causes**: -- Duplicate resource (e.g., email already exists) -- Resource state conflict -- Concurrent modification - -**Example**: -```json -{ - "success": false, - "error": "Conflict", - "errors": { - "email": ["User with this email already exists"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 422 Unprocessable Entity -**Meaning**: Validation failed -**Common Causes**: -- Complex validation rules failed -- Business logic validation failed -- Data integrity constraints violated - -**Example**: -```json -{ - "success": false, - "error": "Validation failed", - "errors": { - "site": ["Site must belong to your account"], - "sector": ["Sector must belong to the selected site"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -### 429 Too Many Requests -**Meaning**: Rate limit exceeded -**Common Causes**: -- Too many requests in time window -- AI function rate limit exceeded -- Authentication rate limit exceeded - -**Response Headers**: -- `X-Throttle-Limit`: Maximum requests allowed -- `X-Throttle-Remaining`: Remaining requests (0) -- `X-Throttle-Reset`: Unix timestamp when limit resets - -**Example**: -```json -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Wait until `X-Throttle-Reset` timestamp before retrying. - -### 500 Internal Server Error -**Meaning**: Server error -**Common Causes**: -- Unexpected server error -- Database error -- External service failure - -**Example**: -```json -{ - "success": false, - "error": "Internal server error", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Retry request. If persistent, contact support with `request_id`. - ---- - -## Field-Specific Error Messages - -### Authentication Errors - -| Field | Error Message | Description | -|-------|---------------|-------------| -| `email` | "This field is required" | Email not provided | -| `email` | "Invalid email format" | Email format invalid | -| `email` | "User with this email already exists" | Email already registered | -| `password` | "This field is required" | Password not provided | -| `password` | "Password must be at least 8 characters" | Password too short | -| `password` | "Invalid credentials" | Wrong password | - -### Planner Module Errors - -| Field | Error Message | Description | -|-------|---------------|-------------| -| `seed_keyword_id` | "This field is required" | Seed keyword not provided | -| `seed_keyword_id` | "Invalid seed keyword" | Seed keyword doesn't exist | -| `site_id` | "This field is required" | Site not provided | -| `site_id` | "Site must belong to your account" | Site access denied | -| `sector_id` | "This field is required" | Sector not provided | -| `sector_id` | "Sector must belong to the selected site" | Sector-site mismatch | -| `status` | "Invalid status value" | Status value not allowed | - -### Writer Module Errors - -| Field | Error Message | Description | -|-------|---------------|-------------| -| `title` | "This field is required" | Title not provided | -| `site_id` | "This field is required" | Site not provided | -| `sector_id` | "This field is required" | Sector not provided | -| `image_type` | "Invalid image type" | Image type not allowed | - -### System Module Errors - -| Field | Error Message | Description | -|-------|---------------|-------------| -| `api_key` | "This field is required" | API key not provided | -| `api_key` | "Invalid API key format" | API key format invalid | -| `integration_type` | "Invalid integration type" | Integration type not allowed | - -### Billing Module Errors - -| Field | Error Message | Description | -|-------|---------------|-------------| -| `amount` | "This field is required" | Amount not provided | -| `amount` | "Amount must be positive" | Invalid amount value | -| `credits` | "Insufficient credits" | Not enough credits available | - ---- - -## Error Handling Best Practices - -### 1. Always Check `success` Field - -```python -response = requests.get(url, headers=headers) -data = response.json() - -if data['success']: - # Handle success - result = data['data'] or data['results'] -else: - # Handle error - error_message = data['error'] - field_errors = data.get('errors', {}) -``` - -### 2. Handle Field-Specific Errors - -```python -if not data['success']: - if 'errors' in data: - for field, errors in data['errors'].items(): - print(f"{field}: {', '.join(errors)}") - else: - print(f"Error: {data['error']}") -``` - -### 3. Use Request ID for Support - -```python -if not data['success']: - request_id = data.get('request_id') - print(f"Error occurred. Request ID: {request_id}") - # Include request_id when contacting support -``` - -### 4. Handle Rate Limiting - -```python -if response.status_code == 429: - reset_time = response.headers.get('X-Throttle-Reset') - wait_seconds = int(reset_time) - int(time.time()) - print(f"Rate limited. Wait {wait_seconds} seconds.") - time.sleep(wait_seconds) - # Retry request -``` - -### 5. Retry on Server Errors - -```python -if response.status_code >= 500: - # Retry with exponential backoff - time.sleep(2 ** retry_count) - # Retry request -``` - ---- - -## Common Error Scenarios - -### Scenario 1: Missing Authentication - -**Request**: -```http -GET /api/v1/planner/keywords/ -(No Authorization header) -``` - -**Response** (401): -```json -{ - "success": false, - "error": "Authentication required", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Include `Authorization: Bearer ` header. - -### Scenario 2: Invalid Resource ID - -**Request**: -```http -GET /api/v1/planner/keywords/99999/ -Authorization: Bearer -``` - -**Response** (404): -```json -{ - "success": false, - "error": "Resource not found", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Verify resource ID exists and belongs to your account. - -### Scenario 3: Validation Error - -**Request**: -```http -POST /api/v1/planner/keywords/ -Authorization: Bearer -Content-Type: application/json - -{ - "seed_keyword_id": null, - "site_id": 1 -} -``` - -**Response** (400): -```json -{ - "success": false, - "error": "Validation failed", - "errors": { - "seed_keyword_id": ["This field is required"], - "sector_id": ["This field is required"] - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Provide all required fields with valid values. - -### Scenario 4: Rate Limit Exceeded - -**Request**: Multiple rapid requests - -**Response** (429): -```http -HTTP/1.1 429 Too Many Requests -X-Throttle-Limit: 60 -X-Throttle-Remaining: 0 -X-Throttle-Reset: 1700123456 - -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Solution**: Wait until `X-Throttle-Reset` timestamp, then retry. - ---- - -## Debugging Tips - -1. **Always include `request_id`** when reporting errors -2. **Check response headers** for rate limit information -3. **Verify authentication token** is valid and not expired -4. **Check field-specific errors** in `errors` object -5. **Review request payload** matches API specification -6. **Use Swagger UI** to test endpoints interactively - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/docs/MIGRATION-GUIDE.md b/backup-api-standard-v1/docs/MIGRATION-GUIDE.md deleted file mode 100644 index 9b8f139e..00000000 --- a/backup-api-standard-v1/docs/MIGRATION-GUIDE.md +++ /dev/null @@ -1,365 +0,0 @@ -# API Migration Guide - -**Version**: 1.0.0 -**Last Updated**: 2025-11-16 - -Guide for migrating existing API consumers to IGNY8 API Standard v1.0. - ---- - -## Overview - -The IGNY8 API v1.0 introduces a unified response format that standardizes all API responses. This guide helps you migrate existing code to work with the new format. - ---- - -## What Changed - -### Before (Legacy Format) - -**Success Response**: -```json -{ - "id": 1, - "name": "Keyword", - "status": "active" -} -``` - -**Error Response**: -```json -{ - "detail": "Not found." -} -``` - -### After (Unified Format v1.0) - -**Success Response**: -```json -{ - "success": true, - "data": { - "id": 1, - "name": "Keyword", - "status": "active" - }, - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Error Response**: -```json -{ - "success": false, - "error": "Resource not found", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - ---- - -## Migration Steps - -### Step 1: Update Response Parsing - -#### Before - -```python -response = requests.get(url, headers=headers) -data = response.json() - -# Direct access -keyword_id = data['id'] -keyword_name = data['name'] -``` - -#### After - -```python -response = requests.get(url, headers=headers) -data = response.json() - -# Check success first -if data['success']: - # Extract data from unified format - keyword_data = data['data'] # or data['results'] for lists - keyword_id = keyword_data['id'] - keyword_name = keyword_data['name'] -else: - # Handle error - error_message = data['error'] - raise Exception(error_message) -``` - -### Step 2: Update Error Handling - -#### Before - -```python -try: - response = requests.get(url, headers=headers) - response.raise_for_status() - data = response.json() -except requests.HTTPError as e: - if e.response.status_code == 404: - print("Not found") - elif e.response.status_code == 400: - print("Bad request") -``` - -#### After - -```python -response = requests.get(url, headers=headers) -data = response.json() - -if not data['success']: - # Unified error format - error_message = data['error'] - field_errors = data.get('errors', {}) - - if response.status_code == 404: - print(f"Not found: {error_message}") - elif response.status_code == 400: - print(f"Validation error: {error_message}") - for field, errors in field_errors.items(): - print(f" {field}: {', '.join(errors)}") -``` - -### Step 3: Update Pagination Handling - -#### Before - -```python -response = requests.get(url, headers=headers) -data = response.json() - -results = data['results'] -next_page = data['next'] -count = data['count'] -``` - -#### After - -```python -response = requests.get(url, headers=headers) -data = response.json() - -if data['success']: - # Paginated response format - results = data['results'] # Same field name - next_page = data['next'] # Same field name - count = data['count'] # Same field name -else: - # Handle error - raise Exception(data['error']) -``` - -### Step 4: Update Frontend Code - -#### Before (JavaScript) - -```javascript -const response = await fetch(url, { headers }); -const data = await response.json(); - -// Direct access -const keywordId = data.id; -const keywordName = data.name; -``` - -#### After (JavaScript) - -```javascript -const response = await fetch(url, { headers }); -const data = await response.json(); - -// Check success first -if (data.success) { - // Extract data from unified format - const keywordData = data.data || data.results; - const keywordId = keywordData.id; - const keywordName = keywordData.name; -} else { - // Handle error - console.error('Error:', data.error); - if (data.errors) { - // Handle field-specific errors - Object.entries(data.errors).forEach(([field, errors]) => { - console.error(`${field}: ${errors.join(', ')}`); - }); - } -} -``` - ---- - -## Helper Functions - -### Python Helper - -```python -def parse_api_response(response): - """Parse unified API response format""" - data = response.json() - - if data.get('success'): - # Return data or results - return data.get('data') or data.get('results') - else: - # Raise exception with error details - error_msg = data.get('error', 'Unknown error') - errors = data.get('errors', {}) - - if errors: - error_msg += f": {errors}" - - raise Exception(error_msg) - -# Usage -response = requests.get(url, headers=headers) -keyword_data = parse_api_response(response) -``` - -### JavaScript Helper - -```javascript -function parseApiResponse(data) { - if (data.success) { - return data.data || data.results; - } else { - const error = new Error(data.error); - error.errors = data.errors || {}; - throw error; - } -} - -// Usage -const response = await fetch(url, { headers }); -const data = await response.json(); -try { - const keywordData = parseApiResponse(data); -} catch (error) { - console.error('API Error:', error.message); - if (error.errors) { - // Handle field-specific errors - } -} -``` - ---- - -## Breaking Changes - -### 1. Response Structure - -**Breaking**: All responses now include `success` field and wrap data in `data` or `results`. - -**Migration**: Update all response parsing code to check `success` and extract `data`/`results`. - -### 2. Error Format - -**Breaking**: Error responses now use unified format with `error` and `errors` fields. - -**Migration**: Update error handling to use new format. - -### 3. Request ID - -**New**: All responses include `request_id` for debugging. - -**Migration**: Optional - can be used for support requests. - ---- - -## Non-Breaking Changes - -### 1. Pagination - -**Status**: Compatible - same field names (`count`, `next`, `previous`, `results`) - -**Migration**: No changes needed, but wrap in success check. - -### 2. Authentication - -**Status**: Compatible - same JWT Bearer token format - -**Migration**: No changes needed. - -### 3. Endpoint URLs - -**Status**: Compatible - same endpoint paths - -**Migration**: No changes needed. - ---- - -## Testing Migration - -### 1. Update Test Code - -```python -# Before -def test_get_keyword(): - response = client.get('/api/v1/planner/keywords/1/') - assert response.status_code == 200 - assert response.json()['id'] == 1 - -# After -def test_get_keyword(): - response = client.get('/api/v1/planner/keywords/1/') - assert response.status_code == 200 - data = response.json() - assert data['success'] == True - assert data['data']['id'] == 1 -``` - -### 2. Test Error Handling - -```python -def test_not_found(): - response = client.get('/api/v1/planner/keywords/99999/') - assert response.status_code == 404 - data = response.json() - assert data['success'] == False - assert data['error'] == "Resource not found" -``` - ---- - -## Migration Checklist - -- [ ] Update response parsing to check `success` field -- [ ] Extract data from `data` or `results` field -- [ ] Update error handling to use unified format -- [ ] Update pagination handling (wrap in success check) -- [ ] Update frontend code (if applicable) -- [ ] Update test code -- [ ] Test all endpoints -- [ ] Update documentation -- [ ] Deploy and monitor - ---- - -## Rollback Plan - -If issues arise during migration: - -1. **Temporary Compatibility Layer**: Add wrapper to convert unified format back to legacy format -2. **Feature Flag**: Use feature flag to toggle between formats -3. **Gradual Migration**: Migrate endpoints one module at a time - ---- - -## Support - -For migration support: -- Review [API Documentation](API-DOCUMENTATION.md) -- Check [Error Codes Reference](ERROR-CODES.md) -- Contact support with `request_id` from failed requests - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/docs/RATE-LIMITING.md b/backup-api-standard-v1/docs/RATE-LIMITING.md deleted file mode 100644 index aa729049..00000000 --- a/backup-api-standard-v1/docs/RATE-LIMITING.md +++ /dev/null @@ -1,439 +0,0 @@ -# Rate Limiting Guide - -**Version**: 1.0.0 -**Last Updated**: 2025-11-16 - -Complete guide for understanding and handling rate limits in the IGNY8 API v1.0. - ---- - -## Overview - -Rate limiting protects the API from abuse and ensures fair resource usage. Different operation types have different rate limits based on their resource intensity. - ---- - -## Rate Limit Headers - -Every API response includes rate limit information in headers: - -- `X-Throttle-Limit`: Maximum requests allowed in the time window -- `X-Throttle-Remaining`: Remaining requests in current window -- `X-Throttle-Reset`: Unix timestamp when the limit resets - -### Example Response Headers - -```http -HTTP/1.1 200 OK -X-Throttle-Limit: 60 -X-Throttle-Remaining: 45 -X-Throttle-Reset: 1700123456 -Content-Type: application/json -``` - ---- - -## Rate Limit Scopes - -Rate limits are scoped by operation type: - -### AI Functions (Expensive Operations) - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `ai_function` | 10/min | Auto-cluster, content generation | -| `image_gen` | 15/min | Image generation (DALL-E, Runware) | -| `planner_ai` | 10/min | AI-powered planner operations | -| `writer_ai` | 10/min | AI-powered writer operations | - -### Content Operations - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `content_write` | 30/min | Content creation, updates | -| `content_read` | 100/min | Content listing, retrieval | - -### Authentication - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `auth` | 20/min | Login, register, password reset | -| `auth_strict` | 5/min | Sensitive auth operations | - -### Planner Operations - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `planner` | 60/min | Keywords, clusters, ideas CRUD | - -### Writer Operations - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `writer` | 60/min | Tasks, content, images CRUD | - -### System Operations - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `system` | 100/min | Settings, prompts, profiles | -| `system_admin` | 30/min | Admin-only system operations | - -### Billing Operations - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `billing` | 30/min | Credit queries, usage logs | -| `billing_admin` | 10/min | Credit management (admin) | - -### Default - -| Scope | Limit | Endpoints | -|-------|-------|-----------| -| `default` | 100/min | Endpoints without explicit scope | - ---- - -## Rate Limit Exceeded (429) - -When rate limit is exceeded, you receive: - -**Status Code**: `429 Too Many Requests` - -**Response**: -```json -{ - "success": false, - "error": "Rate limit exceeded", - "request_id": "550e8400-e29b-41d4-a716-446655440000" -} -``` - -**Headers**: -```http -X-Throttle-Limit: 60 -X-Throttle-Remaining: 0 -X-Throttle-Reset: 1700123456 -``` - -### Handling Rate Limits - -**1. Check Headers Before Request** - -```python -def make_request(url, headers): - response = requests.get(url, headers=headers) - - # Check remaining requests - remaining = int(response.headers.get('X-Throttle-Remaining', 0)) - - if remaining < 5: - # Approaching limit, slow down - time.sleep(1) - - return response.json() -``` - -**2. Handle 429 Response** - -```python -def make_request_with_backoff(url, headers, max_retries=3): - for attempt in range(max_retries): - response = requests.get(url, headers=headers) - - if response.status_code == 429: - # Get reset time - reset_time = int(response.headers.get('X-Throttle-Reset', 0)) - current_time = int(time.time()) - wait_seconds = max(1, reset_time - current_time) - - print(f"Rate limited. Waiting {wait_seconds} seconds...") - time.sleep(wait_seconds) - continue - - return response.json() - - raise Exception("Max retries exceeded") -``` - -**3. Implement Exponential Backoff** - -```python -import time -import random - -def make_request_with_exponential_backoff(url, headers): - max_wait = 60 # Maximum wait time in seconds - base_wait = 1 # Base wait time in seconds - - for attempt in range(5): - response = requests.get(url, headers=headers) - - if response.status_code != 429: - return response.json() - - # Exponential backoff with jitter - wait_time = min( - base_wait * (2 ** attempt) + random.uniform(0, 1), - max_wait - ) - - print(f"Rate limited. Waiting {wait_time:.2f} seconds...") - time.sleep(wait_time) - - raise Exception("Rate limit exceeded after retries") -``` - ---- - -## Best Practices - -### 1. Monitor Rate Limit Headers - -Always check `X-Throttle-Remaining` to avoid hitting limits: - -```python -def check_rate_limit(response): - remaining = int(response.headers.get('X-Throttle-Remaining', 0)) - - if remaining < 10: - print(f"Warning: Only {remaining} requests remaining") - - return remaining -``` - -### 2. Implement Request Queuing - -For bulk operations, queue requests to stay within limits: - -```python -import queue -import threading - -class RateLimitedAPI: - def __init__(self, requests_per_minute=60): - self.queue = queue.Queue() - self.requests_per_minute = requests_per_minute - self.min_interval = 60 / requests_per_minute - self.last_request_time = 0 - - def make_request(self, url, headers): - # Ensure minimum interval between requests - elapsed = time.time() - self.last_request_time - if elapsed < self.min_interval: - time.sleep(self.min_interval - elapsed) - - response = requests.get(url, headers=headers) - self.last_request_time = time.time() - - return response.json() -``` - -### 3. Cache Responses - -Cache frequently accessed data to reduce API calls: - -```python -from functools import lru_cache -import time - -class CachedAPI: - def __init__(self, cache_ttl=300): # 5 minutes - self.cache = {} - self.cache_ttl = cache_ttl - - def get_cached(self, url, headers, cache_key): - # Check cache - if cache_key in self.cache: - data, timestamp = self.cache[cache_key] - if time.time() - timestamp < self.cache_ttl: - return data - - # Fetch from API - response = requests.get(url, headers=headers) - data = response.json() - - # Store in cache - self.cache[cache_key] = (data, time.time()) - - return data -``` - -### 4. Batch Requests When Possible - -Use bulk endpoints instead of multiple individual requests: - -```python -# ❌ Don't: Multiple individual requests -for keyword_id in keyword_ids: - response = requests.get(f"/api/v1/planner/keywords/{keyword_id}/", headers=headers) - -# ✅ Do: Use bulk endpoint if available -response = requests.post( - "/api/v1/planner/keywords/bulk/", - json={"ids": keyword_ids}, - headers=headers -) -``` - ---- - -## Rate Limit Bypass - -### Development/Debug Mode - -Rate limiting is automatically bypassed when: -- `DEBUG=True` in Django settings -- `IGNY8_DEBUG_THROTTLE=True` environment variable -- User belongs to `aws-admin` account -- User has `admin` or `developer` role - -**Note**: Headers are still set for debugging, but requests are not blocked. - ---- - -## Monitoring Rate Limits - -### Track Usage - -```python -class RateLimitMonitor: - def __init__(self): - self.usage_by_scope = {} - - def track_request(self, response, scope): - if scope not in self.usage_by_scope: - self.usage_by_scope[scope] = { - 'total': 0, - 'limited': 0 - } - - self.usage_by_scope[scope]['total'] += 1 - - if response.status_code == 429: - self.usage_by_scope[scope]['limited'] += 1 - - remaining = int(response.headers.get('X-Throttle-Remaining', 0)) - limit = int(response.headers.get('X-Throttle-Limit', 0)) - - usage_percent = ((limit - remaining) / limit) * 100 - - if usage_percent > 80: - print(f"Warning: {scope} at {usage_percent:.1f}% capacity") - - def get_report(self): - return self.usage_by_scope -``` - ---- - -## Troubleshooting - -### Issue: Frequent 429 Errors - -**Causes**: -- Too many requests in short time -- Not checking rate limit headers -- No request throttling implemented - -**Solutions**: -1. Implement request throttling -2. Monitor `X-Throttle-Remaining` header -3. Add delays between requests -4. Use bulk endpoints when available - -### Issue: Rate Limits Too Restrictive - -**Solutions**: -1. Contact support for higher limits (if justified) -2. Optimize requests (cache, batch, reduce frequency) -3. Use development account for testing (bypass enabled) - ---- - -## Code Examples - -### Python - Complete Rate Limit Handler - -```python -import requests -import time -from datetime import datetime - -class RateLimitedClient: - def __init__(self, base_url, token): - self.base_url = base_url - self.headers = { - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json' - } - self.rate_limits = {} - - def _wait_for_rate_limit(self, scope='default'): - """Wait if approaching rate limit""" - if scope in self.rate_limits: - limit_info = self.rate_limits[scope] - remaining = limit_info.get('remaining', 0) - reset_time = limit_info.get('reset_time', 0) - - if remaining < 5: - wait_time = max(0, reset_time - time.time()) - if wait_time > 0: - print(f"Rate limit low. Waiting {wait_time:.1f}s...") - time.sleep(wait_time) - - def _update_rate_limit_info(self, response, scope='default'): - """Update rate limit information from response headers""" - limit = response.headers.get('X-Throttle-Limit') - remaining = response.headers.get('X-Throttle-Remaining') - reset = response.headers.get('X-Throttle-Reset') - - if limit and remaining and reset: - self.rate_limits[scope] = { - 'limit': int(limit), - 'remaining': int(remaining), - 'reset_time': int(reset) - } - - def request(self, method, endpoint, scope='default', **kwargs): - """Make rate-limited request""" - # Wait if approaching limit - self._wait_for_rate_limit(scope) - - # Make request - url = f"{self.base_url}{endpoint}" - response = requests.request(method, url, headers=self.headers, **kwargs) - - # Update rate limit info - self._update_rate_limit_info(response, scope) - - # Handle rate limit error - if response.status_code == 429: - reset_time = int(response.headers.get('X-Throttle-Reset', 0)) - wait_time = max(1, reset_time - time.time()) - print(f"Rate limited. Waiting {wait_time:.1f}s...") - time.sleep(wait_time) - # Retry once - response = requests.request(method, url, headers=self.headers, **kwargs) - self._update_rate_limit_info(response, scope) - - return response.json() - - def get(self, endpoint, scope='default'): - return self.request('GET', endpoint, scope) - - def post(self, endpoint, data, scope='default'): - return self.request('POST', endpoint, scope, json=data) - -# Usage -client = RateLimitedClient("https://api.igny8.com/api/v1", "your_token") - -# Make requests with automatic rate limit handling -keywords = client.get("/planner/keywords/", scope="planner") -``` - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/docs/SECTION-1-2-IMPLEMENTATION-SUMMARY.md b/backup-api-standard-v1/docs/SECTION-1-2-IMPLEMENTATION-SUMMARY.md deleted file mode 100644 index cc00223f..00000000 --- a/backup-api-standard-v1/docs/SECTION-1-2-IMPLEMENTATION-SUMMARY.md +++ /dev/null @@ -1,495 +0,0 @@ -# Section 1 & 2 Implementation Summary - -**API Standard v1.0 Implementation** -**Sections Completed**: Section 1 (Testing) & Section 2 (Documentation) -**Date**: 2025-11-16 -**Status**: ✅ Complete - ---- - -## Overview - -This document summarizes the implementation of **Section 1: Testing** and **Section 2: Documentation** from the Unified API Standard v1.0 implementation plan. - ---- - -## Section 1: Testing ✅ - -### Implementation Summary - -Comprehensive test suite created to verify the Unified API Standard v1.0 implementation across all modules and components. - -### Test Suite Structure - -#### Unit Tests (4 files, ~61 test methods) - -1. **test_response.py** (153 lines) - - Tests for `success_response()`, `error_response()`, `paginated_response()` - - Tests for `get_request_id()` - - Verifies unified response format with `success`, `data`/`results`, `message`, `error`, `errors`, `request_id` - - **18 test methods** - -2. **test_exception_handler.py** (177 lines) - - Tests for `custom_exception_handler()` - - Tests all exception types: - - `ValidationError` (400) - - `AuthenticationFailed` (401) - - `PermissionDenied` (403) - - `NotFound` (404) - - `Throttled` (429) - - Generic exceptions (500) - - Tests debug mode behavior (traceback, view, path, method) - - **12 test methods** - -3. **test_permissions.py** (245 lines) - - Tests for all permission classes: - - `IsAuthenticatedAndActive` - - `HasTenantAccess` - - `IsViewerOrAbove` - - `IsEditorOrAbove` - - `IsAdminOrOwner` - - Tests role-based access control (viewer, editor, admin, owner, developer) - - Tests tenant isolation - - Tests admin/system account bypass logic - - **20 test methods** - -4. **test_throttles.py** (145 lines) - - Tests for `DebugScopedRateThrottle` - - Tests bypass logic: - - DEBUG mode bypass - - Environment flag bypass (`IGNY8_DEBUG_THROTTLE`) - - Admin/developer/system account bypass - - Tests rate parsing and throttle headers - - **11 test methods** - -#### Integration Tests (9 files, ~54 test methods) - -1. **test_integration_base.py** (107 lines) - - Base test class with common fixtures - - Helper methods: - - `assert_unified_response_format()` - Verifies unified response structure - - `assert_paginated_response()` - Verifies pagination format - - Sets up: User, Account, Plan, Site, Sector, Industry, SeedKeyword - -2. **test_integration_planner.py** (120 lines) - - Tests Planner module endpoints: - - `KeywordViewSet` (CRUD operations) - - `ClusterViewSet` (CRUD operations) - - `ContentIdeasViewSet` (CRUD operations) - - Tests AI actions: - - `auto_cluster` - Automatic keyword clustering - - `auto_generate_ideas` - AI content idea generation - - `bulk_queue_to_writer` - Bulk task creation - - Tests unified response format and permissions - - **12 test methods** - -3. **test_integration_writer.py** (65 lines) - - Tests Writer module endpoints: - - `TasksViewSet` (CRUD operations) - - `ContentViewSet` (CRUD operations) - - `ImagesViewSet` (CRUD operations) - - Tests AI actions: - - `auto_generate_content` - AI content generation - - `generate_image_prompts` - Image prompt generation - - `generate_images` - AI image generation - - Tests unified response format and permissions - - **6 test methods** - -4. **test_integration_system.py** (50 lines) - - Tests System module endpoints: - - `AIPromptViewSet` (CRUD operations) - - `SystemSettingsViewSet` (CRUD operations) - - `IntegrationSettingsViewSet` (CRUD operations) - - Tests actions: - - `save_prompt` - Save AI prompt - - `test` - Test integration connection - - `task_progress` - Get task progress - - **5 test methods** - -5. **test_integration_billing.py** (50 lines) - - Tests Billing module endpoints: - - `CreditBalanceViewSet` (balance, summary, limits actions) - - `CreditUsageViewSet` (usage summary) - - `CreditTransactionViewSet` (CRUD operations) - - Tests unified response format and permissions - - **5 test methods** - -6. **test_integration_auth.py** (100 lines) - - Tests Auth module endpoints: - - `AuthViewSet` (register, login, me, change_password, refresh_token, reset_password) - - `UsersViewSet` (CRUD operations) - - `GroupsViewSet` (CRUD operations) - - `AccountsViewSet` (CRUD operations) - - `SiteViewSet` (CRUD operations) - - `SectorViewSet` (CRUD operations) - - `IndustryViewSet` (CRUD operations) - - `SeedKeywordViewSet` (CRUD operations) - - Tests authentication flows and unified response format - - **8 test methods** - -7. **test_integration_errors.py** (95 lines) - - Tests error scenarios: - - 400 Bad Request (validation errors) - - 401 Unauthorized (authentication errors) - - 403 Forbidden (permission errors) - - 404 Not Found (resource not found) - - 429 Too Many Requests (rate limiting) - - 500 Internal Server Error (generic errors) - - Tests unified error format for all scenarios - - **6 test methods** - -8. **test_integration_pagination.py** (100 lines) - - Tests pagination across all modules: - - Default pagination (page size 10) - - Custom page size (1-100) - - Page parameter - - Empty results - - Count, next, previous fields - - Tests pagination on: Keywords, Clusters, Tasks, Content, Users, Accounts - - **10 test methods** - -9. **test_integration_rate_limiting.py** (120 lines) - - Tests rate limiting: - - Throttle headers (`X-Throttle-Limit`, `X-Throttle-Remaining`, `X-Throttle-Reset`) - - Bypass logic (admin/system accounts, DEBUG mode) - - Different throttle scopes (read, write, ai) - - 429 response handling - - **7 test methods** - -### Test Statistics - -- **Total Test Files**: 13 -- **Total Test Methods**: ~115 -- **Total Lines of Code**: ~1,500 -- **Coverage**: 100% of API Standard components - -### What Tests Verify - -1. **Unified Response Format** - - All responses include `success` field (true/false) - - Success responses include `data` (single object) or `results` (list) - - Error responses include `error` (message) and `errors` (field-specific) - - All responses include `request_id` (UUID) - -2. **Status Codes** - - Correct HTTP status codes (200, 201, 400, 401, 403, 404, 429, 500) - - Proper error messages for each status code - - Field-specific errors for validation failures - -3. **Pagination** - - Paginated responses include `count`, `next`, `previous`, `results` - - Page size limits enforced (max 100) - - Empty results handled correctly - - Default page size (10) works correctly - -4. **Error Handling** - - All exceptions wrapped in unified format - - Field-specific errors included in `errors` object - - Debug info (traceback, view, path, method) in DEBUG mode - - Request ID included in all error responses - -5. **Permissions** - - Role-based access control (viewer, editor, admin, owner, developer) - - Tenant isolation (users can only access their account's data) - - Site/sector scoping (users can only access their assigned sites/sectors) - - Admin/system account bypass (full access) - -6. **Rate Limiting** - - Throttle headers present in all responses - - Bypass logic for admin/developer/system account users - - Bypass in DEBUG mode (for development) - - Different throttle scopes (read, write, ai) - -### Test Execution - -```bash -# Run all tests -python manage.py test igny8_core.api.tests --verbosity=2 - -# Run specific test file -python manage.py test igny8_core.api.tests.test_response - -# Run specific test class -python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase - -# Run with coverage -coverage run --source='igny8_core.api' manage.py test igny8_core.api.tests -coverage report -``` - -### Test Results - -All tests pass successfully: -- ✅ Unit tests: 61/61 passing -- ✅ Integration tests: 54/54 passing -- ✅ Total: 115/115 passing - -### Files Created - -- `backend/igny8_core/api/tests/__init__.py` -- `backend/igny8_core/api/tests/test_response.py` -- `backend/igny8_core/api/tests/test_exception_handler.py` -- `backend/igny8_core/api/tests/test_permissions.py` -- `backend/igny8_core/api/tests/test_throttles.py` -- `backend/igny8_core/api/tests/test_integration_base.py` -- `backend/igny8_core/api/tests/test_integration_planner.py` -- `backend/igny8_core/api/tests/test_integration_writer.py` -- `backend/igny8_core/api/tests/test_integration_system.py` -- `backend/igny8_core/api/tests/test_integration_billing.py` -- `backend/igny8_core/api/tests/test_integration_auth.py` -- `backend/igny8_core/api/tests/test_integration_errors.py` -- `backend/igny8_core/api/tests/test_integration_pagination.py` -- `backend/igny8_core/api/tests/test_integration_rate_limiting.py` -- `backend/igny8_core/api/tests/README.md` -- `backend/igny8_core/api/tests/TEST_SUMMARY.md` -- `backend/igny8_core/api/tests/run_tests.py` - ---- - -## Section 2: Documentation ✅ - -### Implementation Summary - -Complete documentation system for IGNY8 API v1.0 including OpenAPI 3.0 schema generation, interactive Swagger UI, and comprehensive documentation files. - -### OpenAPI/Swagger Integration - -#### Package Installation -- ✅ Installed `drf-spectacular>=0.27.0` -- ✅ Added to `INSTALLED_APPS` in `settings.py` -- ✅ Configured `REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS']` - -#### Configuration (`backend/igny8_core/settings.py`) - -```python -SPECTACULAR_SETTINGS = { - 'TITLE': 'IGNY8 API v1.0', - 'DESCRIPTION': 'Comprehensive REST API for content planning, creation, and management...', - 'VERSION': '1.0.0', - 'SCHEMA_PATH_PREFIX': '/api/v1', - 'COMPONENT_SPLIT_REQUEST': True, - 'TAGS': [ - {'name': 'Authentication', 'description': 'User authentication and registration'}, - {'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'}, - {'name': 'Writer', 'description': 'Tasks, content, and images'}, - {'name': 'System', 'description': 'Settings, prompts, and integrations'}, - {'name': 'Billing', 'description': 'Credits, usage, and transactions'}, - ], - 'EXTENSIONS_INFO': { - 'x-code-samples': [ - {'lang': 'Python', 'source': '...'}, - {'lang': 'JavaScript', 'source': '...'} - ] - } -} -``` - -#### Endpoints Created - -- ✅ `/api/schema/` - OpenAPI 3.0 schema (JSON/YAML) -- ✅ `/api/docs/` - Swagger UI (interactive documentation) -- ✅ `/api/redoc/` - ReDoc (alternative documentation UI) - -#### Schema Extensions - -Created `backend/igny8_core/api/schema_extensions.py`: -- ✅ `JWTAuthenticationExtension` - JWT Bearer token authentication -- ✅ `CSRFExemptSessionAuthenticationExtension` - Session authentication -- ✅ Proper OpenAPI security scheme definitions - -#### URL Configuration (`backend/igny8_core/urls.py`) - -```python -from drf_spectacular.views import ( - SpectacularAPIView, - SpectacularRedocView, - SpectacularSwaggerView, -) - -urlpatterns = [ - # ... other URLs ... - path('api/schema/', SpectacularAPIView.as_view(), name='schema'), - path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), - path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), -] -``` - -### Documentation Files Created - -#### 1. API-DOCUMENTATION.md -**Purpose**: Complete API reference -**Contents**: -- Quick start guide -- Authentication guide -- Response format details -- Error handling -- Rate limiting -- Pagination -- Endpoint reference -- Code examples (Python, JavaScript, cURL) - -#### 2. AUTHENTICATION-GUIDE.md -**Purpose**: Authentication and authorization -**Contents**: -- JWT Bearer token authentication -- Token management and refresh -- Code examples (Python, JavaScript) -- Security best practices -- Token expiration handling -- Troubleshooting - -#### 3. ERROR-CODES.md -**Purpose**: Complete error code reference -**Contents**: -- HTTP status codes (200, 201, 400, 401, 403, 404, 409, 422, 429, 500) -- Field-specific error messages -- Error handling best practices -- Common error scenarios -- Debugging tips - -#### 4. RATE-LIMITING.md -**Purpose**: Rate limiting and throttling -**Contents**: -- Rate limit scopes and limits -- Handling rate limits (429 responses) -- Best practices -- Code examples with backoff strategies -- Request queuing and caching - -#### 5. MIGRATION-GUIDE.md -**Purpose**: Migration guide for API consumers -**Contents**: -- What changed in v1.0 -- Step-by-step migration instructions -- Code examples (before/after) -- Breaking and non-breaking changes -- Migration checklist - -#### 6. WORDPRESS-PLUGIN-INTEGRATION.md -**Purpose**: WordPress plugin integration -**Contents**: -- Complete PHP API client class -- Authentication implementation -- Error handling -- WordPress admin integration -- Two-way sync (WordPress → IGNY8) -- Site data fetching (posts, taxonomies, products, attributes) -- Semantic mapping and content restructuring -- Best practices -- Testing examples - -#### 7. README.md -**Purpose**: Documentation index -**Contents**: -- Documentation index -- Quick start guide -- Links to all documentation files -- Support information - -### Documentation Statistics - -- **Total Documentation Files**: 7 -- **Total Pages**: ~100+ pages of documentation -- **Code Examples**: Python, JavaScript, PHP, cURL -- **Coverage**: 100% of API features documented - -### Access Points - -#### Interactive Documentation -- **Swagger UI**: `https://api.igny8.com/api/docs/` -- **ReDoc**: `https://api.igny8.com/api/redoc/` -- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` - -#### Documentation Files -- All files in `docs/` directory -- Index: `docs/README.md` - -### Files Created/Modified - -#### Backend Files -- `backend/igny8_core/settings.py` - Added drf-spectacular configuration -- `backend/igny8_core/urls.py` - Added schema/documentation endpoints -- `backend/igny8_core/api/schema_extensions.py` - Custom authentication extensions -- `backend/requirements.txt` - Added drf-spectacular>=0.27.0 - -#### Documentation Files -- `docs/API-DOCUMENTATION.md` -- `docs/AUTHENTICATION-GUIDE.md` -- `docs/ERROR-CODES.md` -- `docs/RATE-LIMITING.md` -- `docs/MIGRATION-GUIDE.md` -- `docs/WORDPRESS-PLUGIN-INTEGRATION.md` -- `docs/README.md` -- `docs/DOCUMENTATION-SUMMARY.md` -- `docs/SECTION-2-COMPLETE.md` - ---- - -## Verification & Status - -### Section 1: Testing ✅ -- ✅ All test files created -- ✅ All tests passing (115/115) -- ✅ 100% coverage of API Standard components -- ✅ Unit tests: 61/61 passing -- ✅ Integration tests: 54/54 passing -- ✅ Test documentation created - -### Section 2: Documentation ✅ -- ✅ drf-spectacular installed and configured -- ✅ Schema generation working (OpenAPI 3.0) -- ✅ Schema endpoint accessible (`/api/schema/`) -- ✅ Swagger UI accessible (`/api/docs/`) -- ✅ ReDoc accessible (`/api/redoc/`) -- ✅ 7 comprehensive documentation files created -- ✅ Code examples included (Python, JavaScript, PHP, cURL) -- ✅ Changelog updated - ---- - -## Deliverables - -### Section 1 Deliverables -1. ✅ Complete test suite (13 test files, 115 test methods) -2. ✅ Test documentation (README.md, TEST_SUMMARY.md) -3. ✅ Test runner script (run_tests.py) -4. ✅ All tests passing - -### Section 2 Deliverables -1. ✅ OpenAPI 3.0 schema generation -2. ✅ Interactive Swagger UI -3. ✅ ReDoc documentation -4. ✅ 7 comprehensive documentation files -5. ✅ Code examples in multiple languages -6. ✅ Integration guides - ---- - -## Next Steps - -### Completed ✅ -- ✅ Section 1: Testing - Complete -- ✅ Section 2: Documentation - Complete - -### Remaining -- Section 3: Frontend Refactoring (if applicable) -- Section 4: Additional Features (if applicable) -- Section 5: Performance Optimization (if applicable) - ---- - -## Summary - -Both **Section 1: Testing** and **Section 2: Documentation** have been successfully implemented and verified: - -- **Testing**: Comprehensive test suite with 115 test methods covering all API Standard components -- **Documentation**: Complete documentation system with OpenAPI schema, Swagger UI, and 7 comprehensive guides - -All deliverables are complete, tested, and ready for use. - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 -**Status**: ✅ Complete - diff --git a/backup-api-standard-v1/docs/SECTION-2-COMPLETE.md b/backup-api-standard-v1/docs/SECTION-2-COMPLETE.md deleted file mode 100644 index 204e33ad..00000000 --- a/backup-api-standard-v1/docs/SECTION-2-COMPLETE.md +++ /dev/null @@ -1,81 +0,0 @@ -# Section 2: Documentation - COMPLETE ✅ - -**Date Completed**: 2025-11-16 -**Status**: All Documentation Implemented, Verified, and Fully Functional - ---- - -## Summary - -Section 2: Documentation has been successfully implemented with: -- ✅ OpenAPI 3.0 schema generation (drf-spectacular v0.29.0) -- ✅ Interactive Swagger UI and ReDoc -- ✅ 7 comprehensive documentation files -- ✅ Code examples in multiple languages -- ✅ Integration guides for all platforms - ---- - -## Deliverables - -### 1. OpenAPI/Swagger Integration ✅ -- **Package**: drf-spectacular v0.29.0 installed -- **Endpoints**: - - `/api/schema/` - OpenAPI 3.0 schema - - `/api/docs/` - Swagger UI - - `/api/redoc/` - ReDoc -- **Configuration**: Comprehensive settings with API description, tags, code samples - -### 2. Documentation Files ✅ -- **API-DOCUMENTATION.md** - Complete API reference -- **AUTHENTICATION-GUIDE.md** - Auth guide with examples -- **ERROR-CODES.md** - Error code reference -- **RATE-LIMITING.md** - Rate limiting guide -- **MIGRATION-GUIDE.md** - Migration instructions -- **WORDPRESS-PLUGIN-INTEGRATION.md** - WordPress integration -- **README.md** - Documentation index - -### 3. Schema Extensions ✅ -- Custom JWT authentication extension -- Session authentication extension -- Proper OpenAPI security schemes - ---- - -## Verification - -✅ **drf-spectacular**: Installed and configured -✅ **Schema Generation**: Working (database created and migrations applied) -✅ **Schema Endpoint**: `/api/schema/` returns 200 OK with OpenAPI 3.0 schema -✅ **Swagger UI**: `/api/docs/` displays full API documentation -✅ **ReDoc**: `/api/redoc/` displays full API documentation -✅ **Documentation Files**: 7 files created -✅ **Changelog**: Updated with documentation section -✅ **Code Examples**: Python, JavaScript, PHP, cURL included - ---- - -## Access - -- **Swagger UI**: `https://api.igny8.com/api/docs/` -- **ReDoc**: `https://api.igny8.com/api/redoc/` -- **OpenAPI Schema**: `https://api.igny8.com/api/schema/` -- **Documentation Files**: `docs/` directory - ---- - -## Status - -**Section 2: Documentation - COMPLETE** ✅ - -All documentation is implemented, verified, and fully functional: -- Database created and migrations applied -- Schema generation working (OpenAPI 3.0) -- Swagger UI displaying full API documentation -- ReDoc displaying full API documentation -- All endpoints accessible and working - ---- - -**Completed**: 2025-11-16 - diff --git a/backup-api-standard-v1/docs/WORDPRESS-PLUGIN-INTEGRATION.md b/backup-api-standard-v1/docs/WORDPRESS-PLUGIN-INTEGRATION.md deleted file mode 100644 index c421f85d..00000000 --- a/backup-api-standard-v1/docs/WORDPRESS-PLUGIN-INTEGRATION.md +++ /dev/null @@ -1,2055 +0,0 @@ -# WordPress Plugin Integration Guide - -**Version**: 1.0.0 -**Last Updated**: 2025-11-16 - -Complete guide for integrating WordPress plugins with IGNY8 API v1.0. - ---- - -## Overview - -This guide helps WordPress plugin developers integrate with the IGNY8 API using the unified response format. - ---- - -## Authentication - -### Getting Access Token - -```php -function igny8_login($email, $password) { - $response = wp_remote_post('https://api.igny8.com/api/v1/auth/login/', [ - 'headers' => [ - 'Content-Type' => 'application/json' - ], - 'body' => json_encode([ - 'email' => $email, - 'password' => $password - ]) - ]); - - $body = json_decode(wp_remote_retrieve_body($response), true); - - if ($body['success']) { - // Store tokens - update_option('igny8_access_token', $body['data']['access']); - update_option('igny8_refresh_token', $body['data']['refresh']); - return $body['data']['access']; - } else { - return new WP_Error('login_failed', $body['error']); - } -} -``` - -### Using Access Token - -```php -function igny8_get_headers() { - $token = get_option('igny8_access_token'); - - if (!$token) { - return false; - } - - return [ - 'Authorization' => 'Bearer ' . $token, - 'Content-Type' => 'application/json' - ]; -} -``` - ---- - -## API Client Class - -### Complete PHP Implementation - -```php -class Igny8API { - private $base_url = 'https://api.igny8.com/api/v1'; - private $access_token = null; - private $refresh_token = null; - - public function __construct() { - $this->access_token = get_option('igny8_access_token'); - $this->refresh_token = get_option('igny8_refresh_token'); - } - - /** - * Login and store tokens - */ - public function login($email, $password) { - $response = wp_remote_post($this->base_url . '/auth/login/', [ - 'headers' => [ - 'Content-Type' => 'application/json' - ], - 'body' => json_encode([ - 'email' => $email, - 'password' => $password - ]) - ]); - - $body = $this->parse_response($response); - - if ($body['success']) { - $this->access_token = $body['data']['access']; - $this->refresh_token = $body['data']['refresh']; - - update_option('igny8_access_token', $this->access_token); - update_option('igny8_refresh_token', $this->refresh_token); - - return true; - } - - return false; - } - - /** - * Refresh access token - */ - public function refresh_token() { - if (!$this->refresh_token) { - return false; - } - - $response = wp_remote_post($this->base_url . '/auth/refresh/', [ - 'headers' => [ - 'Content-Type' => 'application/json' - ], - 'body' => json_encode([ - 'refresh' => $this->refresh_token - ]) - ]); - - $body = $this->parse_response($response); - - if ($body['success']) { - $this->access_token = $body['data']['access']; - $this->refresh_token = $body['data']['refresh']; - - update_option('igny8_access_token', $this->access_token); - update_option('igny8_refresh_token', $this->refresh_token); - - return true; - } - - return false; - } - - /** - * Parse unified API response - */ - private function parse_response($response) { - if (is_wp_error($response)) { - return [ - 'success' => false, - 'error' => $response->get_error_message() - ]; - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - $status_code = wp_remote_retrieve_response_code($response); - - // Handle non-JSON responses - if (!$body) { - return [ - 'success' => false, - 'error' => 'Invalid response format' - ]; - } - - // Check if response follows unified format - if (isset($body['success'])) { - return $body; - } - - // Legacy format - wrap in unified format - if ($status_code >= 200 && $status_code < 300) { - return [ - 'success' => true, - 'data' => $body - ]; - } else { - return [ - 'success' => false, - 'error' => $body['detail'] ?? 'Unknown error' - ]; - } - } - - /** - * Get headers with authentication - */ - private function get_headers() { - if (!$this->access_token) { - throw new Exception('Not authenticated'); - } - - return [ - 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json' - ]; - } - - /** - * Make GET request - */ - public function get($endpoint) { - $response = wp_remote_get($this->base_url . $endpoint, [ - 'headers' => $this->get_headers() - ]); - - $body = $this->parse_response($response); - - // Handle 401 - token expired - if (!$body['success'] && wp_remote_retrieve_response_code($response) == 401) { - // Try to refresh token - if ($this->refresh_token()) { - // Retry request - $response = wp_remote_get($this->base_url . $endpoint, [ - 'headers' => $this->get_headers() - ]); - $body = $this->parse_response($response); - } - } - - return $body; - } - - /** - * Make POST request - */ - public function post($endpoint, $data) { - $response = wp_remote_post($this->base_url . $endpoint, [ - 'headers' => $this->get_headers(), - 'body' => json_encode($data) - ]); - - $body = $this->parse_response($response); - - // Handle 401 - token expired - if (!$body['success'] && wp_remote_retrieve_response_code($response) == 401) { - // Try to refresh token - if ($this->refresh_token()) { - // Retry request - $response = wp_remote_post($this->base_url . $endpoint, [ - 'headers' => $this->get_headers(), - 'body' => json_encode($data) - ]); - $body = $this->parse_response($response); - } - } - - return $body; - } - - /** - * Make PUT request - */ - public function put($endpoint, $data) { - $response = wp_remote_request($this->base_url . $endpoint, [ - 'method' => 'PUT', - 'headers' => $this->get_headers(), - 'body' => json_encode($data) - ]); - - return $this->parse_response($response); - } - - /** - * Make DELETE request - */ - public function delete($endpoint) { - $response = wp_remote_request($this->base_url . $endpoint, [ - 'method' => 'DELETE', - 'headers' => $this->get_headers() - ]); - - return $this->parse_response($response); - } -} -``` - ---- - -## Usage Examples - -### Get Keywords - -```php -$api = new Igny8API(); - -// Get keywords -$response = $api->get('/planner/keywords/'); - -if ($response['success']) { - $keywords = $response['results']; - $count = $response['count']; - - foreach ($keywords as $keyword) { - echo $keyword['name'] . '
'; - } -} else { - echo 'Error: ' . $response['error']; -} -``` - -### Create Keyword - -```php -$api = new Igny8API(); - -$data = [ - 'seed_keyword_id' => 1, - 'site_id' => 1, - 'sector_id' => 1, - 'status' => 'active' -]; - -$response = $api->post('/planner/keywords/', $data); - -if ($response['success']) { - $keyword = $response['data']; - echo 'Created keyword: ' . $keyword['id']; -} else { - echo 'Error: ' . $response['error']; - if (isset($response['errors'])) { - foreach ($response['errors'] as $field => $errors) { - echo $field . ': ' . implode(', ', $errors) . '
'; - } - } -} -``` - -### Handle Pagination - -```php -$api = new Igny8API(); - -function get_all_keywords($api) { - $all_keywords = []; - $page = 1; - - do { - $response = $api->get("/planner/keywords/?page={$page}&page_size=100"); - - if ($response['success']) { - $all_keywords = array_merge($all_keywords, $response['results']); - $page++; - } else { - break; - } - } while ($response['next']); - - return $all_keywords; -} - -$keywords = get_all_keywords($api); -``` - -### Handle Rate Limiting - -```php -function make_rate_limited_request($api, $endpoint, $max_retries = 3) { - for ($attempt = 0; $attempt < $max_retries; $attempt++) { - $response = $api->get($endpoint); - - // Check if rate limited - if (!$response['success'] && isset($response['error'])) { - if (strpos($response['error'], 'Rate limit') !== false) { - // Wait before retry - sleep(pow(2, $attempt)); // Exponential backoff - continue; - } - } - - return $response; - } - - return ['success' => false, 'error' => 'Max retries exceeded']; -} -``` - ---- - -## Error Handling - -### Unified Error Handling - -```php -function handle_api_response($response) { - if ($response['success']) { - return $response['data'] ?? $response['results']; - } else { - $error_message = $response['error']; - - // Log error with request ID - error_log(sprintf( - 'IGNY8 API Error: %s (Request ID: %s)', - $error_message, - $response['request_id'] ?? 'unknown' - )); - - // Handle field-specific errors - if (isset($response['errors'])) { - foreach ($response['errors'] as $field => $errors) { - error_log(" {$field}: " . implode(', ', $errors)); - } - } - - return new WP_Error('igny8_api_error', $error_message, $response); - } -} -``` - ---- - -## Best Practices - -### 1. Store Tokens Securely - -```php -// Use WordPress options API with encryption -function save_token($token) { - // Encrypt token before storing - $encrypted = base64_encode($token); - update_option('igny8_access_token', $encrypted, false); -} - -function get_token() { - $encrypted = get_option('igny8_access_token'); - return base64_decode($encrypted); -} -``` - -### 2. Implement Token Refresh - -```php -function ensure_valid_token($api) { - // Check if token is about to expire (refresh 1 minute before) - // Token expires in 15 minutes, refresh at 14 minutes - $last_refresh = get_option('igny8_token_refreshed_at', 0); - - if (time() - $last_refresh > 14 * 60) { - if ($api->refresh_token()) { - update_option('igny8_token_refreshed_at', time()); - } - } -} -``` - -### 3. Cache Responses - -```php -function get_cached_keywords($api, $cache_key = 'igny8_keywords', $ttl = 300) { - $cached = get_transient($cache_key); - - if ($cached !== false) { - return $cached; - } - - $response = $api->get('/planner/keywords/'); - - if ($response['success']) { - $keywords = $response['results']; - set_transient($cache_key, $keywords, $ttl); - return $keywords; - } - - return false; -} -``` - -### 4. Handle Rate Limits - -```php -function check_rate_limit($response) { - // Note: WordPress wp_remote_* doesn't expose all headers easily - // Consider using cURL or checking response for 429 status - - if (isset($response['error']) && strpos($response['error'], 'Rate limit') !== false) { - // Wait and retry - sleep(60); - return true; // Should retry - } - - return false; -} -``` - ---- - -## WordPress Admin Integration - -### Settings Page - -```php -function igny8_settings_page() { - ?> -
-

IGNY8 API Settings

-
- - - - - - - - - - -
API Email
API Password
- -
-
- login($_POST['igny8_email'], $_POST['igny8_password'])) { - update_option('igny8_email', $_POST['igny8_email']); - add_settings_error('igny8_settings', 'igny8_connected', 'Successfully connected to IGNY8 API', 'updated'); - } else { - add_settings_error('igny8_settings', 'igny8_error', 'Failed to connect to IGNY8 API', 'error'); - } - } -} -add_action('admin_init', 'igny8_save_settings'); -``` - ---- - -## Testing - -### Unit Tests - -```php -class TestIgny8API extends WP_UnitTestCase { - public function test_login() { - $api = new Igny8API(); - $result = $api->login('test@example.com', 'password'); - - $this->assertTrue($result); - $this->assertNotEmpty(get_option('igny8_access_token')); - } - - public function test_get_keywords() { - $api = new Igny8API(); - $response = $api->get('/planner/keywords/'); - - $this->assertTrue($response['success']); - $this->assertArrayHasKey('results', $response); - $this->assertArrayHasKey('count', $response); - } -} -``` - ---- - -## Troubleshooting - -### Issue: Authentication Fails - -**Check**: -1. Email and password are correct -2. Account is active -3. API endpoint is accessible - -### Issue: Token Expires Frequently - -**Solution**: Implement automatic token refresh before expiration. - -### Issue: Rate Limited - -**Solution**: Implement request throttling and caching. - ---- - -## WordPress Hooks and Two-Way Sync - -### Overview - -The integration supports **two-way synchronization**: -- **IGNY8 → WordPress**: Publishing content from IGNY8 to WordPress -- **WordPress → IGNY8**: Syncing WordPress post status changes back to IGNY8 - -### WordPress Post Hooks - -#### 1. Post Save Hook (`save_post`) - -Hook into WordPress post saves to sync status back to IGNY8: - -```php -/** - * Sync WordPress post status to IGNY8 when post is saved - */ -add_action('save_post', 'igny8_sync_post_status_to_igny8', 10, 3); - -function igny8_sync_post_status_to_igny8($post_id, $post, $update) { - // Skip autosaves and revisions - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - - if (wp_is_post_revision($post_id)) { - return; - } - - // Only sync IGNY8-managed posts - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - // Get post status - $post_status = $post->post_status; - - // Map WordPress status to IGNY8 task status - $task_status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $task_status_map[$post_status] ?? 'draft'; - - // Sync to IGNY8 API - $api = new Igny8API(); - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - - if ($response['success']) { - error_log("IGNY8: Synced post {$post_id} status to task {$task_id}"); - } else { - error_log("IGNY8: Failed to sync post status: " . $response['error']); - } -} -``` - -#### 2. Post Publish Hook (`publish_post`) - -Update keyword status when content is published: - -```php -/** - * Update keyword status when WordPress post is published - */ -add_action('publish_post', 'igny8_update_keywords_on_post_publish', 10, 1); -add_action('publish_page', 'igny8_update_keywords_on_post_publish', 10, 1); -add_action('draft_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1); -add_action('future_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1); - -function igny8_update_keywords_on_post_publish($post_id) { - // Get task ID from post meta - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $api = new Igny8API(); - - // Get task details to find associated cluster/keywords - $task_response = $api->get("/writer/tasks/{$task_id}/"); - - if (!$task_response['success']) { - return; - } - - $task = $task_response['data']; - $cluster_id = $task['cluster_id'] ?? null; - - if ($cluster_id) { - // Get keywords in this cluster - $keywords_response = $api->get("/planner/keywords/?cluster_id={$cluster_id}"); - - if ($keywords_response['success']) { - $keywords = $keywords_response['results']; - - // Update each keyword status to 'mapped' - foreach ($keywords as $keyword) { - $api->put("/planner/keywords/{$keyword['id']}/", [ - 'status' => 'mapped' - ]); - } - } - } - - // Update task status to completed - $api->put("/writer/tasks/{$task_id}/", [ - 'status' => 'completed', - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); -} -``` - -#### 3. Post Status Change Hook (`transition_post_status`) - -Handle all post status transitions: - -```php -/** - * Sync post status changes to IGNY8 - */ -add_action('transition_post_status', 'igny8_sync_post_status_transition', 10, 3); - -function igny8_sync_post_status_transition($new_status, $old_status, $post) { - // Skip if status hasn't changed - if ($new_status === $old_status) { - return; - } - - // Only sync IGNY8-managed posts - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $api = new Igny8API(); - - // Map WordPress status to IGNY8 task status - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived', - 'future' => 'scheduled' - ]; - - $task_status = $status_map[$new_status] ?? 'draft'; - - // Sync to IGNY8 - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post->ID, - 'post_url' => get_permalink($post->ID) - ]); - - if ($response['success']) { - do_action('igny8_post_status_synced', $post->ID, $task_id, $new_status); - } -} -``` - -### Fetching WordPress Post Status - -#### Get Post Status from WordPress - -```php -/** - * Get WordPress post status and sync to IGNY8 - */ -function igny8_fetch_and_sync_post_status($post_id) { - $post = get_post($post_id); - - if (!$post) { - return false; - } - - // Get post status - $wp_status = $post->post_status; - - // Get additional post data - $post_data = [ - 'id' => $post_id, - 'status' => $wp_status, - 'title' => $post->post_title, - 'url' => get_permalink($post_id), - 'modified' => $post->post_modified, - 'published' => $post->post_date - ]; - - // Get task ID - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - - if (!$task_id) { - return false; - } - - // Sync to IGNY8 - $api = new Igny8API(); - - // Map WordPress status to IGNY8 status - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $status_map[$wp_status] ?? 'draft'; - - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => $post_data['url'] - ]); - - return $response['success']; -} -``` - -#### Batch Sync Post Statuses - -```php -/** - * Sync all IGNY8-managed posts status to IGNY8 API - */ -function igny8_batch_sync_post_statuses() { - global $wpdb; - - // Get all posts with IGNY8 task ID - $posts = $wpdb->get_results(" - SELECT p.ID, p.post_status, p.post_title, pm.meta_value as task_id - FROM {$wpdb->posts} p - INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id - WHERE pm.meta_key = '_igny8_task_id' - AND p.post_type IN ('post', 'page') - AND p.post_status != 'trash' - "); - - $api = new Igny8API(); - $synced = 0; - $failed = 0; - - foreach ($posts as $post_data) { - $post_id = $post_data->ID; - $task_id = intval($post_data->task_id); - $wp_status = $post_data->post_status; - - // Map status - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed' - ]; - - $task_status = $status_map[$wp_status] ?? 'draft'; - - // Sync to IGNY8 - $response = $api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - - if ($response['success']) { - $synced++; - } else { - $failed++; - error_log("IGNY8: Failed to sync post {$post_id}: " . $response['error']); - } - } - - return [ - 'synced' => $synced, - 'failed' => $failed, - 'total' => count($posts) - ]; -} -``` - -### Complete Two-Way Sync Example - -```php -/** - * Complete two-way sync implementation - */ -class Igny8WordPressSync { - private $api; - - public function __construct() { - $this->api = new Igny8API(); - - // WordPress → IGNY8 hooks - add_action('save_post', [$this, 'sync_post_to_igny8'], 10, 3); - add_action('publish_post', [$this, 'update_keywords_on_publish'], 10, 1); - add_action('transition_post_status', [$this, 'sync_status_transition'], 10, 3); - - // IGNY8 → WordPress (when content is published from IGNY8) - add_action('igny8_content_published', [$this, 'create_wordpress_post'], 10, 1); - } - - /** - * WordPress → IGNY8: Sync post changes to IGNY8 - */ - public function sync_post_to_igny8($post_id, $post, $update) { - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - - if (wp_is_post_revision($post_id)) { - return; - } - - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $status_map[$post->post_status] ?? 'draft'; - - $response = $this->api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - - if ($response['success']) { - error_log("IGNY8: Synced post {$post_id} to task {$task_id}"); - } - } - - /** - * WordPress → IGNY8: Update keywords when post is published - */ - public function update_keywords_on_publish($post_id) { - $task_id = get_post_meta($post_id, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - // Get task to find cluster - $task_response = $this->api->get("/writer/tasks/{$task_id}/"); - if (!$task_response['success']) { - return; - } - - $task = $task_response['data']; - $cluster_id = $task['cluster_id'] ?? null; - - if ($cluster_id) { - // Update keywords in cluster to 'mapped' - $keywords_response = $this->api->get("/planner/keywords/?cluster_id={$cluster_id}"); - if ($keywords_response['success']) { - foreach ($keywords_response['results'] as $keyword) { - $this->api->put("/planner/keywords/{$keyword['id']}/", [ - 'status' => 'mapped' - ]); - } - } - } - - // Update task status - $this->api->put("/writer/tasks/{$task_id}/", [ - 'status' => 'completed', - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - } - - /** - * WordPress → IGNY8: Handle status transitions - */ - public function sync_status_transition($new_status, $old_status, $post) { - if ($new_status === $old_status) { - return; - } - - $task_id = get_post_meta($post->ID, '_igny8_task_id', true); - if (!$task_id) { - return; - } - - $status_map = [ - 'publish' => 'completed', - 'draft' => 'draft', - 'pending' => 'pending', - 'private' => 'completed', - 'trash' => 'archived' - ]; - - $task_status = $status_map[$new_status] ?? 'draft'; - - $this->api->put("/writer/tasks/{$task_id}/", [ - 'status' => $task_status, - 'assigned_post_id' => $post->ID, - 'post_url' => get_permalink($post->ID) - ]); - } - - /** - * IGNY8 → WordPress: Create WordPress post from IGNY8 content - */ - public function create_wordpress_post($content_data) { - $post_data = [ - 'post_title' => $content_data['title'], - 'post_content' => $content_data['content'], - 'post_status' => $content_data['status'] ?? 'draft', - 'post_type' => 'post', - 'meta_input' => [ - '_igny8_task_id' => $content_data['task_id'], - '_igny8_content_id' => $content_data['content_id'] - ] - ]; - - $post_id = wp_insert_post($post_data); - - if (!is_wp_error($post_id)) { - // Update IGNY8 task with WordPress post ID - $this->api->put("/writer/tasks/{$content_data['task_id']}/", [ - 'assigned_post_id' => $post_id, - 'post_url' => get_permalink($post_id) - ]); - } - - return $post_id; - } -} - -// Initialize sync -new Igny8WordPressSync(); -``` - -### WordPress Post Status Mapping - -| WordPress Status | IGNY8 Task Status | Description | -|------------------|-------------------|-------------| -| `publish` | `completed` | Post is published | -| `draft` | `draft` | Post is draft | -| `pending` | `pending` | Post is pending review | -| `private` | `completed` | Post is private (published) | -| `trash` | `archived` | Post is deleted/trashed | -| `future` | `scheduled` | Post is scheduled | - -### Fetching WordPress Post Data - -```php -/** - * Get WordPress post data for IGNY8 sync - */ -function igny8_get_post_data_for_sync($post_id) { - $post = get_post($post_id); - - if (!$post) { - return false; - } - - return [ - 'id' => $post_id, - 'title' => $post->post_title, - 'status' => $post->post_status, - 'url' => get_permalink($post_id), - 'modified' => $post->post_modified, - 'published' => $post->post_date, - 'author' => get_the_author_meta('display_name', $post->post_author), - 'word_count' => str_word_count(strip_tags($post->post_content)), - 'meta' => [ - 'task_id' => get_post_meta($post_id, '_igny8_task_id', true), - 'content_id' => get_post_meta($post_id, '_igny8_content_id', true), - 'primary_keywords' => get_post_meta($post_id, '_igny8_primary_keywords', true) - ] - ]; -} -``` - -### Scheduled Sync (Cron Job) - -```php -/** - * Scheduled sync of WordPress post statuses to IGNY8 - */ -add_action('igny8_sync_post_statuses', 'igny8_cron_sync_post_statuses'); - -function igny8_cron_sync_post_statuses() { - $result = igny8_batch_sync_post_statuses(); - - error_log(sprintf( - 'IGNY8: Synced %d posts, %d failed', - $result['synced'], - $result['failed'] - )); -} - -// Schedule daily sync -if (!wp_next_scheduled('igny8_sync_post_statuses')) { - wp_schedule_event(time(), 'daily', 'igny8_sync_post_statuses'); -} -``` - ---- - -## Complete Integration Flow - -### IGNY8 → WordPress Flow - -1. Content generated in IGNY8 -2. Task created/updated in IGNY8 -3. WordPress post created via `wp_insert_post()` -4. Post meta saved with `_igny8_task_id` -5. IGNY8 task updated with WordPress post ID - -### WordPress → IGNY8 Flow - -1. User saves/publishes WordPress post -2. `save_post` or `publish_post` hook fires -3. Plugin gets `_igny8_task_id` from post meta -4. Plugin calls IGNY8 API to update task status -5. If published, keywords updated to 'mapped' status -6. IGNY8 task status synced - ---- - -## WordPress Site Data Fetching and Semantic Mapping - -### Overview - -After WordPress site integration and API verification, you can fetch comprehensive site data (posts, taxonomies, products, attributes) and send it to IGNY8 for semantic strategy mapping. This enables content restructuring and site-wide optimization. - ---- - -## Fetching WordPress Posts - -### Get All Post Types - -```php -/** - * Fetch all posts of a specific type from WordPress - */ -function igny8_fetch_wordpress_posts($post_type = 'post', $per_page = 100) { - $api = new Igny8API(); - - // Use WordPress REST API to fetch posts - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wp/v2/%s?per_page=%d&status=publish', - get_site_url(), - $post_type, - $per_page - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $posts = json_decode(wp_remote_retrieve_body($wp_response), true); - - // Format posts for IGNY8 - $formatted_posts = []; - foreach ($posts as $post) { - $formatted_posts[] = [ - 'id' => $post['id'], - 'title' => $post['title']['rendered'], - 'content' => $post['content']['rendered'], - 'excerpt' => $post['excerpt']['rendered'], - 'status' => $post['status'], - 'url' => $post['link'], - 'published' => $post['date'], - 'modified' => $post['modified'], - 'author' => $post['author'], - 'featured_image' => $post['featured_media'] ? wp_get_attachment_url($post['featured_media']) : null, - 'categories' => $post['categories'] ?? [], - 'tags' => $post['tags'] ?? [], - 'post_type' => $post_type, - 'meta' => [ - 'word_count' => str_word_count(strip_tags($post['content']['rendered'])), - 'reading_time' => ceil(str_word_count(strip_tags($post['content']['rendered'])) / 200) - ] - ]; - } - - return $formatted_posts; -} -``` - -### Get All Post Types - -```php -/** - * Fetch all available post types from WordPress - */ -function igny8_fetch_all_post_types() { - $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/types'); - - if (is_wp_error($wp_response)) { - return false; - } - - $types = json_decode(wp_remote_retrieve_body($wp_response), true); - - $post_types = []; - foreach ($types as $type_name => $type_data) { - if ($type_data['public']) { - $post_types[] = [ - 'name' => $type_name, - 'label' => $type_data['name'], - 'description' => $type_data['description'] ?? '', - 'rest_base' => $type_data['rest_base'] ?? $type_name - ]; - } - } - - return $post_types; -} -``` - -### Batch Fetch All Posts - -```php -/** - * Fetch all posts from all post types - */ -function igny8_fetch_all_wordpress_posts() { - $post_types = igny8_fetch_all_post_types(); - $all_posts = []; - - foreach ($post_types as $type) { - $posts = igny8_fetch_wordpress_posts($type['name'], 100); - if ($posts) { - $all_posts = array_merge($all_posts, $posts); - } - } - - return $all_posts; -} -``` - ---- - -## Fetching WordPress Taxonomies - -### Get All Taxonomies - -```php -/** - * Fetch all taxonomies from WordPress - */ -function igny8_fetch_wordpress_taxonomies() { - $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/taxonomies'); - - if (is_wp_error($wp_response)) { - return false; - } - - $taxonomies = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_taxonomies = []; - foreach ($taxonomies as $tax_name => $tax_data) { - if ($tax_data['public']) { - $formatted_taxonomies[] = [ - 'name' => $tax_name, - 'label' => $tax_data['name'], - 'description' => $tax_data['description'] ?? '', - 'hierarchical' => $tax_data['hierarchical'], - 'rest_base' => $tax_data['rest_base'] ?? $tax_name, - 'object_types' => $tax_data['types'] ?? [] - ]; - } - } - - return $formatted_taxonomies; -} -``` - -### Get Taxonomy Terms - -```php -/** - * Fetch all terms for a specific taxonomy - */ -function igny8_fetch_taxonomy_terms($taxonomy, $per_page = 100) { - $api = new Igny8API(); - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wp/v2/%s?per_page=%d', - get_site_url(), - $taxonomy, - $per_page - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $terms = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_terms = []; - foreach ($terms as $term) { - $formatted_terms[] = [ - 'id' => $term['id'], - 'name' => $term['name'], - 'slug' => $term['slug'], - 'description' => $term['description'] ?? '', - 'count' => $term['count'], - 'parent' => $term['parent'] ?? 0, - 'taxonomy' => $taxonomy, - 'url' => $term['link'] - ]; - } - - return $formatted_terms; -} -``` - -### Get All Taxonomy Terms - -```php -/** - * Fetch all terms from all taxonomies - */ -function igny8_fetch_all_taxonomy_terms() { - $taxonomies = igny8_fetch_wordpress_taxonomies(); - $all_terms = []; - - foreach ($taxonomies as $taxonomy) { - $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], 100); - if ($terms) { - $all_terms[$taxonomy['name']] = $terms; - } - } - - return $all_terms; -} -``` - ---- - -## Fetching WooCommerce Products - -### Get All Products - -```php -/** - * Fetch all WooCommerce products - */ -function igny8_fetch_woocommerce_products($per_page = 100) { - // Check if WooCommerce is active - if (!class_exists('WooCommerce')) { - return false; - } - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products?per_page=%d&status=publish', - get_site_url(), - $per_page - ), [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ]); - - if (is_wp_error($wp_response)) { - return false; - } - - $products = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_products = []; - foreach ($products as $product) { - $formatted_products[] = [ - 'id' => $product['id'], - 'name' => $product['name'], - 'slug' => $product['slug'], - 'sku' => $product['sku'], - 'type' => $product['type'], - 'status' => $product['status'], - 'description' => $product['description'], - 'short_description' => $product['short_description'], - 'price' => $product['price'], - 'regular_price' => $product['regular_price'], - 'sale_price' => $product['sale_price'], - 'on_sale' => $product['on_sale'], - 'stock_status' => $product['stock_status'], - 'stock_quantity' => $product['stock_quantity'], - 'categories' => $product['categories'] ?? [], - 'tags' => $product['tags'] ?? [], - 'images' => $product['images'] ?? [], - 'attributes' => $product['attributes'] ?? [], - 'variations' => $product['variations'] ?? [], - 'url' => $product['permalink'] - ]; - } - - return $formatted_products; -} -``` - -### Get Product Categories - -```php -/** - * Fetch WooCommerce product categories - */ -function igny8_fetch_product_categories($per_page = 100) { - if (!class_exists('WooCommerce')) { - return false; - } - - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products/categories?per_page=%d', - get_site_url(), - $per_page - ), [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ]); - - if (is_wp_error($wp_response)) { - return false; - } - - $categories = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_categories = []; - foreach ($categories as $category) { - $formatted_categories[] = [ - 'id' => $category['id'], - 'name' => $category['name'], - 'slug' => $category['slug'], - 'description' => $category['description'] ?? '', - 'count' => $category['count'], - 'parent' => $category['parent'] ?? 0, - 'image' => $category['image']['src'] ?? null - ]; - } - - return $formatted_categories; -} -``` - -### Get Product Attributes - -```php -/** - * Fetch WooCommerce product attributes - */ -function igny8_fetch_product_attributes() { - if (!class_exists('WooCommerce')) { - return false; - } - - $wp_response = wp_remote_get( - get_site_url() . '/wp-json/wc/v3/products/attributes', - [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ] - ); - - if (is_wp_error($wp_response)) { - return false; - } - - $attributes = json_decode(wp_remote_retrieve_body($wp_response), true); - - $formatted_attributes = []; - foreach ($attributes as $attribute) { - // Get attribute terms - $terms_response = wp_remote_get(sprintf( - '%s/wp-json/wc/v3/products/attributes/%d/terms', - get_site_url(), - $attribute['id'] - ), [ - 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret')) - ] - ]); - - $terms = []; - if (!is_wp_error($terms_response)) { - $terms_data = json_decode(wp_remote_retrieve_body($terms_response), true); - foreach ($terms_data as $term) { - $terms[] = [ - 'id' => $term['id'], - 'name' => $term['name'], - 'slug' => $term['slug'] - ]; - } - } - - $formatted_attributes[] = [ - 'id' => $attribute['id'], - 'name' => $attribute['name'], - 'slug' => $attribute['slug'], - 'type' => $attribute['type'], - 'order_by' => $attribute['order_by'], - 'has_archives' => $attribute['has_archives'], - 'terms' => $terms - ]; - } - - return $formatted_attributes; -} -``` - ---- - -## Sending Site Data to IGNY8 for Semantic Mapping - -### Complete Site Data Collection - -```php -/** - * Collect all WordPress site data for IGNY8 semantic mapping - */ -function igny8_collect_site_data() { - $site_data = [ - 'site_url' => get_site_url(), - 'site_name' => get_bloginfo('name'), - 'site_description' => get_bloginfo('description'), - 'collected_at' => current_time('mysql'), - 'posts' => [], - 'taxonomies' => [], - 'products' => [], - 'product_attributes' => [] - ]; - - // Fetch all posts - $post_types = igny8_fetch_all_post_types(); - foreach ($post_types as $type) { - $posts = igny8_fetch_wordpress_posts($type['name'], 100); - if ($posts) { - $site_data['posts'] = array_merge($site_data['posts'], $posts); - } - } - - // Fetch all taxonomies and terms - $taxonomies = igny8_fetch_wordpress_taxonomies(); - foreach ($taxonomies as $taxonomy) { - $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], 100); - if ($terms) { - $site_data['taxonomies'][$taxonomy['name']] = [ - 'taxonomy' => $taxonomy, - 'terms' => $terms - ]; - } - } - - // Fetch WooCommerce products if available - if (class_exists('WooCommerce')) { - $products = igny8_fetch_woocommerce_products(100); - if ($products) { - $site_data['products'] = $products; - } - - $product_categories = igny8_fetch_product_categories(100); - if ($product_categories) { - $site_data['product_categories'] = $product_categories; - } - - $product_attributes = igny8_fetch_product_attributes(); - if ($product_attributes) { - $site_data['product_attributes'] = $product_attributes; - } - } - - return $site_data; -} -``` - -### Send Site Data to IGNY8 API - -```php -/** - * Send WordPress site data to IGNY8 for semantic strategy mapping - */ -function igny8_send_site_data_to_igny8($site_id) { - $api = new Igny8API(); - - // Collect all site data - $site_data = igny8_collect_site_data(); - - // Send to IGNY8 API - // Note: This endpoint may need to be created in IGNY8 API - $response = $api->post("/system/sites/{$site_id}/import/", [ - 'site_data' => $site_data, - 'import_type' => 'full_site_scan' - ]); - - if ($response['success']) { - // Store import ID for tracking - update_option('igny8_last_site_import_id', $response['data']['import_id'] ?? null); - return $response['data']; - } else { - error_log("IGNY8: Failed to send site data: " . $response['error']); - return false; - } -} -``` - -### Incremental Site Data Sync - -```php -/** - * Sync only changed posts/taxonomies since last sync - */ -function igny8_sync_incremental_site_data($site_id) { - $api = new Igny8API(); - - $last_sync = get_option('igny8_last_site_sync', 0); - - // Fetch only posts modified since last sync - $wp_response = wp_remote_get(sprintf( - '%s/wp-json/wp/v2/posts?after=%s&per_page=100', - get_site_url(), - date('c', $last_sync) - )); - - if (is_wp_error($wp_response)) { - return false; - } - - $posts = json_decode(wp_remote_retrieve_body($wp_response), true); - - if (empty($posts)) { - return ['synced' => 0, 'message' => 'No changes since last sync']; - } - - // Format posts - $formatted_posts = []; - foreach ($posts as $post) { - $formatted_posts[] = [ - 'id' => $post['id'], - 'title' => $post['title']['rendered'], - 'content' => $post['content']['rendered'], - 'status' => $post['status'], - 'modified' => $post['modified'], - 'categories' => $post['categories'] ?? [], - 'tags' => $post['tags'] ?? [] - ]; - } - - // Send incremental update to IGNY8 - $response = $api->post("/system/sites/{$site_id}/sync/", [ - 'posts' => $formatted_posts, - 'sync_type' => 'incremental', - 'last_sync' => $last_sync - ]); - - if ($response['success']) { - update_option('igny8_last_site_sync', time()); - return [ - 'synced' => count($formatted_posts), - 'message' => 'Incremental sync completed' - ]; - } - - return false; -} -``` - ---- - -## Semantic Strategy Mapping - -### Map Site Data to IGNY8 Semantic Structure - -```php -/** - * Map WordPress site data to IGNY8 semantic strategy - * This creates sectors, clusters, and keywords based on site structure - */ -function igny8_map_site_to_semantic_strategy($site_id, $site_data) { - $api = new Igny8API(); - - // Extract semantic structure from site data - $semantic_map = [ - 'sectors' => [], - 'clusters' => [], - 'keywords' => [] - ]; - - // Map taxonomies to sectors - foreach ($site_data['taxonomies'] as $tax_name => $tax_data) { - if ($tax_data['taxonomy']['hierarchical']) { - // Hierarchical taxonomies (categories) become sectors - $sector = [ - 'name' => $tax_data['taxonomy']['label'], - 'slug' => $tax_data['taxonomy']['name'], - 'description' => $tax_data['taxonomy']['description'], - 'source' => 'wordpress_taxonomy', - 'source_id' => $tax_name - ]; - - // Map terms to clusters - $clusters = []; - foreach ($tax_data['terms'] as $term) { - $clusters[] = [ - 'name' => $term['name'], - 'slug' => $term['slug'], - 'description' => $term['description'], - 'source' => 'wordpress_term', - 'source_id' => $term['id'] - ]; - - // Extract keywords from posts in this term - $keywords = igny8_extract_keywords_from_term_posts($term['id'], $tax_name); - $semantic_map['keywords'] = array_merge($semantic_map['keywords'], $keywords); - } - - $sector['clusters'] = $clusters; - $semantic_map['sectors'][] = $sector; - } - } - - // Map WooCommerce product categories to sectors - if (!empty($site_data['product_categories'])) { - $product_sector = [ - 'name' => 'Products', - 'slug' => 'products', - 'description' => 'WooCommerce product categories', - 'source' => 'woocommerce', - 'clusters' => [] - ]; - - foreach ($site_data['product_categories'] as $category) { - $product_sector['clusters'][] = [ - 'name' => $category['name'], - 'slug' => $category['slug'], - 'description' => $category['description'], - 'source' => 'woocommerce_category', - 'source_id' => $category['id'] - ]; - } - - $semantic_map['sectors'][] = $product_sector; - } - - // Send semantic map to IGNY8 - $response = $api->post("/planner/sites/{$site_id}/semantic-map/", [ - 'semantic_map' => $semantic_map, - 'site_data' => $site_data - ]); - - return $response; -} -``` - -### Extract Keywords from Posts - -```php -/** - * Extract keywords from posts associated with a taxonomy term - */ -function igny8_extract_keywords_from_term_posts($term_id, $taxonomy) { - $args = [ - 'post_type' => 'any', - 'posts_per_page' => -1, - 'tax_query' => [ - [ - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $term_id - ] - ] - ]; - - $query = new WP_Query($args); - $keywords = []; - - if ($query->have_posts()) { - while ($query->have_posts()) { - $query->the_post(); - - // Extract keywords from post title and content - $title_words = str_word_count(get_the_title(), 1); - $content_words = str_word_count(strip_tags(get_the_content()), 1); - - // Combine and get unique keywords - $all_words = array_merge($title_words, $content_words); - $unique_words = array_unique(array_map('strtolower', $all_words)); - - // Filter out common words (stop words) - $stop_words = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by']; - $keywords = array_merge($keywords, array_diff($unique_words, $stop_words)); - } - wp_reset_postdata(); - } - - // Format keywords - $formatted_keywords = []; - foreach (array_unique($keywords) as $keyword) { - if (strlen($keyword) > 3) { // Only keywords longer than 3 characters - $formatted_keywords[] = [ - 'keyword' => $keyword, - 'source' => 'wordpress_post', - 'source_term_id' => $term_id - ]; - } - } - - return $formatted_keywords; -} -``` - ---- - -## Content Restructuring Workflow - -### Complete Site Analysis and Restructuring - -```php -/** - * Complete workflow: Fetch site data → Map to semantic strategy → Restructure content - */ -function igny8_analyze_and_restructure_site($site_id) { - $api = new Igny8API(); - - // Step 1: Collect all site data - $site_data = igny8_collect_site_data(); - - // Step 2: Send to IGNY8 for analysis - $analysis_response = $api->post("/system/sites/{$site_id}/analyze/", [ - 'site_data' => $site_data, - 'analysis_type' => 'full_site_restructure' - ]); - - if (!$analysis_response['success']) { - return false; - } - - $analysis_id = $analysis_response['data']['analysis_id']; - - // Step 3: Map to semantic strategy - $mapping_response = igny8_map_site_to_semantic_strategy($site_id, $site_data); - - if (!$mapping_response['success']) { - return false; - } - - // Step 4: Get restructuring recommendations - $recommendations_response = $api->get("/system/sites/{$site_id}/recommendations/"); - - if (!$recommendations_response['success']) { - return false; - } - - return [ - 'analysis_id' => $analysis_id, - 'semantic_map' => $mapping_response['data'], - 'recommendations' => $recommendations_response['data'], - 'site_data_summary' => [ - 'total_posts' => count($site_data['posts']), - 'total_taxonomies' => count($site_data['taxonomies']), - 'total_products' => count($site_data['products'] ?? []), - 'total_keywords' => count($site_data['keywords'] ?? []) - ] - ]; -} -``` - -### Scheduled Site Data Sync - -```php -/** - * Scheduled sync of WordPress site data to IGNY8 - */ -add_action('igny8_sync_site_data', 'igny8_cron_sync_site_data'); - -function igny8_cron_sync_site_data() { - $site_id = get_option('igny8_site_id'); - - if (!$site_id) { - return; - } - - // Incremental sync - $result = igny8_sync_incremental_site_data($site_id); - - if ($result) { - error_log(sprintf( - 'IGNY8: Synced %d posts to site %d', - $result['synced'], - $site_id - )); - } -} - -// Schedule daily sync -if (!wp_next_scheduled('igny8_sync_site_data')) { - wp_schedule_event(time(), 'daily', 'igny8_sync_site_data'); -} -``` - ---- - -## Complete Site Integration Class - -```php -/** - * Complete WordPress site integration class - */ -class Igny8SiteIntegration { - private $api; - private $site_id; - - public function __construct($site_id) { - $this->api = new Igny8API(); - $this->site_id = $site_id; - } - - /** - * Full site scan and semantic mapping - */ - public function full_site_scan() { - // Collect all data - $site_data = igny8_collect_site_data(); - - // Send to IGNY8 - $response = $this->api->post("/system/sites/{$this->site_id}/import/", [ - 'site_data' => $site_data, - 'import_type' => 'full_scan' - ]); - - if ($response['success']) { - // Map to semantic strategy - $mapping = igny8_map_site_to_semantic_strategy($this->site_id, $site_data); - - return [ - 'success' => true, - 'import_id' => $response['data']['import_id'] ?? null, - 'semantic_map' => $mapping['data'] ?? null, - 'summary' => [ - 'posts' => count($site_data['posts']), - 'taxonomies' => count($site_data['taxonomies']), - 'products' => count($site_data['products'] ?? []), - 'product_attributes' => count($site_data['product_attributes'] ?? []) - ] - ]; - } - - return ['success' => false, 'error' => $response['error']]; - } - - /** - * Get semantic strategy recommendations - */ - public function get_recommendations() { - $response = $this->api->get("/planner/sites/{$this->site_id}/recommendations/"); - - if ($response['success']) { - return $response['data']; - } - - return false; - } - - /** - * Apply restructuring recommendations - */ - public function apply_restructuring($recommendations) { - $response = $this->api->post("/planner/sites/{$this->site_id}/restructure/", [ - 'recommendations' => $recommendations - ]); - - return $response['success']; - } -} - -// Usage -$integration = new Igny8SiteIntegration($site_id); -$result = $integration->full_site_scan(); - -if ($result['success']) { - echo "Scanned {$result['summary']['posts']} posts, {$result['summary']['taxonomies']} taxonomies"; - - // Get recommendations - $recommendations = $integration->get_recommendations(); - - // Apply restructuring - if ($recommendations) { - $integration->apply_restructuring($recommendations); - } -} -``` - ---- - -## Data Structure Examples - -### Post Data Structure - -```php -[ - 'id' => 123, - 'title' => 'Post Title', - 'content' => 'Post content...', - 'excerpt' => 'Post excerpt...', - 'status' => 'publish', - 'url' => 'https://example.com/post/', - 'published' => '2025-01-01T00:00:00', - 'modified' => '2025-01-02T00:00:00', - 'author' => 1, - 'featured_image' => 'https://example.com/image.jpg', - 'categories' => [1, 2, 3], - 'tags' => [4, 5], - 'post_type' => 'post', - 'meta' => [ - 'word_count' => 500, - 'reading_time' => 3 - ] -] -``` - -### Taxonomy Structure - -```php -[ - 'taxonomy' => [ - 'name' => 'category', - 'label' => 'Categories', - 'hierarchical' => true, - 'object_types' => ['post'] - ], - 'terms' => [ - [ - 'id' => 1, - 'name' => 'Technology', - 'slug' => 'technology', - 'description' => 'Tech posts', - 'count' => 25, - 'parent' => 0 - ] - ] -] -``` - -### Product Structure - -```php -[ - 'id' => 456, - 'name' => 'Product Name', - 'sku' => 'PROD-123', - 'type' => 'simple', - 'price' => '29.99', - 'categories' => [10, 11], - 'tags' => [20], - 'attributes' => [ - [ - 'id' => 1, - 'name' => 'Color', - 'options' => ['Red', 'Blue'] - ] - ], - 'variations' => [789, 790] -] -``` - ---- - -**Last Updated**: 2025-11-16 -**API Version**: 1.0.0 - diff --git a/backup-api-standard-v1/tests/FINAL_TEST_SUMMARY.md b/backup-api-standard-v1/tests/FINAL_TEST_SUMMARY.md deleted file mode 100644 index a9ce436f..00000000 --- a/backup-api-standard-v1/tests/FINAL_TEST_SUMMARY.md +++ /dev/null @@ -1,99 +0,0 @@ -# API Tests - Final Implementation Summary - -## ✅ Section 1: Testing - COMPLETE - -**Date Completed**: 2025-11-16 -**Status**: All Unit Tests Passing ✅ - -## Test Execution Results - -### Unit Tests - ALL PASSING ✅ - -1. **test_response.py** - ✅ 16/16 tests passing - - Tests all response helper functions - - Verifies unified response format - - Tests request ID generation - -2. **test_permissions.py** - ✅ 20/20 tests passing - - Tests all permission classes - - Verifies role-based access control - - Tests tenant isolation and bypass logic - -3. **test_throttles.py** - ✅ 11/11 tests passing - - Tests rate limiting logic - - Verifies bypass mechanisms - - Tests rate parsing - -4. **test_exception_handler.py** - ✅ Ready (imports fixed) - - Tests custom exception handler - - Verifies unified error format - - Tests all exception types - -**Total Unit Tests**: 61 tests - ALL PASSING ✅ - -## Integration Tests Status - -Integration tests have been created and are functional. Some tests may show failures due to: -- Rate limiting (429 responses) - Tests updated to handle this -- Endpoint availability in test environment -- Test data requirements - -**Note**: Integration tests verify unified API format regardless of endpoint status. - -## Fixes Applied - -1. ✅ Fixed `RequestFactory` import (from `django.test` not `rest_framework.test`) -2. ✅ Fixed Account creation to require `owner` field -3. ✅ Fixed migration issues (0009_fix_admin_log_user_fk, 0006_alter_systemstatus) -4. ✅ Updated integration tests to handle rate limiting (429 responses) -5. ✅ Fixed system account creation in permission tests - -## Test Coverage - -- ✅ Response Helpers: 100% -- ✅ Exception Handler: 100% -- ✅ Permissions: 100% -- ✅ Rate Limiting: 100% -- ✅ Integration Tests: Created for all modules - -## Files Created - -1. `test_response.py` - Response helper tests -2. `test_exception_handler.py` - Exception handler tests -3. `test_permissions.py` - Permission class tests -4. `test_throttles.py` - Rate limiting tests -5. `test_integration_base.py` - Base class for integration tests -6. `test_integration_planner.py` - Planner module tests -7. `test_integration_writer.py` - Writer module tests -8. `test_integration_system.py` - System module tests -9. `test_integration_billing.py` - Billing module tests -10. `test_integration_auth.py` - Auth module tests -11. `test_integration_errors.py` - Error scenario tests -12. `test_integration_pagination.py` - Pagination tests -13. `test_integration_rate_limiting.py` - Rate limiting integration tests -14. `README.md` - Test documentation -15. `TEST_SUMMARY.md` - Test statistics -16. `run_tests.py` - Test runner script - -## Verification - -All unit tests have been executed and verified: -```bash -python manage.py test igny8_core.api.tests.test_response igny8_core.api.tests.test_permissions igny8_core.api.tests.test_throttles -``` - -**Result**: ✅ ALL PASSING - -## Next Steps - -1. ✅ Unit tests ready for CI/CD -2. ⚠️ Integration tests may need environment-specific configuration -3. ✅ Changelog updated with testing section -4. ✅ All test files documented - -## Conclusion - -**Section 1: Testing is COMPLETE** ✅ - -All unit tests are passing and verify the Unified API Standard v1.0 implementation. Integration tests are created and functional, with appropriate handling for real-world API conditions (rate limiting, endpoint availability). - diff --git a/backup-api-standard-v1/tests/README.md b/backup-api-standard-v1/tests/README.md deleted file mode 100644 index 10663810..00000000 --- a/backup-api-standard-v1/tests/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# API Tests - -This directory contains comprehensive unit and integration tests for the Unified API Standard v1.0. - -## Test Structure - -### Unit Tests -- `test_response.py` - Tests for response helper functions (success_response, error_response, paginated_response) -- `test_exception_handler.py` - Tests for custom exception handler -- `test_permissions.py` - Tests for permission classes -- `test_throttles.py` - Tests for rate limiting - -### Integration Tests -- `test_integration_base.py` - Base class with common fixtures -- `test_integration_planner.py` - Planner module endpoint tests -- `test_integration_writer.py` - Writer module endpoint tests -- `test_integration_system.py` - System module endpoint tests -- `test_integration_billing.py` - Billing module endpoint tests -- `test_integration_auth.py` - Auth module endpoint tests -- `test_integration_errors.py` - Error scenario tests (400, 401, 403, 404, 429, 500) -- `test_integration_pagination.py` - Pagination tests across all modules -- `test_integration_rate_limiting.py` - Rate limiting integration tests - -## Running Tests - -### Run All Tests -```bash -python manage.py test igny8_core.api.tests --verbosity=2 -``` - -### Run Specific Test File -```bash -python manage.py test igny8_core.api.tests.test_response -python manage.py test igny8_core.api.tests.test_integration_planner -``` - -### Run Specific Test Class -```bash -python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase -``` - -### Run Specific Test Method -```bash -python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase.test_success_response_with_data -``` - -## Test Coverage - -### Unit Tests Coverage -- ✅ Response helpers (100%) -- ✅ Exception handler (100%) -- ✅ Permissions (100%) -- ✅ Rate limiting (100%) - -### Integration Tests Coverage -- ✅ Planner module CRUD + AI actions -- ✅ Writer module CRUD + AI actions -- ✅ System module endpoints -- ✅ Billing module endpoints -- ✅ Auth module endpoints -- ✅ Error scenarios (400, 401, 403, 404, 429, 500) -- ✅ Pagination across all modules -- ✅ Rate limiting headers and bypass logic - -## Test Requirements - -All tests verify: -1. **Unified Response Format**: All endpoints return `{success, data/results, message, errors, request_id}` -2. **Proper Status Codes**: Correct HTTP status codes (200, 201, 400, 401, 403, 404, 429, 500) -3. **Error Format**: Error responses include `error`, `errors`, and `request_id` -4. **Pagination Format**: Paginated responses include `success`, `count`, `next`, `previous`, `results` -5. **Request ID**: All responses include `request_id` for tracking - diff --git a/backup-api-standard-v1/tests/TEST_RESULTS.md b/backup-api-standard-v1/tests/TEST_RESULTS.md deleted file mode 100644 index e080d3c4..00000000 --- a/backup-api-standard-v1/tests/TEST_RESULTS.md +++ /dev/null @@ -1,69 +0,0 @@ -# API Tests - Execution Results - -## Test Execution Summary - -**Date**: 2025-11-16 -**Environment**: Docker Container (igny8_backend) -**Database**: test_igny8_db - -## Unit Tests Status - -### ✅ test_response.py -- **Status**: ✅ ALL PASSING (16/16) -- **Coverage**: Response helpers (success_response, error_response, paginated_response, get_request_id) -- **Result**: All tests verify unified response format correctly - -### ✅ test_throttles.py -- **Status**: ✅ ALL PASSING (11/11) -- **Coverage**: Rate limiting logic, bypass mechanisms, rate parsing -- **Result**: All throttle tests pass - -### ⚠️ test_permissions.py -- **Status**: ⚠️ 1 ERROR (18/19 passing) -- **Issue**: System account creation in test_has_tenant_access_system_account -- **Fix Applied**: Updated to create owner before account -- **Note**: Needs re-run to verify fix - -### ⚠️ test_exception_handler.py -- **Status**: ⚠️ NEEDS VERIFICATION -- **Issue**: Import error fixed (RequestFactory from django.test) -- **Note**: Tests need to be run to verify all pass - -## Integration Tests Status - -### ⚠️ Integration Tests -- **Status**: ⚠️ PARTIAL (Many failures due to rate limiting and endpoint availability) -- **Issues**: - 1. Rate limiting (429 errors) - Tests updated to accept 429 as valid unified format - 2. Some endpoints may not exist or return different status codes - 3. Tests need to be more resilient to handle real API conditions - -### Fixes Applied -1. ✅ Updated integration tests to accept 429 (rate limited) as valid response -2. ✅ Fixed Account creation to require owner -3. ✅ Fixed RequestFactory import -4. ✅ Fixed migration issues (0009, 0006) - -## Test Statistics - -- **Total Test Files**: 13 -- **Total Test Methods**: ~115 -- **Unit Tests Passing**: 45/46 (98%) -- **Integration Tests**: Needs refinement for production environment - -## Next Steps - -1. ✅ Unit tests are production-ready (response, throttles) -2. ⚠️ Fix remaining permission test error -3. ⚠️ Make integration tests more resilient: - - Accept 404/429 as valid responses (still test unified format) - - Skip tests if endpoints don't exist - - Add retry logic for rate-limited requests - -## Recommendations - -1. **Unit Tests**: Ready for CI/CD integration -2. **Integration Tests**: Should be run in staging environment with proper test data -3. **Rate Limiting**: Consider disabling for test environment or using higher limits -4. **Test Data**: Ensure test database has proper fixtures for integration tests - diff --git a/backup-api-standard-v1/tests/TEST_SUMMARY.md b/backup-api-standard-v1/tests/TEST_SUMMARY.md deleted file mode 100644 index f4833eae..00000000 --- a/backup-api-standard-v1/tests/TEST_SUMMARY.md +++ /dev/null @@ -1,160 +0,0 @@ -# API Tests - Implementation Summary - -## Overview -Comprehensive test suite for Unified API Standard v1.0 implementation covering all unit and integration tests. - -## Test Files Created - -### Unit Tests (4 files) -1. **test_response.py** (153 lines) - - Tests for `success_response()`, `error_response()`, `paginated_response()` - - Tests for `get_request_id()` - - 18 test methods covering all response scenarios - -2. **test_exception_handler.py** (177 lines) - - Tests for `custom_exception_handler()` - - Tests all exception types (ValidationError, AuthenticationFailed, PermissionDenied, NotFound, Throttled, etc.) - - Tests debug mode behavior - - 12 test methods - -3. **test_permissions.py** (245 lines) - - Tests for `IsAuthenticatedAndActive`, `HasTenantAccess`, `IsViewerOrAbove`, `IsEditorOrAbove`, `IsAdminOrOwner` - - Tests role-based access control - - Tests tenant isolation - - Tests admin/system account bypass - - 20 test methods - -4. **test_throttles.py** (145 lines) - - Tests for `DebugScopedRateThrottle` - - Tests bypass logic (DEBUG mode, env flag, admin/system accounts) - - Tests rate parsing - - 11 test methods - -### Integration Tests (9 files) -1. **test_integration_base.py** (107 lines) - - Base test class with common fixtures - - Helper methods: `assert_unified_response_format()`, `assert_paginated_response()` - - Sets up: User, Account, Plan, Site, Sector, Industry, SeedKeyword - -2. **test_integration_planner.py** (120 lines) - - Tests Planner module endpoints (keywords, clusters, ideas) - - Tests CRUD operations - - Tests AI actions (auto_cluster) - - Tests error scenarios - - 12 test methods - -3. **test_integration_writer.py** (65 lines) - - Tests Writer module endpoints (tasks, content, images) - - Tests CRUD operations - - Tests error scenarios - - 6 test methods - -4. **test_integration_system.py** (50 lines) - - Tests System module endpoints (status, prompts, settings, integrations) - - 5 test methods - -5. **test_integration_billing.py** (50 lines) - - Tests Billing module endpoints (credits, usage, transactions) - - 5 test methods - -6. **test_integration_auth.py** (100 lines) - - Tests Auth module endpoints (login, register, users, accounts, sites) - - Tests authentication flows - - Tests error scenarios - - 8 test methods - -7. **test_integration_errors.py** (95 lines) - - Tests error scenarios (400, 401, 403, 404, 429, 500) - - Tests unified error format - - 6 test methods - -8. **test_integration_pagination.py** (100 lines) - - Tests pagination across all modules - - Tests page size, page parameter, max page size - - Tests empty results - - 10 test methods - -9. **test_integration_rate_limiting.py** (120 lines) - - Tests rate limiting headers - - Tests bypass logic (admin, system account, DEBUG mode) - - Tests different throttle scopes - - 7 test methods - -## Test Statistics - -- **Total Test Files**: 13 -- **Total Test Methods**: ~115 -- **Total Lines of Code**: ~1,500 -- **Coverage**: 100% of API Standard components - -## Test Categories - -### Unit Tests -- ✅ Response Helpers (100%) -- ✅ Exception Handler (100%) -- ✅ Permissions (100%) -- ✅ Rate Limiting (100%) - -### Integration Tests -- ✅ Planner Module (100%) -- ✅ Writer Module (100%) -- ✅ System Module (100%) -- ✅ Billing Module (100%) -- ✅ Auth Module (100%) -- ✅ Error Scenarios (100%) -- ✅ Pagination (100%) -- ✅ Rate Limiting (100%) - -## What Tests Verify - -1. **Unified Response Format** - - All responses include `success` field - - Success responses include `data` or `results` - - Error responses include `error` and `errors` - - All responses include `request_id` - -2. **Status Codes** - - Correct HTTP status codes (200, 201, 400, 401, 403, 404, 429, 500) - - Proper error messages for each status code - -3. **Pagination** - - Paginated responses include `count`, `next`, `previous`, `results` - - Page size limits enforced - - Empty results handled correctly - -4. **Error Handling** - - All exceptions wrapped in unified format - - Field-specific errors included - - Debug info in DEBUG mode - -5. **Permissions** - - Role-based access control - - Tenant isolation - - Admin/system account bypass - -6. **Rate Limiting** - - Throttle headers present - - Bypass logic for admin/system accounts - - Bypass in DEBUG mode - -## Running Tests - -```bash -# Run all tests -python manage.py test igny8_core.api.tests --verbosity=2 - -# Run specific test file -python manage.py test igny8_core.api.tests.test_response - -# Run specific test class -python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase -``` - -## Next Steps - -1. Run tests in Docker environment -2. Verify all tests pass -3. Add to CI/CD pipeline -4. Monitor test coverage -5. Add performance tests if needed - diff --git a/backup-api-standard-v1/tests/__init__.py b/backup-api-standard-v1/tests/__init__.py deleted file mode 100644 index 3be4cbf7..00000000 --- a/backup-api-standard-v1/tests/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -API Tests Package -Unit and integration tests for unified API standard -""" - diff --git a/backup-api-standard-v1/tests/run_tests.py b/backup-api-standard-v1/tests/run_tests.py deleted file mode 100644 index 95ba543f..00000000 --- a/backup-api-standard-v1/tests/run_tests.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -""" -Test runner script for API tests -Run all tests: python manage.py test igny8_core.api.tests -Run specific test: python manage.py test igny8_core.api.tests.test_response -""" -import os -import sys -import django - -# Setup Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') -django.setup() - -from django.core.management import execute_from_command_line - -if __name__ == '__main__': - # Run all API tests - if len(sys.argv) > 1: - # Custom test specified - execute_from_command_line(['manage.py', 'test'] + sys.argv[1:]) - else: - # Run all API tests - execute_from_command_line(['manage.py', 'test', 'igny8_core.api.tests', '--verbosity=2']) - diff --git a/backup-api-standard-v1/tests/test_exception_handler.py b/backup-api-standard-v1/tests/test_exception_handler.py deleted file mode 100644 index 6e3def68..00000000 --- a/backup-api-standard-v1/tests/test_exception_handler.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Unit tests for custom exception handler -Tests all exception types and status code mappings -""" -from django.test import TestCase, RequestFactory -from django.http import HttpRequest -from rest_framework import status -from rest_framework.exceptions import ( - ValidationError, AuthenticationFailed, PermissionDenied, NotFound, - MethodNotAllowed, NotAcceptable, Throttled -) -from rest_framework.views import APIView -from igny8_core.api.exception_handlers import custom_exception_handler - - -class ExceptionHandlerTestCase(TestCase): - """Test cases for custom exception handler""" - - def setUp(self): - """Set up test fixtures""" - self.factory = RequestFactory() - self.view = APIView() - - def test_validation_error_400(self): - """Test ValidationError returns 400 with unified format""" - request = self.factory.post('/test/', {}) - exc = ValidationError({"field": ["This field is required"]}) - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['success']) - self.assertIn('error', response.data) - self.assertIn('errors', response.data) - self.assertIn('request_id', response.data) - - def test_authentication_failed_401(self): - """Test AuthenticationFailed returns 401 with unified format""" - request = self.factory.get('/test/') - exc = AuthenticationFailed("Authentication required") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], 'Authentication required') - self.assertIn('request_id', response.data) - - def test_permission_denied_403(self): - """Test PermissionDenied returns 403 with unified format""" - request = self.factory.get('/test/') - exc = PermissionDenied("Permission denied") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], 'Permission denied') - self.assertIn('request_id', response.data) - - def test_not_found_404(self): - """Test NotFound returns 404 with unified format""" - request = self.factory.get('/test/') - exc = NotFound("Resource not found") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], 'Resource not found') - self.assertIn('request_id', response.data) - - def test_throttled_429(self): - """Test Throttled returns 429 with unified format""" - request = self.factory.get('/test/') - exc = Throttled() - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], 'Rate limit exceeded') - self.assertIn('request_id', response.data) - - def test_method_not_allowed_405(self): - """Test MethodNotAllowed returns 405 with unified format""" - request = self.factory.post('/test/') - exc = MethodNotAllowed("POST") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertFalse(response.data['success']) - self.assertIn('error', response.data) - self.assertIn('request_id', response.data) - - def test_unhandled_exception_500(self): - """Test unhandled exception returns 500 with unified format""" - request = self.factory.get('/test/') - exc = ValueError("Unexpected error") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], 'Internal server error') - self.assertIn('request_id', response.data) - - def test_exception_handler_includes_request_id(self): - """Test exception handler includes request_id in response""" - request = self.factory.get('/test/') - request.request_id = 'test-request-id-exception' - exc = ValidationError("Test error") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertIn('request_id', response.data) - self.assertEqual(response.data['request_id'], 'test-request-id-exception') - - def test_exception_handler_debug_mode(self): - """Test exception handler includes debug info in DEBUG mode""" - from django.conf import settings - original_debug = settings.DEBUG - - try: - settings.DEBUG = True - request = self.factory.get('/test/') - exc = ValueError("Test error") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertIn('debug', response.data) - self.assertIn('exception_type', response.data['debug']) - self.assertIn('exception_message', response.data['debug']) - self.assertIn('view', response.data['debug']) - self.assertIn('path', response.data['debug']) - self.assertIn('method', response.data['debug']) - finally: - settings.DEBUG = original_debug - - def test_exception_handler_no_debug_mode(self): - """Test exception handler excludes debug info when DEBUG=False""" - from django.conf import settings - original_debug = settings.DEBUG - - try: - settings.DEBUG = False - request = self.factory.get('/test/') - exc = ValueError("Test error") - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertNotIn('debug', response.data) - finally: - settings.DEBUG = original_debug - - def test_field_specific_validation_errors(self): - """Test field-specific validation errors are included""" - request = self.factory.post('/test/', {}) - exc = ValidationError({ - "email": ["Invalid email format"], - "password": ["Password too short", "Password must contain numbers"] - }) - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertIn('errors', response.data) - self.assertIn('email', response.data['errors']) - self.assertIn('password', response.data['errors']) - self.assertEqual(len(response.data['errors']['password']), 2) - - def test_non_field_validation_errors(self): - """Test non-field validation errors are handled""" - request = self.factory.post('/test/', {}) - exc = ValidationError({"non_field_errors": ["General validation error"]}) - context = {'request': request, 'view': self.view} - - response = custom_exception_handler(exc, context) - - self.assertIn('errors', response.data) - self.assertIn('non_field_errors', response.data['errors']) - diff --git a/backup-api-standard-v1/tests/test_integration_auth.py b/backup-api-standard-v1/tests/test_integration_auth.py deleted file mode 100644 index c7a64a06..00000000 --- a/backup-api-standard-v1/tests/test_integration_auth.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Integration tests for Auth module endpoints -Tests login, register, user management return unified format -""" -from rest_framework import status -from django.test import TestCase -from rest_framework.test import APIClient -from igny8_core.auth.models import User, Account, Plan - - -class AuthIntegrationTestCase(TestCase): - """Integration tests for Auth module""" - - def setUp(self): - """Set up test fixtures""" - self.client = APIClient() - - # Create test plan and account - self.plan = Plan.objects.create( - name="Test Plan", - slug="test-plan", - price=0, - credits_per_month=1000 - ) - - # Create test user first (Account needs owner) - self.user = User.objects.create_user( - username='testuser', - email='test@test.com', - password='testpass123', - role='owner' - ) - - # Create test account with owner - self.account = Account.objects.create( - name="Test Account", - slug="test-account", - plan=self.plan, - owner=self.user - ) - - # Update user to have account - self.user.account = self.account - self.user.save() - - def assert_unified_response_format(self, response, expected_success=True): - """Assert response follows unified format""" - self.assertIn('success', response.data) - self.assertEqual(response.data['success'], expected_success) - - if expected_success: - self.assertTrue('data' in response.data or 'results' in response.data) - else: - self.assertIn('error', response.data) - - def test_login_returns_unified_format(self): - """Test POST /api/v1/auth/login/ returns unified format""" - data = { - 'email': 'test@test.com', - 'password': 'testpass123' - } - response = self.client.post('/api/v1/auth/login/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - self.assertIn('user', response.data['data']) - self.assertIn('access', response.data['data']) - - def test_login_invalid_credentials_returns_unified_format(self): - """Test login with invalid credentials returns unified format""" - data = { - 'email': 'test@test.com', - 'password': 'wrongpassword' - } - response = self.client.post('/api/v1/auth/login/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('request_id', response.data) - - def test_register_returns_unified_format(self): - """Test POST /api/v1/auth/register/ returns unified format""" - data = { - 'email': 'newuser@test.com', - 'username': 'newuser', - 'password': 'testpass123', - 'first_name': 'New', - 'last_name': 'User' - } - response = self.client.post('/api/v1/auth/register/', data, format='json') - - # May return 400 if validation fails, but should still be unified format - self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_400_BAD_REQUEST]) - self.assert_unified_response_format(response) - - def test_list_users_returns_unified_format(self): - """Test GET /api/v1/auth/users/ returns unified format""" - self.client.force_authenticate(user=self.user) - response = self.client.get('/api/v1/auth/users/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_list_accounts_returns_unified_format(self): - """Test GET /api/v1/auth/accounts/ returns unified format""" - self.client.force_authenticate(user=self.user) - response = self.client.get('/api/v1/auth/accounts/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_list_sites_returns_unified_format(self): - """Test GET /api/v1/auth/sites/ returns unified format""" - self.client.force_authenticate(user=self.user) - response = self.client.get('/api/v1/auth/sites/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_unauthorized_returns_unified_format(self): - """Test 401 errors return unified format""" - # Don't authenticate - response = self.client.get('/api/v1/auth/users/') - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('request_id', response.data) - diff --git a/backup-api-standard-v1/tests/test_integration_base.py b/backup-api-standard-v1/tests/test_integration_base.py deleted file mode 100644 index 58c7fd52..00000000 --- a/backup-api-standard-v1/tests/test_integration_base.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -Base test class for integration tests -Provides common fixtures and utilities -""" -from django.test import TestCase -from rest_framework.test import APIClient -from rest_framework import status -from igny8_core.auth.models import User, Account, Plan, Site, Sector, Industry, IndustrySector, SeedKeyword - - -class IntegrationTestBase(TestCase): - """Base class for integration tests with common fixtures""" - - def setUp(self): - """Set up test fixtures""" - self.client = APIClient() - - # Create test plan - self.plan = Plan.objects.create( - name="Test Plan", - slug="test-plan", - price=0, - credits_per_month=1000 - ) - - # Create test user first (Account needs owner) - self.user = User.objects.create_user( - username='testuser', - email='test@test.com', - password='testpass123', - role='owner' - ) - - # Create test account with owner - self.account = Account.objects.create( - name="Test Account", - slug="test-account", - plan=self.plan, - owner=self.user - ) - - # Update user to have account - self.user.account = self.account - self.user.save() - - # Create industry and sector - self.industry = Industry.objects.create( - name="Test Industry", - slug="test-industry" - ) - - self.industry_sector = IndustrySector.objects.create( - industry=self.industry, - name="Test Sector", - slug="test-sector" - ) - - # Create site - self.site = Site.objects.create( - name="Test Site", - slug="test-site", - account=self.account, - industry=self.industry - ) - - # Create sector (Sector needs industry_sector reference) - self.sector = Sector.objects.create( - name="Test Sector", - slug="test-sector", - site=self.site, - account=self.account, - industry_sector=self.industry_sector - ) - - # Create seed keyword - self.seed_keyword = SeedKeyword.objects.create( - keyword="test keyword", - industry=self.industry, - sector=self.industry_sector, - volume=1000, - difficulty=50, - intent="informational" - ) - - # Authenticate client - self.client.force_authenticate(user=self.user) - - # Set account on request (simulating middleware) - self.client.force_authenticate(user=self.user) - - def assert_unified_response_format(self, response, expected_success=True): - """Assert response follows unified format""" - self.assertIn('success', response.data) - self.assertEqual(response.data['success'], expected_success) - - if expected_success: - # Success responses should have data or results - self.assertTrue('data' in response.data or 'results' in response.data) - else: - # Error responses should have error - self.assertIn('error', response.data) - - def assert_paginated_response(self, response): - """Assert response is a paginated response""" - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('success', response.data) - self.assertIn('count', response.data) - self.assertIn('results', response.data) - self.assertIn('next', response.data) - self.assertIn('previous', response.data) - diff --git a/backup-api-standard-v1/tests/test_integration_billing.py b/backup-api-standard-v1/tests/test_integration_billing.py deleted file mode 100644 index 3c5bc49f..00000000 --- a/backup-api-standard-v1/tests/test_integration_billing.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Integration tests for Billing module endpoints -Tests credit balance, usage, transactions return unified format -""" -from rest_framework import status -from igny8_core.api.tests.test_integration_base import IntegrationTestBase - - -class BillingIntegrationTestCase(IntegrationTestBase): - """Integration tests for Billing module""" - - def test_credit_balance_returns_unified_format(self): - """Test GET /api/v1/billing/credits/balance/balance/ returns unified format""" - response = self.client.get('/api/v1/billing/credits/balance/balance/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_credit_usage_returns_unified_format(self): - """Test GET /api/v1/billing/credits/usage/ returns unified format""" - response = self.client.get('/api/v1/billing/credits/usage/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_usage_summary_returns_unified_format(self): - """Test GET /api/v1/billing/credits/usage/summary/ returns unified format""" - response = self.client.get('/api/v1/billing/credits/usage/summary/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_usage_limits_returns_unified_format(self): - """Test GET /api/v1/billing/credits/usage/limits/ returns unified format""" - response = self.client.get('/api/v1/billing/credits/usage/limits/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_transactions_returns_unified_format(self): - """Test GET /api/v1/billing/credits/transactions/ returns unified format""" - response = self.client.get('/api/v1/billing/credits/transactions/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - diff --git a/backup-api-standard-v1/tests/test_integration_errors.py b/backup-api-standard-v1/tests/test_integration_errors.py deleted file mode 100644 index 7489ed1f..00000000 --- a/backup-api-standard-v1/tests/test_integration_errors.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Integration tests for error scenarios -Tests 400, 401, 403, 404, 429, 500 responses return unified format -""" -from rest_framework import status -from django.test import TestCase -from rest_framework.test import APIClient -from igny8_core.auth.models import User, Account, Plan -from igny8_core.api.tests.test_integration_base import IntegrationTestBase - - -class ErrorScenariosTestCase(IntegrationTestBase): - """Integration tests for error scenarios""" - - def test_400_bad_request_returns_unified_format(self): - """Test 400 Bad Request returns unified format""" - # Invalid data - data = {'invalid': 'data'} - response = self.client.post('/api/v1/planner/keywords/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('errors', response.data) - self.assertIn('request_id', response.data) - - def test_401_unauthorized_returns_unified_format(self): - """Test 401 Unauthorized returns unified format""" - # Create unauthenticated client - unauthenticated_client = APIClient() - response = unauthenticated_client.get('/api/v1/planner/keywords/') - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertEqual(response.data['error'], 'Authentication required') - self.assertIn('request_id', response.data) - - def test_403_forbidden_returns_unified_format(self): - """Test 403 Forbidden returns unified format""" - # Create viewer user (limited permissions) - viewer_user = User.objects.create_user( - username='viewer', - email='viewer@test.com', - password='testpass123', - role='viewer', - account=self.account - ) - - viewer_client = APIClient() - viewer_client.force_authenticate(user=viewer_user) - - # Try to access admin-only endpoint (if exists) - # For now, test with a protected endpoint that requires editor+ - response = viewer_client.post('/api/v1/planner/keywords/auto_cluster/', {}, format='json') - - # May return 400 (validation) or 403 (permission), both should be unified - self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_403_FORBIDDEN]) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('request_id', response.data) - - def test_404_not_found_returns_unified_format(self): - """Test 404 Not Found returns unified format""" - response = self.client.get('/api/v1/planner/keywords/99999/') - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertEqual(response.data['error'], 'Resource not found') - self.assertIn('request_id', response.data) - - def test_404_invalid_endpoint_returns_unified_format(self): - """Test 404 for invalid endpoint returns unified format""" - response = self.client.get('/api/v1/nonexistent/endpoint/') - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - # DRF may return different format for URL not found, but our handler should catch it - if 'success' in response.data: - self.assert_unified_response_format(response, expected_success=False) - - def test_validation_error_returns_unified_format(self): - """Test validation errors return unified format with field-specific errors""" - # Missing required fields - response = self.client.post('/api/v1/planner/keywords/', {}, format='json') - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('errors', response.data) - # Should have field-specific errors - self.assertIsInstance(response.data['errors'], dict) - diff --git a/backup-api-standard-v1/tests/test_integration_pagination.py b/backup-api-standard-v1/tests/test_integration_pagination.py deleted file mode 100644 index daefe34c..00000000 --- a/backup-api-standard-v1/tests/test_integration_pagination.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Integration tests for pagination -Tests paginated responses across all modules return unified format -""" -from rest_framework import status -from igny8_core.api.tests.test_integration_base import IntegrationTestBase -from igny8_core.modules.planner.models import Keywords -from igny8_core.auth.models import SeedKeyword, Industry, IndustrySector - - -class PaginationIntegrationTestCase(IntegrationTestBase): - """Integration tests for pagination""" - - def setUp(self): - """Set up test fixtures with multiple records""" - super().setUp() - - # Create multiple keywords for pagination testing - for i in range(15): - Keywords.objects.create( - seed_keyword=self.seed_keyword, - site=self.site, - sector=self.sector, - account=self.account, - status='active' - ) - - def test_pagination_default_page_size(self): - """Test pagination with default page size""" - response = self.client.get('/api/v1/planner/keywords/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - self.assertEqual(response.data['count'], 15) - self.assertLessEqual(len(response.data['results']), 10) # Default page size - self.assertIsNotNone(response.data['next']) # Should have next page - - def test_pagination_custom_page_size(self): - """Test pagination with custom page size""" - response = self.client.get('/api/v1/planner/keywords/?page_size=5') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - self.assertEqual(response.data['count'], 15) - self.assertEqual(len(response.data['results']), 5) - self.assertIsNotNone(response.data['next']) - - def test_pagination_page_parameter(self): - """Test pagination with page parameter""" - response = self.client.get('/api/v1/planner/keywords/?page=2&page_size=5') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - self.assertEqual(response.data['count'], 15) - self.assertEqual(len(response.data['results']), 5) - self.assertIsNotNone(response.data['previous']) - - def test_pagination_max_page_size(self): - """Test pagination respects max page size""" - response = self.client.get('/api/v1/planner/keywords/?page_size=200') # Exceeds max of 100 - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - self.assertLessEqual(len(response.data['results']), 100) # Should be capped at 100 - - def test_pagination_empty_results(self): - """Test pagination with empty results""" - # Use a filter that returns no results - response = self.client.get('/api/v1/planner/keywords/?status=nonexistent') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - self.assertEqual(response.data['count'], 0) - self.assertEqual(len(response.data['results']), 0) - self.assertIsNone(response.data['next']) - self.assertIsNone(response.data['previous']) - - def test_pagination_includes_success_field(self): - """Test paginated responses include success field""" - response = self.client.get('/api/v1/planner/keywords/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('success', response.data) - self.assertTrue(response.data['success']) - - def test_pagination_clusters(self): - """Test pagination works for clusters endpoint""" - response = self.client.get('/api/v1/planner/clusters/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_pagination_ideas(self): - """Test pagination works for ideas endpoint""" - response = self.client.get('/api/v1/planner/ideas/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_pagination_tasks(self): - """Test pagination works for tasks endpoint""" - response = self.client.get('/api/v1/writer/tasks/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_pagination_content(self): - """Test pagination works for content endpoint""" - response = self.client.get('/api/v1/writer/content/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - diff --git a/backup-api-standard-v1/tests/test_integration_planner.py b/backup-api-standard-v1/tests/test_integration_planner.py deleted file mode 100644 index 75138f2b..00000000 --- a/backup-api-standard-v1/tests/test_integration_planner.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -Integration tests for Planner module endpoints -Tests CRUD operations and AI actions return unified format -""" -from rest_framework import status -from igny8_core.api.tests.test_integration_base import IntegrationTestBase -from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas - - -class PlannerIntegrationTestCase(IntegrationTestBase): - """Integration tests for Planner module""" - - def test_list_keywords_returns_unified_format(self): - """Test GET /api/v1/planner/keywords/ returns unified format""" - response = self.client.get('/api/v1/planner/keywords/') - - # May get 429 if rate limited - both should have unified format - if response.status_code == status.HTTP_429_TOO_MANY_REQUESTS: - self.assert_unified_response_format(response, expected_success=False) - else: - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_create_keyword_returns_unified_format(self): - """Test POST /api/v1/planner/keywords/ returns unified format""" - data = { - 'seed_keyword_id': self.seed_keyword.id, - 'site_id': self.site.id, - 'sector_id': self.sector.id, - 'status': 'active' - } - response = self.client.post('/api/v1/planner/keywords/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - self.assertIn('id', response.data['data']) - - def test_retrieve_keyword_returns_unified_format(self): - """Test GET /api/v1/planner/keywords/{id}/ returns unified format""" - keyword = Keywords.objects.create( - seed_keyword=self.seed_keyword, - site=self.site, - sector=self.sector, - account=self.account, - status='active' - ) - - response = self.client.get(f'/api/v1/planner/keywords/{keyword.id}/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - self.assertEqual(response.data['data']['id'], keyword.id) - - def test_update_keyword_returns_unified_format(self): - """Test PUT /api/v1/planner/keywords/{id}/ returns unified format""" - keyword = Keywords.objects.create( - seed_keyword=self.seed_keyword, - site=self.site, - sector=self.sector, - account=self.account, - status='active' - ) - - data = { - 'seed_keyword_id': self.seed_keyword.id, - 'site_id': self.site.id, - 'sector_id': self.sector.id, - 'status': 'archived' - } - response = self.client.put(f'/api/v1/planner/keywords/{keyword.id}/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_delete_keyword_returns_unified_format(self): - """Test DELETE /api/v1/planner/keywords/{id}/ returns unified format""" - keyword = Keywords.objects.create( - seed_keyword=self.seed_keyword, - site=self.site, - sector=self.sector, - account=self.account, - status='active' - ) - - response = self.client.delete(f'/api/v1/planner/keywords/{keyword.id}/') - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_list_clusters_returns_unified_format(self): - """Test GET /api/v1/planner/clusters/ returns unified format""" - response = self.client.get('/api/v1/planner/clusters/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_create_cluster_returns_unified_format(self): - """Test POST /api/v1/planner/clusters/ returns unified format""" - data = { - 'name': 'Test Cluster', - 'description': 'Test description', - 'site_id': self.site.id, - 'sector_id': self.sector.id, - 'status': 'active' - } - response = self.client.post('/api/v1/planner/clusters/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_list_ideas_returns_unified_format(self): - """Test GET /api/v1/planner/ideas/ returns unified format""" - response = self.client.get('/api/v1/planner/ideas/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_auto_cluster_returns_unified_format(self): - """Test POST /api/v1/planner/keywords/auto_cluster/ returns unified format""" - keyword = Keywords.objects.create( - seed_keyword=self.seed_keyword, - site=self.site, - sector=self.sector, - account=self.account, - status='active' - ) - - data = { - 'ids': [keyword.id], - 'sector_id': self.sector.id - } - response = self.client.post('/api/v1/planner/keywords/auto_cluster/', data, format='json') - - # Should return either task_id (async) or success response - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_202_ACCEPTED]) - self.assert_unified_response_format(response, expected_success=True) - - def test_keyword_validation_error_returns_unified_format(self): - """Test validation errors return unified format""" - # Missing required fields - response = self.client.post('/api/v1/planner/keywords/', {}, format='json') - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('errors', response.data) - self.assertIn('request_id', response.data) - - def test_keyword_not_found_returns_unified_format(self): - """Test 404 errors return unified format""" - response = self.client.get('/api/v1/planner/keywords/99999/') - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('request_id', response.data) - diff --git a/backup-api-standard-v1/tests/test_integration_rate_limiting.py b/backup-api-standard-v1/tests/test_integration_rate_limiting.py deleted file mode 100644 index 7792351a..00000000 --- a/backup-api-standard-v1/tests/test_integration_rate_limiting.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Integration tests for rate limiting -Tests throttle headers and 429 responses -""" -from rest_framework import status -from django.test import TestCase, override_settings -from rest_framework.test import APIClient -from igny8_core.api.tests.test_integration_base import IntegrationTestBase -from igny8_core.auth.models import User, Account, Plan - - -class RateLimitingIntegrationTestCase(IntegrationTestBase): - """Integration tests for rate limiting""" - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_throttle_headers_present(self): - """Test throttle headers are present in responses""" - response = self.client.get('/api/v1/planner/keywords/') - - # May get 429 if rate limited, or 200 if bypassed - both are valid - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]) - # Throttle headers should be present - # Note: In test environment, throttling may be bypassed, but headers should still be set - # We check if headers exist (they may not be set if throttling is bypassed in tests) - if 'X-Throttle-Limit' in response: - self.assertIn('X-Throttle-Limit', response) - self.assertIn('X-Throttle-Remaining', response) - self.assertIn('X-Throttle-Reset', response) - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_rate_limit_bypass_for_admin(self): - """Test rate limiting is bypassed for admin users""" - # Create admin user - admin_user = User.objects.create_user( - username='admin', - email='admin@test.com', - password='testpass123', - role='admin', - account=self.account - ) - - admin_client = APIClient() - admin_client.force_authenticate(user=admin_user) - - # Make multiple requests - should not be throttled - for i in range(15): - response = admin_client.get('/api/v1/planner/keywords/') - self.assertEqual(response.status_code, status.HTTP_200_OK) - # Should not get 429 - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_rate_limit_bypass_for_system_account(self): - """Test rate limiting is bypassed for system account users""" - # Create system account - system_account = Account.objects.create( - name="AWS Admin", - slug="aws-admin", - plan=self.plan - ) - - system_user = User.objects.create_user( - username='system', - email='system@test.com', - password='testpass123', - role='viewer', - account=system_account - ) - - system_client = APIClient() - system_client.force_authenticate(user=system_user) - - # Make multiple requests - should not be throttled - for i in range(15): - response = system_client.get('/api/v1/planner/keywords/') - self.assertEqual(response.status_code, status.HTTP_200_OK) - # Should not get 429 - - @override_settings(DEBUG=True) - def test_rate_limit_bypass_in_debug_mode(self): - """Test rate limiting is bypassed in DEBUG mode""" - # Make multiple requests - should not be throttled in DEBUG mode - for i in range(15): - response = self.client.get('/api/v1/planner/keywords/') - self.assertEqual(response.status_code, status.HTTP_200_OK) - # Should not get 429 - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=True) - def test_rate_limit_bypass_with_env_flag(self): - """Test rate limiting is bypassed when IGNY8_DEBUG_THROTTLE=True""" - # Make multiple requests - should not be throttled - for i in range(15): - response = self.client.get('/api/v1/planner/keywords/') - self.assertEqual(response.status_code, status.HTTP_200_OK) - # Should not get 429 - - def test_different_throttle_scopes(self): - """Test different endpoints have different throttle scopes""" - # Planner endpoints - may get 429 if rate limited - response = self.client.get('/api/v1/planner/keywords/') - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]) - - # Writer endpoints - may get 429 if rate limited - response = self.client.get('/api/v1/writer/tasks/') - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]) - - # System endpoints - may get 429 if rate limited - response = self.client.get('/api/v1/system/prompts/') - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]) - - # Billing endpoints - may get 429 if rate limited - response = self.client.get('/api/v1/billing/credits/balance/balance/') - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS]) - diff --git a/backup-api-standard-v1/tests/test_integration_system.py b/backup-api-standard-v1/tests/test_integration_system.py deleted file mode 100644 index 32c9348f..00000000 --- a/backup-api-standard-v1/tests/test_integration_system.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Integration tests for System module endpoints -Tests settings, prompts, integrations return unified format -""" -from rest_framework import status -from igny8_core.api.tests.test_integration_base import IntegrationTestBase - - -class SystemIntegrationTestCase(IntegrationTestBase): - """Integration tests for System module""" - - def test_system_status_returns_unified_format(self): - """Test GET /api/v1/system/status/ returns unified format""" - response = self.client.get('/api/v1/system/status/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_list_prompts_returns_unified_format(self): - """Test GET /api/v1/system/prompts/ returns unified format""" - response = self.client.get('/api/v1/system/prompts/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_get_prompt_by_type_returns_unified_format(self): - """Test GET /api/v1/system/prompts/by_type/{type}/ returns unified format""" - response = self.client.get('/api/v1/system/prompts/by_type/clustering/') - - # May return 404 if no prompt exists, but should still be unified format - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND]) - self.assert_unified_response_format(response) - - def test_list_account_settings_returns_unified_format(self): - """Test GET /api/v1/system/settings/account/ returns unified format""" - response = self.client.get('/api/v1/system/settings/account/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_get_integration_settings_returns_unified_format(self): - """Test GET /api/v1/system/settings/integrations/{pk}/ returns unified format""" - response = self.client.get('/api/v1/system/settings/integrations/openai/') - - # May return 404 if not configured, but should still be unified format - self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND]) - self.assert_unified_response_format(response) - diff --git a/backup-api-standard-v1/tests/test_integration_writer.py b/backup-api-standard-v1/tests/test_integration_writer.py deleted file mode 100644 index 7dced2ca..00000000 --- a/backup-api-standard-v1/tests/test_integration_writer.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Integration tests for Writer module endpoints -Tests CRUD operations and AI actions return unified format -""" -from rest_framework import status -from igny8_core.api.tests.test_integration_base import IntegrationTestBase -from igny8_core.modules.writer.models import Tasks, Content, Images - - -class WriterIntegrationTestCase(IntegrationTestBase): - """Integration tests for Writer module""" - - def test_list_tasks_returns_unified_format(self): - """Test GET /api/v1/writer/tasks/ returns unified format""" - response = self.client.get('/api/v1/writer/tasks/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_create_task_returns_unified_format(self): - """Test POST /api/v1/writer/tasks/ returns unified format""" - data = { - 'title': 'Test Task', - 'site_id': self.site.id, - 'sector_id': self.sector.id, - 'status': 'pending' - } - response = self.client.post('/api/v1/writer/tasks/', data, format='json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assert_unified_response_format(response, expected_success=True) - self.assertIn('data', response.data) - - def test_list_content_returns_unified_format(self): - """Test GET /api/v1/writer/content/ returns unified format""" - response = self.client.get('/api/v1/writer/content/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_list_images_returns_unified_format(self): - """Test GET /api/v1/writer/images/ returns unified format""" - response = self.client.get('/api/v1/writer/images/') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assert_paginated_response(response) - - def test_create_image_returns_unified_format(self): - """Test POST /api/v1/writer/images/ returns unified format""" - data = { - 'image_type': 'featured', - 'site_id': self.site.id, - 'sector_id': self.sector.id, - 'status': 'pending' - } - response = self.client.post('/api/v1/writer/images/', data, format='json') - - # May return 400 if site/sector validation fails, but should still be unified format - self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_400_BAD_REQUEST]) - self.assert_unified_response_format(response) - - def test_task_validation_error_returns_unified_format(self): - """Test validation errors return unified format""" - response = self.client.post('/api/v1/writer/tasks/', {}, format='json') - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_unified_response_format(response, expected_success=False) - self.assertIn('error', response.data) - self.assertIn('errors', response.data) - diff --git a/backup-api-standard-v1/tests/test_permissions.py b/backup-api-standard-v1/tests/test_permissions.py deleted file mode 100644 index 3ed15482..00000000 --- a/backup-api-standard-v1/tests/test_permissions.py +++ /dev/null @@ -1,313 +0,0 @@ -""" -Unit tests for permission classes -Tests IsAuthenticatedAndActive, HasTenantAccess, IsViewerOrAbove, IsEditorOrAbove, IsAdminOrOwner -""" -from django.test import TestCase -from rest_framework.test import APIRequestFactory -from rest_framework.views import APIView -from igny8_core.api.permissions import ( - IsAuthenticatedAndActive, HasTenantAccess, IsViewerOrAbove, - IsEditorOrAbove, IsAdminOrOwner -) -from igny8_core.auth.models import User, Account, Plan - - -class PermissionsTestCase(TestCase): - """Test cases for permission classes""" - - def setUp(self): - """Set up test fixtures""" - self.factory = APIRequestFactory() - self.view = APIView() - - # Create test plan - self.plan = Plan.objects.create( - name="Test Plan", - slug="test-plan", - price=0, - credits_per_month=1000 - ) - - # Create owner user first (Account needs owner) - self.owner_user = User.objects.create_user( - username='owner', - email='owner@test.com', - password='testpass123', - role='owner' - ) - - # Create test account with owner - self.account = Account.objects.create( - name="Test Account", - slug="test-account", - plan=self.plan, - owner=self.owner_user - ) - - # Update owner user to have account - self.owner_user.account = self.account - self.owner_user.save() - - self.admin_user = User.objects.create_user( - username='admin', - email='admin@test.com', - password='testpass123', - role='admin', - account=self.account - ) - - self.editor_user = User.objects.create_user( - username='editor', - email='editor@test.com', - password='testpass123', - role='editor', - account=self.account - ) - - self.viewer_user = User.objects.create_user( - username='viewer', - email='viewer@test.com', - password='testpass123', - role='viewer', - account=self.account - ) - - # Create another account for tenant isolation testing - self.other_owner = User.objects.create_user( - username='other_owner', - email='other_owner@test.com', - password='testpass123', - role='owner' - ) - - self.other_account = Account.objects.create( - name="Other Account", - slug="other-account", - plan=self.plan, - owner=self.other_owner - ) - - self.other_owner.account = self.other_account - self.other_owner.save() - - self.other_user = User.objects.create_user( - username='other', - email='other@test.com', - password='testpass123', - role='owner', - account=self.other_account - ) - - def test_is_authenticated_and_active_authenticated(self): - """Test IsAuthenticatedAndActive allows authenticated users""" - permission = IsAuthenticatedAndActive() - request = self.factory.get('/test/') - request.user = self.owner_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_authenticated_and_active_unauthenticated(self): - """Test IsAuthenticatedAndActive denies unauthenticated users""" - permission = IsAuthenticatedAndActive() - request = self.factory.get('/test/') - request.user = None - - result = permission.has_permission(request, self.view) - self.assertFalse(result) - - def test_is_authenticated_and_active_inactive_user(self): - """Test IsAuthenticatedAndActive denies inactive users""" - permission = IsAuthenticatedAndActive() - self.owner_user.is_active = False - self.owner_user.save() - - request = self.factory.get('/test/') - request.user = self.owner_user - - result = permission.has_permission(request, self.view) - self.assertFalse(result) - - def test_has_tenant_access_same_account(self): - """Test HasTenantAccess allows users from same account""" - permission = HasTenantAccess() - request = self.factory.get('/test/') - request.user = self.owner_user - request.account = self.account - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_has_tenant_access_different_account(self): - """Test HasTenantAccess denies users from different account""" - permission = HasTenantAccess() - request = self.factory.get('/test/') - request.user = self.owner_user - request.account = self.other_account - - result = permission.has_permission(request, self.view) - self.assertFalse(result) - - def test_has_tenant_access_admin_bypass(self): - """Test HasTenantAccess allows admin/developer to bypass""" - permission = HasTenantAccess() - request = self.factory.get('/test/') - request.user = self.admin_user - request.account = self.other_account # Different account - - result = permission.has_permission(request, self.view) - self.assertTrue(result) # Admin should bypass - - def test_has_tenant_access_system_account(self): - """Test HasTenantAccess allows system account users to bypass""" - # Create system account owner - system_owner = User.objects.create_user( - username='system_owner_test', - email='system_owner_test@test.com', - password='testpass123', - role='owner' - ) - - # Create system account - system_account = Account.objects.create( - name="AWS Admin", - slug="aws-admin", - plan=self.plan, - owner=system_owner - ) - - system_owner.account = system_account - system_owner.save() - - system_user = User.objects.create_user( - username='system', - email='system@test.com', - password='testpass123', - role='viewer', - account=system_account - ) - - permission = HasTenantAccess() - request = self.factory.get('/test/') - request.user = system_user - request.account = self.account # Different account - - result = permission.has_permission(request, self.view) - self.assertTrue(result) # System account user should bypass - - def test_is_viewer_or_above_viewer(self): - """Test IsViewerOrAbove allows viewer role""" - permission = IsViewerOrAbove() - request = self.factory.get('/test/') - request.user = self.viewer_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_viewer_or_above_editor(self): - """Test IsViewerOrAbove allows editor role""" - permission = IsViewerOrAbove() - request = self.factory.get('/test/') - request.user = self.editor_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_viewer_or_above_admin(self): - """Test IsViewerOrAbove allows admin role""" - permission = IsViewerOrAbove() - request = self.factory.get('/test/') - request.user = self.admin_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_viewer_or_above_owner(self): - """Test IsViewerOrAbove allows owner role""" - permission = IsViewerOrAbove() - request = self.factory.get('/test/') - request.user = self.owner_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_editor_or_above_viewer_denied(self): - """Test IsEditorOrAbove denies viewer role""" - permission = IsEditorOrAbove() - request = self.factory.get('/test/') - request.user = self.viewer_user - - result = permission.has_permission(request, self.view) - self.assertFalse(result) - - def test_is_editor_or_above_editor_allowed(self): - """Test IsEditorOrAbove allows editor role""" - permission = IsEditorOrAbove() - request = self.factory.get('/test/') - request.user = self.editor_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_editor_or_above_admin_allowed(self): - """Test IsEditorOrAbove allows admin role""" - permission = IsEditorOrAbove() - request = self.factory.get('/test/') - request.user = self.admin_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_admin_or_owner_viewer_denied(self): - """Test IsAdminOrOwner denies viewer role""" - permission = IsAdminOrOwner() - request = self.factory.get('/test/') - request.user = self.viewer_user - - result = permission.has_permission(request, self.view) - self.assertFalse(result) - - def test_is_admin_or_owner_editor_denied(self): - """Test IsAdminOrOwner denies editor role""" - permission = IsAdminOrOwner() - request = self.factory.get('/test/') - request.user = self.editor_user - - result = permission.has_permission(request, self.view) - self.assertFalse(result) - - def test_is_admin_or_owner_admin_allowed(self): - """Test IsAdminOrOwner allows admin role""" - permission = IsAdminOrOwner() - request = self.factory.get('/test/') - request.user = self.admin_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_is_admin_or_owner_owner_allowed(self): - """Test IsAdminOrOwner allows owner role""" - permission = IsAdminOrOwner() - request = self.factory.get('/test/') - request.user = self.owner_user - - result = permission.has_permission(request, self.view) - self.assertTrue(result) - - def test_all_permissions_unauthenticated_denied(self): - """Test all permissions deny unauthenticated users""" - permissions = [ - IsAuthenticatedAndActive(), - HasTenantAccess(), - IsViewerOrAbove(), - IsEditorOrAbove(), - IsAdminOrOwner() - ] - - request = self.factory.get('/test/') - request.user = None - - for permission in permissions: - result = permission.has_permission(request, self.view) - self.assertFalse(result, f"{permission.__class__.__name__} should deny unauthenticated users") - diff --git a/backup-api-standard-v1/tests/test_response.py b/backup-api-standard-v1/tests/test_response.py deleted file mode 100644 index 353e9c9b..00000000 --- a/backup-api-standard-v1/tests/test_response.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Unit tests for response helper functions -Tests success_response, error_response, paginated_response -""" -from django.test import TestCase, RequestFactory -from rest_framework import status -from igny8_core.api.response import success_response, error_response, paginated_response, get_request_id - - -class ResponseHelpersTestCase(TestCase): - """Test cases for response helper functions""" - - def setUp(self): - """Set up test fixtures""" - self.factory = RequestFactory() - - def test_success_response_with_data(self): - """Test success_response with data""" - data = {"id": 1, "name": "Test"} - response = success_response(data=data, message="Success") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['success']) - self.assertEqual(response.data['data'], data) - self.assertEqual(response.data['message'], "Success") - - def test_success_response_without_data(self): - """Test success_response without data""" - response = success_response(message="Success") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['success']) - self.assertNotIn('data', response.data) - self.assertEqual(response.data['message'], "Success") - - def test_success_response_with_custom_status(self): - """Test success_response with custom status code""" - data = {"id": 1} - response = success_response(data=data, status_code=status.HTTP_201_CREATED) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue(response.data['success']) - self.assertEqual(response.data['data'], data) - - def test_success_response_with_request_id(self): - """Test success_response includes request_id when request provided""" - request = self.factory.get('/test/') - request.request_id = 'test-request-id-123' - - response = success_response(data={"id": 1}, request=request) - - self.assertTrue(response.data['success']) - self.assertEqual(response.data['request_id'], 'test-request-id-123') - - def test_error_response_with_error_message(self): - """Test error_response with error message""" - response = error_response(error="Validation failed") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], "Validation failed") - - def test_error_response_with_errors_dict(self): - """Test error_response with field-specific errors""" - errors = {"email": ["Invalid email format"], "password": ["Too short"]} - response = error_response(error="Validation failed", errors=errors) - - self.assertFalse(response.data['success']) - self.assertEqual(response.data['error'], "Validation failed") - self.assertEqual(response.data['errors'], errors) - - def test_error_response_status_code_mapping(self): - """Test error_response maps status codes to default error messages""" - # Test 401 - response = error_response(status_code=status.HTTP_401_UNAUTHORIZED) - self.assertEqual(response.data['error'], 'Authentication required') - - # Test 403 - response = error_response(status_code=status.HTTP_403_FORBIDDEN) - self.assertEqual(response.data['error'], 'Permission denied') - - # Test 404 - response = error_response(status_code=status.HTTP_404_NOT_FOUND) - self.assertEqual(response.data['error'], 'Resource not found') - - # Test 409 - response = error_response(status_code=status.HTTP_409_CONFLICT) - self.assertEqual(response.data['error'], 'Conflict') - - # Test 422 - response = error_response(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) - self.assertEqual(response.data['error'], 'Validation failed') - - # Test 429 - response = error_response(status_code=status.HTTP_429_TOO_MANY_REQUESTS) - self.assertEqual(response.data['error'], 'Rate limit exceeded') - - # Test 500 - response = error_response(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertEqual(response.data['error'], 'Internal server error') - - def test_error_response_with_request_id(self): - """Test error_response includes request_id when request provided""" - request = self.factory.get('/test/') - request.request_id = 'test-request-id-456' - - response = error_response(error="Error occurred", request=request) - - self.assertFalse(response.data['success']) - self.assertEqual(response.data['request_id'], 'test-request-id-456') - - def test_error_response_with_debug_info(self): - """Test error_response includes debug info when provided""" - debug_info = {"exception_type": "ValueError", "message": "Test error"} - response = error_response(error="Error", debug_info=debug_info) - - self.assertFalse(response.data['success']) - self.assertEqual(response.data['debug'], debug_info) - - def test_paginated_response_with_data(self): - """Test paginated_response with paginated data""" - paginated_data = { - 'count': 100, - 'next': 'http://test.com/api/v1/test/?page=2', - 'previous': None, - 'results': [{"id": 1}, {"id": 2}] - } - response = paginated_response(paginated_data, message="Success") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['success']) - self.assertEqual(response.data['count'], 100) - self.assertEqual(response.data['next'], paginated_data['next']) - self.assertEqual(response.data['previous'], None) - self.assertEqual(response.data['results'], paginated_data['results']) - self.assertEqual(response.data['message'], "Success") - - def test_paginated_response_without_message(self): - """Test paginated_response without message""" - paginated_data = { - 'count': 50, - 'next': None, - 'previous': None, - 'results': [] - } - response = paginated_response(paginated_data) - - self.assertTrue(response.data['success']) - self.assertEqual(response.data['count'], 50) - self.assertNotIn('message', response.data) - - def test_paginated_response_with_request_id(self): - """Test paginated_response includes request_id when request provided""" - request = self.factory.get('/test/') - request.request_id = 'test-request-id-789' - - paginated_data = { - 'count': 10, - 'next': None, - 'previous': None, - 'results': [] - } - response = paginated_response(paginated_data, request=request) - - self.assertTrue(response.data['success']) - self.assertEqual(response.data['request_id'], 'test-request-id-789') - - def test_paginated_response_fallback(self): - """Test paginated_response handles non-dict input""" - response = paginated_response(None) - - self.assertTrue(response.data['success']) - self.assertEqual(response.data['count'], 0) - self.assertIsNone(response.data['next']) - self.assertIsNone(response.data['previous']) - self.assertEqual(response.data['results'], []) - - def test_get_request_id_from_request_object(self): - """Test get_request_id retrieves from request.request_id""" - request = self.factory.get('/test/') - request.request_id = 'request-id-from-object' - - request_id = get_request_id(request) - self.assertEqual(request_id, 'request-id-from-object') - - def test_get_request_id_from_headers(self): - """Test get_request_id retrieves from headers""" - request = self.factory.get('/test/', HTTP_X_REQUEST_ID='request-id-from-header') - - request_id = get_request_id(request) - self.assertEqual(request_id, 'request-id-from-header') - - def test_get_request_id_generates_new(self): - """Test get_request_id generates new UUID if not found""" - request = self.factory.get('/test/') - - request_id = get_request_id(request) - self.assertIsNotNone(request_id) - self.assertIsInstance(request_id, str) - # UUID format check - import uuid - try: - uuid.UUID(request_id) - except ValueError: - self.fail("Generated request_id is not a valid UUID") - diff --git a/backup-api-standard-v1/tests/test_throttles.py b/backup-api-standard-v1/tests/test_throttles.py deleted file mode 100644 index 373762c7..00000000 --- a/backup-api-standard-v1/tests/test_throttles.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Unit tests for rate limiting -Tests DebugScopedRateThrottle with bypass logic -""" -from django.test import TestCase, override_settings -from rest_framework.test import APIRequestFactory -from rest_framework.views import APIView -from igny8_core.api.throttles import DebugScopedRateThrottle -from igny8_core.auth.models import User, Account, Plan - - -class ThrottlesTestCase(TestCase): - """Test cases for rate limiting""" - - def setUp(self): - """Set up test fixtures""" - self.factory = APIRequestFactory() - self.view = APIView() - self.view.throttle_scope = 'planner' - - # Create test plan and account - self.plan = Plan.objects.create( - name="Test Plan", - slug="test-plan", - price=0, - credits_per_month=1000 - ) - - # Create owner user first - self.owner_user = User.objects.create_user( - username='owner', - email='owner@test.com', - password='testpass123', - role='owner' - ) - - # Create test account with owner - self.account = Account.objects.create( - name="Test Account", - slug="test-account", - plan=self.plan, - owner=self.owner_user - ) - - # Update owner user to have account - self.owner_user.account = self.account - self.owner_user.save() - - # Create regular user - self.user = User.objects.create_user( - username='user', - email='user@test.com', - password='testpass123', - role='viewer', - account=self.account - ) - - # Create admin user - self.admin_user = User.objects.create_user( - username='admin', - email='admin@test.com', - password='testpass123', - role='admin', - account=self.account - ) - - # Create system account owner - self.system_owner = User.objects.create_user( - username='system_owner', - email='system_owner@test.com', - password='testpass123', - role='owner' - ) - - # Create system account user - self.system_account = Account.objects.create( - name="AWS Admin", - slug="aws-admin", - plan=self.plan, - owner=self.system_owner - ) - - self.system_owner.account = self.system_account - self.system_owner.save() - - self.system_user = User.objects.create_user( - username='system', - email='system@test.com', - password='testpass123', - role='viewer', - account=self.system_account - ) - - @override_settings(DEBUG=True) - def test_debug_mode_bypass(self): - """Test throttling is bypassed in DEBUG mode""" - throttle = DebugScopedRateThrottle() - request = self.factory.get('/test/') - request.user = self.user - - result = throttle.allow_request(request, self.view) - self.assertTrue(result) # Should bypass in DEBUG mode - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=True) - def test_env_bypass(self): - """Test throttling is bypassed when IGNY8_DEBUG_THROTTLE=True""" - throttle = DebugScopedRateThrottle() - request = self.factory.get('/test/') - request.user = self.user - - result = throttle.allow_request(request, self.view) - self.assertTrue(result) # Should bypass when IGNY8_DEBUG_THROTTLE=True - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_system_account_bypass(self): - """Test throttling is bypassed for system account users""" - throttle = DebugScopedRateThrottle() - request = self.factory.get('/test/') - request.user = self.system_user - - result = throttle.allow_request(request, self.view) - self.assertTrue(result) # System account users should bypass - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_admin_bypass(self): - """Test throttling is bypassed for admin/developer users""" - throttle = DebugScopedRateThrottle() - request = self.factory.get('/test/') - request.user = self.admin_user - - result = throttle.allow_request(request, self.view) - self.assertTrue(result) # Admin users should bypass - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_get_rate(self): - """Test get_rate returns correct rate for scope""" - throttle = DebugScopedRateThrottle() - throttle.scope = 'planner' - - rate = throttle.get_rate() - self.assertIsNotNone(rate) - self.assertIn('/', rate) # Should be in format "60/min" - - @override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False) - def test_get_rate_default_fallback(self): - """Test get_rate falls back to default if scope not found""" - throttle = DebugScopedRateThrottle() - throttle.scope = 'nonexistent_scope' - - rate = throttle.get_rate() - self.assertIsNotNone(rate) - self.assertEqual(rate, '100/min') # Should fallback to default - - def test_parse_rate_minutes(self): - """Test parse_rate correctly parses minutes""" - throttle = DebugScopedRateThrottle() - - num, duration = throttle.parse_rate('60/min') - self.assertEqual(num, 60) - self.assertEqual(duration, 60) - - def test_parse_rate_seconds(self): - """Test parse_rate correctly parses seconds""" - throttle = DebugScopedRateThrottle() - - num, duration = throttle.parse_rate('10/sec') - self.assertEqual(num, 10) - self.assertEqual(duration, 1) - - def test_parse_rate_hours(self): - """Test parse_rate correctly parses hours""" - throttle = DebugScopedRateThrottle() - - num, duration = throttle.parse_rate('100/hour') - self.assertEqual(num, 100) - self.assertEqual(duration, 3600) - - def test_parse_rate_invalid_format(self): - """Test parse_rate handles invalid format gracefully""" - throttle = DebugScopedRateThrottle() - - num, duration = throttle.parse_rate('invalid') - self.assertEqual(num, 100) # Should default to 100 - self.assertEqual(duration, 60) # Should default to 60 seconds (1 min) - - @override_settings(DEBUG=True) - def test_debug_info_set(self): - """Test debug info is set when bypassing in DEBUG mode""" - throttle = DebugScopedRateThrottle() - request = self.factory.get('/test/') - request.user = self.user - - result = throttle.allow_request(request, self.view) - self.assertTrue(result) - self.assertTrue(hasattr(request, '_throttle_debug_info')) - self.assertIn('scope', request._throttle_debug_info) - self.assertIn('rate', request._throttle_debug_info) - self.assertIn('limit', request._throttle_debug_info) - diff --git a/cmd/check-vite-status.sh b/cmd/check-vite-status.sh deleted file mode 100644 index c2d28fc3..00000000 --- a/cmd/check-vite-status.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -# Quick Vite dev server status check - -echo "╔════════════════════════════════════════════════════════════╗" -echo "║ Vite Dev Server Status Check (Port 8021) ║" -echo "╚════════════════════════════════════════════════════════════╝" -echo "" - -# Check Docker container -echo "📦 Docker Container Status:" -if docker ps --filter "name=igny8_frontend" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -q igny8_frontend; then - docker ps --filter "name=igny8_frontend" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" -else - echo " ❌ Container 'igny8_frontend' not found or not running" -fi -echo "" - -# Check port -echo "🔌 Port 8021 Status:" -if netstat -tuln 2>/dev/null | grep -q ":8021" || ss -tuln 2>/dev/null | grep -q ":8021"; then - echo " ✅ Port 8021 is listening" - netstat -tuln 2>/dev/null | grep ":8021" || ss -tuln 2>/dev/null | grep ":8021" -else - echo " ❌ Port 8021 is not listening" -fi -echo "" - -# Test HTTP response -echo "🌐 HTTP Response Test:" -HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8021/ 2>/dev/null) -if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then - echo " ✅ Server responding (HTTP $HTTP_CODE)" -else - echo " ❌ Server not responding (HTTP $HTTP_CODE or connection failed)" -fi -echo "" - -# Check recent logs -echo "📋 Recent Container Logs (last 10 lines):" -docker logs igny8_frontend --tail 10 2>/dev/null || echo " ⚠️ Could not fetch logs" -echo "" - -echo "════════════════════════════════════════════════════════════" - diff --git a/cmd/logs.sh b/cmd/logs.sh deleted file mode 100644 index 5cd37143..00000000 --- a/cmd/logs.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# Quick log check - last 50 lines - -echo "=== Backend Logs ===" -docker logs igny8_backend --tail 50 - -echo "" -echo "=== Celery Worker Logs ===" -docker logs igny8_celery_worker --tail 50 - -echo "" -echo "=== Celery Beat Logs ===" -docker logs igny8_celery_beat --tail 50 - diff --git a/cmd/restart-backend.sh b/cmd/restart-backend.sh deleted file mode 100644 index 2342f8df..00000000 --- a/cmd/restart-backend.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Restart backend containers to pick up code changes - -echo "🛑 Stopping backend containers..." -docker stop igny8_backend igny8_celery_worker igny8_celery_beat - -echo "⏳ Waiting 3 seconds..." -sleep 3 - -echo "🚀 Starting backend containers..." -docker start igny8_backend igny8_celery_worker igny8_celery_beat - -echo "⏳ Waiting 5 seconds for containers to initialize..." -sleep 5 - -echo "📋 Checking container status..." -docker ps --filter "name=igny8" --format " {{.Names}} | {{.Status}}" - -echo "" -echo "📝 Checking backend logs for errors..." -docker logs igny8_backend --tail 20 - diff --git a/cmd/st.sh b/cmd/st.sh deleted file mode 100644 index 3ec02db0..00000000 --- a/cmd/st.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# Quick status check script for IGNY8 stacks and containers - -echo "╔════════════════════════════════════════════════════════════════════════════╗" -echo "║ IGNY8 STACK & CONTAINER STATUS REPORT ║" -echo "╚════════════════════════════════════════════════════════════════════════════╝" -echo "" - -echo "📦 APP STACK (igny8-app):" -docker ps --filter "label=com.docker.compose.project=igny8-app" --format " ✅ {{.Names}} | Status: {{.Status}} | Ports: {{.Ports}}" -if [ $? -ne 0 ] || [ -z "$(docker ps --filter 'label=com.docker.compose.project=igny8-app' --format '{{.Names}}')" ]; then - echo " ⚠️ No app stack containers found" -fi - -echo "" -echo "🏗️ INFRA STACK (igny8-infra):" -docker ps --filter "label=com.docker.compose.project=igny8-infra" --format " ✅ {{.Names}} | Status: {{.Status}}" -if [ $? -ne 0 ] || [ -z "$(docker ps --filter 'label=com.docker.compose.project=igny8-infra' --format '{{.Names}}')" ]; then - echo " ⚠️ No infra stack containers found" -fi - -echo "" -echo "🌐 NETWORK CONNECTIVITY (igny8_net):" -CONTAINER_COUNT=$(docker network inspect igny8_net --format '{{len .Containers}}' 2>/dev/null || echo "0") -echo " Connected: $CONTAINER_COUNT containers" - -echo "" -echo "🔍 SERVICE HEALTH CHECKS:" -BACKEND_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8011/api/v1/plans/ 2>/dev/null || echo "000") -FRONTEND_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8021/ 2>/dev/null || echo "000") -POSTGRES_HEALTH=$(docker exec igny8_postgres pg_isready -U igny8 2>&1 | grep -q 'accepting' && echo "healthy" || echo "unhealthy") -REDIS_HEALTH=$(docker exec igny8_redis redis-cli ping 2>&1 | grep -q PONG && echo "healthy" || echo "unhealthy") - -echo " Backend API: $BACKEND_CODE $([ "$BACKEND_CODE" = "200" ] && echo "✅" || echo "❌")" -echo " Frontend: $FRONTEND_CODE $([ "$FRONTEND_CODE" = "200" ] && echo "✅" || echo "❌")" -echo " Postgres: $POSTGRES_HEALTH $([ "$POSTGRES_HEALTH" = "healthy" ] && echo "✅" || echo "❌")" -echo " Redis: $REDIS_HEALTH $([ "$REDIS_HEALTH" = "healthy" ] && echo "✅" || echo "❌")" - -echo "" -echo "📋 ALL IGNY8 CONTAINERS:" -docker ps --filter "name=igny8" --format " {{.Names}} | {{.Image}} | {{.Status}}" - -echo "" -echo "════════════════════════════════════════════════════════════════════════════" - diff --git a/igny8-ai-seo-wp-plugin/CHANGELOG_live.md b/igny8-ai-seo-wp-plugin/CHANGELOG_live.md deleted file mode 100644 index 6e5b427f..00000000 --- a/igny8-ai-seo-wp-plugin/CHANGELOG_live.md +++ /dev/null @@ -1,59 +0,0 @@ -## [0.1] - 2025-01-15 - -### Initial Release - Complete Refactor -- **Phase 1 Complete**: Global Role & Scope Index implemented -- **Phase 2 Complete**: Folder restructure & component isolation -- **Phase 2.5 Complete**: Final refactor of layout, routing, and page loading structure -- **Phase 2.5.1 Complete**: Final cleanup of routing and layout includes - -### Major Architecture Changes -- **Modular Structure**: All admin pages physically modularized by module -- **Component System**: UI components (forms, filters, tables, modals) extracted into reusable templates -- **Static Routing**: Eliminated dynamic routing, converted to static file includes -- **Layout Standardization**: All pages follow `ob_start() → $igny8_page_content → global-layout.php` pattern -- **Submodule System**: Complete subpage structure for planner, writer, thinker, settings, help modules - -### Technical Improvements -- **Configuration-Driven UI**: Tables, forms, and filters generated dynamically from config files -- **Complete Component Loading**: All submodules now include filters, actions, table, and pagination -- **JavaScript Integration**: Proper localization and data setup for all submodules -- **Debug Isolation**: Development files moved to dedicated folders with proper guards -- **Help Module**: Centralized help, documentation, and testing functionality - -### Files Restructured -- **Modules**: `/modules/planner/`, `/modules/writer/`, `/modules/thinker/`, `/modules/settings/`, `/modules/help/` -- **Components**: `/modules/components/` with reusable UI templates -- **Config**: `/modules/config/` with centralized configuration arrays -- **Core**: `/core/` with layout, admin, database, and cron functionality -- **AI**: `/ai/` with content generation and image processing - -### Database & Configuration -- **Table Configurations**: Complete table structure definitions in `tables-config.php` -- **Filter Configurations**: Dynamic filter system in `filters-config.php` -- **Import/Export**: Centralized import/export configurations -- **KPI System**: Dashboard metrics and analytics configuration - -### Developer Experience -- **File Organization**: Clear separation of concerns and modular architecture -- **Documentation**: Comprehensive documentation and troubleshooting guides -- **Debug Tools**: System testing and function testing interfaces -- **Code Standards**: Consistent file headers and scope declarations - -## [5.3.0] - 2025-01-15 - -### Critical Cron vs Manual Function Analysis -- **CRITICAL DISCREPANCY IDENTIFIED**: Cron functions have significant differences from manual counterparts -- **Function Dependency Issues**: Cron handlers include extensive fallback logic for functions like `igny8_get_sector_options()` -- **User Context Problems**: Cron handlers manually set admin user context while manual AJAX handlers rely on authenticated user -- **Warning Suppression**: Cron handlers suppress PHP warnings that manual handlers don't, potentially masking issues -- **Database Connection**: Cron handlers explicitly declare `global $wpdb` while manual handlers use it directly -- **Risk Assessment**: Cron functions are at HIGH RISK of failing or behaving differently than manual functions - -### Technical Analysis Findings -- **Auto Cluster**: Manual `igny8_ajax_ai_cluster_keywords()` vs Cron `igny8_auto_cluster_cron_handler()` -- **Auto Ideas**: Manual `igny8_ajax_ai_generate_ideas()` vs Cron `igny8_auto_generate_ideas_cron_handler()` -- **Auto Queue**: Manual `igny8_ajax_queue_ideas_to_writer()` vs Cron `igny8_auto_queue_cron_handler()` -- **Auto Content**: Manual `igny8_ajax_ai_generate_content()` vs Cron `igny8_auto_generate_content_cron_handler()` -- **Auto Image**: Manual `igny8_ajax_ai_generate_images_drafts()` vs Cron `igny8_auto_generate_images_cron_handler()` -- **Auto Publish**: Manual `igny8_ajax_bulk_publish_drafts()` vs Cron `igny8_auto_publish_drafts_cron_handler()` - diff --git a/igny8-ai-seo-wp-plugin/ai/_README.php b/igny8-ai-seo-wp-plugin/ai/_README.php deleted file mode 100644 index fc8f21c1..00000000 --- a/igny8-ai-seo-wp-plugin/ai/_README.php +++ /dev/null @@ -1,14 +0,0 @@ - ['in' => 2.00, 'out' => 8.00], - 'gpt-4o-mini' => ['in' => 0.15, 'out' => 0.60], - 'gpt-4o' => ['in' => 2.50, 'out' => 10.00] -]; - -/** - * Global image model rates configuration - * Rates are per image - */ -$IGNY8_IMAGE_MODEL_RATES = [ - 'dall-e-3' => 0.040, - 'dall-e-2' => 0.020, - 'gpt-image-1' => 0.042, - 'gpt-image-1-mini' => 0.011 -]; - -/** - * Get model rates for a specific model - * - * @param string $model Model name - * @return array Model rates array with 'in' and 'out' keys - */ -function igny8_get_model_rates($model) { - global $IGNY8_MODEL_RATES; - return $IGNY8_MODEL_RATES[$model] ?? $IGNY8_MODEL_RATES['gpt-4.1']; -} - -/** - * Calculate API cost based on model and token usage - * - * @param string $model Model name - * @param int $input_tokens Number of input tokens - * @param int $output_tokens Number of output tokens - * @return array Cost breakdown with 'input_cost', 'output_cost', 'total_cost' - */ -function igny8_calculate_api_cost($model, $input_tokens, $output_tokens) { - $rates = igny8_get_model_rates($model); - - // Debug logging - error_log("Igny8 Cost Calc Debug: Model=$model, Rates=" . json_encode($rates)); - error_log("Igny8 Cost Calc Debug: Input tokens=$input_tokens, Output tokens=$output_tokens"); - - $input_cost = ($input_tokens / 1000000) * $rates['in']; - $output_cost = ($output_tokens / 1000000) * $rates['out']; - $total_cost = $input_cost + $output_cost; - - error_log("Igny8 Cost Calc Debug: Input cost=$input_cost, Output cost=$output_cost, Total cost=$total_cost"); - - return [ - 'input_cost' => $input_cost, - 'output_cost' => $output_cost, - 'total_cost' => $total_cost, - 'model' => $model, - 'input_tokens' => $input_tokens, - 'output_tokens' => $output_tokens - ]; -} - -/** - * Format cost for display - * - * @param float $cost Cost amount - * @param int $decimals Number of decimal places - * @return string Formatted cost string - */ -function igny8_format_cost($cost, $decimals = 4) { - // Convert to cents for better readability - $cents = $cost * 100; - return number_format($cents, 2) . '¢'; -} - -/** - * Get image model rates for a specific model - * - * @param string $model Image model name - * @return float Image model rate per image - */ -function igny8_get_image_model_rates($model) { - global $IGNY8_IMAGE_MODEL_RATES; - return $IGNY8_IMAGE_MODEL_RATES[$model] ?? $IGNY8_IMAGE_MODEL_RATES['dall-e-3']; -} - -/** - * Calculate image generation cost based on model - * - * @param string $model Image model name - * @param int $image_count Number of images generated - * @return array Cost breakdown with 'per_image_cost', 'total_cost' - */ -function igny8_calculate_image_cost($model, $image_count = 1) { - $per_image_rate = igny8_get_image_model_rates($model); - $total_cost = $per_image_rate * $image_count; - - return [ - 'per_image_cost' => $per_image_rate, - 'total_cost' => $total_cost, - 'model' => $model, - 'image_count' => $image_count - ]; -} - -/** - * Get image model display name with pricing and typical uses - * - * @param string $model Image model name - * @return string Formatted model name with pricing and uses - */ -function igny8_get_image_model_display_name($model) { - $model_info = [ - 'dall-e-3' => [ - 'name' => 'DALL·E 3', - 'uses' => 'High-quality image generation with advanced AI capabilities' - ], - 'dall-e-2' => [ - 'name' => 'DALL·E 2', - 'uses' => 'Cost-effective image generation with good quality' - ], - 'gpt-image-1' => [ - 'name' => 'GPT Image 1 (Full)', - 'uses' => 'Full-featured image generation with comprehensive capabilities' - ], - 'gpt-image-1-mini' => [ - 'name' => 'GPT Image 1 Mini', - 'uses' => 'Lightweight, cost-effective image generation for bulk operations' - ] - ]; - - $rate = igny8_get_image_model_rates($model); - $info = $model_info[$model] ?? ['name' => strtoupper($model), 'uses' => 'Image generation']; - - return sprintf( - '%s — $%.3f per image (%s)', - $info['name'], $rate, $info['uses'] - ); -} - -/** - * Get model display name with pricing and typical uses - * - * @param string $model Model name - * @return string Formatted model name with pricing and uses - */ -function igny8_get_model_display_name($model) { - $model_info = [ - 'gpt-4.1' => [ - 'name' => 'GPT-4.1', - 'uses' => 'Content creation, coding, analysis, high-quality content generation' - ], - 'gpt-4o-mini' => [ - 'name' => 'GPT-4o mini', - 'uses' => 'Bulk tasks, lightweight AI, cost-effective for high-volume operations' - ], - 'gpt-4o' => [ - 'name' => 'GPT-4o', - 'uses' => 'Advanced AI for general and multimodal tasks, faster than GPT-4.1' - ] - ]; - - $rates = igny8_get_model_rates($model); - $info = $model_info[$model] ?? ['name' => strtoupper($model), 'uses' => 'General purpose']; - - return sprintf( - '%s — $%.2f / $%.2f per 1M tokens (%s)', - $info['name'], $rates['in'], $rates['out'], $info['uses'] - ); -} diff --git a/igny8-ai-seo-wp-plugin/ai/modules-ai.php b/igny8-ai-seo-wp-plugin/ai/modules-ai.php deleted file mode 100644 index e6937e20..00000000 --- a/igny8-ai-seo-wp-plugin/ai/modules-ai.php +++ /dev/null @@ -1,1809 +0,0 @@ - igny8_get_ai_setting('planner_mode', 'manual'), - 'clustering' => igny8_get_ai_setting('clustering', 'enabled'), - 'ideas' => igny8_get_ai_setting('ideas', 'enabled'), - 'mapping' => igny8_get_ai_setting('mapping', 'enabled'), - 'prompts' => [ - 'clustering' => igny8_get_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt()), - 'ideas' => igny8_get_ai_setting('ideas_prompt', igny8_get_default_ideas_prompt()) - ] - ]; -} - -/** - * Get Writer AI settings - */ -function igny8_get_writer_ai_settings() { - return [ - 'writer_mode' => igny8_get_ai_setting('writer_mode', 'manual'), - 'content_generation' => igny8_get_ai_setting('content_generation', 'enabled'), - 'prompts' => [ - 'content_generation' => get_option('igny8_content_generation_prompt', igny8_content_generation_prompt()) - ] - ]; -} - - -/** - * Process AI request with prompt template - */ -function igny8_process_ai_request($action, $data, $prompt_template) { - // Log AI processing start - igny8_log_ai_event('AI Processing Started', 'ai', $action, 'info', 'Starting AI request processing', 'Action: ' . $action . ', Data count: ' . count($data)); - - // Replace shortcodes with actual data - $prompt = $prompt_template; - - switch ($action) { - case 'clustering': - $keywords_data = igny8_format_keywords_for_ai($data); - $prompt = str_replace('[IGNY8_KEYWORDS]', $keywords_data, $prompt); - - // Add sector information if multiple sectors are configured - $sector_options = igny8_get_sector_options(); - if (count($sector_options) > 1) { - $sector_names = array_column($sector_options, 'label'); - $sector_text = "\n\nAvailable sectors: " . implode(', ', $sector_names) . "\nAssign each cluster to the most suitable sector from the above list."; - $prompt .= $sector_text; - } - - igny8_log_ai_event('Prompt Preparation', 'ai', $action, 'info', 'Keywords data formatted for prompt', 'Keywords: ' . substr($keywords_data, 0, 100) . '...'); - break; - - case 'ideas': - $clusters_data = igny8_format_clusters_for_ai($data); - $cluster_keywords_data = igny8_format_cluster_keywords_for_ai($data); - $prompt = str_replace('[IGNY8_CLUSTERS]', $clusters_data, $prompt); - $prompt = str_replace('[IGNY8_CLUSTER_KEYWORDS]', $cluster_keywords_data, $prompt); - break; - - case 'mapping': - $content_data = igny8_format_content_for_ai($data['content']); - $clusters_data = igny8_format_clusters_for_ai($data['clusters']); - $prompt = str_replace('[IGNY8_CONTENT]', $content_data, $prompt); - $prompt = str_replace('[IGNY8_CLUSTERS]', $clusters_data, $prompt); - break; - - case 'content_generation': - $idea_data = igny8_format_idea_for_ai($data['idea']); - $cluster_data = igny8_format_cluster_for_ai($data['cluster']); - $keywords_data = igny8_format_keywords_for_ai($data['keywords']); - $max_in_article_images = get_option('igny8_max_in_article_images', 1); - - // Safety check: Analyze outline to ensure we don't exceed available H2 sections - $safe_max_images = igny8_calculate_safe_image_quantity($idea_data, $max_in_article_images); - $image_prompts_data = igny8_format_image_prompts_for_ai($safe_max_images); - - $prompt = str_replace('[IGNY8_IDEA]', $idea_data, $prompt); - $prompt = str_replace('[IGNY8_CLUSTER]', $cluster_data, $prompt); - $prompt = str_replace('[IGNY8_KEYWORDS]', $keywords_data, $prompt); - $prompt = str_replace('[IGNY8_DESKTOP_QUANTITY]', $safe_max_images, $prompt); - $prompt = str_replace('[IMAGE_PROMPTS]', $image_prompts_data, $prompt); - // Content generation prompt is now self-contained with 3-part structure - igny8_log_ai_event('Prompt Preparation', 'ai', $action, 'info', 'Content generation data formatted', 'Idea: ' . substr($idea_data, 0, 100) . '..., Max In-Article Images: ' . $max_in_article_images . ', Safe Max: ' . $safe_max_images); - break; - } - - // Check if OpenAI function exists - if (!function_exists('igny8_call_openai')) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Process: igny8_call_openai function not found"); - } - igny8_log_ai_event('AI Function Missing', 'ai', $action, 'error', 'igny8_call_openai function not found', 'OpenAI integration not available'); - return false; - } - - // Get API configuration - $api_key = get_option('igny8_api_key'); - $model = get_option('igny8_model', 'gpt-4.1'); - - if (empty($api_key)) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Process: API key is empty or missing"); - } - igny8_log_ai_event('API Key Missing', 'ai', $action, 'error', 'OpenAI API key not configured', 'Please configure API key in settings'); - return false; - } - - igny8_log_ai_event('OpenAI API Call', 'ai', $action, 'info', 'Calling OpenAI API', 'Model: ' . $model . ', Prompt length: ' . strlen($prompt)); - - // Debug logging for CRON context - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Process: Making OpenAI API call - Model: " . $model . ", Prompt length: " . strlen($prompt)); - } - - // Call OpenAI API - $response = igny8_call_openai($prompt, $api_key, $model); - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Process: OpenAI response received: " . ($response ? 'Success' : 'Failed')); - if ($response && strlen($response) > 0) { - error_log("Igny8 AI Process: Response length: " . strlen($response) . " characters"); - } - } - - if (!$response) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Process: OpenAI API returned no response - this is the failure point"); - } - igny8_log_ai_event('OpenAI API Failed', 'ai', $action, 'error', 'OpenAI API returned no response', 'Check API key and network connection'); - return false; - } - - // Check if response starts with "Error:" - if (strpos($response, 'Error:') === 0) { - $error_details = [ - 'error_message' => $response, - 'model' => $model, - 'prompt_length' => strlen($prompt), - 'api_key_configured' => !empty($api_key), - 'timestamp' => current_time('mysql') - ]; - igny8_log_ai_event('OpenAI API Error', 'ai', $action, 'error', 'OpenAI API returned error', 'Error: ' . $response . ' | Details: ' . json_encode($error_details)); - return false; - } - - // igny8_call_openai returns the content directly, not wrapped in an array - igny8_log_ai_event('OpenAI Response Received', 'ai', $action, 'info', 'Raw response from OpenAI', 'Response length: ' . strlen($response) . ', Preview: ' . substr($response, 0, 100) . '...'); - - // Parse JSON response - try to extract JSON from response - $json_result = igny8_extract_json_from_response($response); - - if (!$json_result) { - $error_details = [ - 'raw_response' => $response, - 'response_length' => strlen($response), - 'model' => $model, - 'action' => $action, - 'timestamp' => current_time('mysql') - ]; - igny8_log_ai_event('JSON Parse Failed', 'ai', $action, 'error', 'Failed to parse OpenAI response as JSON', 'Raw response: ' . substr($response, 0, 500) . '... | Details: ' . json_encode($error_details)); - return false; - } - - igny8_log_ai_event('OpenAI Success', 'ai', $action, 'success', 'OpenAI API returned valid JSON', 'Result keys: ' . json_encode(array_keys($json_result))); - - // Normalize image prompts structure if needed - // Handle case where AI returns featured_image and in_article_images at top level - if (isset($json_result['featured_image']) && !isset($json_result['image_prompts'])) { - $json_result['image_prompts'] = [ - 'featured_image' => $json_result['featured_image'], - 'in_article_images' => $json_result['in_article_images'] ?? [] - ]; - igny8_log_ai_event('Image Prompts Normalized', 'ai', $action, 'info', 'Image prompts structure normalized from top-level fields', 'Moved featured_image and in_article_images to image_prompts object'); - } - - return $json_result; -} - -/** - * Parse 3-part response structure - */ -function igny8_parse_three_part_response($response) { - $result = []; - - // Extract metadata JSON - if (preg_match('/##Metadata Fields JSON##\s*(\{.*?\})/s', $response, $matches)) { - $metadata = json_decode($matches[1], true); - if ($metadata) { - $result['metadata'] = $metadata; - } - } - - // Extract content-related JSON - if (preg_match('/##Content-Related JSON##\s*(\{.*?\})/s', $response, $matches)) { - $content_data = json_decode($matches[1], true); - if ($content_data) { - $result['content_data'] = $content_data; - } - } - - // Extract image prompts - if (preg_match('/##Image Prompts Requirements:##.*?\[IMAGE_PROMPTS\]/s', $response, $matches)) { - $result['image_prompts'] = $matches[0]; - } - - // If we found at least one part, return the result - if (!empty($result)) { - return $result; - } - - return null; -} - -/** - * Extract JSON from AI response (handles cases where AI adds extra text) - */ -function igny8_extract_json_from_response($response) { - // First, try to parse the response directly as JSON - $json_result = json_decode($response, true); - if ($json_result) { - return $json_result; - } - - // Try to parse 3-part structure - $three_part_result = igny8_parse_three_part_response($response); - if ($three_part_result) { - return $three_part_result; - } - - // If that fails, try to find JSON within the response - // Look for content between curly braces - if (preg_match('/\{.*\}/s', $response, $matches)) { - $json_result = json_decode($matches[0], true); - if ($json_result) { - return $json_result; - } - } - - // Handle GPT-4o-mini format: ```json { ... } ``` or """json { ... } """ - if (preg_match('/```json\s*(\{.*?\})\s*```/s', $response, $matches)) { - $json_result = json_decode($matches[1], true); - if ($json_result) { - return $json_result; - } - } - - if (preg_match('/"""json\s*(\{.*?\})\s*"""/s', $response, $matches)) { - $json_result = json_decode($matches[1], true); - if ($json_result) { - return $json_result; - } - } - - // If still no luck, try to clean the response - $cleaned_response = trim($response); - - // Remove common prefixes that AI might add - $prefixes_to_remove = [ - 'Here is the JSON response:', - 'Here\'s the JSON:', - 'JSON Response:', - '```json', - '```', - '"""json', - '"""', - 'Here is the content in JSON format:', - 'The JSON response is:' - ]; - - foreach ($prefixes_to_remove as $prefix) { - if (stripos($cleaned_response, $prefix) === 0) { - $cleaned_response = trim(substr($cleaned_response, strlen($prefix))); - } - } - - // Remove common suffixes - $suffixes_to_remove = [ - '```', - '"""', - 'This JSON contains all the required fields.', - 'Hope this helps!', - 'Let me know if you need any modifications.' - ]; - - foreach ($suffixes_to_remove as $suffix) { - $pos = stripos($cleaned_response, $suffix); - if ($pos !== false) { - $cleaned_response = trim(substr($cleaned_response, 0, $pos)); - } - } - - // Try parsing the cleaned response - $json_result = json_decode($cleaned_response, true); - if ($json_result) { - return $json_result; - } - - // Last resort: try to find and extract JSON object - if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/', $response, $matches)) { - $json_result = json_decode($matches[0], true); - if ($json_result) { - return $json_result; - } - } - - return false; -} - -/** - * Set post categories from AI response - */ -function igny8_set_post_categories($post_id, $categories) { - if (empty($categories) || !is_array($categories)) { - return; - } - - $category_ids = []; - - foreach ($categories as $category) { - $category_id = igny8_get_or_create_category($category); - if ($category_id) { - $category_ids[] = $category_id; - } - } - - if (!empty($category_ids)) { - wp_set_post_categories($post_id, $category_ids); - } -} - -/** - * Get or create category from category string - */ -function igny8_get_or_create_category($category_string) { - if (empty($category_string)) { - return false; - } - - // Handle parent > child format - if (strpos($category_string, ' > ') !== false) { - $parts = explode(' > ', $category_string); - $parent_name = trim($parts[0]); - $child_name = trim($parts[1]); - - // Create or get parent category - $parent_term = get_term_by('name', $parent_name, 'category'); - if (!$parent_term) { - $parent_result = wp_insert_term($parent_name, 'category'); - if (!is_wp_error($parent_result)) { - $parent_id = $parent_result['term_id']; - } else { - return false; - } - } else { - $parent_id = $parent_term->term_id; - } - - // Create or get child category - $child_term = get_term_by('name', $child_name, 'category'); - if (!$child_term) { - $child_result = wp_insert_term($child_name, 'category', ['parent' => $parent_id]); - if (!is_wp_error($child_result)) { - return $child_result['term_id']; - } - } else { - return $child_term->term_id; - } - } else { - // Single category - $term = get_term_by('name', $category_string, 'category'); - if (!$term) { - $result = wp_insert_term($category_string, 'category'); - if (!is_wp_error($result)) { - return $result['term_id']; - } - } else { - return $term->term_id; - } - } - - return false; -} - -/** - * Store cluster and sector metadata - */ -function igny8_store_content_metadata($post_id, $ai_response) { - global $wpdb; - - // Store cluster and sector info if available - $cluster_id = $ai_response['cluster_id'] ?? null; - $sector_id = $ai_response['sector_id'] ?? null; - - if ($cluster_id) { - update_post_meta($post_id, '_igny8_cluster_id', $cluster_id); - } - - if ($sector_id) { - update_post_meta($post_id, '_igny8_sector_id', $sector_id); - } - - // Store keywords if available - if (!empty($ai_response['keywords_used'])) { - update_post_meta($post_id, '_igny8_keywords_used', wp_json_encode($ai_response['keywords_used'])); - } -} - -/** - * Log AI event for debugging - */ -function igny8_log_ai_event($event, $module, $action, $status = 'info', $message = '', $details = '') { - $ai_logs = get_option('igny8_ai_logs', []); - - $log_entry = [ - 'timestamp' => current_time('mysql'), - 'event' => $event, - 'module' => $module, - 'action' => $action, - 'status' => $status, - 'message' => $message, - 'details' => $details - ]; - - // Add to beginning of array (newest first) - array_unshift($ai_logs, $log_entry); - - // Keep only last 100 events - $ai_logs = array_slice($ai_logs, 0, 100); - - update_option('igny8_ai_logs', $ai_logs); -} - -/** - * Format keywords data for AI processing - */ -function igny8_format_keywords_for_ai($keywords) { - $formatted = []; - foreach ($keywords as $keyword) { - $formatted[] = [ - 'id' => isset($keyword->id) ? $keyword->id : null, - 'keyword' => isset($keyword->keyword) ? $keyword->keyword : '', - 'search_volume' => isset($keyword->search_volume) ? $keyword->search_volume : 0, - 'difficulty' => isset($keyword->difficulty) ? $keyword->difficulty : 0 - ]; - } - return json_encode($formatted, JSON_PRETTY_PRINT); -} - -/** - * Format clusters data for AI processing - */ -function igny8_format_clusters_for_ai($clusters) { - $formatted = []; - foreach ($clusters as $cluster) { - $formatted[] = [ - 'id' => $cluster->id, - 'name' => $cluster->cluster_name, - 'sector_id' => $cluster->sector_id, - 'keyword_count' => $cluster->keyword_count, - 'keywords' => $cluster->keywords_list ?? '' - ]; - } - return json_encode($formatted, JSON_PRETTY_PRINT); -} - -/** - * Format cluster keywords data for AI processing - */ -function igny8_format_cluster_keywords_for_ai($clusters) { - $formatted = []; - foreach ($clusters as $cluster) { - $formatted[] = [ - 'cluster_id' => $cluster->id, - 'cluster_name' => $cluster->cluster_name, - 'keywords' => $cluster->keywords_list ? explode(', ', $cluster->keywords_list) : [] - ]; - } - return json_encode($formatted, JSON_PRETTY_PRINT); -} - -/** - * Format content data for AI processing - */ -function igny8_format_content_for_ai($content) { - $formatted = []; - foreach ($content as $item) { - $formatted[] = [ - 'id' => $item->ID, - 'title' => $item->post_title, - 'content' => wp_strip_all_tags($item->post_content), - 'type' => $item->post_type, - 'excerpt' => $item->post_excerpt - ]; - } - return json_encode($formatted, JSON_PRETTY_PRINT); -} - -/** - * Add AI processing task to queue - */ -function igny8_add_ai_queue_task($action, $data, $user_id = null) { - global $wpdb; - - $user_id = $user_id ?: get_current_user_id(); - - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_ai_queue', - [ - 'action' => $action, - 'data' => json_encode($data), - 'user_id' => $user_id, - 'status' => 'pending', - 'created_at' => current_time('mysql'), - 'processed_at' => null, - 'result' => null, - 'error_message' => null - ], - ['%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s'] - ); - - return $result ? $wpdb->insert_id : false; -} - -/** - * Process AI queue tasks - */ -function igny8_process_ai_queue($limit = null) { - if ($limit === null) { - error_log('Igny8 AI Queue: No limit provided'); - return 0; - } - global $wpdb; - - // Get pending tasks - $tasks = $wpdb->get_results($wpdb->prepare(" - SELECT * FROM {$wpdb->prefix}igny8_ai_queue - WHERE status = 'pending' - ORDER BY created_at ASC - LIMIT %d - ", $limit)); - - $processed = 0; - - foreach ($tasks as $task) { - // Mark as processing - $wpdb->update( - $wpdb->prefix . 'igny8_ai_queue', - ['status' => 'processing'], - ['id' => $task->id], - ['%s'], - ['%d'] - ); - - try { - $data = json_decode($task->data, true); - $result = igny8_process_ai_request($task->action, $data, igny8_get_ai_prompt_for_action($task->action)); - - if ($result) { - // Mark as completed - $wpdb->update( - $wpdb->prefix . 'igny8_ai_queue', - [ - 'status' => 'completed', - 'processed_at' => current_time('mysql'), - 'result' => json_encode($result) - ], - ['id' => $task->id], - ['%s', '%s', '%s'], - ['%d'] - ); - - // Process the result based on action - igny8_process_ai_queue_result($task->action, $result); - - } else { - // Mark as failed - $wpdb->update( - $wpdb->prefix . 'igny8_ai_queue', - [ - 'status' => 'failed', - 'processed_at' => current_time('mysql'), - 'error_message' => 'AI processing returned no result' - ], - ['id' => $task->id], - ['%s', '%s', '%s'], - ['%d'] - ); - } - - } catch (Exception $e) { - // Mark as failed with error - $wpdb->update( - $wpdb->prefix . 'igny8_ai_queue', - [ - 'status' => 'failed', - 'processed_at' => current_time('mysql'), - 'error_message' => $e->getMessage() - ], - ['id' => $task->id], - ['%s', '%s', '%s'], - ['%d'] - ); - } - - $processed++; - } - - return $processed; -} - -/** - * Get AI prompt for specific action - */ -function igny8_get_ai_prompt_for_action($action) { - switch ($action) { - case 'clustering': - return igny8_get_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt()); - case 'ideas': - return igny8_get_ai_setting('ideas_prompt', igny8_get_default_ideas_prompt()); - case 'content_generation': - return get_option('igny8_content_generation_prompt', igny8_content_generation_prompt()); - default: - return ''; - } -} - -/** - * Format idea data for AI processing - */ -function igny8_format_idea_for_ai($idea) { - if (is_object($idea)) { - // Handle structured description (JSON) vs plain text - $description = $idea->idea_description ?? ''; - - // Check if description is JSON and format it for AI - if (!empty($description)) { - $decoded = json_decode($description, true); - if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { - // Format structured description for AI - $formatted_description = igny8_format_structured_description_for_ai($decoded); - } else { - // Use as plain text - $formatted_description = $description; - } - } else { - $formatted_description = ''; - } - - return sprintf( - "Title: %s\nDescription: %s\nStructure: %s\nType: %s\nPriority: %s\nEstimated Word Count: %s\nStatus: %s", - $idea->idea_title ?? '', - $formatted_description, - $idea->content_structure ?? '', - $idea->content_type ?? '', - $idea->priority ?? '', - $idea->estimated_word_count ?? '', - $idea->status ?? '' - ); - } - return 'Idea data not available'; -} - -/** - * Format structured description for AI processing - */ -function igny8_format_structured_description_for_ai($structured_description) { - if (!is_array($structured_description) || empty($structured_description['H2'])) { - return 'No structured outline available'; - } - - $formatted = "Content Outline:\n\n"; - - foreach ($structured_description['H2'] as $h2_section) { - $formatted .= "## " . $h2_section['heading'] . "\n"; - - if (!empty($h2_section['subsections'])) { - foreach ($h2_section['subsections'] as $h3_section) { - $formatted .= "### " . $h3_section['subheading'] . "\n"; - $formatted .= "Content Type: " . $h3_section['content_type'] . "\n"; - $formatted .= "Details: " . $h3_section['details'] . "\n\n"; - } - } - } - - return $formatted; -} - - -/** - * Format cluster data for AI processing - */ -function igny8_format_cluster_for_ai($cluster) { - if (is_object($cluster)) { - return sprintf( - "Cluster Name: %s\nDescription: %s\nStatus: %s\nKeyword Count: %s\nTotal Volume: %s\nAverage Difficulty: %s", - $cluster->cluster_name ?? '', - $cluster->description ?? '', - $cluster->status ?? '', - $cluster->keyword_count ?? 0, - $cluster->total_volume ?? 0, - $cluster->avg_difficulty ?? 0 - ); - } - return 'Cluster data not available'; -} - -/** - * Format image prompts structure for AI content generation - * - * @param int $max_in_article_images Number of in-article images to generate - * @return string JSON structure for image prompts - */ -function igny8_format_image_prompts_for_ai($max_in_article_images = 1) { - $image_prompts = [ - 'featured_image' => '[Detailed prompt for featured/hero image based on the article title and main topic]', - 'in_article_images' => [] - ]; - - // Generate in-article image prompts based on quantity - // Each prompt corresponds to H2 sections: 1st H2, 2nd H2, 3rd H2, etc. - for ($i = 1; $i <= $max_in_article_images; $i++) { - $section_number = $i; // Start from 1st H2 (section 1) - $image_prompts['in_article_images'][] = [ - 'prompt-img-' . $i => '[Detailed image prompt based on topics of section ' . $section_number . '(' . $section_number . getOrdinalSuffix($section_number) . ' H2 in outline)]' - ]; - } - - return wp_json_encode($image_prompts, JSON_PRETTY_PRINT); -} - -/** - * Get ordinal suffix for numbers (1st, 2nd, 3rd, 4th, etc.) - * - * @param int $number The number to get ordinal suffix for - * @return string The ordinal suffix - */ -function getOrdinalSuffix($number) { - $ends = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th']; - if ((($number % 100) >= 11) && (($number % 100) <= 13)) { - return 'th'; - } - return $ends[$number % 10]; -} - - - - - - - - - - - -/** - * Create responsive image data for image tracking - * - * @param int $post_id WordPress post ID - * @return void - */ -function igny8_create_responsive_image_data($post_id) { - // Get article images data - $article_images_data = get_post_meta($post_id, '_igny8_article_images_data', true); - if (empty($article_images_data)) { - return; - } - - $article_images_data = json_decode($article_images_data, true); - if (!is_array($article_images_data) || empty($article_images_data)) { - return; - } - - // Create responsive data structure - $responsive_data = []; - - foreach ($article_images_data as $index => $image_data) { - // Find the prompt key (prompt-img-X) - $prompt_key = null; - $prompt_value = null; - - foreach ($image_data as $key => $value) { - if (strpos($key, 'prompt-img-') === 0) { - $prompt_key = $key; - $prompt_value = $value; - break; - } - } - - if (!$prompt_key || !$prompt_value) { - continue; - } - - // Extract image number from prompt key - $image_number = str_replace('prompt-img-', '', $prompt_key); - $section_number = intval($image_number) + 1; // 2nd, 3rd, 4th H2, etc. - - // Create responsive data entry - $responsive_data[] = [ - 'desktop' => [ - 'attachment_id' => null, // Will be filled when images are generated - 'url' => null - ], - 'mobile' => [ - 'attachment_id' => null, // Will be filled when images are generated - 'url' => null - ], - 'section' => 'Section ' . $section_number, - 'prompt' => $prompt_value - ]; - } - - // Save responsive data - update_post_meta($post_id, '_igny8_article_images_responsive', wp_json_encode($responsive_data)); -} - -/** - * Calculate safe image quantity based on outline analysis - * - * @param string $idea_data The idea data containing the outline - * @param int $max_in_article_images Maximum images requested - * @return int Safe maximum images (max 1 less than total H2 sections) - */ -function igny8_calculate_safe_image_quantity($idea_data, $max_in_article_images) { - // Count H2 sections in the outline - $h2_count = igny8_count_h2_sections_in_outline($idea_data); - - // Safety rule: Max images = H2 count - 1 (since we start from 2nd H2) - $max_possible_images = max(0, $h2_count - 1); - - // Use the smaller of requested max or possible max - $safe_max = min($max_in_article_images, $max_possible_images); - - // Log the safety calculation - igny8_log_ai_event('Image Quantity Safety Check', 'ai', 'content_generation', 'info', - 'Calculated safe image quantity', - 'H2 Sections: ' . $h2_count . ', Requested: ' . $max_in_article_images . ', Safe Max: ' . $safe_max - ); - - return $safe_max; -} - -/** - * Count H2 sections in the outline - * - * @param string $idea_data The idea data containing the outline - * @return int Number of H2 sections found - */ -function igny8_count_h2_sections_in_outline($idea_data) { - // Look for H2 patterns in the outline - // Common patterns: "## ", "**", "H2:", "Section", etc. - $patterns = [ - '/##\s+/', // Markdown H2: ## Section - '/\*\*[^*]+\*\*/', // Bold text: **Section** - '/H2:\s*[^\n]+/', // H2: Section - '/Section\s+\d+/i', // Section 1, Section 2, etc. - '/\d+\.\s+[A-Z][^\.]+\./', // Numbered sections: 1. Section Title. - '/^[A-Z][^\.]+\.$/m' // Title case sections ending with period - ]; - - $max_count = 0; - - foreach ($patterns as $pattern) { - preg_match_all($pattern, $idea_data, $matches); - $count = count($matches[0]); - if ($count > $max_count) { - $max_count = $count; - } - } - - // If no clear H2 patterns found, estimate based on content length - if ($max_count === 0) { - // Estimate: roughly 1 H2 per 200-300 words in outline - $word_count = str_word_count($idea_data); - $max_count = max(3, floor($word_count / 250)); // Minimum 3 sections - } - - // Ensure we have at least 2 sections (1 for featured, 1 for in-article) - return max(2, $max_count); -} - - -/** - * Add in-article image to post meta for meta box integration - * - * @param int $post_id WordPress post ID - * @param int $attachment_id WordPress attachment ID - * @param string $label Image label (e.g., 'desktop-1', 'mobile-2') - * @param string $device Device type ('desktop' or 'mobile') - * @param int|null $section Section number (optional) - * @return bool Success status - */ -function igny8_add_inarticle_image_meta($post_id, $attachment_id, $label, $device = 'desktop', $section = null) { - error_log("[IGNY8 DEBUG] igny8_add_inarticle_image_meta called with post_id: $post_id, attachment_id: $attachment_id, label: $label, device: $device, section: $section"); - - $url = wp_get_attachment_url($attachment_id); - if (!$url) { - error_log("[IGNY8 DEBUG] Failed to get attachment URL for attachment ID: $attachment_id"); - return false; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images)) { - $images = []; - } - - $images[$label] = [ - 'label' => $label, - 'attachment_id' => $attachment_id, - 'url' => $url, - 'device' => $device, - 'section' => $section, - ]; - - error_log("[IGNY8 DEBUG] About to save images meta for post $post_id: " . print_r($images, true)); - - $result = update_post_meta($post_id, '_igny8_inarticle_images', $images); - - error_log("[IGNY8 DEBUG] update_post_meta result: " . ($result ? 'SUCCESS' : 'FAILED')); - - return $result !== false; -} -/** - * Process AI queue result and save to database - */ -function igny8_process_ai_queue_result($action, $result) { - global $wpdb; - - switch ($action) { - case 'clustering': - if (isset($result['clusters'])) { - // Get sector options for assignment logic - $sector_options = igny8_get_sector_options(); - $sector_count = count($sector_options); - - foreach ($result['clusters'] as $cluster_data) { - // Determine sector_id based on sector count - $sector_id = 1; // Default fallback - - if ($sector_count == 1) { - // Only 1 sector: assign all clusters to that sector - $sector_id = $sector_options[0]['value']; - } elseif ($sector_count > 1) { - // Multiple sectors: use AI response sector assignment - if (isset($cluster_data['sector']) && !empty($cluster_data['sector'])) { - // Find sector ID by matching sector name from AI response - foreach ($sector_options as $sector) { - if (strtolower(trim($sector['label'])) === strtolower(trim($cluster_data['sector']))) { - $sector_id = $sector['value']; - break; - } - } - } - // If no match found or no sector in AI response, use first sector as fallback - if ($sector_id == 1 && !isset($cluster_data['sector'])) { - $sector_id = $sector_options[0]['value']; - } - } - - $wpdb->insert( - $wpdb->prefix . 'igny8_clusters', - [ - 'cluster_name' => sanitize_text_field($cluster_data['name']), - 'sector_id' => $sector_id, - 'status' => 'active', - 'keyword_count' => count($cluster_data['keywords']), - 'total_volume' => 0, - 'avg_difficulty' => 0, - 'mapped_pages_count' => 0, - 'created_at' => current_time('mysql') - ], - ['%s', '%d', '%s', '%d', '%d', '%f', '%d', '%s'] - ); - - $cluster_id = $wpdb->insert_id; - - // Trigger taxonomy term creation for AI-generated cluster - do_action('igny8_cluster_added', $cluster_id); - - // Update keywords with cluster_id - foreach ($cluster_data['keywords'] as $keyword_name) { - $wpdb->update( - $wpdb->prefix . 'igny8_keywords', - ['cluster_id' => $cluster_id], - ['keyword' => $keyword_name], - ['%d'], - ['%s'] - ); - } - - igny8_update_cluster_metrics($cluster_id); - } - } - break; - - case 'ideas': - if (isset($result['ideas'])) { - foreach ($result['ideas'] as $idea_data) { - $wpdb->insert( - $wpdb->prefix . 'igny8_content_ideas', - [ - 'idea_title' => sanitize_text_field($idea_data['title']), - 'idea_description' => sanitize_textarea_field($idea_data['description']), - 'content_structure' => sanitize_text_field($idea_data['type']), - 'content_type' => 'post', // Default to post for AI generated ideas - 'keyword_cluster_id' => intval($idea_data['cluster_id']), - 'priority' => sanitize_text_field($idea_data['priority']), - 'status' => 'draft', - 'estimated_word_count' => intval($idea_data['estimated_word_count']), - 'ai_generated' => 1, - 'created_at' => current_time('mysql') - ], - ['%s', '%s', '%s', '%d', '%s', '%s', '%d', '%d', '%s'] - ); - } - } - break; - - case 'mapping': - if (isset($result['mappings'])) { - foreach ($result['mappings'] as $mapping_data) { - if ($mapping_data['relevance_score'] >= 0.7) { - $cluster_term_id = $wpdb->get_var($wpdb->prepare(" - SELECT cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d - ", $mapping_data['cluster_id'])); - - if ($cluster_term_id) { - wp_set_object_terms( - $mapping_data['content_id'], - $cluster_term_id, - 'clusters', - true - ); - } - } - } - } - break; - - case 'content_generation': - if (isset($result['title']) && isset($result['content'])) { - // Pass task_id through the AI response for proper task updating - if (isset($data['task_id'])) { - $result['task_id'] = $data['task_id']; - } - - // Pass cluster and sector data from original task - if (isset($data['cluster']) && $data['cluster']) { - $result['cluster_id'] = $data['cluster']->id; - $result['sector_id'] = $data['cluster']->sector_id; - } - - // Create WordPress post from AI response - $post_id = igny8_create_post_from_ai_response($result); - - if ($post_id) { - // Log successful content generation - igny8_log_ai_event('content_created', 'writer', 'content_generation', 'success', - 'Post created successfully', "Post ID: {$post_id}"); - } else { - // Log failure - igny8_log_ai_event('content_failed', 'writer', 'content_generation', 'error', - 'Failed to create post from AI response'); - } - } else { - // Log invalid response format - igny8_log_ai_event('invalid_response', 'writer', 'content_generation', 'error', - 'AI response missing required fields (title, content)'); - } - break; - } -} - - - -/** - * Create WordPress post from AI response - */ -function igny8_create_post_from_ai_response($ai_response) { - global $wpdb; - - try { - // Get cluster and sector data from the task, not from AI response - $cluster_id = null; - $sector_id = null; - - if (!empty($ai_response['task_id'])) { - error_log('Igny8: Looking up task_id: ' . intval($ai_response['task_id'])); - echo "Igny8 DEBUG: Looking up task_id: " . intval($ai_response['task_id']) . "
"; - - $task = $wpdb->get_row($wpdb->prepare( - "SELECT cluster_id, content_structure, content_type FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", - intval($ai_response['task_id']) - )); - - error_log('Igny8: Task lookup result: ' . ($task ? 'Found task' : 'Task not found')); - echo "Igny8 DEBUG: Task lookup result: " . ($task ? 'Found task' : 'Task not found') . "
"; - - if ($task) { - error_log('Igny8: Task cluster_id: ' . ($task->cluster_id ?: 'NULL')); - echo "Igny8 DEBUG: Task cluster_id: " . ($task->cluster_id ?: 'NULL') . "
"; - } - - if ($task && $task->cluster_id) { - $cluster_id = $task->cluster_id; - // Get sector_id from cluster (this is the sector taxonomy term ID) - $cluster_data = $wpdb->get_row($wpdb->prepare( - "SELECT sector_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - intval($cluster_id) - )); - if ($cluster_data) { - $sector_id = $cluster_data->sector_id; // This is already the taxonomy term ID - error_log('Igny8: Found sector_id (term ID): ' . $sector_id); - echo "Igny8 DEBUG: Found sector_id (term ID): " . $sector_id . "
"; - } - } - } else { - error_log('Igny8: No task_id in AI response'); - echo "Igny8 DEBUG: No task_id in AI response
"; - } - - // Get content structure and type from task (not from AI response) - $content_structure = $task->content_structure ?? 'cluster_hub'; - $content_type = $task->content_type ?? 'post'; - $post_type = igny8_map_content_type_to_post_type($content_structure); - - // Prepare content for processing - $content = $ai_response['content'] ?? ''; - $editor_type = get_option('igny8_editor_type', 'block'); - error_log("IGNY8 DEBUG - EDITOR TYPE FROM DB: " . $editor_type); - - // Content is now direct HTML from the new prompt format - // No need to check for nested structures or convert from JSON - igny8_log_ai_event('Content Format Detection', 'writer', 'content_generation', 'info', 'Using direct HTML content from AI response', 'Editor type: ' . $editor_type); - - // NEW PIPELINE: Process content through integrated pipeline - // Step 1: Convert to Gutenberg blocks if using block editor - if ($editor_type === 'block') { - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - Block editor path selected"); - $final_block_content = igny8_convert_to_wp_blocks($content); - error_log("IGNY8 DEBUG - Conversion Completed"); - - // Step 1.5: Validate and fix block structure - $final_block_content = igny8_validate_and_fix_blocks($final_block_content); - - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - About to call insert_igny8_shortcode_blocks_into_blocks()"); - error_log("IGNY8 DEBUG: CALL LOCATION - igny8_create_post_from_ai_response() -> Block Editor Path -> Line 1186"); - $final_block_content = insert_igny8_shortcode_blocks_into_blocks($final_block_content); - - // Check if shortcodes were successfully injected - $has_shortcode = false; - foreach (parse_blocks($final_block_content) as $block) { - if ( - $block['blockName'] === 'core/shortcode' && - isset($block['innerContent']) && - is_array($block['innerContent']) && - preg_match('/\[igny8-image.*?\]/', implode('', $block['innerContent'])) - ) { - $has_shortcode = true; - break; - } - } - - if (!$has_shortcode) { - error_log("IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in parsed blocks"); - igny8_log_ai_event('Shortcode Injection Failed', 'writer', 'content_generation', 'warning', 'No shortcodes found after injection - proceeding without shortcodes', 'Editor type: ' . $editor_type); - // FALLBACK: Continue with post creation without shortcodes - $content = $final_block_content; - } else { - $content = $final_block_content; - } - - igny8_log_ai_event('Content Wrapped as Blocks', 'writer', 'content_generation', 'info', 'HTML content wrapped as Gutenberg blocks', 'Editor type: ' . $editor_type); - } else { - // For classic editor, use plain shortcode logic - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - Classic editor path selected"); - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - About to call insert_igny8_image_shortcodes_classic()"); - error_log("IGNY8 DEBUG: CALL LOCATION - igny8_create_post_from_ai_response() -> Classic Editor Path -> Line 1214"); - $content = insert_igny8_image_shortcodes_classic($content); - - // Check if shortcodes were successfully injected - if (strpos($content, '[igny8-image') === false) { - error_log("IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in content"); - igny8_log_ai_event('Shortcode Injection Failed', 'writer', 'content_generation', 'warning', 'No shortcodes found after injection - proceeding without shortcodes', 'Editor type: ' . $editor_type); - // FALLBACK: Continue with post creation without shortcodes - } - - igny8_log_ai_event('Content Format Detection', 'writer', 'content_generation', 'info', 'Using HTML content with shortcodes for Classic Editor', 'Editor type: ' . $editor_type); - } - - // Get new content decision setting AFTER content processing - $new_content_action = get_option('igny8_new_content_action', 'draft'); - $post_status = ($new_content_action === 'publish') ? 'publish' : 'draft'; - - // Debug logging - error_log('Igny8 DEBUG: New content action setting: ' . $new_content_action); - error_log('Igny8 DEBUG: Post status will be: ' . $post_status); - error_log('Igny8 DEBUG: All options with igny8_new_content_action: ' . print_r(get_option('igny8_new_content_action'), true)); - echo "Igny8 DEBUG: New content action setting: " . $new_content_action . "
"; - echo "Igny8 DEBUG: Post status will be: " . $post_status . "
"; - - $post_data = [ - 'post_title' => sanitize_text_field($ai_response['title'] ?? 'AI Generated Content'), - 'post_content' => $content, - 'post_excerpt' => sanitize_textarea_field($ai_response['meta_description'] ?? ''), - 'post_status' => $post_status, // Use setting from New Content Decision - 'post_type' => $post_type, - 'post_author' => get_current_user_id(), - 'post_date' => current_time('mysql'), - 'meta_input' => [ - '_igny8_ai_generated' => 1, - '_igny8_content_type' => $content_structure, - '_igny8_word_count' => intval($ai_response['word_count'] ?? 0), - '_igny8_keywords_used' => wp_json_encode($ai_response['keywords_used'] ?? []), - '_igny8_internal_links' => wp_json_encode($ai_response['internal_link_opportunities'] ?? []) - ] - ]; - - error_log("IGNY8 DEBUG - POST CONTENT ABOUT TO SAVE:\n" . $post_data['post_content']); - - // Optional debug file write - if (defined('IGNY8_DEBUG_BLOCKS') && IGNY8_DEBUG_BLOCKS === true) { - file_put_contents(WP_CONTENT_DIR . '/igny8-block-output.html', $content); - } - - // Create the post - $post_id = wp_insert_post($post_data); - - if (is_wp_error($post_id)) { - error_log('Igny8: Failed to create post - ' . $post_id->get_error_message()); - igny8_log_ai_event('WordPress Post Creation Failed', 'writer', 'content_generation', 'error', 'Failed to create WordPress post', 'Error: ' . $post_id->get_error_message()); - return false; - } - - igny8_log_ai_event('WordPress Post Created', 'writer', 'content_generation', 'success', 'WordPress post created successfully', 'Post ID: ' . $post_id . ', Title: ' . $ai_response['title']); - - // Note: Task record updating is handled by the AJAX handler - // This function only creates the WordPress post and links it to the task - - // Save AI-generated meta fields to post meta - if (!empty($ai_response['meta_title'])) { - update_post_meta($post_id, '_igny8_meta_title', sanitize_text_field($ai_response['meta_title'])); - igny8_log_ai_event('SEO Meta Title Saved', 'writer', 'content_generation', 'success', 'Meta title saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_meta_title'); - } - if (!empty($ai_response['meta_description'])) { - update_post_meta($post_id, '_igny8_meta_description', sanitize_textarea_field($ai_response['meta_description'])); - igny8_log_ai_event('SEO Meta Description Saved', 'writer', 'content_generation', 'success', 'Meta description saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_meta_description'); - } - // === Igny8 Keyword Meta === - if (!empty($ai_response['primary_keyword'])) { - update_post_meta($post_id, '_igny8_primary_keywords', sanitize_text_field($ai_response['primary_keyword'])); - igny8_log_ai_event('Primary Keywords Saved', 'writer', 'content_generation', 'success', 'Primary keywords saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_primary_keywords'); - } - if (!empty($ai_response['keywords'])) { - update_post_meta($post_id, '_igny8_primary_keywords', sanitize_text_field($ai_response['keywords'])); - igny8_log_ai_event('Primary Keywords Saved', 'writer', 'content_generation', 'success', 'Primary keywords saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_primary_keywords'); - } - - if (!empty($ai_response['secondary_keywords'])) { - $secondary = is_array($ai_response['secondary_keywords']) - ? implode(', ', array_map('sanitize_text_field', $ai_response['secondary_keywords'])) - : sanitize_text_field($ai_response['secondary_keywords']); - update_post_meta($post_id, '_igny8_secondary_keywords', $secondary); - igny8_log_ai_event('Secondary Keywords Saved', 'writer', 'content_generation', 'success', 'Secondary keywords saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_secondary_keywords'); - } - if (!empty($ai_response['word_count'])) { - update_post_meta($post_id, '_igny8_word_count', intval($ai_response['word_count'])); - } - - // === Save Image Prompts === - // Handle featured image prompt (direct field in new format) - if (!empty($ai_response['featured_image'])) { - update_post_meta($post_id, '_igny8_featured_image_prompt', sanitize_textarea_field($ai_response['featured_image'])); - igny8_log_ai_event('Featured Image Prompt Saved', 'writer', 'content_generation', 'success', 'Featured image prompt saved to post meta', 'Post ID: ' . $post_id . ', Field: _igny8_featured_image_prompt'); - } - - // Handle in-article image prompts (direct field in new format) - if (!empty($ai_response['in_article_images']) && is_array($ai_response['in_article_images'])) { - $article_images_data = []; - foreach ($ai_response['in_article_images'] as $index => $image_data) { - // Handle both formats: array of strings or array of objects - if (is_string($image_data)) { - // Old format: array of strings - $clean_value = wp_strip_all_tags($image_data); - $article_images_data[] = [ - 'prompt-img-' . ($index + 1) => sanitize_textarea_field($clean_value) - ]; - } elseif (is_array($image_data)) { - // New format: array of objects with prompt-img-X keys - $sanitized_data = []; - foreach ($image_data as $key => $value) { - if (strpos($key, 'prompt-img-') === 0) { - // Strip HTML tags and sanitize to ensure only plain text - $clean_value = wp_strip_all_tags($value); - $sanitized_data[$key] = sanitize_textarea_field($clean_value); - } - } - if (!empty($sanitized_data)) { - $article_images_data[] = $sanitized_data; - } - } - } - - if (!empty($article_images_data)) { - update_post_meta($post_id, '_igny8_article_images_data', wp_json_encode($article_images_data)); - igny8_log_ai_event('In-Article Image Prompts Saved', 'writer', 'content_generation', 'success', 'In-article image prompts saved to post meta', 'Post ID: ' . $post_id . ', Count: ' . count($article_images_data) . ', Field: _igny8_article_images_data'); - } - } - - // Handle legacy image_prompts format for backward compatibility - // DISABLED: No longer saving to _igny8_image_prompts field - // if (!empty($ai_response['image_prompts'])) { - // update_post_meta($post_id, '_igny8_image_prompts', wp_json_encode($ai_response['image_prompts'])); - // igny8_log_ai_event('Legacy Image Prompts Saved', 'writer', 'content_generation', 'info', 'Legacy image prompts format saved for backward compatibility', 'Post ID: ' . $post_id . ', Field: _igny8_image_prompts'); - // } - - // === Associate Cluster Term === - $cluster_success = false; - if (!empty($cluster_id)) { - global $wpdb; - error_log('Igny8: Attempting to associate cluster_id: ' . intval($cluster_id)); - echo "Igny8 DEBUG: Attempting to associate cluster_id: " . intval($cluster_id) . "
"; - - $cluster_term_id = $wpdb->get_var($wpdb->prepare(" - SELECT cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d - ", intval($cluster_id))); - - error_log('Igny8: Found cluster_term_id: ' . ($cluster_term_id ?: 'NULL')); - echo "Igny8 DEBUG: Found cluster_term_id: " . ($cluster_term_id ?: 'NULL') . "
"; - - if ($cluster_term_id) { - // Check if taxonomy exists - if (!taxonomy_exists('clusters')) { - error_log('Igny8: ERROR - clusters taxonomy does not exist!'); - echo "Igny8 DEBUG: ERROR - clusters taxonomy does not exist!
"; - igny8_log_ai_event('Cluster Association Failed', 'writer', 'content_generation', 'error', 'Clusters taxonomy does not exist', 'Post ID: ' . $post_id . ', Cluster ID: ' . $cluster_id); - } else { - error_log('Igny8: clusters taxonomy exists, attempting association...'); - echo "Igny8 DEBUG: clusters taxonomy exists, attempting association...
"; - $cluster_result = wp_set_object_terms($post_id, intval($cluster_term_id), 'clusters', false); - $cluster_success = !is_wp_error($cluster_result); - error_log('Igny8: Cluster association result: ' . ($cluster_success ? 'SUCCESS' : 'FAILED - ' . ($cluster_result->get_error_message() ?? 'Unknown error'))); - echo "Igny8 DEBUG: Cluster association result: " . ($cluster_success ? 'SUCCESS' : 'FAILED - ' . ($cluster_result->get_error_message() ?? 'Unknown error')) . "
"; - - if ($cluster_success) { - igny8_log_ai_event('Cluster Associated', 'writer', 'content_generation', 'success', 'Post associated with cluster taxonomy', 'Post ID: ' . $post_id . ', Cluster ID: ' . $cluster_id . ', Term ID: ' . $cluster_term_id); - } else { - igny8_log_ai_event('Cluster Association Failed', 'writer', 'content_generation', 'error', 'Failed to associate cluster', 'Post ID: ' . $post_id . ', Error: ' . ($cluster_result->get_error_message() ?? 'Unknown')); - } - } - } else { - error_log('Igny8: Cluster term not found for cluster_id ' . intval($cluster_id)); - echo "Igny8 DEBUG: Cluster term not found for cluster_id " . intval($cluster_id) . "
"; - igny8_log_ai_event('Cluster Term Not Found', 'writer', 'content_generation', 'warning', 'Cluster term not found in database', 'Post ID: ' . $post_id . ', Cluster ID: ' . $cluster_id); - } - } else { - error_log('Igny8: No cluster_id found in task'); - echo "Igny8 DEBUG: No cluster_id found in task
"; - } - - // === Associate Sector Term === - $sector_success = false; - if (!empty($sector_id)) { - error_log('Igny8: Attempting to associate sector_id: ' . intval($sector_id)); - echo "Igny8 DEBUG: Attempting to associate sector_id: " . intval($sector_id) . "
"; - - // sector_id is already the taxonomy term ID, no need to look it up - $sector_term_id = intval($sector_id); - - error_log('Igny8: Using sector_term_id directly: ' . $sector_term_id); - echo "Igny8 DEBUG: Using sector_term_id directly: " . $sector_term_id . "
"; - - // Check if taxonomy exists - if (!taxonomy_exists('sectors')) { - error_log('Igny8: ERROR - sectors taxonomy does not exist!'); - echo "Igny8 DEBUG: ERROR - sectors taxonomy does not exist!
"; - } else { - error_log('Igny8: sectors taxonomy exists, attempting association...'); - echo "Igny8 DEBUG: sectors taxonomy exists, attempting association...
"; - $sector_result = wp_set_object_terms($post_id, $sector_term_id, 'sectors', false); - $sector_success = !is_wp_error($sector_result); - error_log('Igny8: Sector association result: ' . ($sector_success ? 'SUCCESS' : 'FAILED - ' . ($sector_result->get_error_message() ?? 'Unknown error'))); - echo "Igny8 DEBUG: Sector association result: " . ($sector_success ? 'SUCCESS' : 'FAILED - ' . ($sector_result->get_error_message() ?? 'Unknown error')) . "
"; - } - } else { - error_log('Igny8: No sector_id found in task'); - echo "Igny8 DEBUG: No sector_id found in task
"; - } - - // Handle tags if content type supports them - if (in_array($post_type, ['post', 'product']) && !empty($ai_response['tags'])) { - $tags = array_map('trim', $ai_response['tags']); - wp_set_post_tags($post_id, $tags); - igny8_log_ai_event('Tags Added', 'writer', 'content_generation', 'success', 'Post tags added', 'Post ID: ' . $post_id . ', Tags: ' . implode(', ', $tags)); - } - - // Handle categories - if (!empty($ai_response['categories'])) { - igny8_set_post_categories($post_id, $ai_response['categories']); - igny8_log_ai_event('Categories Added', 'writer', 'content_generation', 'success', 'Post categories added', 'Post ID: ' . $post_id); - } - - // Store cluster and sector metadata - igny8_store_content_metadata($post_id, $ai_response); - - // Add meta description if available - if (!empty($ai_response['meta_description'])) { - update_post_meta($post_id, '_yoast_wpseo_metadesc', $ai_response['meta_description']); - } - - // Final summary log - igny8_log_ai_event('Content Generation Complete', 'writer', 'content_generation', 'success', 'All content components saved successfully', 'Post ID: ' . $post_id . ', Status: ' . $post_status . ', Type: ' . $post_type); - - - - return $post_id; - - } catch (Exception $e) { - error_log('Igny8: Exception creating post - ' . $e->getMessage()); - return false; - } -} - -/** - * Map AI content type to WordPress post type - */ -function igny8_map_content_type_to_post_type($content_type) { - $mapping = [ - 'blog_post' => 'post', - 'landing_page' => 'page', - 'product_page' => 'product', - 'guide_tutorial' => 'post', - 'news_article' => 'post', - 'review' => 'post', - 'comparison' => 'post', - 'email' => 'post', - 'social_media' => 'post', - 'page' => 'page', - 'product' => 'product', - 'guide' => 'post', - 'tutorial' => 'post' - ]; - - return $mapping[$content_type] ?? 'post'; -} - -/** - * Get available models for content generation - */ -function igny8_get_available_models() { - return [ - 'gpt-4.1' => 'GPT-4.1 (Content creation, coding, analysis, high-quality content generation)', - 'gpt-4o-mini' => 'GPT-4o mini (Bulk tasks, lightweight AI, cost-effective for high-volume operations)', - 'gpt-4o' => 'GPT-4o (Advanced AI, better general performance, multimodal)' - ]; -} - -/** - * Get queue status for user - */ -function igny8_get_ai_queue_status($user_id = null) { - global $wpdb; - - $user_id = $user_id ?: get_current_user_id(); - - $status = $wpdb->get_row($wpdb->prepare(" - SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, - SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing, - SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed - FROM {$wpdb->prefix}igny8_ai_queue - WHERE user_id = %d - ", $user_id)); - - return $status; -} - -/** - * Get image dimensions based on size preset and provider - * - * @param string $size_preset Size preset (featured, desktop, mobile) - * @param string $provider Image provider (runware, openai, dalle) - * @return array ['width' => int, 'height' => int] - */ -function igny8_get_image_dimensions($size_preset = 'featured', $provider = 'runware') { - // Size presets for different image types - $size_presets = [ - 'runware' => [ - 'featured' => ['width' => 1280, 'height' => 832], - 'desktop' => ['width' => 1024, 'height' => 1024], - 'mobile' => ['width' => 960, 'height' => 1280] - ], - 'openai' => [ - 'featured' => ['width' => 1024, 'height' => 1024], // OpenAI only supports square - 'desktop' => ['width' => 1024, 'height' => 1024], - 'mobile' => ['width' => 1024, 'height' => 1024] - ], - 'dalle' => [ - 'featured' => ['width' => 1024, 'height' => 1024], // Placeholder for DALL-E - 'desktop' => ['width' => 1024, 'height' => 1024], - 'mobile' => ['width' => 1024, 'height' => 1024] - ] - ]; - - // Get dimensions for the provider and size - if (isset($size_presets[$provider][$size_preset])) { - return $size_presets[$provider][$size_preset]; - } - - // Fallback to featured size for the provider - if (isset($size_presets[$provider]['featured'])) { - return $size_presets[$provider]['featured']; - } - - // Ultimate fallback - return ['width' => 1280, 'height' => 832]; -} - - - -/** - * DEPRECATED: Generate featured image for post from post meta prompt - * - * This function has been moved to ai/writer/images/image-generation.php - * and is now included directly in the plugin bootstrap process. - * - * @deprecated 5.2.0 Function moved to ai/writer/images/image-generation.php - */ - -/** - * Validate and fix Gutenberg block structure - * Ensures all heading blocks have proper level attributes - */ -function igny8_validate_and_fix_blocks($block_content) { - if (empty($block_content)) { - return $block_content; - } - - $blocks = parse_blocks($block_content); - $fixed_blocks = []; - - foreach ($blocks as $index => $block) { - // Fix heading blocks missing level attribute - if (($block['blockName'] ?? null) === 'core/heading') { - $level = $block['attrs']['level'] ?? null; - - if ($level === null) { - // Try to extract level from innerHTML - $inner_html = $block['innerHTML'] ?? ''; - if (preg_match('/]*>/i', $inner_html, $matches)) { - $detected_level = intval($matches[1]); - $block['attrs']['level'] = $detected_level; - error_log("IGNY8 BLOCKS: Fixed heading block #$index - detected level $detected_level from innerHTML"); - } else { - // Default to H2 if we can't detect - $block['attrs']['level'] = 2; - error_log("IGNY8 BLOCKS: Fixed heading block #$index - defaulted to level 2"); - } - } - } - - $fixed_blocks[] = $block; - } - - return serialize_blocks($fixed_blocks); -} - -/** - * Inject plain Igny8 shortcodes after H2 for Classic Editor only (no block markup). - */ -function insert_igny8_image_shortcodes_classic($html_content) { - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - insert_igny8_image_shortcodes_classic()"); - error_log("IGNY8 DEBUG: CALLED FROM - igny8_create_post_from_ai_response() function in ai/modules-ai.php"); - error_log("IGNY8 DEBUG - CLASSIC: Starting shortcode injection"); - error_log("IGNY8 DEBUG - CLASSIC: Input content length: " . strlen($html_content)); - - if (empty($html_content)) { - error_log("IGNY8 DEBUG - CLASSIC: Content is empty, returning"); - return $html_content; - } - - $pattern = '/(]*>.*?<\/h2>)/i'; - $matches = []; - preg_match_all($pattern, $html_content, $matches, PREG_OFFSET_CAPTURE); - - error_log("IGNY8 DEBUG - CLASSIC: Found " . count($matches[0]) . " H2 headings"); - - if (empty($matches[0])) { - error_log("IGNY8 DEBUG - CLASSIC: No H2 headings found, returning original content"); - return $html_content; - } - - $offset = 0; - $image_index = 0; - - foreach (array_reverse($matches[0]) as $match) { - $image_index++; - error_log("IGNY8 DEBUG - CLASSIC: Processing H2 #{$image_index}"); - - // Skip first H2 - if ($image_index === count($matches[0])) { - error_log("IGNY8 DEBUG - CLASSIC: Skipping first H2"); - continue; - } - - // Inject plain shortcodes (no Gutenberg markup) - $shortcode = "\n\n[igny8-image id=\"desktop-{$image_index}\"] [igny8-image id=\"mobile-{$image_index}\"]\n\n"; - error_log("IGNY8 DEBUG - CLASSIC: Injecting shortcode: " . trim($shortcode)); - - $insert_pos = $match[1] + strlen($match[0]) + $offset; - $html_content = substr_replace($html_content, $shortcode, $insert_pos, 0); - - $offset += strlen($shortcode); - } - - error_log("IGNY8 DEBUG - CLASSIC: Final content length: " . strlen($html_content)); - error_log("IGNY8 DEBUG - CLASSIC: Shortcodes in final content: " . (strpos($html_content, '[igny8-image') !== false ? 'YES' : 'NO')); - - return $html_content; -} - -/** - * Inject Gutenberg shortcode blocks after each

heading block (core/heading, level 2) - * Adds minimal, meaningful logs. Silences irrelevant debug spam. - * - * @param string $block_content Serialized Gutenberg block content - * @return string|false Modified content or false if injection fails - */ -function insert_igny8_shortcode_blocks_into_blocks($block_content) { - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN MODULE-AI.PHP - insert_igny8_shortcode_blocks_into_blocks()"); - error_log("IGNY8 DEBUG: CALLED FROM - igny8_create_post_from_ai_response() function in ai/modules-ai.php"); - - if (empty($block_content)) { - error_log("IGNY8 BLOCKS: No content passed to shortcode injector"); - return $block_content; - } - - $blocks = parse_blocks($block_content); - $output = []; - $h2_count = 0; - $injected = 0; - $heading_blocks_found = 0; - $valid_h2_blocks = 0; - - error_log("IGNY8 BLOCKS: Parsed " . count($blocks) . " total blocks"); - - foreach ($blocks as $index => $block) { - $output[] = $block; - - if (($block['blockName'] ?? null) === 'core/heading') { - $heading_blocks_found++; - $level = $block['attrs']['level'] ?? null; - - error_log("IGNY8 BLOCKS: Heading block #$index - level: " . ($level ?? 'NULL') . ", innerHTML: " . substr($block['innerHTML'] ?? '', 0, 50) . "..."); - - if ($level !== 2) { - if ($level === null) { - error_log("IGNY8 BLOCKS: Skipping heading block #$index — missing 'level' attribute"); - } else { - error_log("IGNY8 BLOCKS: Skipping heading block #$index — level $level (not H2)"); - } - continue; - } - - $valid_h2_blocks++; - $h2_count++; - - if ($h2_count === 1) { - error_log("IGNY8 BLOCKS: Skipping first H2 (no shortcode)"); - continue; - } - - $shortcode = "[igny8-image id=\"desktop-{$h2_count}\"] [igny8-image id=\"mobile-{$h2_count}\"]"; - error_log("IGNY8 BLOCKS: Injecting shortcode after H2 #{$h2_count}: " . $shortcode); - - $output[] = [ - 'blockName' => 'core/shortcode', - 'attrs' => [], - 'innerBlocks' => [], - 'innerHTML' => $shortcode, - 'innerContent' => [$shortcode] - ]; - $injected++; - } - } - - error_log("IGNY8 BLOCKS: Summary - Total headings: $heading_blocks_found, Valid H2s: $valid_h2_blocks, Shortcodes injected: $injected"); - - $result = serialize_blocks($output); - $parsed_result = parse_blocks($result); - $confirmed = false; - - foreach ($parsed_result as $b) { - if ( - ($b['blockName'] ?? '') === 'core/shortcode' && - strpos($b['innerContent'][0] ?? '', '[igny8-image') !== false - ) { - $confirmed = true; - break; - } - } - - if (!$confirmed) { - error_log("IGNY8 BLOCKS: ❌ Shortcode injection failed — no blocks found after serialization"); - igny8_log_ai_event( - 'Shortcode Injection Failed', - 'writer', - 'content_generation', - 'error', - 'No shortcodes found after injection (post-parse)', - 'Editor type: block' - ); - return false; - } - - error_log("IGNY8 BLOCKS: ✅ Injected {$injected} shortcode blocks after H2 headings"); - return $result; -} - -/** - * Wrap plain HTML content as Gutenberg blocks - * - * @param string $html_content Plain HTML content - * @return string Gutenberg block markup - */ -function wrap_html_as_blocks($html_content) { - if (empty($html_content)) { - return $html_content; - } - - // Split content into lines for processing - $lines = explode("\n", $html_content); - $block_content = []; - - foreach ($lines as $line) { - $line = trim($line); - if (empty($line)) { - continue; - } - - // Wrap different HTML elements as Gutenberg blocks - if (preg_match('/^]*>(.*?)<\/h2>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^]*>(.*?)<\/h3>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^]*>(.*?)<\/p>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^]*>(.*?)<\/ul>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^]*>(.*?)<\/ol>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^]*>(.*?)<\/blockquote>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^]*>(.*?)<\/table>$/i', $line, $matches)) { - $block_content[] = '' . $line . ''; - } elseif (preg_match('/^\[igny8-image[^\]]*\]/', $line)) { - // Handle shortcodes - wrap in shortcode block - $block_content[] = '' . $line . ''; - } else { - // For any other content, wrap as paragraph - $block_content[] = '

' . $line . '

'; - } - } - - return implode("\n", $block_content); -} - - diff --git a/igny8-ai-seo-wp-plugin/ai/openai-api.php b/igny8-ai-seo-wp-plugin/ai/openai-api.php deleted file mode 100644 index b215d326..00000000 --- a/igny8-ai-seo-wp-plugin/ai/openai-api.php +++ /dev/null @@ -1,1729 +0,0 @@ -prefix . 'igny8_logs'; - $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'"); - - if ($table_exists) { - // Get table structure to determine correct column names - $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name"); - $column_names = array_column($columns, 'Field'); - - // Prepare data based on actual table structure - $log_data = [ - 'timestamp' => $timestamp, - 'post_id' => get_queried_object_id(), - 'user_id' => get_current_user_id(), - 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown' - ]; - - // Insert based on actual table structure - if (in_array('log_type', $column_names) && in_array('message', $column_names) && in_array('data', $column_names)) { - // New structure from install.php - $wpdb->insert( - $table_name, - [ - 'log_type' => 'field_detection', - 'message' => "[{$level}] {$message}", - 'data' => json_encode($log_data), - 'user_id' => get_current_user_id() - ], - ['%s', '%s', '%s', '%d'] - ); - } else { - // Fallback: just log to error log if table structure doesn't match - error_log("IGNY8: Logs table structure mismatch, skipping database log"); - } - } else { - error_log("IGNY8: Logs table does not exist, skipping database log"); - } -} - -/** - * Build combined content for personalization - */ -function igny8_build_combined_content($for_field_detection = false, $post_id = null) { - // Check if Content Engine is enabled and use Content Engine-specific settings - $content_engine_status = get_option('igny8_content_engine_global_status', 'enabled'); - - // Use provided post_id or fall back to queried object - if ($post_id === null) { - $post_id = get_queried_object_id(); - } - - $post_type = get_post_type($post_id); - $enabled_post_types = get_option('igny8_content_engine_enabled_post_types', []); - - if ($content_engine_status === 'enabled' && in_array($post_type, $enabled_post_types)) { - // Use Content Engine-specific settings - $include_context = get_option('igny8_content_engine_include_page_context', '0') === '1'; - $input_scope = get_option('igny8_content_engine_input_scope', '300'); - } else { - // Use global settings - $include_context = get_option('igny8_include_page_context', '0') === '1'; - $input_scope = get_option('igny8_input_scope', '300'); - } - - $final_content = ''; - - // ✅ Use PageContent from form if available - if (!empty($_POST['PageContent'])) { - $final_content .= "[SOURCE:PageContent from form]\n\n"; - $final_content .= trim(sanitize_text_field($_POST['PageContent'])); - } else { - // ✅ Fallback to raw post content or term description - $queried = get_post($post_id); - - if ($queried instanceof WP_Post) { - // 🎯 Post/page/product — use post content with proper scope - $raw_content = get_post_field('post_content', $queried->ID); - if (!empty($raw_content)) { - $final_content .= "[SOURCE:Post Content]\n\n"; - - // Apply scope logic - only add dynamic messages for field detection - if ($for_field_detection) { - // Add dynamic messages for field detection - if ($input_scope === 'title') { - $final_content .= "Use this blog/page title to define the fields:\n\n"; - $final_content .= get_the_title($queried->ID); - } elseif ($input_scope === '300') { - $final_content .= "Use these 300 words to define the fields:\n\n"; - $final_content .= wp_trim_words(strip_tags($raw_content), 300, '...'); - } elseif ($input_scope === '600') { - $final_content .= "Use these 600 words to define the fields:\n\n"; - $final_content .= wp_trim_words(strip_tags($raw_content), 600, '...'); - } else { - $final_content .= "Use this whole content to define the fields:\n\n"; - $final_content .= strip_tags($raw_content); - } - } else { - // For content generation, just add content without dynamic messages - if ($input_scope === 'title') { - $final_content .= get_the_title($queried->ID); - } elseif ($input_scope === '300') { - $final_content .= wp_trim_words(strip_tags($raw_content), 300, '...'); - } elseif ($input_scope === '600') { - $final_content .= wp_trim_words(strip_tags($raw_content), 600, '...'); - } else { - $final_content .= strip_tags($raw_content); - } - } - } - - } elseif (isset($queried->description) && !empty($queried->description)) { - // 🏷️ Archive (term) — use term description - $final_content .= "[SOURCE:Term Description]\n\n"; - $final_content .= wp_trim_words(strip_tags($queried->description), 300, '...'); - } - } - - return trim($final_content) ?: 'No content available.'; -} - -/** - * Check content for moderation violations using OpenAI's moderation API - */ -function igny8_check_moderation($text, $api_key) { - $res = wp_remote_post('https://api.openai.com/v1/moderations', [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $api_key, - 'Content-Type' => 'application/json', - ], - 'body' => json_encode(['input' => $text]), - 'timeout' => 20, - ]); - - if (is_wp_error($res)) { - return ['flagged' => false, 'error' => $res->get_error_message()]; - } - - $body = json_decode(wp_remote_retrieve_body($res), true); - return [ - 'flagged' => $body['results'][0]['flagged'] ?? false, - 'categories' => $body['results'][0]['categories'] ?? [], - ]; -} - -/** - * Test OpenAI API connection - */ -function igny8_test_connection($api_key, $with_response = false) { - // Get the current model setting - $model = get_option('igny8_model', 'gpt-4.1'); - - if ($with_response) { - // Test with actual API call - // Prepare request body with model-specific parameters - $request_body = [ - 'model' => $model, - 'messages' => [ - [ - 'role' => 'user', - 'content' => 'test ping, reply with: OK! Ping Received. Also tell me: what is your maximum token limit that I can use in 1 request?' - ] - ] - ]; - - // Model-specific parameters - $request_body['temperature'] = 0.7; - - // Log the complete request to file - $log_data = [ - 'timestamp' => current_time('mysql'), - 'model' => $model, - 'request_body' => $request_body, - 'headers' => [ - 'Authorization' => 'Bearer ' . substr($api_key, 0, 10) . '...', - 'Content-Type' => 'application/json' - ] - ]; - - $log_file = ABSPATH . 'igny8_api_request_log.json'; - file_put_contents($log_file, json_encode($log_data, JSON_PRETTY_PRINT)); - error_log("Igny8 Debug: Complete API request logged to: " . $log_file); - - $res = wp_remote_post('https://api.openai.com/v1/chat/completions', [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $api_key, - 'Content-Type' => 'application/json', - ], - 'body' => json_encode($request_body), - 'timeout' => 15, - ]); - } else { - // Simple connection test without API call - $res = wp_remote_get('https://api.openai.com/v1/models', [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $api_key, - ], - 'timeout' => 10, - ]); - } - - if (is_wp_error($res)) { - return $res->get_error_message(); - } - - $code = wp_remote_retrieve_response_code($res); - $body = wp_remote_retrieve_body($res); - - // Log the complete response to file - if ($with_response) { - $response_log_data = [ - 'timestamp' => current_time('mysql'), - 'response_code' => $code, - 'response_body' => json_decode($body, true), - 'raw_response' => $body - ]; - - $response_log_file = ABSPATH . 'igny8_api_response_log.json'; - file_put_contents($response_log_file, json_encode($response_log_data, JSON_PRETTY_PRINT)); - error_log("Igny8 Debug: Complete API response logged to: " . $response_log_file); - } - - if ($code >= 200 && $code < 300) { - if ($with_response) { - // Handle API response test - $response_data = json_decode($body, true); - - if (isset($response_data['choices'][0]['message']['content'])) { - $response_text = trim($response_data['choices'][0]['message']['content']); - - // Extract token usage information - $input_tokens = $response_data['usage']['prompt_tokens'] ?? 0; - $output_tokens = $response_data['usage']['completion_tokens'] ?? 0; - $total_tokens = $response_data['usage']['total_tokens'] ?? 0; - - // Calculate cost using model rates - $rates = igny8_get_model_rates($model); - $cost = ($input_tokens * $rates['in'] + $output_tokens * $rates['out']) / 1000000; - - return [ - 'success' => true, - 'message' => 'API connection and response test successful!', - 'model_used' => $model, - 'response' => $response_text, - 'tokens_used' => $input_tokens . ' / ' . $output_tokens, - 'total_tokens' => $total_tokens, - 'cost' => '$' . number_format($cost, 4), - 'full_response' => $response_data - ]; - } else { - return [ - 'success' => false, - 'message' => 'API responded but no content received', - 'response' => $body - ]; - } - } else { - // Handle simple connection test - return [ - 'success' => true, - 'message' => 'API connection successful!', - 'model_used' => $model, - 'response' => 'Connection verified without API call' - ]; - } - } else { - return [ - 'success' => false, - 'message' => 'HTTP ' . $code . ' – ' . $body - ]; - } -} - -/** - * Log API call with cost calculation and error handling - */ -function igny8_log_api_call($model, $input_tokens, $output_tokens, $api_id = null, $status = 'success', $error_message = '') { - global $wpdb; - - try { - // Calculate cost using model rates - $cost_data = igny8_calculate_api_cost($model, $input_tokens, $output_tokens); - - // Debug logging for cost calculation - error_log("Igny8 Cost Debug: Model=$model, Input=$input_tokens, Output=$output_tokens"); - error_log("Igny8 Cost Debug: Calculated total_cost=" . $cost_data['total_cost']); - - - // Prepare log data with sanitization - $log_data = [ - 'event_type' => 'api_call', - 'api_id' => $api_id ? esc_sql($api_id) : null, - 'status' => esc_sql($status), - 'level' => $status === 'success' ? 'info' : 'error', - 'message' => sprintf( - 'Model: %s | Input: %d tokens | Output: %d tokens | Cost: %s', - esc_sql($model), - intval($input_tokens), - intval($output_tokens), - igny8_format_cost($cost_data['total_cost']) - ), - 'context' => wp_json_encode([ - 'model' => $model, - 'input_tokens' => intval($input_tokens), - 'output_tokens' => intval($output_tokens), - 'total_cost' => $cost_data['total_cost'], - 'input_cost' => $cost_data['input_cost'], - 'output_cost' => $cost_data['output_cost'], - 'error_message' => $error_message - ]), - 'source' => 'openai_api', - 'user_id' => get_current_user_id(), - 'created_at' => current_time('mysql') - ]; - - // Insert with error handling - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_logs', - $log_data, - ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s'] - ); - - if ($result === false) { - error_log('Igny8 API Logging Error: ' . $wpdb->last_error); - } else { - error_log("Igny8 Cost Debug: Successfully stored cost=" . $cost_data['total_cost'] . " in database"); - } - - // Maintain 50 row limit for performance - igny8_maintain_logs_limit(); - - } catch (Exception $e) { - error_log('Igny8 API Logging Exception: ' . $e->getMessage()); - } -} - -/** - * Maintain logs table limit for performance - */ -function igny8_maintain_logs_limit() { - global $wpdb; - - // Keep only the 50 most recent logs - $wpdb->query(" - DELETE FROM {$wpdb->prefix}igny8_logs - WHERE id NOT IN ( - SELECT id FROM ( - SELECT id FROM {$wpdb->prefix}igny8_logs - ORDER BY created_at DESC - LIMIT 50 - ) AS latest_logs - ) - "); -} - -/** - * Call OpenAI API for content generation - */ -function igny8_call_openai($prompt, $api_key, $model) { - // Debug logging for CRON context - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: Starting API call - Model: " . $model . ", Prompt length: " . strlen($prompt)); - } - - $body_data = [ - 'model' => $model, - 'messages' => [['role' => 'user', 'content' => $prompt]], - ]; - - // Model-specific parameters - $body_data['temperature'] = 0.7; - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: Request body prepared - Temperature: " . $body_data['temperature']); - } - - // Log the complete request to file - $log_data = [ - 'timestamp' => current_time('mysql'), - 'model' => $model, - 'request_body' => $body_data, - 'headers' => [ - 'Authorization' => 'Bearer ' . substr($api_key, 0, 10) . '...', - 'Content-Type' => 'application/json' - ], - 'prompt_length' => strlen($prompt), - 'prompt_preview' => substr($prompt, 0, 200) . '...' - ]; - - $log_file = ABSPATH . 'igny8_all_api_requests.json'; - - // Read existing logs and append new one - $existing_logs = []; - if (file_exists($log_file)) { - $existing_content = file_get_contents($log_file); - if (!empty($existing_content)) { - $existing_logs = json_decode($existing_content, true) ?: []; - } - } - - // Add new request to logs - $existing_logs[] = $log_data; - - // Keep only last 50 requests to prevent file from growing too large - if (count($existing_logs) > 50) { - $existing_logs = array_slice($existing_logs, -50); - } - - file_put_contents($log_file, json_encode($existing_logs, JSON_PRETTY_PRINT)); - error_log("Igny8 Debug: API request logged to: " . $log_file); - - $args = [ - 'body' => json_encode($body_data), - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . $api_key, - ], - 'timeout' => 60, - ]; - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: Making HTTP request to OpenAI API..."); - } - - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args); - - if (is_wp_error($response)) { - // Log API error with detailed information - $error_message = $response->get_error_message(); - $error_code = $response->get_error_code(); - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: HTTP request failed - " . $error_message); - } - - // Enhanced error logging - igny8_log_api_call($model, 0, 0, null, 'error', $error_message); - - // Log detailed error information - $error_details = [ - 'error_code' => $error_code, - 'error_message' => $error_message, - 'model' => $model, - 'api_key_configured' => !empty($api_key), - 'prompt_length' => strlen($prompt), - 'timestamp' => current_time('mysql'), - 'request_url' => 'https://api.openai.com/v1/chat/completions' - ]; - - // Log to AI events for debug module - igny8_log_ai_event('OpenAI HTTP Error', 'ai', 'api_call', 'error', 'HTTP request failed', 'Error: ' . $error_message . ' | Details: ' . json_encode($error_details)); - - return 'Error: ' . $error_message; - } - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: HTTP request successful, processing response..."); - } - - $response_code = wp_remote_retrieve_response_code($response); - $response_body = wp_remote_retrieve_body($response); - $response_data = json_decode($response_body, true); - - // Check for HTTP errors (non-200 status codes) - if ($response_code !== 200) { - $error_message = "HTTP {$response_code} error"; - if (isset($response_data['error']['message'])) { - $error_message .= ": " . $response_data['error']['message']; - } - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: HTTP error - " . $error_message); - } - - // Log detailed HTTP error - $error_details = [ - 'http_code' => $response_code, - 'error_message' => $error_message, - 'model' => $model, - 'api_key_configured' => !empty($api_key), - 'response_body' => $response_body, - 'timestamp' => current_time('mysql') - ]; - - igny8_log_ai_event('OpenAI HTTP Error', 'ai', 'api_call', 'error', 'HTTP ' . $response_code . ' error', 'Error: ' . $error_message . ' | Details: ' . json_encode($error_details)); - - return 'Error: ' . $error_message; - } - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 OpenAI Call: Response decoded - Has data: " . ($response_data ? 'Yes' : 'No')); - if ($response_data && isset($response_data['choices'])) { - error_log("Igny8 OpenAI Call: Response has " . count($response_data['choices']) . " choices"); - } - } - - // Log the complete response to file - $response_log_data = [ - 'timestamp' => current_time('mysql'), - 'model' => $model, - 'response_code' => wp_remote_retrieve_response_code($response), - 'response_body' => $response_data, - 'raw_response' => $response_body - ]; - - $response_log_file = ABSPATH . 'igny8_all_api_responses.json'; - - // Read existing response logs and append new one - $existing_response_logs = []; - if (file_exists($response_log_file)) { - $existing_content = file_get_contents($response_log_file); - if (!empty($existing_content)) { - $existing_response_logs = json_decode($existing_content, true) ?: []; - } - } - - // Add new response to logs - $existing_response_logs[] = $response_log_data; - - // Keep only last 50 responses to prevent file from growing too large - if (count($existing_response_logs) > 50) { - $existing_response_logs = array_slice($existing_response_logs, -50); - } - - file_put_contents($response_log_file, json_encode($existing_response_logs, JSON_PRETTY_PRINT)); - error_log("Igny8 Debug: API response logged to: " . $response_log_file); - - // Extract API response data for logging - $api_id = $response_data['id'] ?? null; - $usage = $response_data['usage'] ?? []; - $input_tokens = $usage['prompt_tokens'] ?? 0; - $output_tokens = $usage['completion_tokens'] ?? 0; - - // Log successful API call - igny8_log_api_call($model, $input_tokens, $output_tokens, $api_id, 'success'); - - return $response_data['choices'][0]['message']['content'] ?? 'No response.'; -} - -/** - * Get content scope based on settings - */ -function igny8_get_content_scope($post_id, $scope) { - $content = igny8_build_combined_content(true); - return $content; -} - -/** - * Generate personalized content - */ -function igny8_generate_content($post_id, $field_inputs, $options = []) { - global $wpdb; - - $api_key = get_option('igny8_api_key'); - $model = get_option('igny8_model', 'gpt-4.1'); - - if (empty($api_key)) { - return ['success' => false, 'message' => 'OpenAI API key not configured']; - } - - // Build inputs string - $inputs_string = ''; - foreach ($field_inputs as $key => $value) { - if ($key !== 'PageContent' && !empty($value)) { - $inputs_string .= ucfirst($key) . ': ' . $value . "\n"; - } - } - - // Get content - $content = igny8_build_combined_content(false, $post_id); - - // Get rewrite prompt - $rewrite_prompt = get_option('igny8_content_engine_rewrite_prompt', 'Rewrite the following content to be personalized for a reader with these characteristics: - -[INPUTS] - -Original content: -[CONTENT] - -Make the content feel like it was written specifically for this person while maintaining the original message and tone.'); - - $prompt = str_replace(['[INPUTS]', '[CONTENT]'], [$inputs_string, $content], $rewrite_prompt); - - // Log the final prompt being sent to OpenAI - igny8_log_field_detection_process('INFO', 'Final prompt being sent to OpenAI:'); - igny8_log_field_detection_process('INFO', 'INPUTS: ' . $inputs_string); - igny8_log_field_detection_process('INFO', 'CONTENT: ' . substr($content, 0, 200) . '...'); - igny8_log_field_detection_process('INFO', 'PROMPT: ' . substr($prompt, 0, 500) . '...'); - - // Call OpenAI - $generated_content = igny8_call_openai($prompt, $api_key, $model); - - if (strpos($generated_content, 'Error:') === 0) { - return ['success' => false, 'message' => $generated_content]; - } - - // Save variation if requested - $variation_id = null; - if ($options['save_variation'] ?? false) { - $variation_id = igny8_save_variation($post_id, $field_inputs, $generated_content); - } - - return [ - 'success' => true, - 'content' => $generated_content, - 'variation_id' => $variation_id, - 'message' => 'Content generated successfully' - ]; -} - -/** - * Save content variation - */ -function igny8_save_variation($post_id, $field_inputs, $content) { - global $wpdb; - - $fields_hash = md5(wp_json_encode($field_inputs)); - $fields_json = wp_json_encode($field_inputs); - - // Check if variation already exists - $existing = $wpdb->get_var($wpdb->prepare( - "SELECT id FROM {$wpdb->prefix}igny8_variations WHERE post_id = %d AND fields_hash = %s", - $post_id, $fields_hash - )); - - if ($existing) { - // Update existing variation - $wpdb->update( - $wpdb->prefix . 'igny8_variations', - [ - 'content' => $content, - 'created_at' => current_time('mysql') - ], - ['id' => $existing], - ['%s', '%s'], - ['%d'] - ); - return $existing; - } else { - // Insert new variation - $wpdb->insert( - $wpdb->prefix . 'igny8_variations', - [ - 'post_id' => $post_id, - 'fields_hash' => $fields_hash, - 'fields_json' => $fields_json, - 'content' => $content, - 'created_at' => current_time('mysql') - ], - ['%d', '%s', '%s', '%s', '%s'] - ); - return $wpdb->insert_id; - } -} - -/** - * Get cached variation - */ -function igny8_get_cached_variation($post_id, $field_inputs) { - global $wpdb; - - $fields_hash = md5(wp_json_encode($field_inputs)); - - $variation = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}igny8_variations WHERE post_id = %d AND fields_hash = %s", - $post_id, $fields_hash - )); - - return $variation ? (array) $variation : null; -} - -/** - * Register shortcode [igny8] - */ - - -/** - * Automatically inject Igny8 shortcode into content - */ -function igny8_inject_shortcode_into_content($content) { - // Only run on frontend - if (is_admin()) { - return $content; - } - - // Check if Content Engine is enabled globally - $global_status = get_option('igny8_content_engine_global_status', 'enabled'); - if ($global_status !== 'enabled') { - return $content; - } - - // Get current post type - $post_type = get_post_type(); - if (!$post_type) { - return $content; - } - - // Check if this post type is enabled for personalization - $enabled_post_types = get_option('igny8_content_engine_enabled_post_types', []); - if (!in_array($post_type, $enabled_post_types)) { - return $content; - } - - // Get insertion position - $insertion_position = get_option('igny8_content_engine_insertion_position', 'before'); - - // Get display mode - $display_mode = get_option('igny8_content_engine_display_mode', 'always'); - - // Check if we should show personalization based on display mode - if ($display_mode === 'logged_in' && !is_user_logged_in()) { - return $content; - } - - if ($display_mode === 'logged_out' && is_user_logged_in()) { - return $content; - } - - // Inject shortcode based on position - $shortcode = '[igny8]'; - - switch ($insertion_position) { - case 'before': - return $shortcode . $content; - case 'after': - return $content . $shortcode; - case 'replace': - return $shortcode; - default: - return $shortcode . $content; - } -} - -// Hook into the_content filter -add_filter('the_content', 'igny8_inject_shortcode_into_content'); - -// Register AJAX actions -add_action('wp_ajax_igny8_get_fields', 'igny8_ajax_get_fields'); -add_action('wp_ajax_nopriv_igny8_get_fields', 'igny8_ajax_get_fields'); -add_action('wp_ajax_igny8_generate_custom', 'igny8_ajax_generate_custom'); -add_action('wp_ajax_nopriv_igny8_generate_custom', 'igny8_ajax_generate_custom'); -add_action('wp_ajax_igny8_save_content_manual', 'igny8_ajax_save_content_manual'); -add_action('wp_ajax_nopriv_igny8_save_content_manual', 'igny8_ajax_save_content_manual'); -add_action('wp_ajax_igny8_test_personalize', 'igny8_ajax_test_personalize'); -add_action('wp_ajax_nopriv_igny8_test_personalize', 'igny8_ajax_test_personalize'); - -/** - * AJAX Handler for getting personalization fields - */ -function igny8_ajax_get_fields() { - try { - // Log the start of field detection process - igny8_log_field_detection_process('START', 'Field detection process initiated'); - - // Debug logging - error_log('IGNY8 AJAX Debug - Request method: ' . $_SERVER['REQUEST_METHOD']); - error_log('IGNY8 AJAX Debug - GET params: ' . print_r($_GET, true)); - error_log('IGNY8 AJAX Debug - POST params: ' . print_r($_POST, true)); - - // Check nonce for security - handle both GET and POST - $nonce_param = isset($_GET['nonce']) ? $_GET['nonce'] : (isset($_POST['nonce']) ? $_POST['nonce'] : ''); - if (!wp_verify_nonce($nonce_param, 'igny8_ajax_nonce')) { - igny8_log_field_detection_process('ERROR', 'Security check failed - invalid nonce'); - wp_send_json_error('Security check failed - invalid nonce'); - } - - $post_id = isset($_GET['post_id']) ? absint($_GET['post_id']) : 0; - $form_fields = isset($_GET['form_fields']) ? sanitize_text_field($_GET['form_fields']) : ''; - - if (!$post_id) { - igny8_log_field_detection_process('ERROR', 'Invalid post ID: ' . $post_id); - wp_send_json_error('Invalid post ID'); - } - - igny8_log_field_detection_process('INFO', 'Processing post ID: ' . $post_id); - - // Get content for field detection - $content = igny8_build_combined_content(true, $post_id); - - if (empty($content)) { - igny8_log_field_detection_process('ERROR', 'No content found for field detection'); - wp_send_json_error('No content found for field detection'); - } - - igny8_log_field_detection_process('INFO', 'Content retrieved successfully. Length: ' . strlen($content)); - - // Check if field detection is enabled - $field_mode = get_option('igny8_content_engine_field_mode', 'auto'); - igny8_log_field_detection_process('INFO', 'Field detection mode: ' . $field_mode); - - if ($field_mode === 'auto') { - // Use AI to detect fields - igny8_log_field_detection_process('INFO', 'Starting AI field detection'); - - $api_key = get_option('igny8_api_key'); - $model = get_option('igny8_model', 'gpt-4.1'); - $detection_prompt = get_option('igny8_content_engine_detection_prompt', ''); - - if (empty($api_key)) { - igny8_log_field_detection_process('ERROR', 'OpenAI API key not configured'); - wp_send_json_error('OpenAI API key not configured. Please set it in Personalize > Settings.'); - } - - if (empty($detection_prompt)) { - igny8_log_field_detection_process('ERROR', 'Detection prompt not configured'); - wp_send_json_error('Detection prompt not configured. Please set it in Personalize > Settings.'); - } - - // Replace [CONTENT] placeholder in prompt - $prompt = str_replace('[CONTENT]', $content, $detection_prompt); - igny8_log_field_detection_process('INFO', 'Prompt prepared for OpenAI API'); - - // Call OpenAI for field detection - $response = igny8_call_openai($prompt, $api_key, $model, 1000); - - if (strpos($response, 'Error:') === 0) { - igny8_log_field_detection_process('ERROR', 'OpenAI API error: ' . $response); - $fields = []; // Fallback to empty fields on API error - } else { - igny8_log_field_detection_process('INFO', 'OpenAI API response received successfully'); - - // Try to parse the JSON response - $fields_data = json_decode($response, true); - if ($fields_data) { - // Handle both formats: array of fields or object with fields property - if (is_array($fields_data) && isset($fields_data[0]) && is_array($fields_data[0])) { - // Direct array format: [field1, field2, ...] - $fields = $fields_data; - igny8_log_field_detection_process('SUCCESS', 'Fields detected successfully (array format): ' . count($fields) . ' fields'); - } elseif (isset($fields_data['fields']) && is_array($fields_data['fields'])) { - // Object format: {fields: [field1, field2, ...]} - $fields = $fields_data['fields']; - igny8_log_field_detection_process('SUCCESS', 'Fields detected successfully (object format): ' . count($fields) . ' fields'); - } else { - igny8_log_field_detection_process('ERROR', 'Invalid JSON structure - neither array nor object with fields property'); - igny8_log_field_detection_process('ERROR', 'Raw OpenAI response: ' . $response); - $fields = []; // Fallback to empty fields - } - - // Log each detected field - if (!empty($fields)) { - foreach ($fields as $field) { - igny8_log_field_detection_process('FIELD', 'Detected field: ' . $field['label'] . ' (type: ' . $field['type'] . ')'); - } - } - } else { - igny8_log_field_detection_process('ERROR', 'Failed to parse OpenAI response as valid JSON'); - igny8_log_field_detection_process('ERROR', 'Raw OpenAI response: ' . $response); - $fields = []; // Fallback to empty fields - } - } - } else { - // Use fixed fields from configuration - igny8_log_field_detection_process('INFO', 'Using manual field configuration'); - - $fields = []; - $fixed_fields_config = get_option('igny8_content_engine_fixed_fields_config', []); - - if (!empty($fixed_fields_config)) { - // Use the configured fields directly - $fields = $fixed_fields_config; - igny8_log_field_detection_process('SUCCESS', 'Using configured fields: ' . count($fields) . ' fields'); - } elseif (!empty($form_fields)) { - // Fallback to form_fields parameter if no config - $field_names = explode(',', $form_fields); - foreach ($field_names as $field_name) { - $fields[] = [ - 'label' => trim($field_name), - 'type' => 'text', - 'options' => 'Example 1, Example 2' - ]; - } - igny8_log_field_detection_process('INFO', 'Using fallback fields from parameter: ' . count($fields) . ' fields'); - } - } - - // If no fields were generated, provide a fallback - if (empty($fields)) { - igny8_log_field_detection_process('ERROR', 'No fields could be generated'); - - // Provide more detailed error information - $error_details = [ - 'field_mode' => $field_mode, - 'api_key_set' => !empty(get_option('igny8_api_key')), - 'detection_prompt_set' => !empty(get_option('igny8_content_engine_detection_prompt')), - 'content_length' => strlen($content), - 'post_id' => $post_id - ]; - - igny8_log_field_detection_process('ERROR', 'Field generation failed - Details: ' . json_encode($error_details)); - - wp_send_json_error([ - 'message' => 'No fields could be generated. Please check your settings and try again.', - 'details' => $error_details - ]); - } - - igny8_log_field_detection_process('INFO', 'Generating form HTML for ' . count($fields) . ' fields'); - - // Generate form HTML with progress messages - ob_start(); - ?> -
-
-
- ✅ Field Detection Complete! Found personalization fields. -
-
- - - Fields were automatically detected using AI analysis of your content. - - Fields were loaded from your manual configuration. - - -
-
- -
-
- $field): ?> -
- - - - - - - -
- -
-
- -
-
-
- - - getMessage()); - error_log('Igny8 AJAX Error: ' . $e->getMessage()); - error_log('Igny8 AJAX Trace: ' . $e->getTraceAsString()); - wp_send_json_error('Error: ' . $e->getMessage()); - } -} - -/** - * AJAX Handler for generating personalized content - */ -function igny8_ajax_generate_custom() { - try { - // Log the start of content generation process - igny8_log_field_detection_process('START', 'Content generation process initiated'); - - // Check nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce')) { - igny8_log_field_detection_process('ERROR', 'Security check failed for content generation'); - wp_send_json_error('Security check failed'); - } - - $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0; - - if (!$post_id) { - igny8_log_field_detection_process('ERROR', 'Invalid post ID for content generation: ' . $post_id); - wp_send_json_error('Invalid post ID'); - } - - igny8_log_field_detection_process('INFO', 'Generating content for post ID: ' . $post_id); - - // Get field inputs from form - $field_inputs = []; - foreach ($_POST as $key => $value) { - if ($key !== 'action' && $key !== 'nonce' && $key !== 'post_id') { - $field_inputs[sanitize_text_field($key)] = sanitize_text_field($value); - } - } - - igny8_log_field_detection_process('INFO', 'Field inputs collected: ' . count($field_inputs) . ' fields'); - - // Log each field input - foreach ($field_inputs as $key => $value) { - igny8_log_field_detection_process('FIELD', 'Field input: ' . $key . ' = ' . $value); - } - - // Generate personalized content - igny8_log_field_detection_process('INFO', 'Starting OpenAI content generation'); - $result = igny8_generate_content($post_id, $field_inputs, []); - - if ($result['success']) { - igny8_log_field_detection_process('SUCCESS', 'Content generated successfully. Length: ' . strlen($result['content'])); - - // Return enhanced content with generation details - $enhanced_content = ' -
-
-
- ✨ Personalized Content Generated! -
-
- - Content was personalized using AI based on your inputs. - Generated on ' . current_time('F j, Y \a\t g:i A') . ' - -
-
- -
-
- ' . wp_kses_post($result['content']) . ' -
- - ' . (current_user_can('manage_options') ? ' -
- - -
' : '') . ' -
-
- - '; - - wp_send_json_success($enhanced_content); - } else { - igny8_log_field_detection_process('ERROR', 'Content generation failed: ' . $result['message']); - wp_send_json_error($result['message']); - } - - } catch (Exception $e) { - igny8_log_field_detection_process('ERROR', 'Exception in content generation: ' . $e->getMessage()); - error_log('Igny8 Content Generation Error: ' . $e->getMessage()); - error_log('Igny8 Content Generation Trace: ' . $e->getTraceAsString()); - wp_send_json_error('Error: ' . $e->getMessage()); - } -} - -/** - * AJAX Handler for saving content manually - */ -function igny8_ajax_save_content_manual() { - try { - // Check nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce')) { - wp_send_json_error('Security check failed'); - } - - $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0; - $content = isset($_POST['content']) ? wp_kses_post($_POST['content']) : ''; - $field_inputs = isset($_POST['field_inputs']) ? json_decode(stripslashes($_POST['field_inputs']), true) : []; - - if (!$post_id || empty($content)) { - wp_send_json_error('Invalid post ID or content'); - } - - // Save content variation - $result = igny8_save_variation($post_id, $field_inputs, $content); - - if ($result) { - wp_send_json_success('Content saved successfully'); - } else { - wp_send_json_error('Failed to save content'); - } - - } catch (Exception $e) { - wp_send_json_error('Error: ' . $e->getMessage()); - } -} - -/** - * Simple test AJAX handler - */ -function igny8_ajax_test_personalize() { - try { - // Check nonce for security - $nonce_param = isset($_GET['nonce']) ? $_GET['nonce'] : (isset($_POST['nonce']) ? $_POST['nonce'] : ''); - if (!wp_verify_nonce($nonce_param, 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed - invalid nonce'); - } - - wp_send_json_success([ - 'message' => 'Personalization AJAX is working!', - 'post_id' => get_queried_object_id(), - 'timestamp' => current_time('mysql') - ]); - - } catch (Exception $e) { - wp_send_json_error('Error: ' . $e->getMessage()); - } -} - -/** - * AJAX Handler for testing API connection - */ -function igny8_ajax_test_api() { - // Check nonce for security - if (!wp_verify_nonce($_POST['nonce'] ?? '', 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check permissions - if (!current_user_can('edit_posts')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get API key - $api_key = get_option('igny8_api_key', ''); - - if (empty($api_key)) { - wp_send_json_error(['message' => 'API key not configured']); - } - - // Check if response test is requested - $with_response = isset($_POST['with_response']) && $_POST['with_response'] === '1'; - - // Test the connection - $result = igny8_test_connection($api_key, $with_response); - - // Handle the new response format - if (is_array($result)) { - if ($result['success']) { - $response_data = [ - 'message' => $result['message'], - 'model_used' => $result['model_used'], - 'response' => $result['response'] - ]; - - // Add cost and token information if available - if (isset($result['full_response']['usage'])) { - $usage = $result['full_response']['usage']; - $model = $result['model_used']; - $cost_data = igny8_calculate_api_cost($model, $usage['prompt_tokens'], $usage['completion_tokens']); - - $response_data['tokens'] = $usage['prompt_tokens'] . ' / ' . $usage['completion_tokens']; - $response_data['cost'] = igny8_format_cost($cost_data['total_cost']); - } - - wp_send_json_success($response_data); - } else { - wp_send_json_error([ - 'message' => $result['message'], - 'details' => $result['response'] ?? '' - ]); - } - } else { - // Handle legacy string responses - if ($result === true) { - wp_send_json_success(['message' => 'Connection successful']); - } else { - $error_message = is_string($result) ? $result : 'Connection failed'; - wp_send_json_error(['message' => $error_message]); - } - } -} - - - -// Register the test AJAX handler -add_action('wp_ajax_igny8_test_personalize', 'igny8_ajax_test_personalize'); -add_action('wp_ajax_nopriv_igny8_test_personalize', 'igny8_ajax_test_personalize'); - -// Register the API test AJAX handler -add_action('wp_ajax_igny8_test_api', 'igny8_ajax_test_api'); - - -// Register the test field detection AJAX handler -add_action('wp_ajax_igny8_test_field_detection', 'igny8_ajax_test_field_detection'); - -// Register debug AJAX handler -add_action('wp_ajax_igny8_debug_personalization', 'igny8_ajax_debug_personalization'); -add_action('wp_ajax_nopriv_igny8_debug_personalization', 'igny8_ajax_debug_personalization'); - -// Register test field detection with sample content -add_action('wp_ajax_igny8_test_sample_field_detection', 'igny8_ajax_test_sample_field_detection'); - -// Register save variation AJAX handler -add_action('wp_ajax_igny8_save_variation', 'igny8_ajax_save_variation'); - -/** - * AJAX Handler for testing field detection - */ -function igny8_ajax_test_field_detection() { - try { - // Check nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check permissions - if (!current_user_can('edit_posts')) { - wp_send_json_error('Insufficient permissions'); - } - - $content = isset($_POST['content']) ? sanitize_textarea_field($_POST['content']) : ''; - - if (empty($content)) { - wp_send_json_error('No content provided for testing'); - } - - // Log the test - igny8_log_field_detection_process('INFO', 'Field detection test initiated by admin'); - - // Check if field detection is enabled - $field_mode = get_option('igny8_content_engine_field_mode', 'auto'); - - if ($field_mode === 'auto') { - // Use AI to detect fields - $api_key = get_option('igny8_api_key'); - $model = get_option('igny8_model', 'gpt-4.1'); - $detection_prompt = get_option('igny8_content_engine_detection_prompt', ''); - - if (empty($api_key)) { - wp_send_json_error('OpenAI API key not configured'); - } - - if (empty($detection_prompt)) { - wp_send_json_error('Detection prompt not configured'); - } - - // Replace [CONTENT] placeholder in prompt - $prompt = str_replace('[CONTENT]', $content, $detection_prompt); - - // Call OpenAI for field detection - $response = igny8_call_openai($prompt, $api_key, $model, 1000); - - if (strpos($response, 'Error:') === 0) { - wp_send_json_error('OpenAI API error: ' . $response); - } else { - // Try to parse the JSON response - $fields_data = json_decode($response, true); - if ($fields_data) { - // Handle both formats: array of fields or object with fields property - if (is_array($fields_data) && isset($fields_data[0]) && is_array($fields_data[0])) { - // Direct array format: [field1, field2, ...] - convert to expected format - $fields_data = ['fields' => $fields_data]; - igny8_log_field_detection_process('SUCCESS', 'Test field detection successful (array format): ' . count($fields_data['fields']) . ' fields detected'); - } elseif (isset($fields_data['fields']) && is_array($fields_data['fields'])) { - // Object format: {fields: [field1, field2, ...]} - igny8_log_field_detection_process('SUCCESS', 'Test field detection successful (object format): ' . count($fields_data['fields']) . ' fields detected'); - } else { - igny8_log_field_detection_process('ERROR', 'Test field detection failed: Invalid JSON structure'); - wp_send_json_error('Invalid JSON structure - neither array nor object with fields property'); - } - wp_send_json_success($fields_data); - } else { - igny8_log_field_detection_process('ERROR', 'Test field detection failed: Invalid JSON response'); - wp_send_json_error('Failed to parse OpenAI response as valid JSON'); - } - } - } else { - // Use fixed fields from configuration - $fixed_fields_config = get_option('igny8_content_engine_fixed_fields_config', []); - - if (!empty($fixed_fields_config)) { - $fields_data = ['fields' => $fixed_fields_config]; - igny8_log_field_detection_process('SUCCESS', 'Test field detection successful: ' . count($fixed_fields_config) . ' configured fields'); - wp_send_json_success($fields_data); - } else { - igny8_log_field_detection_process('ERROR', 'Test field detection failed: No configured fields'); - wp_send_json_error('No fields configured for manual mode'); - } - } - - } catch (Exception $e) { - igny8_log_field_detection_process('ERROR', 'Exception in test field detection: ' . $e->getMessage()); - wp_send_json_error('Error: ' . $e->getMessage()); - } -} - -/** - * Debug AJAX handler to check personalization setup - */ -function igny8_ajax_debug_personalization() { - try { - // Check nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce')) { - wp_send_json_error('Security check failed'); - } - - $debug_info = [ - 'timestamp' => current_time('mysql'), - 'wordpress_version' => get_bloginfo('version'), - 'plugin_version' => get_option('igny8_version', 'unknown'), - 'current_user' => wp_get_current_user()->user_login, - 'is_admin' => current_user_can('manage_options'), - 'ajax_url' => admin_url('admin-ajax.php'), - 'post_id' => get_queried_object_id(), - 'post_type' => get_post_type(), - ]; - - // Check Content Engine settings - $debug_info['content_engine'] = [ - 'global_status' => get_option('igny8_content_engine_global_status', 'not_set'), - 'enabled_post_types' => get_option('igny8_content_engine_enabled_post_types', []), - 'display_mode' => get_option('igny8_content_engine_display_mode', 'not_set'), - 'insertion_position' => get_option('igny8_content_engine_insertion_position', 'not_set'), - 'teaser_text' => get_option('igny8_content_engine_teaser_text', 'not_set'), - 'field_mode' => get_option('igny8_content_engine_field_mode', 'not_set'), - 'detection_prompt' => get_option('igny8_content_engine_detection_prompt', 'not_set'), - 'api_key_set' => !empty(get_option('igny8_api_key', '')), - 'model' => get_option('igny8_model', 'not_set'), - ]; - - // Check if shortcode would be injected - $debug_info['shortcode_injection'] = [ - 'content_filter_active' => has_filter('the_content', 'igny8_inject_shortcode_into_content'), - 'would_show_personalization' => igny8_should_show_personalization(), - 'post_type_enabled' => igny8_is_post_type_enabled_for_personalization(), - ]; - - // Check database tables - global $wpdb; - $debug_info['database'] = [ - 'logs_table_exists' => $wpdb->get_var("SHOW TABLES LIKE '{$wpdb->prefix}igny8_logs'") ? true : false, - 'variations_table_exists' => $wpdb->get_var("SHOW TABLES LIKE '{$wpdb->prefix}igny8_variations'") ? true : false, - 'logs_count' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_logs") ?: 0, - ]; - - wp_send_json_success($debug_info); - - } catch (Exception $e) { - wp_send_json_error('Debug error: ' . $e->getMessage()); - } -} - -/** - * Check if personalization should be shown for current context - */ -function igny8_should_show_personalization() { - // Only run on frontend - if (is_admin()) { - return false; - } - - // Check if Content Engine is enabled globally - $global_status = get_option('igny8_content_engine_global_status', 'enabled'); - if ($global_status !== 'enabled') { - return false; - } - - // Get current post type - $post_type = get_post_type(); - if (!$post_type) { - return false; - } - - // Check if this post type is enabled for personalization - $enabled_post_types = get_option('igny8_content_engine_enabled_post_types', []); - if (!in_array($post_type, $enabled_post_types)) { - return false; - } - - // Get display mode - $display_mode = get_option('igny8_content_engine_display_mode', 'always'); - - // Check if we should show personalization based on display mode - if ($display_mode === 'logged_in' && !is_user_logged_in()) { - return false; - } - - if ($display_mode === 'logged_out' && is_user_logged_in()) { - return false; - } - - return true; -} - -/** - * Check if current post type is enabled for personalization - */ -function igny8_is_post_type_enabled_for_personalization() { - $post_type = get_post_type(); - if (!$post_type) { - return false; - } - - $enabled_post_types = get_option('igny8_content_engine_enabled_post_types', []); - return in_array($post_type, $enabled_post_types); -} - -/** - * Test field detection with sample cabinet content - */ -function igny8_ajax_test_sample_field_detection() { - try { - // Check nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check permissions - if (!current_user_can('edit_posts')) { - wp_send_json_error('Insufficient permissions'); - } - - // Sample cabinet content - $sample_content = "Revamp Your Space with Style and Utility Introducing our exquisite Chic Wood Countertop Cabinet, a seamless fusion of sophistication and functionality tailored for your kitchen or bathroom. Crafted with precision, this cabinet is meticulously designed to elevate your home's aesthetic while offering a streamlined solution for organizing your essentials. Highlight Features Dual Window Doors: Transparent panels provide a clear view of neatly arranged items, safeguarding them from dust and moisture. Multi-Purpose Storage: Perfect for stowing spice jars, vanity beauty essentials, and other petite necessities. Premium Wood Construction: Crafted with high-quality wood for enduring durability and a touch of natural warmth that enhances any decor. Compact Design: Tailored to fit seamlessly on countertops, delivering exceptional space-saving prowess. Where and When to Utilize? Versatile in its application, this cabinet thrives in diverse environments. Whether adorning your kitchen to keep spices and condiments accessible or gracing the bathroom to house beauty and skincare essentials, its compact stature renders it a perfect fit for small apartments, dormitories, or any space craving efficient organization."; - - // Get detection prompt - $detection_prompt = get_option('igny8_content_engine_detection_prompt', ''); - if (empty($detection_prompt)) { - wp_send_json_error('Detection prompt not configured'); - } - - // Replace [CONTENT] placeholder in prompt - $prompt = str_replace('[CONTENT]', $sample_content, $detection_prompt); - - // Get API settings - $api_key = get_option('igny8_api_key'); - $model = get_option('igny8_model', 'gpt-4.1'); - - if (empty($api_key)) { - wp_send_json_error('OpenAI API key not configured'); - } - - // Call OpenAI for field detection - $response = igny8_call_openai($prompt, $api_key, $model, 1000); - - if (strpos($response, 'Error:') === 0) { - wp_send_json_error('OpenAI API error: ' . $response); - } else { - // Try to parse the JSON response - $fields_data = json_decode($response, true); - if ($fields_data && isset($fields_data['fields'])) { - wp_send_json_success([ - 'message' => 'Field detection test successful', - 'sample_content' => $sample_content, - 'detected_fields' => $fields_data, - 'raw_response' => $response - ]); - } else { - wp_send_json_error([ - 'message' => 'Failed to parse OpenAI response as valid JSON', - 'raw_response' => $response - ]); - } - } - - } catch (Exception $e) { - wp_send_json_error('Error: ' . $e->getMessage()); - } -} - -/** - * AJAX Handler for saving content variations - */ -function igny8_ajax_save_variation() { - try { - // Check nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check if user has permission to save content - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0; - $content = isset($_POST['content']) ? wp_kses_post($_POST['content']) : ''; - $field_inputs = isset($_POST['field_inputs']) ? json_decode(stripslashes($_POST['field_inputs']), true) : []; - - if (!$post_id || empty($content)) { - wp_send_json_error('Missing required data'); - } - - // Save to variations table - global $wpdb; - $table_name = $wpdb->prefix . 'igny8_variations'; - - $result = $wpdb->insert( - $table_name, - [ - 'post_id' => $post_id, - 'field_inputs' => json_encode($field_inputs), - 'personalized_content' => $content, - 'created_at' => current_time('mysql'), - 'created_by' => get_current_user_id() - ], - ['%d', '%s', '%s', '%s', '%d'] - ); - - if ($result === false) { - wp_send_json_error('Failed to save variation to database'); - } - - wp_send_json_success([ - 'variation_id' => $wpdb->insert_id, - 'message' => 'Content variation saved successfully' - ]); - - } catch (Exception $e) { - wp_send_json_error('Error: ' . $e->getMessage()); - } -} diff --git a/igny8-ai-seo-wp-plugin/ai/prompts-library.php b/igny8-ai-seo-wp-plugin/ai/prompts-library.php deleted file mode 100644 index a26ad494..00000000 --- a/igny8-ai-seo-wp-plugin/ai/prompts-library.php +++ /dev/null @@ -1,310 +0,0 @@ - Child\", \"Optional 2nd category if needed\"], - [IMAGE_PROMPTS] -} - -=========================== -CONTENT FORMAT & STRUCTURE -=========================== - -- Use only valid WP-supported HTML blocks:

,

,

,

    /
      , and -- Do not add extra line breaks, empty tags, or inconsistent spacing -- Use proper table structure when using tables: -
      - - - - - - - - -
      col heading1col heading2
      cell1cell2
      - -=========================== -CONTENT FLOW RULES -=========================== - -**INTRODUCTION:** -- Start with 1 italicized hook (30–40 words) -- Follow with 2 narrative paragraphs (each 50–60 words; 2–3 sentences max) -- No headings allowed in intro - -**H2 SECTIONS (5–8 total):** -Each section should be 250–300 words and follow this format: -1. Two narrative paragraphs (80–120 words each, 2–3 sentences) -2. One list or table (must come *after* a paragraph) -3. Optional closing paragraph (40–60 words) -4. Insert 2–3

      subsections naturally after main paragraphs - -**Formatting Rules:** -- Vary use of unordered lists, ordered lists, and tables across sections -- Never begin any section or sub-section with a list or table - -=========================== -KEYWORD & SEO RULES -=========================== - -- **Primary keyword** must appear in: - - The title - - First paragraph of the introduction - - At least 2 H2 headings - -- **Secondary keywords** must be used naturally, not forced - -- **Tone & style guidelines:** - - No robotic or passive voice - - Avoid generic intros like \"In today's world…\" - - Don't repeat heading in opening sentence - - Vary sentence structure and length - -=========================== -IMAGE PROMPT RULES -=========================== - -- Provide detailed, specific, and relevant image prompts -- Each prompt should reflect the topic/subtopic in that section -- Avoid vague or generic prompts - -=========================== -INPUT VARIABLES -=========================== - -CONTENT IDEA DETAILS: -[IGNY8_IDEA] - -KEYWORD CLUSTER: -[IGNY8_CLUSTER] - -ASSOCIATED KEYWORDS: -[IGNY8_KEYWORDS] - -=========================== -OUTPUT FORMAT -=========================== - -Return ONLY the final JSON object. -Do NOT include any comments, formatting, or explanations."; -} diff --git a/igny8-ai-seo-wp-plugin/ai/runware-api.php b/igny8-ai-seo-wp-plugin/ai/runware-api.php deleted file mode 100644 index eb2fd74b..00000000 --- a/igny8-ai-seo-wp-plugin/ai/runware-api.php +++ /dev/null @@ -1,191 +0,0 @@ - 'Bearer ' . $api_key, - 'Content-Type' => 'application/json', - ]; - - $body = [ - 'model' => $model, - 'prompt' => $prompt, - 'size' => '1024x1024', - 'quality' => 'standard', - 'n' => 1 - ]; - - $args = [ - 'method' => 'POST', - 'headers' => $headers, - 'body' => json_encode($body), - 'timeout' => 60, - ]; - - $response = wp_remote_post($url, $args); - - if (is_wp_error($response)) { - igny8_log_ai_event('runway_api_error', 'error', [ - 'message' => $response->get_error_message(), - 'prompt' => $prompt, - 'model' => $model - ]); - return $response; - } - - $response_code = wp_remote_retrieve_response_code($response); - $response_body = wp_remote_retrieve_body($response); - $response_data = json_decode($response_body, true); - - if ($response_code !== 200) { - $error_message = isset($response_data['error']['message']) ? $response_data['error']['message'] : 'Unknown error'; - igny8_log_ai_event('runway_api_error', 'error', [ - 'code' => $response_code, - 'message' => $error_message, - 'prompt' => $prompt, - 'model' => $model - ]); - return new WP_Error('api_error', $error_message); - } - - // Log successful API call - igny8_log_ai_event('runway_api_success', 'success', [ - 'model' => $model, - 'prompt_length' => strlen($prompt), - 'cost' => 0.009, // Runware pricing - 'service' => 'runware' - ]); - - return $response_data; -} - -/** - * Download and save image from Runware response - * - * @param array $response_data The API response data - * @param string $filename The desired filename - * @return string|WP_Error Saved file path or error - */ -function igny8_runway_save_image($response_data, $filename) { - if (!isset($response_data['data'][0]['url'])) { - return new WP_Error('no_image_url', 'No image URL in response'); - } - - $image_url = $response_data['data'][0]['url']; - - // Create uploads directory - $upload_dir = wp_upload_dir(); - $igny8_dir = $upload_dir['basedir'] . '/igny8-ai-images/'; - - if (!file_exists($igny8_dir)) { - wp_mkdir_p($igny8_dir); - } - - // Download image - $image_response = wp_remote_get($image_url); - - if (is_wp_error($image_response)) { - return $image_response; - } - - $image_data = wp_remote_retrieve_body($image_response); - $file_path = $igny8_dir . $filename; - - $saved = file_put_contents($file_path, $image_data); - - if ($saved === false) { - return new WP_Error('save_failed', 'Failed to save image file'); - } - - return $file_path; -} - -/** - * Test Runware API connection - * - * @return array Test result - */ -function igny8_test_runway_connection() { - $test_prompt = 'A simple test image: a red circle on white background'; - - $response = igny8_runway_generate_image($test_prompt); - - if (is_wp_error($response)) { - return [ - 'success' => false, - 'message' => $response->get_error_message(), - 'details' => 'Runware API connection failed' - ]; - } - - return [ - 'success' => true, - 'message' => 'Runware API connection successful', - 'details' => 'Test image generation completed successfully' - ]; -} - -/** - * Get available Runware models - * - * @return array Available models - */ -function igny8_get_runway_models() { - return [ - 'gen3a_turbo' => [ - 'name' => 'Gen-3 Alpha Turbo', - 'description' => 'Fast, high-quality image generation', - 'cost' => 0.055 - ], - 'gen3a' => [ - 'name' => 'Gen-3 Alpha', - 'description' => 'Standard quality image generation', - 'cost' => 0.055 - ] - ]; -} - -/** - * Log AI event for Runway API - * - * @param string $event Event type - * @param string $status Success/error status - * @param array $context Additional context data - */ -function igny8_log_runway_event($event, $status, $context = []) { - igny8_log_ai_event($event, $status, array_merge($context, [ - 'service' => 'runware', - 'timestamp' => current_time('mysql') - ])); -} diff --git a/igny8-ai-seo-wp-plugin/ai/writer/images/image-generation.php b/igny8-ai-seo-wp-plugin/ai/writer/images/image-generation.php deleted file mode 100644 index 45c8fa2a..00000000 --- a/igny8-ai-seo-wp-plugin/ai/writer/images/image-generation.php +++ /dev/null @@ -1,534 +0,0 @@ - false, 'error' => 'Post not found']; - } - - // Get featured image prompt from post meta - $featured_image_prompt = get_post_meta($post_id, '_igny8_featured_image_prompt', true); - - if (empty($featured_image_prompt)) { - return ['success' => false, 'error' => 'No featured image prompt found in post meta']; - } - - // Get image generation settings from prompts page - $image_type = get_option('igny8_image_type', 'realistic'); - $image_service = get_option('igny8_image_service', 'openai'); - $image_format = get_option('igny8_image_format', 'jpg'); - $negative_prompt = get_option('igny8_negative_prompt', 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title'); - - // Get image model settings based on service - $image_model = get_option('igny8_image_model', 'dall-e-3'); - $runware_model = get_option('igny8_runware_model', 'runware:97@1'); - $prompt_template = wp_unslash(get_option('igny8_image_prompt_template', 'Create a high-quality {image_type} image to use as a featured photo for a blog post titled "{post_title}". The image should visually represent the theme, mood, and subject implied by the image prompt: {image_prompt}. Focus on a realistic, well-composed scene that naturally communicates the topic without text or logos. Use balanced lighting, pleasing composition, and photographic detail suitable for lifestyle or editorial web content. Avoid adding any visible or readable text, brand names, or illustrative effects. **And make sure image is not blurry.**')); - - // Get dimensions based on image size type and service - $dimensions = igny8_get_image_dimensions($image_size_type, $image_service); - $image_width = $dimensions['width']; - $image_height = $dimensions['height']; - - // Get API keys - $openai_key = get_option('igny8_api_key', ''); - $runware_key = get_option('igny8_runware_api_key', ''); - - $required_key = ($image_service === 'runware') ? $runware_key : $openai_key; - if (empty($required_key)) { - return ['success' => false, 'error' => ($image_service === 'runware' ? 'Runware' : 'OpenAI') . ' API key not configured']; - } - - // Build final prompt - $prompt = str_replace( - ['{image_type}', '{post_title}', '{image_prompt}'], - [$image_type, $post->post_title, $featured_image_prompt], - $prompt_template - ); - - try { - // Event 7: API request sent - error_log('Igny8: IMAGE_GEN_EVENT_7 - API request sent to ' . $image_service . ' for post: ' . $post_id); - - if ($image_service === 'runware') { - // Runware API Call - $payload = [ - [ - 'taskType' => 'authentication', - 'apiKey' => $runware_key - ], - [ - 'taskType' => 'imageInference', - 'taskUUID' => wp_generate_uuid4(), - 'positivePrompt' => $prompt, - 'negativePrompt' => $negative_prompt, - 'model' => $runware_model, - 'width' => $image_width, - 'height' => $image_height, - 'steps' => 30, - 'CFGScale' => 7.5, - 'numberResults' => 1, - 'outputFormat' => $image_format - ] - ]; - - $response = wp_remote_post('https://api.runware.ai/v1', [ - 'headers' => ['Content-Type' => 'application/json'], - 'body' => json_encode($payload), - 'timeout' => 150, // Increased to 150 seconds for image generation - 'httpversion' => '1.1', - 'sslverify' => true - ]); - - if (is_wp_error($response)) { - error_log('Igny8: IMAGE_GEN_ERROR - Runware API request failed: ' . $response->get_error_message()); - return ['success' => false, 'error' => 'Runware API Error: ' . $response->get_error_message()]; - } - - $response_body = wp_remote_retrieve_body($response); - $response_data = json_decode($response_body, true); - - error_log('Igny8: IMAGE_GEN - Runware API response: ' . substr($response_body, 0, 200)); - - if (isset($response_data['data'][0]['imageURL'])) { - $image_url = $response_data['data'][0]['imageURL']; - - // Event 8: Image URL received - error_log('Igny8: IMAGE_GEN_EVENT_8 - Image URL received from ' . $image_service . ' for post: ' . $post_id); - - // Generate filename - $filename = sanitize_file_name($post->post_title) . '_featured_' . time() . '.' . $image_format; - - // Event 9: Image saved to WordPress - error_log('Igny8: IMAGE_GEN_EVENT_9 - Saving image to WordPress for post: ' . $post_id); - - // Download image from Runware URL - require_once(ABSPATH . 'wp-admin/includes/media.php'); - require_once(ABSPATH . 'wp-admin/includes/file.php'); - require_once(ABSPATH . 'wp-admin/includes/image.php'); - - error_log('Igny8: IMAGE_GEN - Downloading image from URL: ' . $image_url); - $temp_file = download_url($image_url); - - if (is_wp_error($temp_file)) { - error_log('Igny8: IMAGE_GEN_EVENT_9_ERROR - Failed to download image: ' . $temp_file->get_error_message()); - return ['success' => false, 'error' => 'Failed to download image: ' . $temp_file->get_error_message()]; - } - - error_log('Igny8: IMAGE_GEN - Image downloaded to temp file: ' . $temp_file); - - // Prepare file array for media_handle_sideload - $file_array = [ - 'name' => $filename, - 'tmp_name' => $temp_file - ]; - - // Upload to WordPress media library - error_log('Igny8: IMAGE_GEN - Uploading to media library with media_handle_sideload'); - $attachment_id = media_handle_sideload($file_array, $post_id, $post->post_title . ' Featured Image'); - - // Clean up temp file - if (file_exists($temp_file)) { - @unlink($temp_file); - } - - if (is_wp_error($attachment_id)) { - error_log('Igny8: IMAGE_GEN_EVENT_9_ERROR - media_handle_sideload failed: ' . $attachment_id->get_error_message()); - return ['success' => false, 'error' => 'Failed to upload image: ' . $attachment_id->get_error_message()]; - } - - error_log('Igny8: IMAGE_GEN - Successfully created attachment ID: ' . $attachment_id); - - if (!is_wp_error($attachment_id)) { - // Set as featured image - set_post_thumbnail($post_id, $attachment_id); - - // Generate attachment metadata - $attachment_data = wp_generate_attachment_metadata($attachment_id, get_attached_file($attachment_id)); - wp_update_attachment_metadata($attachment_id, $attachment_data); - - // Get attachment URL - $attachment_url = wp_get_attachment_url($attachment_id); - - error_log('Igny8: IMAGE_GEN_EVENT_9_SUCCESS - Image saved successfully, attachment ID: ' . $attachment_id); - - return [ - 'success' => true, - 'attachment_id' => $attachment_id, - 'image_url' => $attachment_url, - 'provider' => 'runware' - ]; - } else { - error_log('Igny8: IMAGE_GEN_EVENT_9_ERROR - Failed to save image: ' . $attachment_id->get_error_message()); - return ['success' => false, 'error' => 'Failed to register image: ' . $attachment_id->get_error_message()]; - } - } else { - error_log('Igny8: IMAGE_GEN_EVENT_8_ERROR - No image URL in response'); - $error_msg = isset($response_data['errors'][0]['message']) ? $response_data['errors'][0]['message'] : 'Unknown Runware API error'; - return ['success' => false, 'error' => $error_msg]; - } - } else { - // OpenAI API Call with selected model - $data = [ - 'model' => $image_model, - 'prompt' => $prompt, - 'n' => 1, - 'size' => $image_width . 'x' . $image_height - ]; - - $args = [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . $openai_key, - ], - 'body' => json_encode($data), - 'timeout' => 60, - ]; - - $response = wp_remote_post('https://api.openai.com/v1/images/generations', $args); - - if (is_wp_error($response)) { - return ['success' => false, 'error' => 'OpenAI API Error: ' . $response->get_error_message()]; - } - - $response_code = wp_remote_retrieve_response_code($response); - $response_body = wp_remote_retrieve_body($response); - - if ($response_code === 200) { - $body = json_decode($response_body, true); - if (isset($body['data'][0]['url'])) { - $image_url = $body['data'][0]['url']; - $revised_prompt = $body['data'][0]['revised_prompt'] ?? null; - - // Generate filename (OpenAI always returns PNG) - $filename = sanitize_file_name($post->post_title) . '_featured_' . time() . '.png'; - - // Download and register in WordPress Media Library - $attachment_id = wp_insert_attachment([ - 'post_mime_type' => 'image/png', - 'post_title' => $post->post_title . ' Featured Image', - 'post_content' => '', - 'post_status' => 'inherit' - ], $image_url, $post_id); - - if (!is_wp_error($attachment_id)) { - // Set as featured image - set_post_thumbnail($post_id, $attachment_id); - - // Get attachment URL - $attachment_url = wp_get_attachment_url($attachment_id); - - return [ - 'success' => true, - 'attachment_id' => $attachment_id, - 'image_url' => $attachment_url, - 'provider' => 'openai', - 'revised_prompt' => $revised_prompt - ]; - } else { - return ['success' => false, 'error' => 'Failed to register image: ' . $attachment_id->get_error_message()]; - } - } - } else { - return ['success' => false, 'error' => 'HTTP ' . $response_code . ' error']; - } - } - } catch (Exception $e) { - return ['success' => false, 'error' => 'Exception: ' . $e->getMessage()]; - } - - return ['success' => false, 'error' => 'Unknown error occurred']; -} - -/** - * Generate single article image for post from post meta prompts - * - * @param int $post_id WordPress post ID - * @param string $device_type Device type: 'desktop' or 'mobile' - * @param int $index Image index (1-based) - * @return array Result with success status and attachment_id or error - */ -function igny8_generate_single_article_image($post_id, $device_type = 'desktop', $index = 1) { - // Get post - $post = get_post($post_id); - if (!$post) { - return ['success' => false, 'error' => 'Post not found']; - } - - // Get article image prompts from post meta - $article_images_data = get_post_meta($post_id, '_igny8_article_images_data', true); - - // Debug: Log the raw data to see what's actually stored - error_log('IGNY8 DEBUG: Raw article_images_data: ' . substr($article_images_data, 0, 200) . '...'); - - $article_images = json_decode($article_images_data, true); - - // Check for JSON decode errors - if (json_last_error() !== JSON_ERROR_NONE) { - error_log('IGNY8 DEBUG: JSON decode error: ' . json_last_error_msg()); - error_log('IGNY8 DEBUG: Raw data causing error: ' . $article_images_data); - - // Try to clean the data by stripping HTML tags - $cleaned_data = wp_strip_all_tags($article_images_data); - $article_images = json_decode($cleaned_data, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - error_log('IGNY8 DEBUG: Still invalid JSON after cleaning: ' . json_last_error_msg()); - return ['success' => false, 'error' => 'Invalid JSON in article images data: ' . json_last_error_msg()]; - } else { - error_log('IGNY8 DEBUG: Successfully cleaned and parsed JSON'); - } - } - - if (empty($article_images) || !is_array($article_images)) { - return ['success' => false, 'error' => 'No article image prompts found in post meta']; - } - - // Find the prompt for the requested index - $image_key = 'prompt-img-' . $index; - $prompt = ''; - - foreach ($article_images as $image_data) { - if (isset($image_data[$image_key])) { - $prompt = $image_data[$image_key]; - break; - } - } - - if (empty($prompt)) { - return ['success' => false, 'error' => 'No prompt found for image index ' . $index]; - } - - // Get image generation settings - $image_type = get_option('igny8_image_type', 'realistic'); - $image_service = get_option('igny8_image_service', 'openai'); - $image_format = get_option('igny8_image_format', 'jpg'); - $negative_prompt = get_option('igny8_negative_prompt', 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title'); - - // Get image model settings based on service - $image_model = get_option('igny8_image_model', 'dall-e-3'); - $runware_model = get_option('igny8_runware_model', 'runware:97@1'); - - // Get dimensions based on device type and service - $dimensions = igny8_get_image_dimensions($device_type, $image_service); - $image_width = $dimensions['width']; - $image_height = $dimensions['height']; - - // Get API keys - $openai_key = get_option('igny8_api_key', ''); - $runware_key = get_option('igny8_runware_api_key', ''); - - $required_key = ($image_service === 'runware') ? $runware_key : $openai_key; - if (empty($required_key)) { - return ['success' => false, 'error' => ($image_service === 'runware' ? 'Runware' : 'OpenAI') . ' API key not configured']; - } - - // Enhance prompt if needed - $full_prompt = $prompt; - if (strlen($prompt) < 50 || strpos($prompt, 'Create') !== 0) { - $section = "Section " . $index; - $full_prompt = "Create a high-quality {$image_type} image for the section titled '{$section}'. {$prompt}"; - } - - try { - error_log('Igny8: ARTICLE_IMAGE_GEN - Generating ' . $device_type . ' image for post: ' . $post_id . ', index: ' . $index); - - if ($image_service === 'runware') { - // Runware API Call - $payload = [ - [ - 'taskType' => 'authentication', - 'apiKey' => $runware_key - ], - [ - 'taskType' => 'imageInference', - 'taskUUID' => wp_generate_uuid4(), - 'positivePrompt' => $full_prompt, - 'negativePrompt' => $negative_prompt, - 'model' => $runware_model, - 'width' => $image_width, - 'height' => $image_height, - 'steps' => 30, - 'CFGScale' => 7.5, - 'numberResults' => 1, - 'outputFormat' => $image_format - ] - ]; - - $response = wp_remote_post('https://api.runware.ai/v1', [ - 'headers' => ['Content-Type' => 'application/json'], - 'body' => json_encode($payload), - 'timeout' => 150, - 'httpversion' => '1.1', - 'sslverify' => true - ]); - - if (is_wp_error($response)) { - error_log('Igny8: ARTICLE_IMAGE_GEN_ERROR - Runware API request failed: ' . $response->get_error_message()); - return ['success' => false, 'error' => 'Runware API Error: ' . $response->get_error_message()]; - } - - $response_body = wp_remote_retrieve_body($response); - $response_data = json_decode($response_body, true); - - if (isset($response_data['data'][0]['imageURL'])) { - $image_url = $response_data['data'][0]['imageURL']; - - // Generate filename - $filename = sanitize_file_name($post->post_title) . '_' . $device_type . '_' . $index . '_' . time() . '.' . $image_format; - - // Download image from Runware URL - require_once(ABSPATH . 'wp-admin/includes/media.php'); - require_once(ABSPATH . 'wp-admin/includes/file.php'); - require_once(ABSPATH . 'wp-admin/includes/image.php'); - - $temp_file = download_url($image_url); - - if (is_wp_error($temp_file)) { - return ['success' => false, 'error' => 'Failed to download image: ' . $temp_file->get_error_message()]; - } - - // Prepare file array for media_handle_sideload - $file_array = [ - 'name' => $filename, - 'tmp_name' => $temp_file - ]; - - // Upload to WordPress media library - $attachment_id = media_handle_sideload($file_array, $post_id, $post->post_title . ' ' . ucfirst($device_type) . ' Image ' . $index); - - // Clean up temp file - if (file_exists($temp_file)) { - @unlink($temp_file); - } - - if (is_wp_error($attachment_id)) { - return ['success' => false, 'error' => 'Failed to upload image: ' . $attachment_id->get_error_message()]; - } - - // Generate attachment metadata - $attachment_data = wp_generate_attachment_metadata($attachment_id, get_attached_file($attachment_id)); - wp_update_attachment_metadata($attachment_id, $attachment_data); - - // Add custom metadata - update_post_meta($attachment_id, '_igny8_image_type', $device_type); - update_post_meta($attachment_id, '_igny8_provider', 'runware'); - update_post_meta($attachment_id, '_igny8_section', 'Section ' . $index); - - // Get attachment URL - $attachment_url = wp_get_attachment_url($attachment_id); - - error_log('Igny8: ARTICLE_IMAGE_GEN_SUCCESS - ' . $device_type . ' image generated, attachment ID: ' . $attachment_id); - - return [ - 'success' => true, - 'attachment_id' => $attachment_id, - 'image_url' => $attachment_url, - 'provider' => 'runware', - 'device_type' => $device_type, - 'index' => $index - ]; - } else { - $error_msg = isset($response_data['errors'][0]['message']) ? $response_data['errors'][0]['message'] : 'Unknown Runware API error'; - return ['success' => false, 'error' => $error_msg]; - } - } else { - // OpenAI API Call with selected model - $data = [ - 'model' => $image_model, - 'prompt' => $full_prompt, - 'n' => 1, - 'size' => '1024x1024' // OpenAI only supports square - ]; - - $args = [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . $openai_key, - ], - 'body' => json_encode($data), - 'timeout' => 60, - ]; - - $response = wp_remote_post('https://api.openai.com/v1/images/generations', $args); - - if (is_wp_error($response)) { - return ['success' => false, 'error' => 'OpenAI API Error: ' . $response->get_error_message()]; - } - - $response_code = wp_remote_retrieve_response_code($response); - $response_body = wp_remote_retrieve_body($response); - - if ($response_code === 200) { - $body = json_decode($response_body, true); - if (isset($body['data'][0]['url'])) { - $image_url = $body['data'][0]['url']; - $revised_prompt = $body['data'][0]['revised_prompt'] ?? null; - - // Generate filename (OpenAI always returns PNG) - $filename = sanitize_file_name($post->post_title) . '_' . $device_type . '_' . $index . '_' . time() . '.png'; - - // Download and register in WordPress Media Library - $attachment_id = wp_insert_attachment([ - 'post_mime_type' => 'image/png', - 'post_title' => $post->post_title . ' ' . ucfirst($device_type) . ' Image ' . $index, - 'post_content' => '', - 'post_status' => 'inherit' - ], $image_url, $post_id); - - if (!is_wp_error($attachment_id)) { - // Add custom metadata - update_post_meta($attachment_id, '_igny8_image_type', $device_type); - update_post_meta($attachment_id, '_igny8_provider', 'openai'); - update_post_meta($attachment_id, '_igny8_section', 'Section ' . $index); - - // Get attachment URL - $attachment_url = wp_get_attachment_url($attachment_id); - - return [ - 'success' => true, - 'attachment_id' => $attachment_id, - 'image_url' => $attachment_url, - 'provider' => 'openai', - 'device_type' => $device_type, - 'index' => $index, - 'revised_prompt' => $revised_prompt - ]; - } else { - return ['success' => false, 'error' => 'Failed to register image: ' . $attachment_id->get_error_message()]; - } - } - } else { - return ['success' => false, 'error' => 'HTTP ' . $response_code . ' error']; - } - } - } catch (Exception $e) { - return ['success' => false, 'error' => 'Exception: ' . $e->getMessage()]; - } - - return ['success' => false, 'error' => 'Unknown error occurred']; -} diff --git a/igny8-ai-seo-wp-plugin/assets/css/core-backup.css b/igny8-ai-seo-wp-plugin/assets/css/core-backup.css deleted file mode 100644 index 666107d3..00000000 --- a/igny8-ai-seo-wp-plugin/assets/css/core-backup.css +++ /dev/null @@ -1,3039 +0,0 @@ -/* IGNY8 UNIFIED CORE CSS – A-Z COMPACT */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); - -.is-dismissible {display: none} -#wpcontent{padding-left: 0px} - -/* === 1. TOKENS === */ -:root { - /* Primary Brand Blue (Rocket Cyan-based) */ - --blue: #0693e3; /* Rocket vivid cyan blue – primary brand & main CTA */ - --blue-dark: #0472b8; /* Darkened cyan for hover / active / gradient depth */ - - /* Success Green (cooler to match cyan) */ - --green: #0bbf87; /* Slightly cooler teal-green for success states */ - --green-dark: #08966b; /* Deeper teal-green for hover / active */ - - /* Amber / Warning (warmed up to complement cyan) */ - --amber: #ff7a00; /* Rocket's vivid orange for highlight / warning */ - --amber-dark: #cc5f00; /* Darker orange for hover / strong warning */ - - /* Danger / Destructive */ - --red-dark: #d13333; /* Refreshed red with better contrast against cyan */ - - --purple: #5d4ae3; /* Purple for highlighting / special emphasis */ - --purple-dark:#3a2f94; /* Darker purple for hover / active */ - - --navy-bg: #0d1b2a; /* Sidebar background */ - --navy-bg-2: #142b3f; /* Slightly lighter navy, hover/active */ - --surface: #f8fafc; /* Page background (soft gray-white) */ - --panel: #ffffff; /* Cards / panel foreground */ - --panel-2: #f1f5f9; /* Sub-panel / hover card background */ - - --text: #555a68; /* main headings/body text */ - --text-dim: #64748b; /* secondary/subtext */ - --text-light: #e5eaf0; /* text on dark sidebar */ - --stroke: #e2e8f0; /* table/grid borders and dividers */ - - --radius:6px;--sidebar-width:220px;--header-height:75px - - /* === UNIFIED GRADIENTS === */ - --igny8-gradient-blue: linear-gradient(135deg, var(--blue) 0%, var(--blue-dark) 100%); - --igny8-gradient-panel: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%); - --igny8-gradient-success: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%); - --igny8-gradient-warning: linear-gradient(135deg, var(--amber) 0%, var(--amber-dark) 100%); - --igny8-gradient-danger: linear-gradient(135deg, #ef4444 0%, var(--red-dark) 100%); - --igny8-gradient-info: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); - --igny8-gradient-purple: linear-gradient(135deg, var(--purple) 0%, var(--purple-dark) 100%); - --igny8-gradient-gray: linear-gradient(135deg, #6b7280 0%, #374151 100%); - --igny8-gradient-light: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); - --igny8-gradient-dark: linear-gradient(135deg, #1f2937 0%, #111827 100%); - - /* === UNIFIED BACKGROUNDS === */ - --igny8-bg-success: #d4edda; - --igny8-bg-success-border: #c3e6cb; - --igny8-bg-success-text: #155724; - --igny8-bg-warning: #fff3cd; - --igny8-bg-warning-border: #ffeaa7; - --igny8-bg-warning-text: #856404; - --igny8-bg-danger: #f8d7da; - --igny8-bg-danger-border: #f5c6cb; - --igny8-bg-danger-text: #721c24; - --igny8-bg-info: #d1ecf1; - --igny8-bg-info-border: #bee5eb; - --igny8-bg-info-text: #0c5460; - --igny8-bg-light: #f8f9fa; - --igny8-bg-light-border: #e9ecef; - --igny8-bg-light-text: #495057; - } - - .igny8-card-header.gradient { background: var(--igny8-gradient-panel); padding:10px 14px; border-radius: var(--radius) var(--radius) 0 0; } - -/* === 1.5. UNIFIED BACKGROUNDS, BADGES & GRADIENTS === */ -/* Background Utilities */ -.igny8-bg-success { background: var(--igny8-bg-success); border: 1px solid var(--igny8-bg-success-border); color: var(--igny8-bg-success-text); } -.igny8-bg-warning { background: var(--igny8-bg-warning); border: 1px solid var(--igny8-bg-warning-border); color: var(--igny8-bg-warning-text); } -.igny8-bg-danger { background: var(--igny8-bg-danger); border: 1px solid var(--igny8-bg-danger-border); color: var(--igny8-bg-danger-text); } -.igny8-bg-info { background: var(--igny8-bg-info); border: 1px solid var(--igny8-bg-info-border); color: var(--igny8-bg-info-text); } -.igny8-bg-light { background: var(--igny8-bg-light); border: 1px solid var(--igny8-bg-light-border); color: var(--igny8-bg-light-text); } - -/* Gradient Backgrounds */ -.igny8-gradient-blue { background: var(--igny8-gradient-blue); } -.igny8-gradient-success { background: var(--igny8-gradient-success); } -.igny8-gradient-warning { background: var(--igny8-gradient-warning); } -.igny8-gradient-danger { background: var(--igny8-gradient-danger); } -.igny8-gradient-info { background: var(--igny8-gradient-info); } -.igny8-gradient-purple { background: var(--igny8-gradient-purple); } -.igny8-gradient-gray { background: var(--igny8-gradient-gray); } -.igny8-gradient-light { background: var(--igny8-gradient-light); } -.igny8-gradient-dark { background: var(--igny8-gradient-dark); } - -/* Unified Badge System */ -.igny8-badge { padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 500; color: #fff; white-space: nowrap; display: inline-block; } -.igny8-badge-primary { background: var(--blue); } -.igny8-badge-success { background: var(--green); } -.igny8-badge-warning { background: var(--amber); } -.igny8-badge-danger { background: #ef4444; } -.igny8-badge-info { background: #3b82f6; } -.igny8-badge-purple { background: var(--purple); } -.igny8-badge-gray { background: #6b7280; } -.igny8-badge-dark-red { background: var(--red-dark); } -.igny8-badge-outline { background: transparent; border: 1px solid rgba(255,255,255,0.3); color: rgba(255,255,255,0.9); } - -/* Badge with Gradients */ -.igny8-badge-gradient-blue { background: var(--igny8-gradient-blue); } -.igny8-badge-gradient-success { background: var(--igny8-gradient-success); } -.igny8-badge-gradient-warning { background: var(--igny8-gradient-warning); } -.igny8-badge-gradient-danger { background: var(--igny8-gradient-danger); } -.igny8-badge-gradient-info { background: var(--igny8-gradient-info); } -.igny8-badge-gradient-purple { background: var(--igny8-gradient-purple); } - -/* Badge Sizes */ -.igny8-badge-sm { padding: 2px 6px; font-size: 10px; } -.igny8-badge-lg { padding: 6px 14px; font-size: 14px; } - -/* Badge with Icons */ -.igny8-badge-icon { display: inline-flex; align-items: center; gap: 4px; } -.igny8-badge-icon .dashicons { font-size: 12px; } - -/* Title and Status with Badge Layouts */ -.igny8-title-with-badge, .igny8-status-with-badge { display: flex; align-items: center; justify-content: space-between; gap: 8px; } -.igny8-title-text, .igny8-status-text { flex: 1; } -.igny8-title-actions, .igny8-status-actions { display: flex; align-items: center; gap: 4px; } - -/* Progress Bar Gradients */ -.igny8-progress-bar { background: var(--panel-2); border-radius: 10px; height: 24px; overflow: hidden; position: relative; } -.igny8-progress-fill { background: var(--igny8-gradient-blue); transition: width 0.5s ease; position: relative; height: 100%; } -.igny8-progress-fill-success { background: var(--igny8-gradient-success); } -.igny8-progress-fill-warning { background: var(--igny8-gradient-warning); } -.igny8-progress-fill-danger { background: var(--igny8-gradient-danger); } - -/* Button Gradients */ -.igny8-btn-gradient-blue { background: var(--igny8-gradient-blue); } -.igny8-btn-gradient-success { background: var(--igny8-gradient-success); } -.igny8-btn-gradient-warning { background: var(--igny8-gradient-warning); } -.igny8-btn-gradient-danger { background: var(--igny8-gradient-danger); } -.igny8-btn-gradient-info { background: var(--igny8-gradient-info); } -.igny8-btn-gradient-purple { background: var(--igny8-gradient-purple); } -.igny8-btn-gradient-gray { background: var(--igny8-gradient-gray); } - -/* Card Gradients */ -.igny8-card-gradient { background: var(--igny8-gradient-panel); } -.igny8-card-gradient-blue { background: var(--igny8-gradient-blue); color: white; } -.igny8-card-gradient-success { background: var(--igny8-gradient-success); color: white; } -.igny8-card-gradient-warning { background: var(--igny8-gradient-warning); color: white; } -.igny8-card-gradient-danger { background: var(--igny8-gradient-danger); color: white; } - -/* Modal and Overlay Backgrounds */ -.igny8-modal-bg { background: rgba(0,0,0,0.5); } -.igny8-overlay-light { background: rgba(255,255,255,0.9); } -.igny8-overlay-dark { background: rgba(0,0,0,0.8); } - -/* Status Indicators */ -.igny8-status-success { background: var(--igny8-bg-success); border-left: 4px solid var(--green); } -.igny8-status-warning { background: var(--igny8-bg-warning); border-left: 4px solid var(--amber); } -.igny8-status-danger { background: var(--igny8-bg-danger); border-left: 4px solid #ef4444; } -.igny8-status-info { background: var(--igny8-bg-info); border-left: 4px solid #3b82f6; } - -/* === 2. RESET & BASE === */ -*{margin:0;padding:0;box-sizing:border-box} -body{font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--surface);color:var(--text);line-height:1.4;font-size:14px;font-weight:600;} -a{text-decoration:none;color:inherit} - -/* === 3. LAYOUT === */ -.igny8-page-wrapper{display:flex;min-height:100vh;width: 100%;margin: auto;} -.igny8-main-area{flex:1;display:flex;flex-direction:column;background:var(--surface)} -.igny8-content{flex:1;padding:20px;background: #fff;box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08)} -.igny8-footer{background:var(--navy-bg-2);padding:10px 20px;text-align:center;color:var(--text-light);font-size:13px;border-top:1px solid rgba(255,255,255,.1)} - - -/* === 4. SIDEBAR === */ -.igny8-sidebar{width:var(--sidebar-width);background:var(--navy-bg-2);color:var(--text-light);display:flex;flex-direction:column;padding:16px 12px} -.igny8-sidebar-logo{font-size:20px;font-weight:600;text-align:center;margin-bottom:16px} -.igny8-version-badge{text-align:center;margin-bottom:16px} -.igny8-version-badge .igny8-badge{font-size:11px;font-weight:600;letter-spacing:0.5px} -.igny8-breadcrumb{font-size:12px;color:rgba(255,255,255,0.7);margin-bottom:20px;text-align:center;line-height:1.4} -.igny8-breadcrumb-link{color:#f59e0b;text-decoration:none;transition:color .2s} -.igny8-breadcrumb-link:hover{color:#fff} -.igny8-breadcrumb-separator{margin:0 6px;opacity:0.6} -.igny8-breadcrumb-current{color:rgba(255,255,255,0.9);font-weight:500} -.igny8-sidebar-nav{display:flex;flex-direction:column;gap:8px} -.igny8-sidebar-link{display:flex;align-items:center;gap:10px;font-size:14px;padding:8px 12px;border-radius:var(--radius);color:var(--text-light);transition:background .2s} -.igny8-sidebar-link:hover{background:rgba(255,255,255,.08);color: #fff;} -.igny8-sidebar-link.active{background:var(--blue);color:#fff} -.igny8-sidebar-metrics{margin-top:auto;display:flex;flex-direction:column;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)} -.igny8-sidebar-metric{display:flex;justify-content:space-between;font-size:13px} -.igny8-sidebar-metric .label{opacity:.8} -.igny8-sidebar-footer-container{position:relative;width:100%;margin-top:auto;padding:16px 12px 16px 12px} -.igny8-sidebar-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:12px;display:flex;flex-direction:column;gap:6px} - -/* === 5. HEADER === */ -.igny8-header{display:flex;align-items:center;justify-content:space-between;background:var(--navy-bg-2);height:var(--header-height);padding:0 20px;border-bottom:1px solid rgba(255,255,255,.1);color:var(--text-light)} -.igny8-header-left{display:flex;align-items:center;gap:20px} -.igny8-page-title h1{font-size:22px;font-weight:600;color:#fff;margin:0 0 4px 0;gap:20px} -.igny8-page-description{font-size:13px;color:rgba(255,255,255,0.8);margin:0;line-height:1.3} -.igny8-breadcrumbs{font-size:13px;color:var(--text-light);opacity:.8} -.igny8-breadcrumbs a{color:var(--blue);font-weight:500} -.igny8-header-center{display:flex;align-items:center;justify-content:center;flex:1;text-align:center} -.igny8-marquee-ticker{font-size:13px;color:var(--text-light);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.igny8-header-right{display:flex;align-items:center;gap:20px} -.igny8-metrics{display:flex;align-items:center;gap:8px} -.igny8-badge{padding:4px 10px;border-radius:4px;font-size:12px;font-weight:500;color:#fff;white-space:nowrap} -.igny8-badge.igny8-btn-primary{background:var(--blue)} -.igny8-badge.igny8-btn-success{background:var(--green)} -.igny8-badge.igny8-btn-warning{background:var(--amber)} -.igny8-badge.igny8-btn-outline{background:transparent;border:1px solid rgba(255,255,255,0.3);color:rgba(255,255,255,0.9)} -.igny8-header-icons{display: flex; - align-items: center; - gap: 14px; - margin: 0 20px 10px 0; - align-content: center; - } -.igny8-header-icons .dashicons{font-size:26px;cursor:pointer;color:var(--text-light);transition:color .2s} -.igny8-header-icons .dashicons:hover{color:var(--blue)} - -/* Fix for dashicons in buttons */ -.igny8-btn .dashicons { font-family: dashicons !important; font-size: 16px; line-height: 1; text-decoration: none; font-weight: normal; font-style: normal; vertical-align: top; margin-right: 6px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.igny8-icon-only svg { font-family: inherit !important; font-style: normal !important; font-weight: normal !important; } -.igny8-actions-cell button { font-family: inherit !important; } -.igny8-actions-cell button svg { font-family: inherit !important; } -.dashicons { font-variant: normal; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - -/* === 6. BUTTONS === */ -.igny8-btn{display:inline-flex;align-items:center;justify-content:center;padding:4px 12px;font-size:13px;font-weight:500;line-height:1.3;border:none;border-radius:var(--radius,6px);cursor:pointer;transition:all .2s ease-in-out;color:#fff;text-decoration:none;white-space:nowrap;margin: 0 5px} -.igny8-btn:disabled,.igny8-btn.disabled{opacity:.5;cursor:not-allowed} -.igny8-btn-primary{background:var(--blue,#3b82f6)} -.igny8-btn-primary:hover{background:var(--blue-dark,#2563eb)} -.igny8-btn-secondary{background:var(--text-dim,#64748b);color:#fff} -.igny8-btn-secondary:hover{background:#475569} -.igny8-btn-outline{background:transparent;border:1px solid var(--stroke,#e2e8f0);color:var(--text,#0f172a)} -.igny8-btn-outline:hover{background:rgba(0,0,0,.05)} -.igny8-btn-success{background:var(--green,#10b981)} -.igny8-btn-success:hover{background:var(--green-dark,#059669)} -.igny8-btn-accent{background:var(--amber,#f59e0b)} -.igny8-btn-accent:hover{background:var(--amber-dark,#d97706)} -.igny8-btn-danger{background:#ef4444} -.igny8-btn-danger:hover{opacity:.9} -.igny8-btn-icon{width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center} - -/* === 9. TABLE === */ -.igny8-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--stroke);border-radius:4px;overflow:visible;font-size:14px} -.igny8-table thead th{background:var(--navy-bg-2);color:var(--text-light);font-weight:500;text-align:left;padding:6px 10px;border-bottom:1px solid var(--stroke)} -.igny8-table tbody td{padding:6px 10px;border-bottom:1px solid var(--stroke);color:var(--text);overflow:visible;position:relative} -.igny8-table tbody tr:hover{background:var(--panel-2)} -.igny8-table th {font-size: 110%;} -.igny8-col-checkbox{width:36px;text-align:center} -.igny8-col-actions{width:80px;text-align:center} - -/* === 10. PAGINATION === */ -.igny8-pagination{margin: 25px 0;display:flex;justify-content:center;align-items:center;} -.igny8-btn-pagination{height:auto;padding:3px 9px;font-size:12px;border-radius:4px;color:var(--blue);border:1px solid var(--blue);background:transparent;cursor:pointer;transition:all .2s} -.igny8-btn-pagination:hover:not(:disabled){background:var(--blue);color:#fff} -.igny8-btn-pagination:disabled{opacity:.4;cursor:default} -.igny8-btn-pagination.active{background:var(--blue);color:#fff;border-color:var(--blue)} - -/* === 11. MODAL === */ -.igny8-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;z-index:10000} -.igny8-modal.open{display:flex} -.igny8-modal-content{background:#fff;border-radius:6px;box-shadow:0 20px 40px rgba(0,0,0,.25);max-width:500px;width:90%;max-height:90vh;overflow:auto} -.igny8-modal-header,.igny8-modal-footer{padding:12px 16px;border-bottom:1px solid var(--stroke);display:flex;justify-content:space-between;align-items:center} -.igny8-modal-footer{border-top:1px solid var(--stroke)} -.igny8-btn-close{background:none;border:none;font-size:18px;cursor:pointer;color:var(--text-dim)} - -/* === 12. UTILITIES === */ -.igny8-flex{display:flex}.igny8-ml-auto{margin-left:auto} -.igny8-text-muted{color:var(--text-dim)}.igny8-mb-20{margin-bottom:20px} -.igny8-error-box{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;border-radius:4px;padding:12px;margin:10px 0;font-size:13px} - -/* === 13. RESPONSIVE === */ -@media(max-width:768px){.igny8-sidebar{display:none}.igny8-filters{flex-direction:column;align-items:flex-start}.igny8-filter-bar{flex-wrap:wrap}.igny8-table-actions{flex-wrap:wrap;gap:8px}} - -.is-dismissible {display: none} -#wpcontent{padding-left: 0px} - -.igny8-metrics-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;} -.igny8-page-title{font-size:1.4rem;font-weight:600;margin:0;} -.igny8-metrics-bar{display:flex;gap:8px;} -.igny8-badge{padding:4px 10px;border-radius:4px;font-size:.85rem;font-weight:500;color:#fff;} -.igny8-badge-primary{background:var(--blue-dark)} -.igny8-badge-success{background:#10b981;} -.igny8-badge-warning{background:#f59e0b;} -.igny8-badge-info{background:#3b82f6;} - -/* === 7. Filters === */ -.igny8-filters{display:flex;align-items:center;justify-content: center;gap:10px;margin:25px 0} -.igny8-filter-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:#f9fafb;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08)} -.igny8-filter-group{position:relative;display:flex;align-items:center;gap:6px} -.igny8-search-input{width:200px;padding:6px 10px;border:1px solid var(--igny8-stroke);border-radius:4px;font-size:14px} -.igny8-filter-actions{display:flex;align-items:center;gap:8px} - -/* === 8. Dropdowns === */ -.select{position:relative;min-width:120px} -.select-btn{display:flex;align-items:center;justify-content:space-between;width:100%;padding:6px 10px;font-size:13px;background:#fff;border:1px solid var(--igny8-stroke);border-radius:4px;cursor:pointer;box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08);color:var(--text);font-weight:500} -.select-list{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid var(--igny8-stroke);border-radius:4px;box-shadow:0 2px 6px rgba(0,0,0,.08);z-index:999999;max-height:200px;overflow-y:auto} -.select-item{padding:6px 10px;font-size:14px;cursor:pointer;border-bottom:1px solid #f1f5f9} -.select-item:last-child{border-bottom:none} -.select-item:hover{background:#f1f5f9} -.select.open .select-list {display:block;} -.select-arrow {font-size: 10px} -.igny8-table-actions{display:flex;align-items:center;gap:8px;justify-content: space-between; margin-bottom: 10px;} - -/* === 15. Icon Buttons === */ - -.igny8-icon-only{background:none;border:none;padding:0;margin:0 4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:opacity .2s} -.igny8-icon-only svg{width:18px;height:18px} -.igny8-icon-edit svg{color:var(--blue,#3b82f6)} -.igny8-icon-save svg {color: #fff;} -.igny8-icon-save {background-color: var(--success, #10b981);} -.igny8-icon-cancel svg {color: #fff;} -.igny8-icon-cancel {background-color: var(--text-dim, #64748b);} -.igny8-icon-edit:hover svg{color:var(--text-dim)} -.igny8-icon-delete svg{color:#ef4444} -.igny8-icon-delete:hover svg{color:#dc2626} -.igny8-icon-save{background:var(--green,#10b981);color:#fff;border-radius:8px;padding:0px;transition:background .2s} -.igny8-icon-save:hover{background:var(--green-dark,#059669)} -.igny8-icon-cancel{background:var(--text-dim,#64748b);color:#fff;border-radius:8px;padding:0px;transition:background .2s} -.igny8-icon-cancel:hover{background:var(--text,#0f172a)} -.igny8-icon-play svg{color:var(--blue,#3b82f6)} -.igny8-icon-play:hover svg{color:var(--text-dim)} -.igny8-icon-external svg{color:var(--text-dim,#64748b)} -.igny8-icon-external:hover svg{color:var(--blue,#3b82f6)} -.igny8-actions{white-space:nowrap} - -.igny8-page-title {display: flex;flex-direction: row;justify-content: space-around;align-items: center;flex-wrap: wrap;} -.igny8-sidebar-logo h2 {color: #b8d3ff;font-size: 1.3em;margin: 10px 0;font-weight: 900} - -/* ---------- GRID LAYOUTS ---------- */ - -.igny8-grid { display: grid; gap: 20px; } -.igny8-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: stretch; } -.igny8-grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin:50px 0;} -.igny8-grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; } - - -.igny8-module-cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px;margin:24px 0;} -.igny8-dashboard-cards,.igny8-grid-3{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:22px;margin:5px;} - -.igny8-grid-4{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:22px;margin-bottom:24px;} - - - -.igny8-card { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); padding: 18px; box-shadow: 0 2px 6px rgba(0,0,0,0.10), 0 4px 10px rgba(13,27,42,0.06); transition: box-shadow .25s ease, transform .2s ease; height: auto; display: flex; flex-direction: column; } -.igny8-card:hover { transform: translateY(-2px); box-shadow: 0 6px 14px rgba(0,0,0,0.14), 0 8px 20px rgba(13,27,42,0.10); } -.igny8-card-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; background:linear-gradient(90deg,var(--blue) 0%,var(--blue-dark) 100%); color:#fff; padding:12px 16px; border-radius:var(--radius) var(--radius) 0 0; margin:-10px -10px 12px -10px; } -.igny8-card-title { font:600 15px/1.4 'Inter',system-ui,sans-serif; margin:10px; color: #fff; display: flex;justify-content: space-between;} -.igny8-card-body { font:400 14px/1.55 'Inter',system-ui,sans-serif; color:var(--text); flex: 1; } - - -.igny8-help-text { font-size:13px; color:var(--text-dim); margin-top:6px; } -.igny8-status-badge { font-size:12px; font-weight:500; border-radius:4px; padding:2px 6px; line-height:1.2; } -.igny8-status-badge.mapped { background:#d1fae5; color:#065f46; } -.igny8-status-badge.unmapped { background:#fee2e2; color:#991b1b; } -.igny8-status-ok { color:var(--green); font-weight:500; } -.igny8-form-row { margin-bottom:18px; } -.igny8-form-row label { font-weight:500; font-size:14px; display:block; margin-bottom:6px; } -.igny8-form-row input[type="text"], .igny8-form-row select, .igny8-form-row textarea { width:100%; padding:8px 12px; font-size:14px; border:1px solid var(--stroke); border-radius:var(--radius); background:#fff; transition:border .2s, box-shadow .2s; } -.igny8-form-row input:focus, .igny8-form-row select:focus, .igny8-form-row textarea:focus { border-color:var(--blue); box-shadow:0 0 0 2px rgba(59,130,246,.18); outline:none; } -.igny8-radio-group, .igny8-checkbox-group { display:flex; flex-wrap:wrap; gap:16px; } -.igny8-radio-option, .igny8-checkbox-option { display:flex; align-items:center; gap:6px; font-size:14px; cursor:pointer; } -.igny8-toggle-switch { position:relative; width:42px; height:22px; display:inline-block; } -.igny8-toggle-switch input { opacity:0; width:0; height:0; } -.igny8-toggle-slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background:#cbd5e1; border-radius:22px; transition:background .3s; } -.igny8-toggle-slider:before { content:""; position:absolute; height:18px; width:18px; left:2px; bottom:2px; background:#fff; border-radius:50%; transition:transform .3s, box-shadow .3s; box-shadow:0 1px 2px rgba(0,0,0,0.3); } -.igny8-toggle-switch input:checked + .igny8-toggle-slider { background:var(--blue); } -.igny8-toggle-switch input:checked + .igny8-toggle-slider:before { transform:translateX(20px); } -.igny8-table-compact th, .igny8-table-compact td { padding:6px 8px; font-size:13px; } -.igny8-flex { display:flex; gap: 20px} -.igny8-ml-auto { margin-left:auto; } -.igny8-pad-5 { padding:5px; } -.igny8-mb-20 { margin-bottom:20px; } -@media (max-width:1024px) { .igny8-grid-3 { grid-template-columns:repeat(2, minmax(0, 1fr)); } .igny8-grid-4 { grid-template-columns:repeat(2, minmax(0, 1fr)); } } -@media (max-width:768px) { .igny8-grid-2, .igny8-grid-3, .igny8-grid-4 { grid-template-columns:1fr; } } - -/* ---------- THEME ADD-ONS ---------- */ -/* Gradient helpers */ - -/* ---------- GLOBAL ELEMENTS ---------- */ -.igny8-content h1,.igny8-settings h2,.igny8-welcome-section h2{font:700 22px/1.3 'Inter',system-ui,sans-serif;color:var(--navy-bg);margin-bottom:16px;} -.igny8-content p,.igny8-settings-section p,.igny8-welcome-text{font-size:14px;color:var(--text-dim);} -.igny8-settings-section h3,.igny8-card h3{font:600 16px/1.3 'Inter',system-ui,sans-serif;margin-bottom:12px;color:var(--blue-dark);} -.igny8-help-text{font-size:12px;color:var(--text-dim);margin-top:8px;} - -/* ---------- SUBHEADER ---------- */ -.igny8-submenu-buttons a{font-size:13px;padding:6px 16px;border-radius:var(--radius);background:var(--blue);color:#fff;font-weight:500;border:none;box-shadow:0 2px 6px rgba(0,0,0,.15);transition:all .2s ease;} -.igny8-submenu-buttons a:hover{background:var(--blue-dark);transform:translateY(-1px);box-shadow:0 4px 10px rgba(0,0,0,.2);} -.igny8-submenu-buttons a.active{background:var(--green);} - - -/* ---------- CARDS ---------- */ -.igny8-card,.igny8-module-card{margin: 15px 0;border:1px solid var(--stroke);background:var(--panel);border-radius:var(--radius);padding:10px;box-shadow:0 2px 6px rgba(0,0,0,.08),0 4px 10px rgba(13,27,42,.06);transition:all .25s ease; width: 100%;} -.igny8-card:hover,.igny8-module-card:hover{transform:translateY(-2px);box-shadow:0 6px 14px rgba(0,0,0,.12),0 8px 22px rgba(13,27,42,.10);} -.igny8-card-header{border-bottom:1px solid var(--stroke);padding-bottom:6px;margin-bottom:12px;} -h3,h4,h5,h6,igny8-card-title,.igny8-card-header h3{margin:0;font:600 16px/1.4 'Inter',system-ui,sans-serif;color:var(--blue-dark);} -.igny8-card-body{font-size:14px;color:var(--text);padding: 5px 15px;} -.igny8-card-actions{margin-top:12px;display:flex;gap:12px;} - -/* ---------- MODULE CARD HEADER ACCENTS ---------- */ -.igny8-module-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid var(--stroke);} -.igny8-module-header h4{font:600 16px/1.3 'Inter',system-ui,sans-serif;margin:0;color:var(--navy-bg);} -.igny8-module-card:nth-child(1) .igny8-module-header h4{border-left:3px solid var(--blue);padding-left:6px;} -.igny8-module-card:nth-child(2) .igny8-module-header h4{border-left:3px solid var(--amber-dark);padding-left:6px;} -.igny8-module-card:nth-child(3) .igny8-module-header h4{border-left:3px solid var(--green);padding-left:6px;} -.igny8-module-card:nth-child(4) .igny8-module-header h4{border-left:3px solid var(--blue-dark);padding-left:6px;} -.igny8-module-card:nth-child(5) .igny8-module-header h4{border-left:3px solid var(--amber-dark);padding-left:6px;} -.igny8-module-card:nth-child(6) .igny8-module-header h4{border-left:3px solid var(--green-dark);padding-left:6px;} -.igny8-module-card:nth-child(7) .igny8-module-header h4{border-left:3px solid var(--blue);padding-left:6px;} -.igny8-module-icon{background:rgba(59,130,246,0.08);padding:8px;border-radius:50%;display:flex;align-items:center;justify-content:center;} -.igny8-module-description p{font-size:14px;color:var(--text-dim);line-height:1.5;margin-bottom:12px;} - -/* ---------- FORM ELEMENTS ---------- */ -.igny8-form-row{margin-bottom:16px;} -.igny8-form-row label{font:600 13px/1.4 'Inter',system-ui,sans-serif;margin-bottom:5px;display:block;color:var(--navy-bg);} -.igny8-form-row input[type="text"],.igny8-form-row textarea,.igny8-form-row select{width:100%;padding:7px 12px;font-size:14px;border:1px solid var(--stroke);border-radius:var(--radius);transition:border .2s,box-shadow .2s;} -.igny8-form-row input:focus,.igny8-form-row textarea:focus,.igny8-form-row select:focus{border-color:var(--blue);box-shadow:0 0 0 2px rgba(59,130,246,.18);outline:none;} -.igny8-radio-group,.igny8-checkbox-group{display:flex;flex-wrap:wrap;gap:14px;} -.igny8-radio-option,.igny8-checkbox-option{display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;} -.igny8-radio-option input:checked+label{color:var(--blue-dark);font-weight:600;} -.igny8-checkbox-option input:checked+label{color:var(--green-dark);font-weight:600;} -.igny8-form-row textarea[name*="prompt"]{border-left:3px solid var(--amber-dark);} -.igny8-form-row select[name*="style"]{border-left:3px solid var(--green);} -.igny8-form-row input[type="text"]:focus{border-color:var(--blue-dark);} -/*textarea[name*="prompt"]{border-left:3px solid var(--amber-dark);}*/ -.igny8-textarea-orange {border-left: 3px solid var(--amber-dark);} -.igny8-textarea-green {border-left: 3px solid var(--green);} -.igny8-textarea-blue {border-left: 3px solid var(--blue);} - - -/* ---------- TABLES ---------- */ -.igny8-table-wrapper table,.igny8-table{width:100%;border-collapse:collapse;font-size:13px;} -.igny8-table-wrapper th,.igny8-table-wrapper td,.igny8-table th,.igny8-table td{border:1px solid var(--stroke);padding:6px 8px;text-align:left;} -.igny8-table-wrapper thead{background:var(--panel-2);} -.igny8-table-wrapper th,.igny8-table th{color:var(--navy-bg);font-weight:600;} -.igny8-table thead{background:var(--navy-bg);color:#fff;} -.igny8-table td code{color:var(--blue-dark);} -.igny8-status-ok{color:var(--green-dark);font-weight:500;} - -/* ---------- BUTTONS ---------- */ -.igny8-btn,.button.button-primary,#submit.button-primary{display:inline-flex;align-items:center;justify-content:center;padding:5px 10px;font-size:12px;font-weight:500;border-radius:var(--radius);cursor:pointer;transition:all .25s ease;text-decoration:none;box-shadow:0 2px 6px rgba(0,0,0,.15);} -.igny8-btn-primary{background:var(--blue);color:#fff;border:none;} -.igny8-btn-primary:hover{background:var(--blue-dark);transform:translateY(-1px);color: #fff;} -.igny8-btn-outline{background:transparent;border:1px solid var(--stroke);color:var(--text);} -.igny8-btn-outline:hover{background:var(--blue);color:#fff;box-shadow:0 4px 10px rgba(0,0,0,.15);} -.igny8-btn-danger{background:var(--red-dark);color:#fff;} -.igny8-btn-danger:hover{opacity:.9;} -#igny8-export-btn{border-color:var(--blue);color:var(--blue);} -#igny8-export-btn:hover{background:var(--blue);color:#fff;} -#igny8-import-btn{border-color:var(--green);color:var(--green);} -#igny8-import-btn:hover{background:var(--green);color:#fff;} -.button.button-primary,#submit.button-primary{background:var(--green);border:none;color:#fff;} -#submit.button-primary:hover{background:var(--green-dark);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.2);} - -/* ---------- TOGGLE SWITCH ---------- */ -/* Toggle switch styles are defined above at lines 230-235 */ - -/* ---------- ALERT NOTICES ---------- */ -.notice-error{border-left:4px solid var(--red-dark)!important;background:rgba(185,28,28,0.05);} -.notice-warning{border-left:4px solid var(--amber-dark)!important;background:rgba(217,119,6,0.05);} -.notice p{font-size:13px;} - -/* ---------- DASHBOARD QUICK STATS ---------- */ -.igny8-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(80px,1fr));gap:16px;} -.igny8-stat{text-align:center;background:var(--panel-2);padding:10px;border-radius:var(--radius);box-shadow:inset 0 1px 2px rgba(0,0,0,.05);} -.igny8-stat-number{display:block;font:600 18px/1.2 'Inter',system-ui,sans-serif;color:var(--blue-dark);} -.igny8-stat-label{font-size:13px;color:var(--text-dim);} - -/* ---------- DASHBOARD ACTIVITY ---------- */ -.igny8-activity-list{display:flex;flex-direction:column;gap:8px;} -.igny8-activity-item{display:flex;justify-content:space-between;padding:6px 10px;background:var(--panel-2);border-radius:var(--radius);} -.igny8-activity-time{font-size:13px;color:var(--amber-dark);font-weight:500;} -.igny8-activity-desc{font-size:13px;color:var(--text);} - - - -/* ---------- MODAL CORE ---------- */ -.igny8-modal-content{background:var(--panel);border-radius:var(--radius);width:420px;max-width:90%;margin:auto;padding:0;box-shadow:0 8px 24px rgba(0,0,0,.22),0 12px 32px rgba(13,27,42,.18);font-family:'Inter',system-ui,sans-serif;animation:fadeInScale .25s ease;} -@keyframes fadeInScale{0%{opacity:0;transform:scale(.96);}100%{opacity:1;transform:scale(1);}} - -/* ---------- HEADER ---------- */ -.igny8-modal-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--stroke);background:linear-gradient(90deg,var(--panel-2) 0%,#fff 100%);} -.igny8-modal-header h3{margin:0;font:600 16px/1.3 'Inter',system-ui,sans-serif;color:var(--blue-dark);} -.igny8-btn-close{background:transparent;border:none;font-size:20px;line-height:1;color:var(--text-dim);cursor:pointer;transition:color .2s;} -.igny8-btn-close:hover{color:var(--red-dark);} - -/* ---------- BODY ---------- */ -.igny8-modal-body{padding:16px 18px;font-size:14px;color:var(--text);} -.igny8-modal-body p{margin-bottom:10px;} -.igny8-modal-body strong{font-weight:600;color:var(--navy-bg);} -.igny8-modal-body ul{margin:6px 0 0 18px;padding:0;font-size:13px;color:var(--text-dim);line-height:1.4;} -.igny8-modal-body ul li{list-style:disc;} -.igny8-text-danger{color:var(--red-dark);font-weight:500;margin-top:10px;} - -/* ---------- FOOTER ---------- */ -.igny8-modal-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 18px;border-top:1px solid var(--stroke);background:var(--panel-2);} -.igny8-btn-secondary{background:var(--text-dim);color:#fff;padding:6px 14px;font-size:13px;border:none;border-radius:var(--radius);cursor:pointer;transition:all .25s ease;box-shadow:0 2px 5px rgba(0,0,0,.15);} -.igny8-btn-secondary:hover{background:#475569;transform:translateY(-1px);} -.igny8-btn-danger{background:var(--red-dark);color:#fff;padding:6px 14px;font-size:13px;border:none;border-radius:var(--radius);cursor:pointer;transition:all .25s ease;box-shadow:0 2px 5px rgba(0,0,0,.15);} -.igny8-btn-danger:hover{background:#991b1b;transform:translateY(-1px);} - -/* Automation UI Components */ -.igny8-automation-table { - margin-top: 16px; -} - -.igny8-status-badge { - display: inline-block; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-status-success { - background: var(--green); - color: white; -} - -.igny8-status-disabled { - background: var(--text-dim); - color: white; -} - -.igny8-toggle { - position: relative; - display: inline-block; - width: 44px; - height: 24px; - cursor: pointer; -} - -.igny8-toggle input { - opacity: 0; - width: 0; - height: 0; -} - -.igny8-toggle-slider { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--text-dim); - border-radius: 24px; - transition: 0.3s; -} - -.igny8-toggle-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background: white; - border-radius: 50%; - transition: 0.3s; -} - -.igny8-toggle input:checked + .igny8-toggle-slider { - background: var(--green); -} - -.igny8-toggle input:checked + .igny8-toggle-slider:before { - transform: translateX(20px); -} - -/* Mode Toggle Row */ -.igny8-mode-toggle-row { - display: flex; - align-items: center; - justify-content: center; - margin: 15px 0; -} - -.igny8-mode-toggle-label { - display: flex; - align-items: center; - gap: 12px; -} - -.igny8-mode-label { - font-size: 14px; - font-weight: 500; - color: var(--text); -} - -/* Cron Schedule Modal */ -.igny8-cron-config { - margin: 20px 0; -} - -.igny8-cron-item { - margin-bottom: 20px; - padding: 16px; - border: 1px solid var(--stroke); - border-radius: var(--radius); - background: var(--panel-2); -} - -.igny8-cron-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.igny8-cron-status { - font-size: 12px; - font-weight: 500; -} - -.igny8-cron-url { - display: flex; - align-items: center; - gap: 12px; - background: var(--panel); - padding: 12px; - border-radius: var(--radius); - border: 1px solid var(--stroke); -} - -.igny8-cron-url code { - flex: 1; - background: none; - color: var(--blue); - font-size: 13px; - word-break: break-all; -} - -.igny8-btn-copy { - background: var(--blue); - color: white; - border: none; - padding: 6px 12px; - border-radius: 4px; - font-size: 12px; - cursor: pointer; - transition: background 0.2s ease; -} - -.igny8-btn-copy:hover { - background: var(--blue-dark); -} - -.igny8-cron-info { - margin-top: 24px; - padding: 16px; - background: var(--panel-2); - border-radius: var(--radius); - border: 1px solid var(--stroke); -} - -.igny8-cron-info h4 { - margin: 0 0 8px 0; - font-size: 14px; - color: var(--text); -} - -.igny8-cron-info code { - background: var(--panel); - padding: 4px 8px; - border-radius: 4px; - font-family: monospace; - color: var(--blue); -} - -.igny8-sidebar-divider {border-bottom: 1px solid rgba(255, 255, 255, .1);margin: 10px 0} - -.igny8-pad-sm { padding: 5px; } -.igny8-pad-md { padding: 10px; } -.igny8-pad-lg { padding: 20px; } -.igny8-pad-xl { padding: 30px; } - - -td.igny8-col-actions button.igny8-btn.igny8-btn-success.igny8-btn-sm, td.igny8-col-actions button.igny8-btn.igny8-btn-danger.igny8-btn-sm {padding: 4px;} -th.igny8-col-actions {min-width:150px;} -td.igny8-col-actions {text-align: center;} - -/* === UI LAYER COMPONENTS === */ - -/* Page Layout Components */ -.igny8-main-content { flex: 1; display: flex; flex-direction: column; } -.igny8-header { padding: 10px 0; border-bottom: 1px solid var(--stroke)} -.igny8-header h1 { font-size: 24px; font-weight: 600; color: #fff; margin: 0; } - -/* Module Header */ -.igny8-module-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; } -.igny8-module-info h2 { font-size: 20px; font-weight: 600; margin: 0 0 8px 0; } -.igny8-module-description { color: var(--text-dim); font-size: 14px; margin: 0; } -.igny8-module-kpis { display: flex; gap: 20px; } -.igny8-kpi-item { text-align: center; } -.igny8-kpi-value { display: block; font-size: 24px; font-weight: 700; color: var(--blue); } -.igny8-kpi-label { font-size: 12px; color: var(--text-dim); text-transform: uppercase; } - -/* Submodule Navigation */ -.igny8-submodule-nav { margin-bottom: 20px; } -.igny8-submodule-tabs { display: flex; list-style: none; border-bottom: 1px solid var(--stroke); } -.igny8-submodule-tab { margin-right: 2px; } -.igny8-submodule-tab a { display: block; padding: 12px 16px; color: var(--text-dim); border-bottom: 2px solid transparent; transition: all 0.2s; } -.igny8-submodule-tab.active a, .igny8-submodule-tab a:hover { color: var(--blue); border-bottom-color: var(--blue); } - -/* Submodule Header */ -.igny8-submodule-header { margin-bottom: 20px; } -.igny8-back-link { margin-bottom: 12px; } -.igny8-btn-back { display: inline-flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 14px; } -.igny8-btn-back:hover { color: var(--blue); } -.igny8-submodule-title { font-size: 18px; font-weight: 600; margin: 0 0 8px 0; } -.igny8-submodule-description { color: var(--text-dim); font-size: 14px; margin: 0; } - -/* Filters Bar */ -.igny8-filters-bar { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); padding: 16px; margin-bottom: 20px; } -.igny8-filters-row { display: flex; gap: 16px; align-items: end; flex-wrap: wrap; } -.igny8-filter-item { display: flex; flex-direction: column; gap: 4px; min-width: 120px; } -.igny8-filter-label { font-size: 12px; font-weight: 500; color: var(--text); } -.igny8-filter-search, .igny8-filter-text, .igny8-filter-select, .igny8-filter-date { padding: 8px 12px; border: 1px solid var(--stroke); border-radius: var(--radius); font-size: 14px; } -.igny8-search-wrapper { position: relative; } -.igny8-search-icon { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: var(--text-dim); } -.igny8-filter-actions { display: flex; gap: 8px; } - -/* Table Actions */ -.igny8-table-actions { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } -.igny8-actions-primary, .igny8-actions-secondary { display: flex; gap: 8px; } -.igny8-bulk-actions { display: flex; align-items: center; gap: 12px; } -.igny8-bulk-select-all { display: flex; align-items: center; gap: 6px; font-size: 14px; } -.igny8-bulk-action-select { padding: 6px 10px; border: 1px solid var(--stroke); border-radius: var(--radius); } -.igny8-bulk-count { font-size: 12px; color: var(--text-dim); } - -/* Table */ -.igny8-table-wrapper { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); overflow: hidden; } -.igny8-table { width: 100%; border-collapse: collapse; } -.igny8-table th, .igny8-table td { padding: 12px; text-align: left; border-bottom: 1px solid var(--stroke); } -.igny8-table th { background: var(--panel-2); font-weight: 600; font-size: 13px; } -.igny8-table tbody tr:hover { background: var(--panel-2); } -.igny8-sortable-link { color: inherit; display: flex; align-items: center; gap: 4px; } -.igny8-sortable-link:hover { color: var(--blue); } -td.igny8-align-center, .igny8-align-center { text-align: center; } -.igny8-align-right { text-align: right; } -.igny8-empty-cell { text-align: center; color: var(--text-dim); font-style: italic; padding: 40px; } - -/* Row Actions */ -.igny8-row-actions { display: flex; gap: 8px; align-items: center; } -.igny8-action-separator { color: var(--text-dim); } - -/* Badges */ -.igny8-badge { padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: capitalize; } -.igny8-badge-default { background: rgba(103, 112, 109, 0.1); color: var(--text); } -.igny8-badge-success { background: rgba(16,185,129,0.1); color: var(--green-dark); } -.igny8-badge-info { background: rgba(59,130,246,0.1); color: var(--blue-dark); } -.igny8-badge-warning { background: rgba(245,158,11,0.1); color: var(--amber-dark); } -.igny8-badge-danger { background: rgba(239,68,68,0.1); color: var(--red-dark); } -.igny8-badge-secondary { background: rgba(100,116,139,0.1); color: var(--text-dim); } -.igny8-badge-dark-red { background: rgba(220,38,38,0.1); color: #dc2626; } - -/* Pagination */ -.igny8-pagination-wrapper { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: var(--panel-2); } -.igny8-pagination-info { font-size: 14px; color: var(--text-dim); } -.igny8-pagination-list { display: flex; list-style: none; gap: 4px; } -.igny8-pagination-btn { display: flex; align-items: center; gap: 4px; padding: 8px 12px; border: 1px solid var(--stroke); background: var(--panel); color: var(--text); border-radius: var(--radius); font-size: 14px; transition: all 0.2s; } -.igny8-pagination-btn:hover { background: var(--blue); color: white; border-color: var(--blue); } -.igny8-btn-current { background: var(--blue); color: white; border-color: var(--blue); } -.igny8-per-page-selector { display: flex; align-items: center; gap: 8px; font-size: 14px; } - -/* Forms */ -.igny8-form-wrapper { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); padding: 20px; } -.igny8-form-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; } -.igny8-form-fields { display: flex; flex-direction: column; gap: 16px; } - -/* Hidden elements - no inline styles */ -.igny8-count-hidden { display: none; } -/* Legacy notification hidden class - now handled by unified system */ - -/* Module cards grid */ -.igny8-module-cards-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin: 20px 0; -} -.igny8-form-field { display: flex; flex-direction: column; gap: 6px; } -.igny8-field-label { font-size: 14px; font-weight: 500; color: var(--text); } -.igny8-required { color: var(--red-dark); } -.igny8-input, .igny8-textarea, .igny8-select { padding: 10px 12px; border: 1px solid var(--stroke); border-radius: var(--radius); font-size: 14px; } -.igny8-input:focus, .igny8-textarea:focus, .igny8-select:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 2px rgba(59,130,246,0.1); } -.igny8-field-description { font-size: 12px; color: var(--text-dim); } -.igny8-form-actions { display: flex; gap: 12px; margin-top: 20px; } - - -/* Layout Helpers */ -.igny8-submodule-layout { display: flex; flex-direction: column} - - -/*Styled Select Components (matching existing select styles) */ -.igny8-styled-select { position: relative; min-width: 120px; } -.igny8-styled-select-btn { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 6px 10px; font-size: 14px; background: #fff; border: 1px solid var(--igny8-stroke); border-radius: 4px; cursor: pointer; box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08); } -.igny8-styled-select-options { display: none; position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: #fff; border: 1px solid var(--igny8-stroke); border-radius: 4px; box-shadow: 0 2px 6px rgba(0,0,0,.08); z-index: 999999; max-height: 200px; overflow-y: auto; } -.igny8-styled-select-item { padding: 6px 10px; font-size: 14px; cursor: pointer; border-bottom: 1px solid #f1f5f9; } -.igny8-styled-select-item:last-child { border-bottom: none; } -.igny8-styled-select-item:hover { background: #f1f5f9; } - -.dd-arrow { font-size: 10px; margin-left: 10px; } -.igny8-input-sm { width: 100%; padding: 6px 8px; font-size: 13px; border: 1px solid var(--igny8-stroke); border-radius: 4px; background: #fff; box-sizing: border-box; } -.igny8-text-sm { font-size: 13px; } -.igny8-mb-5 { margin-bottom: 5px; } -.igny8-text-muted { color: var(--text-dim); } -.igny8-p-5 { padding: 5px 10px !important; } -.igny8-text-xs { font-size: 12px !important; } -.igny8-flex { display: flex; } -.igny8-flex-gap-10 { gap: 10px; } -.igny8-styled-select-options { min-width: 250px; padding: 12px; box-sizing: border-box; } -.igny8-dropdown-panel { pointer-events: auto; } -.igny8-dropdown-panel input, .igny8-dropdown-panel button { pointer-events: auto; } -/* Legacy planner notification - now handled by unified system */ - -/* === CHARTS SYSTEM === */ -/* === 14. Charts & Metrics=== */ - - -/* Header Metrics Container - 2 Row Layout */ -.igny8-header .metrics-container { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(2, 1fr); - gap: 8px; - margin: 0; - height: 100%; - max-width: 600px; -} - -/* Header Metric Cards - Clean Modern Design */ -.igny8-header .igny8-metric-card { - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 6px; - padding: 6px 8px; - text-align: center; - transition: all 0.2s ease; - cursor: default; - backdrop-filter: blur(8px); - position: relative; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 32px; -} - -.igny8-header .igny8-metric-card:hover { - background: rgba(255, 255, 255, 0.12); - border-color: rgba(255, 255, 255, 0.25); - transform: translateY(-1px); -} - -.igny8-header .igny8-metric-card.blue { - border-top: 2px solid var(--blue); -} - -.igny8-header .igny8-metric-card.green { - border-top: 2px solid var(--green); -} - -.igny8-header .igny8-metric-card.amber { - border-top: 2px solid var(--amber); -} - -.igny8-header .igny8-metric-card.purple { - border-top: 2px solid var(--purple); -} - -.igny8-header .igny8-metric-card.orange { - border-top: 2px solid var(--amber); -} - -.igny8-header .igny8-metric-card.red { - border-top: 2px solid var(--red-dark); -} - -.igny8-header .igny8-metric-card.gray { - border-top: 2px solid #6b7280; -} - -.igny8-header .igny8-metric-number { - font-size: 14px; - font-weight: 700; - margin: 0; - color: #ffffff; - text-shadow: 0 1px 2px rgba(0,0,0,0.4); - line-height: 1; -} - -.igny8-header .igny8-metric-label { - font-size: 9px; - font-weight: 500; - margin: 1px 0 0 0; - color: rgba(255, 255, 255, 0.75); - text-transform: uppercase; - letter-spacing: 0.3px; - line-height: 1; -} - - - -/* =================================================================== - STATUS CIRCLE STYLES FOR SYSTEM SUMMARY - =================================================================== */ - -.bg-circle { - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 11px; - color: white; - cursor: help; - transition: all 0.2s ease; - border: 2px solid transparent; -} - -.bg-circle:hover { - transform: scale(1.1); - border-color: rgba(255, 255, 255, 0.3); -} - -.bg-success { - background-color: var(--green); - box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); -} - -.bg-error { - background-color: var(--red); - box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3); -} - -.bg-success:hover { - background-color: var(--green-dark); - box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); -} - -.bg-error:hover { - background-color: var(--red-dark); - box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); -} - -/* =================================================================== - RADIO BUTTON STYLES FOR DEBUG SETTINGS - =================================================================== */ - -input[type="radio"] { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - width: 16px; - height: 16px; - border: 2px solid #d0d1d3; - border-radius: 50%; - background-color: var(--panel); - cursor: pointer; - position: relative; - transition: all 0.2s ease; -} - -input[type="radio"]:hover { - border-color: var(--blue); -} - -input[type="radio"]:checked { - border-color: var(--blue); - background-color: var(--blue); -} - -input[type="radio"]:checked::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 6px; - height: 6px; - border-radius: 50%; - background-color: white; -} - -input[type="radio"]:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.bg-circle {width: 50px;height: 50px;border-radius: 50%;} -.bg-circle-sm {width: 18px;height: 18px;border-radius: 50%;} -.bg-success {background: var(--green);} -.bg-danger,.bg-error {background: var(--red-dark);} -.bg-warning,.bg-amber {background: var(--amber);} -.bg-info {background: var(--blue);} -.bg-secondary {background: var(--text-dim);} - - -.igny8-form-group, .igny8-radio-group {padding: 5px 0} -.igny8-radio-group {margin: 0 15px} -/* =================================================================== - SYSTEM-WIDE DEBUG TABLE STYLES - =================================================================== */ - -/* ========================================= - Planner Settings Styles - ========================================= */ - .igny8-metrics-compact { - display: grid; - grid-template-columns: repeat(4, auto); - gap: 6px 15px; - padding: 4px 6px; - background: #526e8d3b; - border-radius: 8px; - width: fit-content; - float: right; -} - - .metric { - background: rgba(255, 255, 255, 0.05); - padding: 4px 8px; - border-radius: 6px; - text-align: center; - font-family: 'Inter', sans-serif; - min-width: 90px; - transition: 0.15s ease-in-out; - border-left: 3px solid rgba(255, 255, 255, 0.1); - display: flex; - flex-direction: row-reverse; - gap: 10px; - justify-content: space-between; - } - - .metric:hover { - background: rgba(255,255,255,0.08); - transform: translateY(-1px); - } - - .metric .val { - display: block; - font-size: 14px; - font-weight: 600; - color: #fff; - line-height: 1.2; - } - - .metric .lbl { - font-size: 10px; - letter-spacing: 0.3px; - color: rgba(255,255,255,0.6); - text-transform: uppercase; - } - - /* Color Variants */ - .metric.green { border-left-color: var(--green, #00c985); } - .metric.amber { border-left-color: var(--amber, #f39c12); } - .metric.purple { border-left-color: var(--purple, #9b59b6); } - .metric.blue { border-left-color: var(--blue, #3498db); } - .metric.teal { border-left-color: var(--teal, #1abc9c); } - -/* === DASHBOARD OVERVIEW STYLES === */ - -/* Dashboard Sections */ -.igny8-dashboard-section { - margin-bottom: 24px; - height: 100%; - display: flex; - flex-direction: column; -} - -/* Progress Bar Styles */ -.igny8-progress-item { - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid var(--stroke); -} - -.igny8-progress-item:last-child { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.igny8-progress-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.igny8-progress-label { - font: 500 14px/1.4 'Inter', system-ui, sans-serif; - color: var(--text); -} - -.igny8-progress-percent { - font: 600 14px/1.4 'Inter', system-ui, sans-serif; - color: var(--blue-dark); -} - -.igny8-progress-bar { - width: 100%; - height: 8px; - background: var(--panel-2); - border-radius: 4px; - overflow: hidden; - margin-bottom: 6px; -} - -.igny8-progress-fill { - height: 100%; - border-radius: 4px; - transition: width 0.3s ease; -} - -.igny8-progress-blue { - background: linear-gradient(90deg, var(--blue) 0%, var(--blue-dark) 100%); -} - -.igny8-progress-green { - background: linear-gradient(90deg, var(--green) 0%, var(--green-dark) 100%); -} - -.igny8-progress-amber { - background: linear-gradient(90deg, var(--amber) 0%, var(--amber-dark) 100%); -} - -.igny8-progress-purple { - background: linear-gradient(90deg, #8b5cf6 0%, #7c3aed 100%); -} - -.igny8-progress-text-dim { - background: linear-gradient(90deg, var(--text-dim) 0%, #64748b 100%); -} - -.igny8-progress-red { - background: linear-gradient(90deg, #e53e3e 0%, #c53030 100%); -} - -.igny8-progress-details { - font: 400 12px/1.4 'Inter', system-ui, sans-serif; - color: var(--text-dim); -} - -/* Status Cards */ -.igny8-status-cards { - gap: 20px; -} - -.igny8-status-card { - cursor: pointer; - transition: all 0.2s ease; - border: none; - background: var(--panel); -} - -.igny8-status-card:hover { - transform: translateY(-3px); - box-shadow: 0 8px 20px rgba(0,0,0,0.15), 0 12px 28px rgba(13,27,42,0.12); -} - -/* Colored Status Card Variants */ -.igny8-status-blue { - background: linear-gradient(135deg, var(--blue) 0%, var(--blue-dark) 100%); - color: white; -} - -.igny8-status-green { - background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%); - color: white; -} - -.igny8-status-amber { - background: linear-gradient(135deg, var(--amber) 0%, var(--amber-dark) 100%); - color: white; -} - -.igny8-clickable-card { - cursor: pointer; -} - -.igny8-status-metric { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; -} - -.igny8-status-count { - font: 700 28px/1.2 'Inter', system-ui, sans-serif; - color: rgba(255, 255, 255, 0.95); - margin: 0; - text-shadow: 0 1px 2px rgba(0,0,0,0.1); -} - -.igny8-status-label { - font: 500 13px/1.3 'Inter', system-ui, sans-serif; - color: rgba(255, 255, 255, 0.85); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-status-icon { - position: absolute; - top: 16px; - right: 16px; - opacity: 0.7; -} - -.igny8-status-card .igny8-card-body { - position: relative; - padding: 0 20px; - display: flex; - align-items: center; - justify-content: space-between; -} - -/* Next Actions Panel */ -.igny8-next-actions { - display: flex; - flex-direction: column; - gap: 12px; -} - -.igny8-action-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: var(--panel-2); - border-radius: 6px; - border-left: 3px solid var(--blue); - transition: all 0.2s ease; -} - -.igny8-action-item:hover { - background: #f8fafc; - border-left-color: var(--blue-dark); -} - -.igny8-action-item.igny8-action-complete { - border-left-color: var(--green); - background: #f0fdf4; -} - -.igny8-action-text { - font: 500 14px/1.4 'Inter', system-ui, sans-serif; - color: var(--text); -} - -.igny8-action-status { - font: 500 13px/1.4 'Inter', system-ui, sans-serif; - color: var(--green); -} - -.igny8-btn-text { - background: none; - border: none; - color: var(--blue); - font: 500 13px/1.4 'Inter', system-ui, sans-serif; - padding: 4px 8px; - border-radius: 4px; - text-decoration: none; - transition: all 0.2s ease; -} - -.igny8-btn-text:hover { - background: var(--blue); - color: white; - text-decoration: none; -} - -/* Info Box Styles */ -.igny8-info-box { - background: #f0f8ff; - border: 1px solid #b3d9ff; - border-radius: 6px; - padding: 16px; - margin: 16px 0; -} - -.igny8-info-box p { - margin: 0 0 12px 0; - color: #555; -} - -.igny8-info-box p:last-child { - margin-bottom: 0; -} - -/* =================================================================== - UNIFIED NOTIFICATION SYSTEM - =================================================================== */ - -/* Single Global Notification Container */ -#igny8-global-notification { - position: fixed; - top: 20px; - right: 20px; - padding: 12px 20px; - border-radius: 6px; - color: white; - font-weight: 500; - z-index: 9999; - max-width: 400px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - transition: all 0.3s ease; - transform: translateX(0); - font-family: 'Inter', system-ui, sans-serif; - font-size: 14px; - line-height: 1.4; - display: none; -} - -/* Notification Type Styles */ -#igny8-global-notification.success { - background: var(--green); - border-left: 4px solid var(--green-dark); -} - -#igny8-global-notification.error { - background: var(--red-dark); - border-left: 4px solid #b91c1c; -} - -#igny8-global-notification.warning { - background: var(--amber); - border-left: 4px solid var(--amber-dark); -} - -#igny8-global-notification.info { - background: var(--blue); - border-left: 4px solid var(--blue-dark); -} - -/* Animation States */ -#igny8-global-notification.show { - display: block !important; - animation: slideInRight 0.3s ease-out; - opacity: 1; - visibility: visible; -} - -#igny8-global-notification.hide { - animation: slideOutRight 0.3s ease-in; - opacity: 0; - visibility: hidden; -} - -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(100%); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes slideOutRight { - from { - opacity: 1; - transform: translateX(0); - } - to { - opacity: 0; - transform: translateX(100%); - } -} - -/* Hover Effects */ -#igny8-global-notification:hover { - transform: translateX(-5px); - box-shadow: 0 6px 16px rgba(0,0,0,0.2); -} - -/* Textarea Color Variants */ -.igny8-textarea-green { - border-left: 3px solid var(--green); -} - -.igny8-textarea-orange { - border-left: 3px solid var(--amber); -} - -.igny8-textarea-blue { - border-left: 3px solid var(--blue); -} - -.igny8-textarea-purple { - border-left: 3px solid var(--purple); -} - -.igny8-textarea-teal { - border-left: 3px solid var(--teal); -} - -.igny8-textarea-indigo { - border-left: 3px solid var(--indigo); -} - -/* Recent Activity Styles */ -.igny8-recent-activity { - display: flex; - flex-direction: column; - gap: 12px; -} - -.igny8-list-item { - padding: 12px; - border: 1px solid var(--stroke); - border-radius: var(--radius); - background: var(--panel); - transition: all 0.2s ease; -} - -.igny8-list-item:hover { - border-color: var(--blue); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1); -} - -.igny8-item-content { - display: flex; - flex-direction: column; - gap: 6px; -} - -.igny8-item-title a { - font-weight: 600; - color: var(--text); - text-decoration: none; - font-size: 14px; -} - -.igny8-item-title a:hover { - color: var(--blue); -} - -.igny8-item-meta { - display: flex; - align-items: center; - gap: 12px; - font-size: 12px; - color: var(--text-muted); -} - -.igny8-item-cluster { - color: var(--text-muted); -} - -.igny8-item-date { - color: var(--text-muted); -} - -.igny8-empty-state { - text-align: center; - padding: 40px 20px; - color: var(--text-muted); -} - -.igny8-empty-state p { - margin-bottom: 16px; -} - -/* Content Types and Publishing Stats */ -.igny8-content-types, .igny8-publishing-stats { - display: flex; - flex-direction: column; - gap: 16px; -} - -.igny8-type-item, .igny8-stat-item { - display: flex; - flex-direction: column; - gap: 8px; -} - -.igny8-type-info, .igny8-stat-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.igny8-type-name, .igny8-stat-label { - font-weight: 600; - color: var(--text); - font-size: 14px; -} - -.igny8-type-count, .igny8-stat-count { - font-weight: 700; - color: var(--blue); - font-size: 16px; -} - -.igny8-type-bar, .igny8-stat-bar { - height: 6px; - background: var(--stroke); - border-radius: 3px; - overflow: hidden; -} - -.igny8-type-progress, .igny8-stat-progress { - height: 100%; - background: var(--blue); - border-radius: 3px; - transition: width 0.3s ease; -} - -.igny8-stat-progress.igny8-progress-amber { - background: var(--amber); -} - -.igny8-stat-progress.igny8-progress-green { - background: var(--green); -} - -.igny8-stat-progress.igny8-progress-purple { - background: var(--purple); -} - -/* Enhanced Analytics Cards */ -.igny8-equal-height { - align-items: stretch; -} - -.igny8-analytics-card .igny8-card { - height: 100%; - display: flex; - flex-direction: column; -} - -.igny8-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid var(--stroke); -} - -.igny8-card-header-content { - display: flex; - align-items: flex-start; - gap: 12px; - flex: 1; -} - -.igny8-card-icon { - flex-shrink: 0; - width: 40px; - height: 40px; - background: rgba(59, 130, 246, 0.1); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; -} - -.igny8-card-title-text h3 { - margin: 0 0 4px 0; - font-size: 16px; - font-weight: 600; - color: var(--text); -} - -.igny8-card-subtitle { - margin: 0; - font-size: 12px; - color: var(--text-dim); - font-weight: 400; -} - -.igny8-card-subtitle.igny8-centered { - text-align: center; -} - -/* Standard Dashboard Card Headers */ -.igny8-standard-header { - background: #fff !important; - border-bottom: 2px solid var(--blue) !important; - margin-bottom: 16px !important; - padding: 16px !important; - border-radius: 0 !important; -} - -.igny8-standard-header .igny8-card-title-text h3 { - color: var(--navy-bg-2) !important; - font-weight: 600 !important; - font-size: 26px !important; - margin: 0 0 4px 0 !important; -} - -.igny8-standard-header .igny8-card-subtitle { - color: var(--text-dim) !important; - font-size: 13px !important; - font-weight: 400 !important; -} - -.igny8-standard-header .igny8-card-icon { - background: rgba(59, 130, 246, 0.08) !important; - border-radius: 8px !important; - width: 40px !important; - height: 40px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - flex-shrink: 0 !important; -} - -.igny8-standard-header .igny8-card-header-content { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - width: 100% !important; -} - -/* Dashboard Icon Styles */ -.igny8-dashboard-icon-sm { - color: rgba(255,255,255,0.7) !important; - font-size: 24px !important; -} - -.igny8-dashboard-icon-lg { - font-size: 26px !important; -} - -.igny8-dashboard-icon-blue { - color: var(--blue) !important; -} - -.igny8-dashboard-icon-amber { - color: var(--amber) !important; -} - -.igny8-dashboard-icon-green { - color: var(--green) !important; -} - -.igny8-dashboard-icon-purple { - color: #8b5cf6 !important; -} - -.igny8-dashboard-icon-dim { - color: var(--text-dim) !important; - font-size: 32px !important; - margin-bottom: 12px !important; -} - -.igny8-card-metric { - text-align: right; - flex-shrink: 0; -} - -.igny8-metric-value { - display: block; - font-size: 24px; - font-weight: 700; - color: var(--blue); - line-height: 1; -} - -.igny8-metric-label { - display: block; - font-size: 11px; - color: var(--text-dim); - text-transform: uppercase; - font-weight: 500; - margin-top: 2px; -} - -.igny8-analytics-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.igny8-analytics-item { - display: flex; - flex-direction: column; - gap: 8px; -} - -.igny8-analytics-item.igny8-analytics-total { - padding-top: 16px; - border-top: 1px solid var(--stroke); - margin-top: 8px; -} - -.igny8-item-info { - display: flex; - justify-content: space-between; - align-items: center; -} - -.igny8-item-label { - font-size: 14px; - font-weight: 500; - color: var(--text); -} - -.igny8-item-value { - font-size: 16px; - font-weight: 700; - color: var(--blue); -} - -.igny8-item-progress { - display: flex; - align-items: center; - gap: 12px; -} - -.igny8-progress-track { - flex: 1; - height: 6px; - background: var(--stroke); - border-radius: 3px; - overflow: hidden; -} - -.igny8-progress-percent { - font-size: 12px; - font-weight: 600; - color: var(--text-dim); - min-width: 32px; - text-align: right; -} - -.igny8-empty-analytics { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px 20px; - text-align: center; - color: var(--text-dim); -} - -.igny8-empty-analytics p { - margin: 12px 0 16px 0; - font-size: 14px; -} - -.igny8-btn-sm { - padding: 6px 12px; - font-size: 12px; -} - -.igny8-status-desc { - font-size: 12px; - color: #fff; -} - -/* Step-by-Step UX Guide */ -.igny8-step-guide { - background: var(--panel); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 2px 6px rgba(0,0,0,0.08), 0 4px 10px rgba(13,27,42,0.06); -} - -.igny8-step-guide-header { - display: flex; - align-items: center; - margin-bottom: 15px; -} - -.igny8-step-guide-header h3 { - margin: 0; - color: var(--blue-dark); - font-size: 16px; - font-weight: 600; -} - -.igny8-step-guide-header .dashicons { - margin-right: 8px; - color: var(--blue); -} - -.igny8-steps-container { - display: flex; - gap: 15px; - overflow-x: auto; - padding-bottom: 10px; - scrollbar-width: thin; - scrollbar-color: var(--stroke) transparent; -} - -.igny8-steps-container::-webkit-scrollbar { - height: 6px; -} - -.igny8-steps-container::-webkit-scrollbar-track { - background: transparent; -} - -.igny8-steps-container::-webkit-scrollbar-thumb { - background: var(--stroke); - border-radius: 3px; -} - -.igny8-step { - flex: 0 0 auto; - min-width: 180px; - max-width: 220px; - background: var(--bg); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 15px; - position: relative; - transition: all 0.2s ease; -} - -.igny8-step:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.igny8-step-number { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - background: var(--blue); - color: white; - border-radius: 50%; - font-size: 12px; - font-weight: 600; - margin-bottom: 8px; -} - -.igny8-step.completed .igny8-step-number { - background: var(--green); -} - -.igny8-step.current .igny8-step-number { - background: var(--amber); - color: var(--text); -} - -.igny8-step-title { - font-size: 14px; - font-weight: 600; - color: var(--text); - margin-bottom: 6px; - line-height: 1.3; -} - -.igny8-step-status { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 8px; -} - -.igny8-step-status-icon { - font-size: 14px; -} - -.igny8-step-status-text { - font-size: 12px; - font-weight: 500; - color: var(--text-dim); -} - -.igny8-step.completed .igny8-step-status-text { - color: var(--green-dark); -} - -.igny8-step.current .igny8-step-status-text { - color: var(--amber-dark); -} - -.igny8-step.completed .igny8-step-status-icon { - color: var(--green); -} - -.igny8-step.current .igny8-step-status-icon { - color: var(--amber); -} - -.igny8-step-data { - font-size: 11px; - color: var(--text-dim); - margin-bottom: 8px; - line-height: 1.3; -} - -.igny8-step-action { - margin-top: 8px; -} - -.igny8-step-action .igny8-btn { - font-size: 11px; - padding: 4px 8px; - border-radius: 3px; -} - -.igny8-step-connector { - position: absolute; - top: 50%; - right: -8px; - width: 16px; - height: 2px; - background: var(--stroke); - transform: translateY(-50%); -} - -.igny8-step:last-child .igny8-step-connector { - display: none; -} - -.igny8-step.completed + .igny8-step .igny8-step-connector { - background: var(--green); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .igny8-step { - min-width: 160px; - } - - .igny8-steps-container { - gap: 10px; - } -} - -/* System-Wide Workflow Guide */ -.igny8-system-workflow { - background: var(--panel); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 25px; - margin-bottom: 25px; - box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 6px 16px rgba(13,27,42,0.06); -} - -.igny8-system-workflow-header { - display: flex; - align-items: center; - margin-bottom: 20px; -} - -.igny8-system-workflow-header h2 { - margin: 0; - color: var(--blue-dark); - font-size: 20px; - font-weight: 700; -} - -.igny8-system-workflow-header .dashicons { - margin-right: 12px; - color: var(--blue); - font-size: 24px; -} - -.igny8-system-workflow-subtitle { - color: var(--text-dim); - font-size: 14px; - margin-top: 5px; - margin-bottom: 0; -} - -.igny8-system-steps-container { - display: flex; - gap: 12px; - overflow-x: auto; - padding-bottom: 15px; - scrollbar-width: thin; - scrollbar-color: var(--stroke) transparent; - max-width: 1200px; -} - -.igny8-system-steps-container::-webkit-scrollbar { - height: 8px; -} - -.igny8-system-steps-container::-webkit-scrollbar-track { - background: transparent; -} - -.igny8-system-steps-container::-webkit-scrollbar-thumb { - background: var(--stroke); - border-radius: 4px; -} - -.igny8-system-step { - flex: 0 0 auto; - min-width: 160px; - max-width: 180px; - background: var(--bg); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 16px; - position: relative; - transition: all 0.3s ease; - cursor: pointer; -} - -.igny8-system-step:hover { - transform: translateY(-3px); - box-shadow: 0 6px 20px rgba(0,0,0,0.12); - border-color: var(--blue); -} - -.igny8-system-step.disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.igny8-system-step.disabled:hover { - transform: none; - box-shadow: none; - border-color: var(--stroke); -} - -.igny8-system-step-number { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: var(--blue); - color: white; - border-radius: 50%; - font-size: 13px; - font-weight: 700; - margin-bottom: 10px; -} - -.igny8-system-step.completed .igny8-system-step-number { - background: var(--green); -} - -.igny8-system-step.in_progress .igny8-system-step-number { - background: var(--amber); - color: var(--text); -} - -.igny8-system-step.missing .igny8-system-step-number { - background: var(--text-dim); -} - -.igny8-system-step.disabled .igny8-system-step-number { - background: var(--stroke); - color: var(--text-dim); -} - -.igny8-system-step-title { - font-size: 13px; - font-weight: 600; - color: var(--text); - margin-bottom: 8px; - line-height: 1.3; -} - -.igny8-system-step-status { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 8px; -} - -.igny8-system-step-status-icon { - font-size: 16px; -} - -.igny8-system-step-status-text { - font-size: 11px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-system-step.completed .igny8-system-step-status-text { - color: var(--green-dark); -} - -.igny8-system-step.in_progress .igny8-system-step-status-text { - color: var(--amber-dark); -} - -.igny8-system-step.missing .igny8-system-step-status-text { - color: var(--text-dim); -} - -.igny8-system-step.completed .igny8-system-step-status-icon { - color: var(--green); -} - -.igny8-system-step.in_progress .igny8-system-step-status-icon { - color: var(--amber); -} - -.igny8-system-step.missing .igny8-system-step-status-icon { - color: var(--text-dim); -} - -.igny8-system-step-data { - font-size: 10px; - color: var(--text-dim); - margin-bottom: 10px; - line-height: 1.4; -} - -.igny8-system-step-action { - margin-top: 8px; -} - -.igny8-system-step-action .igny8-btn { - font-size: 10px; - padding: 4px 8px; - border-radius: 3px; - width: 100%; - text-align: center; -} - -.igny8-system-step-connector { - position: absolute; - top: 50%; - right: -7px; - width: 14px; - height: 2px; - background: var(--stroke); - transform: translateY(-50%); - z-index: 1; -} - -.igny8-system-step:last-child .igny8-system-step-connector { - display: none; -} - -.igny8-system-step.completed + .igny8-system-step .igny8-system-step-connector { - background: var(--green); -} - -.igny8-system-step.in_progress + .igny8-system-step .igny8-system-step-connector { - background: var(--amber); -} - -/* System workflow responsive adjustments */ -@media (max-width: 1200px) { - .igny8-system-step { - min-width: 140px; - max-width: 160px; - } -} - -@media (max-width: 768px) { - .igny8-system-step { - min-width: 120px; - max-width: 140px; - padding: 12px; - } - - .igny8-system-steps-container { - gap: 8px; - } - - .igny8-system-step-number { - width: 24px; - height: 24px; - font-size: 11px; - } - - .igny8-system-step-title { - font-size: 12px; - } -} -.workflow-steps { - display: flex; -} -/* === WORDPRESS ADMIN STYLES === */ -/* Ensure WordPress admin styles are available for cron pages */ -.wp-list-table { - border: 1px solid #c3c4c7; - border-spacing: 0; - width: 100%; - clear: both; - margin: 0; -} - -.wp-list-table.widefat { - border-collapse: collapse; -} - -.wp-list-table.fixed { - table-layout: fixed; -} - -.wp-list-table.striped tbody tr:nth-child(odd) { - background-color: #f6f7f7; -} - -.wp-list-table.striped tbody tr:nth-child(even) { - background-color: #fff; -} - -.wp-list-table th, -.wp-list-table td { - border-bottom: 1px solid #c3c4c7; - padding: 8px 10px; - text-align: left; - vertical-align: top; -} - -.wp-list-table th { - background-color: #f1f1f1; - font-weight: 600; - color: #1d2327; -} - -.wp-list-table tbody tr:hover { - background-color: #f0f6fc; -} - - - - - - -/* WordPress admin wrap styles */ -.wrap { - margin: 0 20px 0 2px; -} - -.wrap h1 { - margin: 0 0 20px; - padding: 0; - font-size: 23px; - font-weight: 400; - line-height: 1.3; - color: #1d2327; -} - - - - - - -/* WordPress admin notice styles */ -.notice { - background: #fff; - border-left: 4px solid #fff; - box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); - margin: 5px 15px 2px; - padding: 1px 12px; -} - -.notice.notice-success { - border-left-color: #00a32a; -} - -.notice p { - margin: .5em 0; - padding: 2px; -} - -/* WordPress admin submit styles */ -.submit { - padding: 0; - margin: 0; -} - - -.ai-integration, .new-content-status { - border-right: 3px solid #ccc; - margin-right: 25px; - padding-right: 25px; -} -#igny8-ai-integration-form .igny8-form-group h4 {margin-bottom: 35px;} -.new-content-status .igny8-form-group h4 {margin-bottom: 20px;} - - -.igny8-flex-row { - display: flex; - align-items: center; - align-content: center; - -} - -/* Workflow section styling */ -.igny8-workflow-section { - margin-top: 30px; -} - -.igny8-step-card { - border-left: 4px solid #e5e7eb; - transition: all 0.3s ease; -} - -.igny8-step-card.completed { - border-left-color: #10b981; - background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%); -} - -.igny8-step-card.current { - border-left-color: #3b82f6; - background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); -} - -.igny8-step-card.pending { - border-left-color: #f59e0b; - background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); -} - -.igny8-step-header { - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 15px; -} - -.igny8-step-number { - background: #6b7280; - color: white; - width: 35px; - height: 35px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - font-size: 16px; - flex-shrink: 0; -} - -.igny8-step-card.completed .igny8-step-number { - background: #10b981; -} - -.igny8-step-card.current .igny8-step-number { - background: #3b82f6; -} - -.igny8-step-card.pending .igny8-step-number { - background: #f59e0b; -} - -.igny8-step-title { - font-size: 18px; - font-weight: 600; - color: #1f2937; - margin: 0; -} - -.igny8-step-status { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; -} - -.igny8-step-status-icon { - font-size: 16px; -} - -.igny8-step-status-text { - font-size: 14px; - font-weight: 500; -} - -.igny8-step-card.completed .igny8-step-status-text { - color: #10b981; -} - -.igny8-step-card.current .igny8-step-status-text { - color: #3b82f6; -} - -.igny8-step-card.pending .igny8-step-status-text { - color: #f59e0b; -} - -.igny8-step-data { - color: #6b7280; - font-size: 14px; - margin-bottom: 15px; -} - -.igny8-step-action { - margin-top: 15px; -} - -.igny8-grid-4 { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 20px; -} - -/* Card layout optimization for settings cards */ -.igny8-flex-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - width: 100%; -} - -.igny8-card-header-content { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - gap: 20px; -} - -.igny8-card-title-text { - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; - min-width: 0; -} - -.igny8-card-title-text h3 { - margin: 0 0 5px 0; - font-size: 18px; - font-weight: 600; - color: #1f2937; -} - -.igny8-card-title-text .igny8-card-subtitle { - margin: 0; - font-size: 14px; - color: #6b7280; -} - -.igny8-flex-row form { - display: flex; - align-items: flex-end; - gap: 20px; - flex: 1; - justify-content: flex-end; - flex-direction: column; - align-content: flex-end; -} - -.igny8-form-group {display: flex;align-items: flex-start;gap: 5px;flex: 1;flex-direction: column;} - -/* Editor Type Selection Styles */ -.igny8-editor-option { - display: block; - margin-bottom: 15px; - padding: 15px; - border: 2px solid #e1e5e9; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s ease; - background: #fff; -} - -.igny8-editor-option:hover { - border-color: #0073aa; - background-color: #f8f9fa; -} - -.igny8-editor-option.selected { - border-color: #0073aa; - background-color: #f0f8ff; - box-shadow: 0 2px 8px rgba(0, 115, 170, 0.1); -} - -.igny8-editor-option input[type="radio"] { - margin-right: 10px; - transform: scale(1.2); -} - -.igny8-editor-option-content { - display: inline-block; - vertical-align: top; - width: calc(100% - 30px); -} - -.igny8-editor-option-title { - font-size: 16px; - font-weight: 600; - color: #333; - margin: 0 0 5px 0; -} - -.igny8-editor-option-description { - margin: 5px 0 0 0; - color: #666; - font-size: 14px; - line-height: 1.4; -} - -.igny8-form-actions { - display: flex; - align-items: center; - flex-shrink: 0; - justify-content: flex-end; -} - -.igny8-card-body { - padding: 20px; -} - -.igny8-mode-toggle-label, -.igny8-radio-group { - display: flex; - align-items: center; - gap: 10px; - white-space: nowrap; -} - -.igny8-mode-toggle-label { - gap: 15px; -} - - - -/* Responsive adjustments */ -@media (max-width: 768px) { - .igny8-grid-4 { - grid-template-columns: 1fr; - } - - .igny8-step-header { - flex-direction: column; - text-align: center; - gap: 10px; - } - - .igny8-flex-row { - flex-direction: column; - align-items: stretch; - gap: 15px; - } - - .igny8-card-header-content { - flex-direction: column; - align-items: stretch; - } - - .igny8-flex-row form { - flex-direction: column; - align-items: stretch; - } - - .igny8-form-group { - justify-content: flex-start; - } -} - -#igny8-new-content-form .igny8-flex-row .igny8-form-actions, #igny8-ai-integration-form .igny8-flex-row .igny8-form-actions {margin-top: 0;} - - -.igny8-card .igny8-standard-header .igny8-card-title-text h3 { - font-size: 20px !important; -} - -.igny8-error-log { - width: 800px; -} -.igny8-form-group select{min-width: 200px;} -.igny8-form-group textarea {width: 80%;} - -/* Title with Badge Layout */ -.igny8-title-with-badge { - display: flex; - align-items: center; - gap: 8px; -} - -.igny8-title-actions { - display: flex; - align-items: center; - gap: 4px; - margin-left: auto; -} - -.igny8-title-text { - flex: 1; -} - -.igny8-menu-toggle { - padding: 8px; - border: none; - background: transparent; - cursor: pointer; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} - -.igny8-menu-toggle:hover { - background: rgba(0, 0, 0, 0.05); - transform: scale(1.1); -} - -.igny8-hamburger { - display: flex; - flex-direction: column; - gap: 3px; - width: 16px; - height: 14px; -} - -.igny8-hamburger span { - display: block; - width: 100%; - height: 2px; - background: var(--blue); - border-radius: 1px; - transition: all 0.2s ease; -} - -.igny8-menu-toggle:hover .igny8-hamburger span { - background: var(--blue-dark); -} - -/* Expandable Description Row */ -.igny8-description-row { - display: none; - background: var(--panel-2); - border-top: 1px solid var(--border); -} - -.igny8-description-row.expanded { - display: table-row; -} - -.igny8-description-content-cell { - padding: 16px; - color: var(--text); - line-height: 1.5; -} - -.igny8-description-content { - background: var(--panel-1); - border-radius: 6px; - padding: 12px; - border: 1px solid var(--border); -} - -.igny8-description-content p { - margin: 0 0 8px 0; -} - -.igny8-description-content p:last-child { - margin-bottom: 0; -} - -/* Description Section Styling */ -.description-section { - margin-bottom: 16px; - padding: 12px; - background: var(--panel-2); - border-radius: 6px; - border-left: 4px solid var(--blue); -} - -.description-section:last-child { - margin-bottom: 0; -} - -.section-heading { - margin: 0 0 8px 0; - color: var(--blue); - font-size: 14px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.section-content { - position: relative; -} - -.content-type-badge { - display: inline-block; - background: var(--blue); - color: white; - padding: 2px 8px; - border-radius: 12px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 8px; -} - -.content-details { - color: var(--text); - line-height: 1.5; - font-size: 13px; -} - -.description-item { - margin-bottom: 8px; - padding: 8px; - background: var(--panel-2); - border-radius: 4px; - border-left: 3px solid var(--blue); -} - -.description-item:last-child { - margin-bottom: 0; -} - -.description-item strong { - color: var(--blue); - display: block; - margin-bottom: 4px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.description-text { - color: var(--text); - line-height: 1.5; - white-space: pre-wrap; -} - -/* Image Prompts Toggle Styles */ -.igny8-image-prompts-display { - display: flex; - align-items: center; - gap: 8px; -} - -.igny8-image-icon { - font-size: 16px; - display: inline-block; - width: 16px; - height: 16px; - line-height: 1; -} - -.igny8-image-prompts-toggle { - background: none; - border: none; - padding: 8px; - margin-bottom: 3px; - cursor: pointer; - border-radius: 4px; - transition: all 0.2s ease; - -} - -.igny8-image-prompts-toggle:hover { - background: rgba(0, 0, 0, 0.05); - transform: scale(1.1); -} - -/* Expandable Image Prompts Row */ -.igny8-image-prompts-row { - display: none; - background: var(--panel-2); - border-top: 1px solid var(--border); -} - -.igny8-image-prompts-row.expanded { - display: table-row; -} - -.igny8-image-prompts-content-cell { - padding: 16px; - color: var(--text); - line-height: 1.5; -} - -.igny8-image-prompts-content { - background: var(--panel-1); - border-radius: 6px; - padding: 12px; - border: 1px solid var(--border); -} - -.igny8-image-prompts-content .prompt-item { - margin-bottom: 8px; - padding: 8px; - background: var(--panel-2); - border-radius: 4px; - border-left: 3px solid var(--blue); -} - -.igny8-image-prompts-content .prompt-item:last-child { - margin-bottom: 0; -} - -.igny8-image-prompts-content .prompt-item strong { - color: var(--blue); - display: block; - margin-bottom: 4px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-image-prompts-content .prompt-item:not(:last-child) { - margin-bottom: 12px; -} - -/* Ensure dashicons are properly styled */ -.igny8-image-icon.dashicons { - font-family: dashicons; - font-size: 16px; - color: var(--blue); - vertical-align: middle; -} - -.igny8-image-icon.dashicons:hover { - color: var(--blue-dark); -} - -/* Status with Badge Layout */ -.igny8-status-with-badge { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.igny8-status-text { - flex: 1; - min-width: 0; -} - -.igny8-status-with-badge .igny8-badge { - font-size: 10px; - padding: 2px 6px; - border-radius: 8px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Static image size options styling */ -.igny8-size-options-static { - display: flex; - gap: 10px; - margin-bottom: 10px; -} - -.igny8-size-static { - flex: 1; - padding: 12px 8px; - border: 2px solid var(--border-light); - border-radius: 8px; - text-align: center; - min-height: 60px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - opacity: 0.7; -} - -.igny8-size-static .size-label { - font-weight: 600; - font-size: 14px; - margin-bottom: 4px; -} - -.igny8-size-static .size-dimensions { - font-size: 12px; - opacity: 0.8; -} - -/* Different colors for each size option */ -/* DALL-E sizes */ -.igny8-size-square { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-color: #667eea; -} - -.igny8-size-portrait { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - color: white; - border-color: #f093fb; -} - -.igny8-size-landscape { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - color: white; - border-color: #4facfe; -} - -/* Runware sizes */ -.igny8-size-featured { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-color: #667eea; -} - -.igny8-size-desktop { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - color: white; - border-color: #f093fb; -} - -.igny8-size-mobile { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - color: white; - border-color: #4facfe; -} - -/* Image provider styling */ -.igny8-provider-info { - margin-bottom: 10px; -} - -.igny8-provider-badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - background: var(--bg-light); - border: 1px solid var(--border-light); - border-radius: 8px; - font-weight: 500; - color: var(--text-primary); -} - -.igny8-provider-badge .dashicons { - color: var(--blue); - font-size: 16px; -} -.igny8-card.igny8-prompt-section { - display: flex; - flex-direction: row; -} - -.igny8-card.igny8-prompt-section .igny8-dashboard-section { - width: 100%; -} - -/* Image Size Checkbox and Quantity Input Styling */ -.igny8-size-checkbox-container { - display: flex; - flex-direction: column; - gap: 15px; - margin-top: 10px; -} - -.igny8-size-option { - display: flex; - align-items: center; - padding: 15px; - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 8px; - transition: all 0.2s ease; - justify-content: space-between; -} - -.igny8-size-option:hover { - background: #f1f5f9; - border-color: #cbd5e1; -} - -.igny8-checkbox-label { - display: flex; - align-items: center; - cursor: pointer; - font-weight: 500; - color: #374151; - margin-right: 15px; -} - -.igny8-checkbox-label input[type="checkbox"] { - margin-right: 8px; - width: 16px; - height: 16px; - accent-color: var(--blue); -} - -.igny8-checkbox-text { - font-size: 14px; - font-weight: 500; -} - -.igny8-quantity-input { - display: flex; - align-items: center; - gap: 5px; -} - -.igny8-quantity-input label { - font-size: 12px; - color: #6b7280; - font-weight: 500; -} - -.igny8-quantity-input input[type="number"] { - padding: 4px 8px; - border: 1px solid #d1d5db; - border-radius: 4px; - font-size: 12px; - text-align: center; -} - -.igny8-quantity-input input[type="number"]:disabled { - background-color: #f3f4f6; - color: #9ca3af; - cursor: not-allowed; -} - -.igny8-size-info { - - font-size: 12px; - color: #6b7280; - background: #e5e7eb; - padding: 4px 8px; - border-radius: 4px; - font-weight: 500; -} - -/* Featured Image Row Styling */ -.igny8-featured-image-row { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-color: #667eea; -} - -.igny8-featured-image-row .igny8-size-info { - background: rgba(255, 255, 255, 0.2); - color: white; - font-weight: 600; -} - -.igny8-featured-image-row .igny8-size-info:first-child { - font-size: 14px; - font-weight: 700; -} \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/assets/css/core.css b/igny8-ai-seo-wp-plugin/assets/css/core.css deleted file mode 100644 index 666107d3..00000000 --- a/igny8-ai-seo-wp-plugin/assets/css/core.css +++ /dev/null @@ -1,3039 +0,0 @@ -/* IGNY8 UNIFIED CORE CSS – A-Z COMPACT */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); - -.is-dismissible {display: none} -#wpcontent{padding-left: 0px} - -/* === 1. TOKENS === */ -:root { - /* Primary Brand Blue (Rocket Cyan-based) */ - --blue: #0693e3; /* Rocket vivid cyan blue – primary brand & main CTA */ - --blue-dark: #0472b8; /* Darkened cyan for hover / active / gradient depth */ - - /* Success Green (cooler to match cyan) */ - --green: #0bbf87; /* Slightly cooler teal-green for success states */ - --green-dark: #08966b; /* Deeper teal-green for hover / active */ - - /* Amber / Warning (warmed up to complement cyan) */ - --amber: #ff7a00; /* Rocket's vivid orange for highlight / warning */ - --amber-dark: #cc5f00; /* Darker orange for hover / strong warning */ - - /* Danger / Destructive */ - --red-dark: #d13333; /* Refreshed red with better contrast against cyan */ - - --purple: #5d4ae3; /* Purple for highlighting / special emphasis */ - --purple-dark:#3a2f94; /* Darker purple for hover / active */ - - --navy-bg: #0d1b2a; /* Sidebar background */ - --navy-bg-2: #142b3f; /* Slightly lighter navy, hover/active */ - --surface: #f8fafc; /* Page background (soft gray-white) */ - --panel: #ffffff; /* Cards / panel foreground */ - --panel-2: #f1f5f9; /* Sub-panel / hover card background */ - - --text: #555a68; /* main headings/body text */ - --text-dim: #64748b; /* secondary/subtext */ - --text-light: #e5eaf0; /* text on dark sidebar */ - --stroke: #e2e8f0; /* table/grid borders and dividers */ - - --radius:6px;--sidebar-width:220px;--header-height:75px - - /* === UNIFIED GRADIENTS === */ - --igny8-gradient-blue: linear-gradient(135deg, var(--blue) 0%, var(--blue-dark) 100%); - --igny8-gradient-panel: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%); - --igny8-gradient-success: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%); - --igny8-gradient-warning: linear-gradient(135deg, var(--amber) 0%, var(--amber-dark) 100%); - --igny8-gradient-danger: linear-gradient(135deg, #ef4444 0%, var(--red-dark) 100%); - --igny8-gradient-info: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); - --igny8-gradient-purple: linear-gradient(135deg, var(--purple) 0%, var(--purple-dark) 100%); - --igny8-gradient-gray: linear-gradient(135deg, #6b7280 0%, #374151 100%); - --igny8-gradient-light: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); - --igny8-gradient-dark: linear-gradient(135deg, #1f2937 0%, #111827 100%); - - /* === UNIFIED BACKGROUNDS === */ - --igny8-bg-success: #d4edda; - --igny8-bg-success-border: #c3e6cb; - --igny8-bg-success-text: #155724; - --igny8-bg-warning: #fff3cd; - --igny8-bg-warning-border: #ffeaa7; - --igny8-bg-warning-text: #856404; - --igny8-bg-danger: #f8d7da; - --igny8-bg-danger-border: #f5c6cb; - --igny8-bg-danger-text: #721c24; - --igny8-bg-info: #d1ecf1; - --igny8-bg-info-border: #bee5eb; - --igny8-bg-info-text: #0c5460; - --igny8-bg-light: #f8f9fa; - --igny8-bg-light-border: #e9ecef; - --igny8-bg-light-text: #495057; - } - - .igny8-card-header.gradient { background: var(--igny8-gradient-panel); padding:10px 14px; border-radius: var(--radius) var(--radius) 0 0; } - -/* === 1.5. UNIFIED BACKGROUNDS, BADGES & GRADIENTS === */ -/* Background Utilities */ -.igny8-bg-success { background: var(--igny8-bg-success); border: 1px solid var(--igny8-bg-success-border); color: var(--igny8-bg-success-text); } -.igny8-bg-warning { background: var(--igny8-bg-warning); border: 1px solid var(--igny8-bg-warning-border); color: var(--igny8-bg-warning-text); } -.igny8-bg-danger { background: var(--igny8-bg-danger); border: 1px solid var(--igny8-bg-danger-border); color: var(--igny8-bg-danger-text); } -.igny8-bg-info { background: var(--igny8-bg-info); border: 1px solid var(--igny8-bg-info-border); color: var(--igny8-bg-info-text); } -.igny8-bg-light { background: var(--igny8-bg-light); border: 1px solid var(--igny8-bg-light-border); color: var(--igny8-bg-light-text); } - -/* Gradient Backgrounds */ -.igny8-gradient-blue { background: var(--igny8-gradient-blue); } -.igny8-gradient-success { background: var(--igny8-gradient-success); } -.igny8-gradient-warning { background: var(--igny8-gradient-warning); } -.igny8-gradient-danger { background: var(--igny8-gradient-danger); } -.igny8-gradient-info { background: var(--igny8-gradient-info); } -.igny8-gradient-purple { background: var(--igny8-gradient-purple); } -.igny8-gradient-gray { background: var(--igny8-gradient-gray); } -.igny8-gradient-light { background: var(--igny8-gradient-light); } -.igny8-gradient-dark { background: var(--igny8-gradient-dark); } - -/* Unified Badge System */ -.igny8-badge { padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 500; color: #fff; white-space: nowrap; display: inline-block; } -.igny8-badge-primary { background: var(--blue); } -.igny8-badge-success { background: var(--green); } -.igny8-badge-warning { background: var(--amber); } -.igny8-badge-danger { background: #ef4444; } -.igny8-badge-info { background: #3b82f6; } -.igny8-badge-purple { background: var(--purple); } -.igny8-badge-gray { background: #6b7280; } -.igny8-badge-dark-red { background: var(--red-dark); } -.igny8-badge-outline { background: transparent; border: 1px solid rgba(255,255,255,0.3); color: rgba(255,255,255,0.9); } - -/* Badge with Gradients */ -.igny8-badge-gradient-blue { background: var(--igny8-gradient-blue); } -.igny8-badge-gradient-success { background: var(--igny8-gradient-success); } -.igny8-badge-gradient-warning { background: var(--igny8-gradient-warning); } -.igny8-badge-gradient-danger { background: var(--igny8-gradient-danger); } -.igny8-badge-gradient-info { background: var(--igny8-gradient-info); } -.igny8-badge-gradient-purple { background: var(--igny8-gradient-purple); } - -/* Badge Sizes */ -.igny8-badge-sm { padding: 2px 6px; font-size: 10px; } -.igny8-badge-lg { padding: 6px 14px; font-size: 14px; } - -/* Badge with Icons */ -.igny8-badge-icon { display: inline-flex; align-items: center; gap: 4px; } -.igny8-badge-icon .dashicons { font-size: 12px; } - -/* Title and Status with Badge Layouts */ -.igny8-title-with-badge, .igny8-status-with-badge { display: flex; align-items: center; justify-content: space-between; gap: 8px; } -.igny8-title-text, .igny8-status-text { flex: 1; } -.igny8-title-actions, .igny8-status-actions { display: flex; align-items: center; gap: 4px; } - -/* Progress Bar Gradients */ -.igny8-progress-bar { background: var(--panel-2); border-radius: 10px; height: 24px; overflow: hidden; position: relative; } -.igny8-progress-fill { background: var(--igny8-gradient-blue); transition: width 0.5s ease; position: relative; height: 100%; } -.igny8-progress-fill-success { background: var(--igny8-gradient-success); } -.igny8-progress-fill-warning { background: var(--igny8-gradient-warning); } -.igny8-progress-fill-danger { background: var(--igny8-gradient-danger); } - -/* Button Gradients */ -.igny8-btn-gradient-blue { background: var(--igny8-gradient-blue); } -.igny8-btn-gradient-success { background: var(--igny8-gradient-success); } -.igny8-btn-gradient-warning { background: var(--igny8-gradient-warning); } -.igny8-btn-gradient-danger { background: var(--igny8-gradient-danger); } -.igny8-btn-gradient-info { background: var(--igny8-gradient-info); } -.igny8-btn-gradient-purple { background: var(--igny8-gradient-purple); } -.igny8-btn-gradient-gray { background: var(--igny8-gradient-gray); } - -/* Card Gradients */ -.igny8-card-gradient { background: var(--igny8-gradient-panel); } -.igny8-card-gradient-blue { background: var(--igny8-gradient-blue); color: white; } -.igny8-card-gradient-success { background: var(--igny8-gradient-success); color: white; } -.igny8-card-gradient-warning { background: var(--igny8-gradient-warning); color: white; } -.igny8-card-gradient-danger { background: var(--igny8-gradient-danger); color: white; } - -/* Modal and Overlay Backgrounds */ -.igny8-modal-bg { background: rgba(0,0,0,0.5); } -.igny8-overlay-light { background: rgba(255,255,255,0.9); } -.igny8-overlay-dark { background: rgba(0,0,0,0.8); } - -/* Status Indicators */ -.igny8-status-success { background: var(--igny8-bg-success); border-left: 4px solid var(--green); } -.igny8-status-warning { background: var(--igny8-bg-warning); border-left: 4px solid var(--amber); } -.igny8-status-danger { background: var(--igny8-bg-danger); border-left: 4px solid #ef4444; } -.igny8-status-info { background: var(--igny8-bg-info); border-left: 4px solid #3b82f6; } - -/* === 2. RESET & BASE === */ -*{margin:0;padding:0;box-sizing:border-box} -body{font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--surface);color:var(--text);line-height:1.4;font-size:14px;font-weight:600;} -a{text-decoration:none;color:inherit} - -/* === 3. LAYOUT === */ -.igny8-page-wrapper{display:flex;min-height:100vh;width: 100%;margin: auto;} -.igny8-main-area{flex:1;display:flex;flex-direction:column;background:var(--surface)} -.igny8-content{flex:1;padding:20px;background: #fff;box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08)} -.igny8-footer{background:var(--navy-bg-2);padding:10px 20px;text-align:center;color:var(--text-light);font-size:13px;border-top:1px solid rgba(255,255,255,.1)} - - -/* === 4. SIDEBAR === */ -.igny8-sidebar{width:var(--sidebar-width);background:var(--navy-bg-2);color:var(--text-light);display:flex;flex-direction:column;padding:16px 12px} -.igny8-sidebar-logo{font-size:20px;font-weight:600;text-align:center;margin-bottom:16px} -.igny8-version-badge{text-align:center;margin-bottom:16px} -.igny8-version-badge .igny8-badge{font-size:11px;font-weight:600;letter-spacing:0.5px} -.igny8-breadcrumb{font-size:12px;color:rgba(255,255,255,0.7);margin-bottom:20px;text-align:center;line-height:1.4} -.igny8-breadcrumb-link{color:#f59e0b;text-decoration:none;transition:color .2s} -.igny8-breadcrumb-link:hover{color:#fff} -.igny8-breadcrumb-separator{margin:0 6px;opacity:0.6} -.igny8-breadcrumb-current{color:rgba(255,255,255,0.9);font-weight:500} -.igny8-sidebar-nav{display:flex;flex-direction:column;gap:8px} -.igny8-sidebar-link{display:flex;align-items:center;gap:10px;font-size:14px;padding:8px 12px;border-radius:var(--radius);color:var(--text-light);transition:background .2s} -.igny8-sidebar-link:hover{background:rgba(255,255,255,.08);color: #fff;} -.igny8-sidebar-link.active{background:var(--blue);color:#fff} -.igny8-sidebar-metrics{margin-top:auto;display:flex;flex-direction:column;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)} -.igny8-sidebar-metric{display:flex;justify-content:space-between;font-size:13px} -.igny8-sidebar-metric .label{opacity:.8} -.igny8-sidebar-footer-container{position:relative;width:100%;margin-top:auto;padding:16px 12px 16px 12px} -.igny8-sidebar-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:12px;display:flex;flex-direction:column;gap:6px} - -/* === 5. HEADER === */ -.igny8-header{display:flex;align-items:center;justify-content:space-between;background:var(--navy-bg-2);height:var(--header-height);padding:0 20px;border-bottom:1px solid rgba(255,255,255,.1);color:var(--text-light)} -.igny8-header-left{display:flex;align-items:center;gap:20px} -.igny8-page-title h1{font-size:22px;font-weight:600;color:#fff;margin:0 0 4px 0;gap:20px} -.igny8-page-description{font-size:13px;color:rgba(255,255,255,0.8);margin:0;line-height:1.3} -.igny8-breadcrumbs{font-size:13px;color:var(--text-light);opacity:.8} -.igny8-breadcrumbs a{color:var(--blue);font-weight:500} -.igny8-header-center{display:flex;align-items:center;justify-content:center;flex:1;text-align:center} -.igny8-marquee-ticker{font-size:13px;color:var(--text-light);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.igny8-header-right{display:flex;align-items:center;gap:20px} -.igny8-metrics{display:flex;align-items:center;gap:8px} -.igny8-badge{padding:4px 10px;border-radius:4px;font-size:12px;font-weight:500;color:#fff;white-space:nowrap} -.igny8-badge.igny8-btn-primary{background:var(--blue)} -.igny8-badge.igny8-btn-success{background:var(--green)} -.igny8-badge.igny8-btn-warning{background:var(--amber)} -.igny8-badge.igny8-btn-outline{background:transparent;border:1px solid rgba(255,255,255,0.3);color:rgba(255,255,255,0.9)} -.igny8-header-icons{display: flex; - align-items: center; - gap: 14px; - margin: 0 20px 10px 0; - align-content: center; - } -.igny8-header-icons .dashicons{font-size:26px;cursor:pointer;color:var(--text-light);transition:color .2s} -.igny8-header-icons .dashicons:hover{color:var(--blue)} - -/* Fix for dashicons in buttons */ -.igny8-btn .dashicons { font-family: dashicons !important; font-size: 16px; line-height: 1; text-decoration: none; font-weight: normal; font-style: normal; vertical-align: top; margin-right: 6px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.igny8-icon-only svg { font-family: inherit !important; font-style: normal !important; font-weight: normal !important; } -.igny8-actions-cell button { font-family: inherit !important; } -.igny8-actions-cell button svg { font-family: inherit !important; } -.dashicons { font-variant: normal; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - -/* === 6. BUTTONS === */ -.igny8-btn{display:inline-flex;align-items:center;justify-content:center;padding:4px 12px;font-size:13px;font-weight:500;line-height:1.3;border:none;border-radius:var(--radius,6px);cursor:pointer;transition:all .2s ease-in-out;color:#fff;text-decoration:none;white-space:nowrap;margin: 0 5px} -.igny8-btn:disabled,.igny8-btn.disabled{opacity:.5;cursor:not-allowed} -.igny8-btn-primary{background:var(--blue,#3b82f6)} -.igny8-btn-primary:hover{background:var(--blue-dark,#2563eb)} -.igny8-btn-secondary{background:var(--text-dim,#64748b);color:#fff} -.igny8-btn-secondary:hover{background:#475569} -.igny8-btn-outline{background:transparent;border:1px solid var(--stroke,#e2e8f0);color:var(--text,#0f172a)} -.igny8-btn-outline:hover{background:rgba(0,0,0,.05)} -.igny8-btn-success{background:var(--green,#10b981)} -.igny8-btn-success:hover{background:var(--green-dark,#059669)} -.igny8-btn-accent{background:var(--amber,#f59e0b)} -.igny8-btn-accent:hover{background:var(--amber-dark,#d97706)} -.igny8-btn-danger{background:#ef4444} -.igny8-btn-danger:hover{opacity:.9} -.igny8-btn-icon{width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center} - -/* === 9. TABLE === */ -.igny8-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--stroke);border-radius:4px;overflow:visible;font-size:14px} -.igny8-table thead th{background:var(--navy-bg-2);color:var(--text-light);font-weight:500;text-align:left;padding:6px 10px;border-bottom:1px solid var(--stroke)} -.igny8-table tbody td{padding:6px 10px;border-bottom:1px solid var(--stroke);color:var(--text);overflow:visible;position:relative} -.igny8-table tbody tr:hover{background:var(--panel-2)} -.igny8-table th {font-size: 110%;} -.igny8-col-checkbox{width:36px;text-align:center} -.igny8-col-actions{width:80px;text-align:center} - -/* === 10. PAGINATION === */ -.igny8-pagination{margin: 25px 0;display:flex;justify-content:center;align-items:center;} -.igny8-btn-pagination{height:auto;padding:3px 9px;font-size:12px;border-radius:4px;color:var(--blue);border:1px solid var(--blue);background:transparent;cursor:pointer;transition:all .2s} -.igny8-btn-pagination:hover:not(:disabled){background:var(--blue);color:#fff} -.igny8-btn-pagination:disabled{opacity:.4;cursor:default} -.igny8-btn-pagination.active{background:var(--blue);color:#fff;border-color:var(--blue)} - -/* === 11. MODAL === */ -.igny8-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;z-index:10000} -.igny8-modal.open{display:flex} -.igny8-modal-content{background:#fff;border-radius:6px;box-shadow:0 20px 40px rgba(0,0,0,.25);max-width:500px;width:90%;max-height:90vh;overflow:auto} -.igny8-modal-header,.igny8-modal-footer{padding:12px 16px;border-bottom:1px solid var(--stroke);display:flex;justify-content:space-between;align-items:center} -.igny8-modal-footer{border-top:1px solid var(--stroke)} -.igny8-btn-close{background:none;border:none;font-size:18px;cursor:pointer;color:var(--text-dim)} - -/* === 12. UTILITIES === */ -.igny8-flex{display:flex}.igny8-ml-auto{margin-left:auto} -.igny8-text-muted{color:var(--text-dim)}.igny8-mb-20{margin-bottom:20px} -.igny8-error-box{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;border-radius:4px;padding:12px;margin:10px 0;font-size:13px} - -/* === 13. RESPONSIVE === */ -@media(max-width:768px){.igny8-sidebar{display:none}.igny8-filters{flex-direction:column;align-items:flex-start}.igny8-filter-bar{flex-wrap:wrap}.igny8-table-actions{flex-wrap:wrap;gap:8px}} - -.is-dismissible {display: none} -#wpcontent{padding-left: 0px} - -.igny8-metrics-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;} -.igny8-page-title{font-size:1.4rem;font-weight:600;margin:0;} -.igny8-metrics-bar{display:flex;gap:8px;} -.igny8-badge{padding:4px 10px;border-radius:4px;font-size:.85rem;font-weight:500;color:#fff;} -.igny8-badge-primary{background:var(--blue-dark)} -.igny8-badge-success{background:#10b981;} -.igny8-badge-warning{background:#f59e0b;} -.igny8-badge-info{background:#3b82f6;} - -/* === 7. Filters === */ -.igny8-filters{display:flex;align-items:center;justify-content: center;gap:10px;margin:25px 0} -.igny8-filter-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:#f9fafb;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08)} -.igny8-filter-group{position:relative;display:flex;align-items:center;gap:6px} -.igny8-search-input{width:200px;padding:6px 10px;border:1px solid var(--igny8-stroke);border-radius:4px;font-size:14px} -.igny8-filter-actions{display:flex;align-items:center;gap:8px} - -/* === 8. Dropdowns === */ -.select{position:relative;min-width:120px} -.select-btn{display:flex;align-items:center;justify-content:space-between;width:100%;padding:6px 10px;font-size:13px;background:#fff;border:1px solid var(--igny8-stroke);border-radius:4px;cursor:pointer;box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08);color:var(--text);font-weight:500} -.select-list{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid var(--igny8-stroke);border-radius:4px;box-shadow:0 2px 6px rgba(0,0,0,.08);z-index:999999;max-height:200px;overflow-y:auto} -.select-item{padding:6px 10px;font-size:14px;cursor:pointer;border-bottom:1px solid #f1f5f9} -.select-item:last-child{border-bottom:none} -.select-item:hover{background:#f1f5f9} -.select.open .select-list {display:block;} -.select-arrow {font-size: 10px} -.igny8-table-actions{display:flex;align-items:center;gap:8px;justify-content: space-between; margin-bottom: 10px;} - -/* === 15. Icon Buttons === */ - -.igny8-icon-only{background:none;border:none;padding:0;margin:0 4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:opacity .2s} -.igny8-icon-only svg{width:18px;height:18px} -.igny8-icon-edit svg{color:var(--blue,#3b82f6)} -.igny8-icon-save svg {color: #fff;} -.igny8-icon-save {background-color: var(--success, #10b981);} -.igny8-icon-cancel svg {color: #fff;} -.igny8-icon-cancel {background-color: var(--text-dim, #64748b);} -.igny8-icon-edit:hover svg{color:var(--text-dim)} -.igny8-icon-delete svg{color:#ef4444} -.igny8-icon-delete:hover svg{color:#dc2626} -.igny8-icon-save{background:var(--green,#10b981);color:#fff;border-radius:8px;padding:0px;transition:background .2s} -.igny8-icon-save:hover{background:var(--green-dark,#059669)} -.igny8-icon-cancel{background:var(--text-dim,#64748b);color:#fff;border-radius:8px;padding:0px;transition:background .2s} -.igny8-icon-cancel:hover{background:var(--text,#0f172a)} -.igny8-icon-play svg{color:var(--blue,#3b82f6)} -.igny8-icon-play:hover svg{color:var(--text-dim)} -.igny8-icon-external svg{color:var(--text-dim,#64748b)} -.igny8-icon-external:hover svg{color:var(--blue,#3b82f6)} -.igny8-actions{white-space:nowrap} - -.igny8-page-title {display: flex;flex-direction: row;justify-content: space-around;align-items: center;flex-wrap: wrap;} -.igny8-sidebar-logo h2 {color: #b8d3ff;font-size: 1.3em;margin: 10px 0;font-weight: 900} - -/* ---------- GRID LAYOUTS ---------- */ - -.igny8-grid { display: grid; gap: 20px; } -.igny8-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: stretch; } -.igny8-grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin:50px 0;} -.igny8-grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; } - - -.igny8-module-cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px;margin:24px 0;} -.igny8-dashboard-cards,.igny8-grid-3{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:22px;margin:5px;} - -.igny8-grid-4{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:22px;margin-bottom:24px;} - - - -.igny8-card { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); padding: 18px; box-shadow: 0 2px 6px rgba(0,0,0,0.10), 0 4px 10px rgba(13,27,42,0.06); transition: box-shadow .25s ease, transform .2s ease; height: auto; display: flex; flex-direction: column; } -.igny8-card:hover { transform: translateY(-2px); box-shadow: 0 6px 14px rgba(0,0,0,0.14), 0 8px 20px rgba(13,27,42,0.10); } -.igny8-card-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; background:linear-gradient(90deg,var(--blue) 0%,var(--blue-dark) 100%); color:#fff; padding:12px 16px; border-radius:var(--radius) var(--radius) 0 0; margin:-10px -10px 12px -10px; } -.igny8-card-title { font:600 15px/1.4 'Inter',system-ui,sans-serif; margin:10px; color: #fff; display: flex;justify-content: space-between;} -.igny8-card-body { font:400 14px/1.55 'Inter',system-ui,sans-serif; color:var(--text); flex: 1; } - - -.igny8-help-text { font-size:13px; color:var(--text-dim); margin-top:6px; } -.igny8-status-badge { font-size:12px; font-weight:500; border-radius:4px; padding:2px 6px; line-height:1.2; } -.igny8-status-badge.mapped { background:#d1fae5; color:#065f46; } -.igny8-status-badge.unmapped { background:#fee2e2; color:#991b1b; } -.igny8-status-ok { color:var(--green); font-weight:500; } -.igny8-form-row { margin-bottom:18px; } -.igny8-form-row label { font-weight:500; font-size:14px; display:block; margin-bottom:6px; } -.igny8-form-row input[type="text"], .igny8-form-row select, .igny8-form-row textarea { width:100%; padding:8px 12px; font-size:14px; border:1px solid var(--stroke); border-radius:var(--radius); background:#fff; transition:border .2s, box-shadow .2s; } -.igny8-form-row input:focus, .igny8-form-row select:focus, .igny8-form-row textarea:focus { border-color:var(--blue); box-shadow:0 0 0 2px rgba(59,130,246,.18); outline:none; } -.igny8-radio-group, .igny8-checkbox-group { display:flex; flex-wrap:wrap; gap:16px; } -.igny8-radio-option, .igny8-checkbox-option { display:flex; align-items:center; gap:6px; font-size:14px; cursor:pointer; } -.igny8-toggle-switch { position:relative; width:42px; height:22px; display:inline-block; } -.igny8-toggle-switch input { opacity:0; width:0; height:0; } -.igny8-toggle-slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background:#cbd5e1; border-radius:22px; transition:background .3s; } -.igny8-toggle-slider:before { content:""; position:absolute; height:18px; width:18px; left:2px; bottom:2px; background:#fff; border-radius:50%; transition:transform .3s, box-shadow .3s; box-shadow:0 1px 2px rgba(0,0,0,0.3); } -.igny8-toggle-switch input:checked + .igny8-toggle-slider { background:var(--blue); } -.igny8-toggle-switch input:checked + .igny8-toggle-slider:before { transform:translateX(20px); } -.igny8-table-compact th, .igny8-table-compact td { padding:6px 8px; font-size:13px; } -.igny8-flex { display:flex; gap: 20px} -.igny8-ml-auto { margin-left:auto; } -.igny8-pad-5 { padding:5px; } -.igny8-mb-20 { margin-bottom:20px; } -@media (max-width:1024px) { .igny8-grid-3 { grid-template-columns:repeat(2, minmax(0, 1fr)); } .igny8-grid-4 { grid-template-columns:repeat(2, minmax(0, 1fr)); } } -@media (max-width:768px) { .igny8-grid-2, .igny8-grid-3, .igny8-grid-4 { grid-template-columns:1fr; } } - -/* ---------- THEME ADD-ONS ---------- */ -/* Gradient helpers */ - -/* ---------- GLOBAL ELEMENTS ---------- */ -.igny8-content h1,.igny8-settings h2,.igny8-welcome-section h2{font:700 22px/1.3 'Inter',system-ui,sans-serif;color:var(--navy-bg);margin-bottom:16px;} -.igny8-content p,.igny8-settings-section p,.igny8-welcome-text{font-size:14px;color:var(--text-dim);} -.igny8-settings-section h3,.igny8-card h3{font:600 16px/1.3 'Inter',system-ui,sans-serif;margin-bottom:12px;color:var(--blue-dark);} -.igny8-help-text{font-size:12px;color:var(--text-dim);margin-top:8px;} - -/* ---------- SUBHEADER ---------- */ -.igny8-submenu-buttons a{font-size:13px;padding:6px 16px;border-radius:var(--radius);background:var(--blue);color:#fff;font-weight:500;border:none;box-shadow:0 2px 6px rgba(0,0,0,.15);transition:all .2s ease;} -.igny8-submenu-buttons a:hover{background:var(--blue-dark);transform:translateY(-1px);box-shadow:0 4px 10px rgba(0,0,0,.2);} -.igny8-submenu-buttons a.active{background:var(--green);} - - -/* ---------- CARDS ---------- */ -.igny8-card,.igny8-module-card{margin: 15px 0;border:1px solid var(--stroke);background:var(--panel);border-radius:var(--radius);padding:10px;box-shadow:0 2px 6px rgba(0,0,0,.08),0 4px 10px rgba(13,27,42,.06);transition:all .25s ease; width: 100%;} -.igny8-card:hover,.igny8-module-card:hover{transform:translateY(-2px);box-shadow:0 6px 14px rgba(0,0,0,.12),0 8px 22px rgba(13,27,42,.10);} -.igny8-card-header{border-bottom:1px solid var(--stroke);padding-bottom:6px;margin-bottom:12px;} -h3,h4,h5,h6,igny8-card-title,.igny8-card-header h3{margin:0;font:600 16px/1.4 'Inter',system-ui,sans-serif;color:var(--blue-dark);} -.igny8-card-body{font-size:14px;color:var(--text);padding: 5px 15px;} -.igny8-card-actions{margin-top:12px;display:flex;gap:12px;} - -/* ---------- MODULE CARD HEADER ACCENTS ---------- */ -.igny8-module-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid var(--stroke);} -.igny8-module-header h4{font:600 16px/1.3 'Inter',system-ui,sans-serif;margin:0;color:var(--navy-bg);} -.igny8-module-card:nth-child(1) .igny8-module-header h4{border-left:3px solid var(--blue);padding-left:6px;} -.igny8-module-card:nth-child(2) .igny8-module-header h4{border-left:3px solid var(--amber-dark);padding-left:6px;} -.igny8-module-card:nth-child(3) .igny8-module-header h4{border-left:3px solid var(--green);padding-left:6px;} -.igny8-module-card:nth-child(4) .igny8-module-header h4{border-left:3px solid var(--blue-dark);padding-left:6px;} -.igny8-module-card:nth-child(5) .igny8-module-header h4{border-left:3px solid var(--amber-dark);padding-left:6px;} -.igny8-module-card:nth-child(6) .igny8-module-header h4{border-left:3px solid var(--green-dark);padding-left:6px;} -.igny8-module-card:nth-child(7) .igny8-module-header h4{border-left:3px solid var(--blue);padding-left:6px;} -.igny8-module-icon{background:rgba(59,130,246,0.08);padding:8px;border-radius:50%;display:flex;align-items:center;justify-content:center;} -.igny8-module-description p{font-size:14px;color:var(--text-dim);line-height:1.5;margin-bottom:12px;} - -/* ---------- FORM ELEMENTS ---------- */ -.igny8-form-row{margin-bottom:16px;} -.igny8-form-row label{font:600 13px/1.4 'Inter',system-ui,sans-serif;margin-bottom:5px;display:block;color:var(--navy-bg);} -.igny8-form-row input[type="text"],.igny8-form-row textarea,.igny8-form-row select{width:100%;padding:7px 12px;font-size:14px;border:1px solid var(--stroke);border-radius:var(--radius);transition:border .2s,box-shadow .2s;} -.igny8-form-row input:focus,.igny8-form-row textarea:focus,.igny8-form-row select:focus{border-color:var(--blue);box-shadow:0 0 0 2px rgba(59,130,246,.18);outline:none;} -.igny8-radio-group,.igny8-checkbox-group{display:flex;flex-wrap:wrap;gap:14px;} -.igny8-radio-option,.igny8-checkbox-option{display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;} -.igny8-radio-option input:checked+label{color:var(--blue-dark);font-weight:600;} -.igny8-checkbox-option input:checked+label{color:var(--green-dark);font-weight:600;} -.igny8-form-row textarea[name*="prompt"]{border-left:3px solid var(--amber-dark);} -.igny8-form-row select[name*="style"]{border-left:3px solid var(--green);} -.igny8-form-row input[type="text"]:focus{border-color:var(--blue-dark);} -/*textarea[name*="prompt"]{border-left:3px solid var(--amber-dark);}*/ -.igny8-textarea-orange {border-left: 3px solid var(--amber-dark);} -.igny8-textarea-green {border-left: 3px solid var(--green);} -.igny8-textarea-blue {border-left: 3px solid var(--blue);} - - -/* ---------- TABLES ---------- */ -.igny8-table-wrapper table,.igny8-table{width:100%;border-collapse:collapse;font-size:13px;} -.igny8-table-wrapper th,.igny8-table-wrapper td,.igny8-table th,.igny8-table td{border:1px solid var(--stroke);padding:6px 8px;text-align:left;} -.igny8-table-wrapper thead{background:var(--panel-2);} -.igny8-table-wrapper th,.igny8-table th{color:var(--navy-bg);font-weight:600;} -.igny8-table thead{background:var(--navy-bg);color:#fff;} -.igny8-table td code{color:var(--blue-dark);} -.igny8-status-ok{color:var(--green-dark);font-weight:500;} - -/* ---------- BUTTONS ---------- */ -.igny8-btn,.button.button-primary,#submit.button-primary{display:inline-flex;align-items:center;justify-content:center;padding:5px 10px;font-size:12px;font-weight:500;border-radius:var(--radius);cursor:pointer;transition:all .25s ease;text-decoration:none;box-shadow:0 2px 6px rgba(0,0,0,.15);} -.igny8-btn-primary{background:var(--blue);color:#fff;border:none;} -.igny8-btn-primary:hover{background:var(--blue-dark);transform:translateY(-1px);color: #fff;} -.igny8-btn-outline{background:transparent;border:1px solid var(--stroke);color:var(--text);} -.igny8-btn-outline:hover{background:var(--blue);color:#fff;box-shadow:0 4px 10px rgba(0,0,0,.15);} -.igny8-btn-danger{background:var(--red-dark);color:#fff;} -.igny8-btn-danger:hover{opacity:.9;} -#igny8-export-btn{border-color:var(--blue);color:var(--blue);} -#igny8-export-btn:hover{background:var(--blue);color:#fff;} -#igny8-import-btn{border-color:var(--green);color:var(--green);} -#igny8-import-btn:hover{background:var(--green);color:#fff;} -.button.button-primary,#submit.button-primary{background:var(--green);border:none;color:#fff;} -#submit.button-primary:hover{background:var(--green-dark);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.2);} - -/* ---------- TOGGLE SWITCH ---------- */ -/* Toggle switch styles are defined above at lines 230-235 */ - -/* ---------- ALERT NOTICES ---------- */ -.notice-error{border-left:4px solid var(--red-dark)!important;background:rgba(185,28,28,0.05);} -.notice-warning{border-left:4px solid var(--amber-dark)!important;background:rgba(217,119,6,0.05);} -.notice p{font-size:13px;} - -/* ---------- DASHBOARD QUICK STATS ---------- */ -.igny8-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(80px,1fr));gap:16px;} -.igny8-stat{text-align:center;background:var(--panel-2);padding:10px;border-radius:var(--radius);box-shadow:inset 0 1px 2px rgba(0,0,0,.05);} -.igny8-stat-number{display:block;font:600 18px/1.2 'Inter',system-ui,sans-serif;color:var(--blue-dark);} -.igny8-stat-label{font-size:13px;color:var(--text-dim);} - -/* ---------- DASHBOARD ACTIVITY ---------- */ -.igny8-activity-list{display:flex;flex-direction:column;gap:8px;} -.igny8-activity-item{display:flex;justify-content:space-between;padding:6px 10px;background:var(--panel-2);border-radius:var(--radius);} -.igny8-activity-time{font-size:13px;color:var(--amber-dark);font-weight:500;} -.igny8-activity-desc{font-size:13px;color:var(--text);} - - - -/* ---------- MODAL CORE ---------- */ -.igny8-modal-content{background:var(--panel);border-radius:var(--radius);width:420px;max-width:90%;margin:auto;padding:0;box-shadow:0 8px 24px rgba(0,0,0,.22),0 12px 32px rgba(13,27,42,.18);font-family:'Inter',system-ui,sans-serif;animation:fadeInScale .25s ease;} -@keyframes fadeInScale{0%{opacity:0;transform:scale(.96);}100%{opacity:1;transform:scale(1);}} - -/* ---------- HEADER ---------- */ -.igny8-modal-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--stroke);background:linear-gradient(90deg,var(--panel-2) 0%,#fff 100%);} -.igny8-modal-header h3{margin:0;font:600 16px/1.3 'Inter',system-ui,sans-serif;color:var(--blue-dark);} -.igny8-btn-close{background:transparent;border:none;font-size:20px;line-height:1;color:var(--text-dim);cursor:pointer;transition:color .2s;} -.igny8-btn-close:hover{color:var(--red-dark);} - -/* ---------- BODY ---------- */ -.igny8-modal-body{padding:16px 18px;font-size:14px;color:var(--text);} -.igny8-modal-body p{margin-bottom:10px;} -.igny8-modal-body strong{font-weight:600;color:var(--navy-bg);} -.igny8-modal-body ul{margin:6px 0 0 18px;padding:0;font-size:13px;color:var(--text-dim);line-height:1.4;} -.igny8-modal-body ul li{list-style:disc;} -.igny8-text-danger{color:var(--red-dark);font-weight:500;margin-top:10px;} - -/* ---------- FOOTER ---------- */ -.igny8-modal-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 18px;border-top:1px solid var(--stroke);background:var(--panel-2);} -.igny8-btn-secondary{background:var(--text-dim);color:#fff;padding:6px 14px;font-size:13px;border:none;border-radius:var(--radius);cursor:pointer;transition:all .25s ease;box-shadow:0 2px 5px rgba(0,0,0,.15);} -.igny8-btn-secondary:hover{background:#475569;transform:translateY(-1px);} -.igny8-btn-danger{background:var(--red-dark);color:#fff;padding:6px 14px;font-size:13px;border:none;border-radius:var(--radius);cursor:pointer;transition:all .25s ease;box-shadow:0 2px 5px rgba(0,0,0,.15);} -.igny8-btn-danger:hover{background:#991b1b;transform:translateY(-1px);} - -/* Automation UI Components */ -.igny8-automation-table { - margin-top: 16px; -} - -.igny8-status-badge { - display: inline-block; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-status-success { - background: var(--green); - color: white; -} - -.igny8-status-disabled { - background: var(--text-dim); - color: white; -} - -.igny8-toggle { - position: relative; - display: inline-block; - width: 44px; - height: 24px; - cursor: pointer; -} - -.igny8-toggle input { - opacity: 0; - width: 0; - height: 0; -} - -.igny8-toggle-slider { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--text-dim); - border-radius: 24px; - transition: 0.3s; -} - -.igny8-toggle-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background: white; - border-radius: 50%; - transition: 0.3s; -} - -.igny8-toggle input:checked + .igny8-toggle-slider { - background: var(--green); -} - -.igny8-toggle input:checked + .igny8-toggle-slider:before { - transform: translateX(20px); -} - -/* Mode Toggle Row */ -.igny8-mode-toggle-row { - display: flex; - align-items: center; - justify-content: center; - margin: 15px 0; -} - -.igny8-mode-toggle-label { - display: flex; - align-items: center; - gap: 12px; -} - -.igny8-mode-label { - font-size: 14px; - font-weight: 500; - color: var(--text); -} - -/* Cron Schedule Modal */ -.igny8-cron-config { - margin: 20px 0; -} - -.igny8-cron-item { - margin-bottom: 20px; - padding: 16px; - border: 1px solid var(--stroke); - border-radius: var(--radius); - background: var(--panel-2); -} - -.igny8-cron-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.igny8-cron-status { - font-size: 12px; - font-weight: 500; -} - -.igny8-cron-url { - display: flex; - align-items: center; - gap: 12px; - background: var(--panel); - padding: 12px; - border-radius: var(--radius); - border: 1px solid var(--stroke); -} - -.igny8-cron-url code { - flex: 1; - background: none; - color: var(--blue); - font-size: 13px; - word-break: break-all; -} - -.igny8-btn-copy { - background: var(--blue); - color: white; - border: none; - padding: 6px 12px; - border-radius: 4px; - font-size: 12px; - cursor: pointer; - transition: background 0.2s ease; -} - -.igny8-btn-copy:hover { - background: var(--blue-dark); -} - -.igny8-cron-info { - margin-top: 24px; - padding: 16px; - background: var(--panel-2); - border-radius: var(--radius); - border: 1px solid var(--stroke); -} - -.igny8-cron-info h4 { - margin: 0 0 8px 0; - font-size: 14px; - color: var(--text); -} - -.igny8-cron-info code { - background: var(--panel); - padding: 4px 8px; - border-radius: 4px; - font-family: monospace; - color: var(--blue); -} - -.igny8-sidebar-divider {border-bottom: 1px solid rgba(255, 255, 255, .1);margin: 10px 0} - -.igny8-pad-sm { padding: 5px; } -.igny8-pad-md { padding: 10px; } -.igny8-pad-lg { padding: 20px; } -.igny8-pad-xl { padding: 30px; } - - -td.igny8-col-actions button.igny8-btn.igny8-btn-success.igny8-btn-sm, td.igny8-col-actions button.igny8-btn.igny8-btn-danger.igny8-btn-sm {padding: 4px;} -th.igny8-col-actions {min-width:150px;} -td.igny8-col-actions {text-align: center;} - -/* === UI LAYER COMPONENTS === */ - -/* Page Layout Components */ -.igny8-main-content { flex: 1; display: flex; flex-direction: column; } -.igny8-header { padding: 10px 0; border-bottom: 1px solid var(--stroke)} -.igny8-header h1 { font-size: 24px; font-weight: 600; color: #fff; margin: 0; } - -/* Module Header */ -.igny8-module-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; } -.igny8-module-info h2 { font-size: 20px; font-weight: 600; margin: 0 0 8px 0; } -.igny8-module-description { color: var(--text-dim); font-size: 14px; margin: 0; } -.igny8-module-kpis { display: flex; gap: 20px; } -.igny8-kpi-item { text-align: center; } -.igny8-kpi-value { display: block; font-size: 24px; font-weight: 700; color: var(--blue); } -.igny8-kpi-label { font-size: 12px; color: var(--text-dim); text-transform: uppercase; } - -/* Submodule Navigation */ -.igny8-submodule-nav { margin-bottom: 20px; } -.igny8-submodule-tabs { display: flex; list-style: none; border-bottom: 1px solid var(--stroke); } -.igny8-submodule-tab { margin-right: 2px; } -.igny8-submodule-tab a { display: block; padding: 12px 16px; color: var(--text-dim); border-bottom: 2px solid transparent; transition: all 0.2s; } -.igny8-submodule-tab.active a, .igny8-submodule-tab a:hover { color: var(--blue); border-bottom-color: var(--blue); } - -/* Submodule Header */ -.igny8-submodule-header { margin-bottom: 20px; } -.igny8-back-link { margin-bottom: 12px; } -.igny8-btn-back { display: inline-flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 14px; } -.igny8-btn-back:hover { color: var(--blue); } -.igny8-submodule-title { font-size: 18px; font-weight: 600; margin: 0 0 8px 0; } -.igny8-submodule-description { color: var(--text-dim); font-size: 14px; margin: 0; } - -/* Filters Bar */ -.igny8-filters-bar { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); padding: 16px; margin-bottom: 20px; } -.igny8-filters-row { display: flex; gap: 16px; align-items: end; flex-wrap: wrap; } -.igny8-filter-item { display: flex; flex-direction: column; gap: 4px; min-width: 120px; } -.igny8-filter-label { font-size: 12px; font-weight: 500; color: var(--text); } -.igny8-filter-search, .igny8-filter-text, .igny8-filter-select, .igny8-filter-date { padding: 8px 12px; border: 1px solid var(--stroke); border-radius: var(--radius); font-size: 14px; } -.igny8-search-wrapper { position: relative; } -.igny8-search-icon { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: var(--text-dim); } -.igny8-filter-actions { display: flex; gap: 8px; } - -/* Table Actions */ -.igny8-table-actions { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } -.igny8-actions-primary, .igny8-actions-secondary { display: flex; gap: 8px; } -.igny8-bulk-actions { display: flex; align-items: center; gap: 12px; } -.igny8-bulk-select-all { display: flex; align-items: center; gap: 6px; font-size: 14px; } -.igny8-bulk-action-select { padding: 6px 10px; border: 1px solid var(--stroke); border-radius: var(--radius); } -.igny8-bulk-count { font-size: 12px; color: var(--text-dim); } - -/* Table */ -.igny8-table-wrapper { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); overflow: hidden; } -.igny8-table { width: 100%; border-collapse: collapse; } -.igny8-table th, .igny8-table td { padding: 12px; text-align: left; border-bottom: 1px solid var(--stroke); } -.igny8-table th { background: var(--panel-2); font-weight: 600; font-size: 13px; } -.igny8-table tbody tr:hover { background: var(--panel-2); } -.igny8-sortable-link { color: inherit; display: flex; align-items: center; gap: 4px; } -.igny8-sortable-link:hover { color: var(--blue); } -td.igny8-align-center, .igny8-align-center { text-align: center; } -.igny8-align-right { text-align: right; } -.igny8-empty-cell { text-align: center; color: var(--text-dim); font-style: italic; padding: 40px; } - -/* Row Actions */ -.igny8-row-actions { display: flex; gap: 8px; align-items: center; } -.igny8-action-separator { color: var(--text-dim); } - -/* Badges */ -.igny8-badge { padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: capitalize; } -.igny8-badge-default { background: rgba(103, 112, 109, 0.1); color: var(--text); } -.igny8-badge-success { background: rgba(16,185,129,0.1); color: var(--green-dark); } -.igny8-badge-info { background: rgba(59,130,246,0.1); color: var(--blue-dark); } -.igny8-badge-warning { background: rgba(245,158,11,0.1); color: var(--amber-dark); } -.igny8-badge-danger { background: rgba(239,68,68,0.1); color: var(--red-dark); } -.igny8-badge-secondary { background: rgba(100,116,139,0.1); color: var(--text-dim); } -.igny8-badge-dark-red { background: rgba(220,38,38,0.1); color: #dc2626; } - -/* Pagination */ -.igny8-pagination-wrapper { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: var(--panel-2); } -.igny8-pagination-info { font-size: 14px; color: var(--text-dim); } -.igny8-pagination-list { display: flex; list-style: none; gap: 4px; } -.igny8-pagination-btn { display: flex; align-items: center; gap: 4px; padding: 8px 12px; border: 1px solid var(--stroke); background: var(--panel); color: var(--text); border-radius: var(--radius); font-size: 14px; transition: all 0.2s; } -.igny8-pagination-btn:hover { background: var(--blue); color: white; border-color: var(--blue); } -.igny8-btn-current { background: var(--blue); color: white; border-color: var(--blue); } -.igny8-per-page-selector { display: flex; align-items: center; gap: 8px; font-size: 14px; } - -/* Forms */ -.igny8-form-wrapper { background: var(--panel); border: 1px solid var(--stroke); border-radius: var(--radius); padding: 20px; } -.igny8-form-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; } -.igny8-form-fields { display: flex; flex-direction: column; gap: 16px; } - -/* Hidden elements - no inline styles */ -.igny8-count-hidden { display: none; } -/* Legacy notification hidden class - now handled by unified system */ - -/* Module cards grid */ -.igny8-module-cards-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin: 20px 0; -} -.igny8-form-field { display: flex; flex-direction: column; gap: 6px; } -.igny8-field-label { font-size: 14px; font-weight: 500; color: var(--text); } -.igny8-required { color: var(--red-dark); } -.igny8-input, .igny8-textarea, .igny8-select { padding: 10px 12px; border: 1px solid var(--stroke); border-radius: var(--radius); font-size: 14px; } -.igny8-input:focus, .igny8-textarea:focus, .igny8-select:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 2px rgba(59,130,246,0.1); } -.igny8-field-description { font-size: 12px; color: var(--text-dim); } -.igny8-form-actions { display: flex; gap: 12px; margin-top: 20px; } - - -/* Layout Helpers */ -.igny8-submodule-layout { display: flex; flex-direction: column} - - -/*Styled Select Components (matching existing select styles) */ -.igny8-styled-select { position: relative; min-width: 120px; } -.igny8-styled-select-btn { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 6px 10px; font-size: 14px; background: #fff; border: 1px solid var(--igny8-stroke); border-radius: 4px; cursor: pointer; box-shadow: 0 2px 6px 3px rgba(0, 0, 0, .08); } -.igny8-styled-select-options { display: none; position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: #fff; border: 1px solid var(--igny8-stroke); border-radius: 4px; box-shadow: 0 2px 6px rgba(0,0,0,.08); z-index: 999999; max-height: 200px; overflow-y: auto; } -.igny8-styled-select-item { padding: 6px 10px; font-size: 14px; cursor: pointer; border-bottom: 1px solid #f1f5f9; } -.igny8-styled-select-item:last-child { border-bottom: none; } -.igny8-styled-select-item:hover { background: #f1f5f9; } - -.dd-arrow { font-size: 10px; margin-left: 10px; } -.igny8-input-sm { width: 100%; padding: 6px 8px; font-size: 13px; border: 1px solid var(--igny8-stroke); border-radius: 4px; background: #fff; box-sizing: border-box; } -.igny8-text-sm { font-size: 13px; } -.igny8-mb-5 { margin-bottom: 5px; } -.igny8-text-muted { color: var(--text-dim); } -.igny8-p-5 { padding: 5px 10px !important; } -.igny8-text-xs { font-size: 12px !important; } -.igny8-flex { display: flex; } -.igny8-flex-gap-10 { gap: 10px; } -.igny8-styled-select-options { min-width: 250px; padding: 12px; box-sizing: border-box; } -.igny8-dropdown-panel { pointer-events: auto; } -.igny8-dropdown-panel input, .igny8-dropdown-panel button { pointer-events: auto; } -/* Legacy planner notification - now handled by unified system */ - -/* === CHARTS SYSTEM === */ -/* === 14. Charts & Metrics=== */ - - -/* Header Metrics Container - 2 Row Layout */ -.igny8-header .metrics-container { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(2, 1fr); - gap: 8px; - margin: 0; - height: 100%; - max-width: 600px; -} - -/* Header Metric Cards - Clean Modern Design */ -.igny8-header .igny8-metric-card { - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 6px; - padding: 6px 8px; - text-align: center; - transition: all 0.2s ease; - cursor: default; - backdrop-filter: blur(8px); - position: relative; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 32px; -} - -.igny8-header .igny8-metric-card:hover { - background: rgba(255, 255, 255, 0.12); - border-color: rgba(255, 255, 255, 0.25); - transform: translateY(-1px); -} - -.igny8-header .igny8-metric-card.blue { - border-top: 2px solid var(--blue); -} - -.igny8-header .igny8-metric-card.green { - border-top: 2px solid var(--green); -} - -.igny8-header .igny8-metric-card.amber { - border-top: 2px solid var(--amber); -} - -.igny8-header .igny8-metric-card.purple { - border-top: 2px solid var(--purple); -} - -.igny8-header .igny8-metric-card.orange { - border-top: 2px solid var(--amber); -} - -.igny8-header .igny8-metric-card.red { - border-top: 2px solid var(--red-dark); -} - -.igny8-header .igny8-metric-card.gray { - border-top: 2px solid #6b7280; -} - -.igny8-header .igny8-metric-number { - font-size: 14px; - font-weight: 700; - margin: 0; - color: #ffffff; - text-shadow: 0 1px 2px rgba(0,0,0,0.4); - line-height: 1; -} - -.igny8-header .igny8-metric-label { - font-size: 9px; - font-weight: 500; - margin: 1px 0 0 0; - color: rgba(255, 255, 255, 0.75); - text-transform: uppercase; - letter-spacing: 0.3px; - line-height: 1; -} - - - -/* =================================================================== - STATUS CIRCLE STYLES FOR SYSTEM SUMMARY - =================================================================== */ - -.bg-circle { - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 11px; - color: white; - cursor: help; - transition: all 0.2s ease; - border: 2px solid transparent; -} - -.bg-circle:hover { - transform: scale(1.1); - border-color: rgba(255, 255, 255, 0.3); -} - -.bg-success { - background-color: var(--green); - box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); -} - -.bg-error { - background-color: var(--red); - box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3); -} - -.bg-success:hover { - background-color: var(--green-dark); - box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); -} - -.bg-error:hover { - background-color: var(--red-dark); - box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); -} - -/* =================================================================== - RADIO BUTTON STYLES FOR DEBUG SETTINGS - =================================================================== */ - -input[type="radio"] { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - width: 16px; - height: 16px; - border: 2px solid #d0d1d3; - border-radius: 50%; - background-color: var(--panel); - cursor: pointer; - position: relative; - transition: all 0.2s ease; -} - -input[type="radio"]:hover { - border-color: var(--blue); -} - -input[type="radio"]:checked { - border-color: var(--blue); - background-color: var(--blue); -} - -input[type="radio"]:checked::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 6px; - height: 6px; - border-radius: 50%; - background-color: white; -} - -input[type="radio"]:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.bg-circle {width: 50px;height: 50px;border-radius: 50%;} -.bg-circle-sm {width: 18px;height: 18px;border-radius: 50%;} -.bg-success {background: var(--green);} -.bg-danger,.bg-error {background: var(--red-dark);} -.bg-warning,.bg-amber {background: var(--amber);} -.bg-info {background: var(--blue);} -.bg-secondary {background: var(--text-dim);} - - -.igny8-form-group, .igny8-radio-group {padding: 5px 0} -.igny8-radio-group {margin: 0 15px} -/* =================================================================== - SYSTEM-WIDE DEBUG TABLE STYLES - =================================================================== */ - -/* ========================================= - Planner Settings Styles - ========================================= */ - .igny8-metrics-compact { - display: grid; - grid-template-columns: repeat(4, auto); - gap: 6px 15px; - padding: 4px 6px; - background: #526e8d3b; - border-radius: 8px; - width: fit-content; - float: right; -} - - .metric { - background: rgba(255, 255, 255, 0.05); - padding: 4px 8px; - border-radius: 6px; - text-align: center; - font-family: 'Inter', sans-serif; - min-width: 90px; - transition: 0.15s ease-in-out; - border-left: 3px solid rgba(255, 255, 255, 0.1); - display: flex; - flex-direction: row-reverse; - gap: 10px; - justify-content: space-between; - } - - .metric:hover { - background: rgba(255,255,255,0.08); - transform: translateY(-1px); - } - - .metric .val { - display: block; - font-size: 14px; - font-weight: 600; - color: #fff; - line-height: 1.2; - } - - .metric .lbl { - font-size: 10px; - letter-spacing: 0.3px; - color: rgba(255,255,255,0.6); - text-transform: uppercase; - } - - /* Color Variants */ - .metric.green { border-left-color: var(--green, #00c985); } - .metric.amber { border-left-color: var(--amber, #f39c12); } - .metric.purple { border-left-color: var(--purple, #9b59b6); } - .metric.blue { border-left-color: var(--blue, #3498db); } - .metric.teal { border-left-color: var(--teal, #1abc9c); } - -/* === DASHBOARD OVERVIEW STYLES === */ - -/* Dashboard Sections */ -.igny8-dashboard-section { - margin-bottom: 24px; - height: 100%; - display: flex; - flex-direction: column; -} - -/* Progress Bar Styles */ -.igny8-progress-item { - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid var(--stroke); -} - -.igny8-progress-item:last-child { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.igny8-progress-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.igny8-progress-label { - font: 500 14px/1.4 'Inter', system-ui, sans-serif; - color: var(--text); -} - -.igny8-progress-percent { - font: 600 14px/1.4 'Inter', system-ui, sans-serif; - color: var(--blue-dark); -} - -.igny8-progress-bar { - width: 100%; - height: 8px; - background: var(--panel-2); - border-radius: 4px; - overflow: hidden; - margin-bottom: 6px; -} - -.igny8-progress-fill { - height: 100%; - border-radius: 4px; - transition: width 0.3s ease; -} - -.igny8-progress-blue { - background: linear-gradient(90deg, var(--blue) 0%, var(--blue-dark) 100%); -} - -.igny8-progress-green { - background: linear-gradient(90deg, var(--green) 0%, var(--green-dark) 100%); -} - -.igny8-progress-amber { - background: linear-gradient(90deg, var(--amber) 0%, var(--amber-dark) 100%); -} - -.igny8-progress-purple { - background: linear-gradient(90deg, #8b5cf6 0%, #7c3aed 100%); -} - -.igny8-progress-text-dim { - background: linear-gradient(90deg, var(--text-dim) 0%, #64748b 100%); -} - -.igny8-progress-red { - background: linear-gradient(90deg, #e53e3e 0%, #c53030 100%); -} - -.igny8-progress-details { - font: 400 12px/1.4 'Inter', system-ui, sans-serif; - color: var(--text-dim); -} - -/* Status Cards */ -.igny8-status-cards { - gap: 20px; -} - -.igny8-status-card { - cursor: pointer; - transition: all 0.2s ease; - border: none; - background: var(--panel); -} - -.igny8-status-card:hover { - transform: translateY(-3px); - box-shadow: 0 8px 20px rgba(0,0,0,0.15), 0 12px 28px rgba(13,27,42,0.12); -} - -/* Colored Status Card Variants */ -.igny8-status-blue { - background: linear-gradient(135deg, var(--blue) 0%, var(--blue-dark) 100%); - color: white; -} - -.igny8-status-green { - background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%); - color: white; -} - -.igny8-status-amber { - background: linear-gradient(135deg, var(--amber) 0%, var(--amber-dark) 100%); - color: white; -} - -.igny8-clickable-card { - cursor: pointer; -} - -.igny8-status-metric { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; -} - -.igny8-status-count { - font: 700 28px/1.2 'Inter', system-ui, sans-serif; - color: rgba(255, 255, 255, 0.95); - margin: 0; - text-shadow: 0 1px 2px rgba(0,0,0,0.1); -} - -.igny8-status-label { - font: 500 13px/1.3 'Inter', system-ui, sans-serif; - color: rgba(255, 255, 255, 0.85); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-status-icon { - position: absolute; - top: 16px; - right: 16px; - opacity: 0.7; -} - -.igny8-status-card .igny8-card-body { - position: relative; - padding: 0 20px; - display: flex; - align-items: center; - justify-content: space-between; -} - -/* Next Actions Panel */ -.igny8-next-actions { - display: flex; - flex-direction: column; - gap: 12px; -} - -.igny8-action-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: var(--panel-2); - border-radius: 6px; - border-left: 3px solid var(--blue); - transition: all 0.2s ease; -} - -.igny8-action-item:hover { - background: #f8fafc; - border-left-color: var(--blue-dark); -} - -.igny8-action-item.igny8-action-complete { - border-left-color: var(--green); - background: #f0fdf4; -} - -.igny8-action-text { - font: 500 14px/1.4 'Inter', system-ui, sans-serif; - color: var(--text); -} - -.igny8-action-status { - font: 500 13px/1.4 'Inter', system-ui, sans-serif; - color: var(--green); -} - -.igny8-btn-text { - background: none; - border: none; - color: var(--blue); - font: 500 13px/1.4 'Inter', system-ui, sans-serif; - padding: 4px 8px; - border-radius: 4px; - text-decoration: none; - transition: all 0.2s ease; -} - -.igny8-btn-text:hover { - background: var(--blue); - color: white; - text-decoration: none; -} - -/* Info Box Styles */ -.igny8-info-box { - background: #f0f8ff; - border: 1px solid #b3d9ff; - border-radius: 6px; - padding: 16px; - margin: 16px 0; -} - -.igny8-info-box p { - margin: 0 0 12px 0; - color: #555; -} - -.igny8-info-box p:last-child { - margin-bottom: 0; -} - -/* =================================================================== - UNIFIED NOTIFICATION SYSTEM - =================================================================== */ - -/* Single Global Notification Container */ -#igny8-global-notification { - position: fixed; - top: 20px; - right: 20px; - padding: 12px 20px; - border-radius: 6px; - color: white; - font-weight: 500; - z-index: 9999; - max-width: 400px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - transition: all 0.3s ease; - transform: translateX(0); - font-family: 'Inter', system-ui, sans-serif; - font-size: 14px; - line-height: 1.4; - display: none; -} - -/* Notification Type Styles */ -#igny8-global-notification.success { - background: var(--green); - border-left: 4px solid var(--green-dark); -} - -#igny8-global-notification.error { - background: var(--red-dark); - border-left: 4px solid #b91c1c; -} - -#igny8-global-notification.warning { - background: var(--amber); - border-left: 4px solid var(--amber-dark); -} - -#igny8-global-notification.info { - background: var(--blue); - border-left: 4px solid var(--blue-dark); -} - -/* Animation States */ -#igny8-global-notification.show { - display: block !important; - animation: slideInRight 0.3s ease-out; - opacity: 1; - visibility: visible; -} - -#igny8-global-notification.hide { - animation: slideOutRight 0.3s ease-in; - opacity: 0; - visibility: hidden; -} - -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(100%); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes slideOutRight { - from { - opacity: 1; - transform: translateX(0); - } - to { - opacity: 0; - transform: translateX(100%); - } -} - -/* Hover Effects */ -#igny8-global-notification:hover { - transform: translateX(-5px); - box-shadow: 0 6px 16px rgba(0,0,0,0.2); -} - -/* Textarea Color Variants */ -.igny8-textarea-green { - border-left: 3px solid var(--green); -} - -.igny8-textarea-orange { - border-left: 3px solid var(--amber); -} - -.igny8-textarea-blue { - border-left: 3px solid var(--blue); -} - -.igny8-textarea-purple { - border-left: 3px solid var(--purple); -} - -.igny8-textarea-teal { - border-left: 3px solid var(--teal); -} - -.igny8-textarea-indigo { - border-left: 3px solid var(--indigo); -} - -/* Recent Activity Styles */ -.igny8-recent-activity { - display: flex; - flex-direction: column; - gap: 12px; -} - -.igny8-list-item { - padding: 12px; - border: 1px solid var(--stroke); - border-radius: var(--radius); - background: var(--panel); - transition: all 0.2s ease; -} - -.igny8-list-item:hover { - border-color: var(--blue); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1); -} - -.igny8-item-content { - display: flex; - flex-direction: column; - gap: 6px; -} - -.igny8-item-title a { - font-weight: 600; - color: var(--text); - text-decoration: none; - font-size: 14px; -} - -.igny8-item-title a:hover { - color: var(--blue); -} - -.igny8-item-meta { - display: flex; - align-items: center; - gap: 12px; - font-size: 12px; - color: var(--text-muted); -} - -.igny8-item-cluster { - color: var(--text-muted); -} - -.igny8-item-date { - color: var(--text-muted); -} - -.igny8-empty-state { - text-align: center; - padding: 40px 20px; - color: var(--text-muted); -} - -.igny8-empty-state p { - margin-bottom: 16px; -} - -/* Content Types and Publishing Stats */ -.igny8-content-types, .igny8-publishing-stats { - display: flex; - flex-direction: column; - gap: 16px; -} - -.igny8-type-item, .igny8-stat-item { - display: flex; - flex-direction: column; - gap: 8px; -} - -.igny8-type-info, .igny8-stat-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.igny8-type-name, .igny8-stat-label { - font-weight: 600; - color: var(--text); - font-size: 14px; -} - -.igny8-type-count, .igny8-stat-count { - font-weight: 700; - color: var(--blue); - font-size: 16px; -} - -.igny8-type-bar, .igny8-stat-bar { - height: 6px; - background: var(--stroke); - border-radius: 3px; - overflow: hidden; -} - -.igny8-type-progress, .igny8-stat-progress { - height: 100%; - background: var(--blue); - border-radius: 3px; - transition: width 0.3s ease; -} - -.igny8-stat-progress.igny8-progress-amber { - background: var(--amber); -} - -.igny8-stat-progress.igny8-progress-green { - background: var(--green); -} - -.igny8-stat-progress.igny8-progress-purple { - background: var(--purple); -} - -/* Enhanced Analytics Cards */ -.igny8-equal-height { - align-items: stretch; -} - -.igny8-analytics-card .igny8-card { - height: 100%; - display: flex; - flex-direction: column; -} - -.igny8-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid var(--stroke); -} - -.igny8-card-header-content { - display: flex; - align-items: flex-start; - gap: 12px; - flex: 1; -} - -.igny8-card-icon { - flex-shrink: 0; - width: 40px; - height: 40px; - background: rgba(59, 130, 246, 0.1); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; -} - -.igny8-card-title-text h3 { - margin: 0 0 4px 0; - font-size: 16px; - font-weight: 600; - color: var(--text); -} - -.igny8-card-subtitle { - margin: 0; - font-size: 12px; - color: var(--text-dim); - font-weight: 400; -} - -.igny8-card-subtitle.igny8-centered { - text-align: center; -} - -/* Standard Dashboard Card Headers */ -.igny8-standard-header { - background: #fff !important; - border-bottom: 2px solid var(--blue) !important; - margin-bottom: 16px !important; - padding: 16px !important; - border-radius: 0 !important; -} - -.igny8-standard-header .igny8-card-title-text h3 { - color: var(--navy-bg-2) !important; - font-weight: 600 !important; - font-size: 26px !important; - margin: 0 0 4px 0 !important; -} - -.igny8-standard-header .igny8-card-subtitle { - color: var(--text-dim) !important; - font-size: 13px !important; - font-weight: 400 !important; -} - -.igny8-standard-header .igny8-card-icon { - background: rgba(59, 130, 246, 0.08) !important; - border-radius: 8px !important; - width: 40px !important; - height: 40px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - flex-shrink: 0 !important; -} - -.igny8-standard-header .igny8-card-header-content { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - width: 100% !important; -} - -/* Dashboard Icon Styles */ -.igny8-dashboard-icon-sm { - color: rgba(255,255,255,0.7) !important; - font-size: 24px !important; -} - -.igny8-dashboard-icon-lg { - font-size: 26px !important; -} - -.igny8-dashboard-icon-blue { - color: var(--blue) !important; -} - -.igny8-dashboard-icon-amber { - color: var(--amber) !important; -} - -.igny8-dashboard-icon-green { - color: var(--green) !important; -} - -.igny8-dashboard-icon-purple { - color: #8b5cf6 !important; -} - -.igny8-dashboard-icon-dim { - color: var(--text-dim) !important; - font-size: 32px !important; - margin-bottom: 12px !important; -} - -.igny8-card-metric { - text-align: right; - flex-shrink: 0; -} - -.igny8-metric-value { - display: block; - font-size: 24px; - font-weight: 700; - color: var(--blue); - line-height: 1; -} - -.igny8-metric-label { - display: block; - font-size: 11px; - color: var(--text-dim); - text-transform: uppercase; - font-weight: 500; - margin-top: 2px; -} - -.igny8-analytics-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.igny8-analytics-item { - display: flex; - flex-direction: column; - gap: 8px; -} - -.igny8-analytics-item.igny8-analytics-total { - padding-top: 16px; - border-top: 1px solid var(--stroke); - margin-top: 8px; -} - -.igny8-item-info { - display: flex; - justify-content: space-between; - align-items: center; -} - -.igny8-item-label { - font-size: 14px; - font-weight: 500; - color: var(--text); -} - -.igny8-item-value { - font-size: 16px; - font-weight: 700; - color: var(--blue); -} - -.igny8-item-progress { - display: flex; - align-items: center; - gap: 12px; -} - -.igny8-progress-track { - flex: 1; - height: 6px; - background: var(--stroke); - border-radius: 3px; - overflow: hidden; -} - -.igny8-progress-percent { - font-size: 12px; - font-weight: 600; - color: var(--text-dim); - min-width: 32px; - text-align: right; -} - -.igny8-empty-analytics { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px 20px; - text-align: center; - color: var(--text-dim); -} - -.igny8-empty-analytics p { - margin: 12px 0 16px 0; - font-size: 14px; -} - -.igny8-btn-sm { - padding: 6px 12px; - font-size: 12px; -} - -.igny8-status-desc { - font-size: 12px; - color: #fff; -} - -/* Step-by-Step UX Guide */ -.igny8-step-guide { - background: var(--panel); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 2px 6px rgba(0,0,0,0.08), 0 4px 10px rgba(13,27,42,0.06); -} - -.igny8-step-guide-header { - display: flex; - align-items: center; - margin-bottom: 15px; -} - -.igny8-step-guide-header h3 { - margin: 0; - color: var(--blue-dark); - font-size: 16px; - font-weight: 600; -} - -.igny8-step-guide-header .dashicons { - margin-right: 8px; - color: var(--blue); -} - -.igny8-steps-container { - display: flex; - gap: 15px; - overflow-x: auto; - padding-bottom: 10px; - scrollbar-width: thin; - scrollbar-color: var(--stroke) transparent; -} - -.igny8-steps-container::-webkit-scrollbar { - height: 6px; -} - -.igny8-steps-container::-webkit-scrollbar-track { - background: transparent; -} - -.igny8-steps-container::-webkit-scrollbar-thumb { - background: var(--stroke); - border-radius: 3px; -} - -.igny8-step { - flex: 0 0 auto; - min-width: 180px; - max-width: 220px; - background: var(--bg); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 15px; - position: relative; - transition: all 0.2s ease; -} - -.igny8-step:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.igny8-step-number { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - background: var(--blue); - color: white; - border-radius: 50%; - font-size: 12px; - font-weight: 600; - margin-bottom: 8px; -} - -.igny8-step.completed .igny8-step-number { - background: var(--green); -} - -.igny8-step.current .igny8-step-number { - background: var(--amber); - color: var(--text); -} - -.igny8-step-title { - font-size: 14px; - font-weight: 600; - color: var(--text); - margin-bottom: 6px; - line-height: 1.3; -} - -.igny8-step-status { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 8px; -} - -.igny8-step-status-icon { - font-size: 14px; -} - -.igny8-step-status-text { - font-size: 12px; - font-weight: 500; - color: var(--text-dim); -} - -.igny8-step.completed .igny8-step-status-text { - color: var(--green-dark); -} - -.igny8-step.current .igny8-step-status-text { - color: var(--amber-dark); -} - -.igny8-step.completed .igny8-step-status-icon { - color: var(--green); -} - -.igny8-step.current .igny8-step-status-icon { - color: var(--amber); -} - -.igny8-step-data { - font-size: 11px; - color: var(--text-dim); - margin-bottom: 8px; - line-height: 1.3; -} - -.igny8-step-action { - margin-top: 8px; -} - -.igny8-step-action .igny8-btn { - font-size: 11px; - padding: 4px 8px; - border-radius: 3px; -} - -.igny8-step-connector { - position: absolute; - top: 50%; - right: -8px; - width: 16px; - height: 2px; - background: var(--stroke); - transform: translateY(-50%); -} - -.igny8-step:last-child .igny8-step-connector { - display: none; -} - -.igny8-step.completed + .igny8-step .igny8-step-connector { - background: var(--green); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .igny8-step { - min-width: 160px; - } - - .igny8-steps-container { - gap: 10px; - } -} - -/* System-Wide Workflow Guide */ -.igny8-system-workflow { - background: var(--panel); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 25px; - margin-bottom: 25px; - box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 6px 16px rgba(13,27,42,0.06); -} - -.igny8-system-workflow-header { - display: flex; - align-items: center; - margin-bottom: 20px; -} - -.igny8-system-workflow-header h2 { - margin: 0; - color: var(--blue-dark); - font-size: 20px; - font-weight: 700; -} - -.igny8-system-workflow-header .dashicons { - margin-right: 12px; - color: var(--blue); - font-size: 24px; -} - -.igny8-system-workflow-subtitle { - color: var(--text-dim); - font-size: 14px; - margin-top: 5px; - margin-bottom: 0; -} - -.igny8-system-steps-container { - display: flex; - gap: 12px; - overflow-x: auto; - padding-bottom: 15px; - scrollbar-width: thin; - scrollbar-color: var(--stroke) transparent; - max-width: 1200px; -} - -.igny8-system-steps-container::-webkit-scrollbar { - height: 8px; -} - -.igny8-system-steps-container::-webkit-scrollbar-track { - background: transparent; -} - -.igny8-system-steps-container::-webkit-scrollbar-thumb { - background: var(--stroke); - border-radius: 4px; -} - -.igny8-system-step { - flex: 0 0 auto; - min-width: 160px; - max-width: 180px; - background: var(--bg); - border: 1px solid var(--stroke); - border-radius: var(--radius); - padding: 16px; - position: relative; - transition: all 0.3s ease; - cursor: pointer; -} - -.igny8-system-step:hover { - transform: translateY(-3px); - box-shadow: 0 6px 20px rgba(0,0,0,0.12); - border-color: var(--blue); -} - -.igny8-system-step.disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.igny8-system-step.disabled:hover { - transform: none; - box-shadow: none; - border-color: var(--stroke); -} - -.igny8-system-step-number { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: var(--blue); - color: white; - border-radius: 50%; - font-size: 13px; - font-weight: 700; - margin-bottom: 10px; -} - -.igny8-system-step.completed .igny8-system-step-number { - background: var(--green); -} - -.igny8-system-step.in_progress .igny8-system-step-number { - background: var(--amber); - color: var(--text); -} - -.igny8-system-step.missing .igny8-system-step-number { - background: var(--text-dim); -} - -.igny8-system-step.disabled .igny8-system-step-number { - background: var(--stroke); - color: var(--text-dim); -} - -.igny8-system-step-title { - font-size: 13px; - font-weight: 600; - color: var(--text); - margin-bottom: 8px; - line-height: 1.3; -} - -.igny8-system-step-status { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 8px; -} - -.igny8-system-step-status-icon { - font-size: 16px; -} - -.igny8-system-step-status-text { - font-size: 11px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-system-step.completed .igny8-system-step-status-text { - color: var(--green-dark); -} - -.igny8-system-step.in_progress .igny8-system-step-status-text { - color: var(--amber-dark); -} - -.igny8-system-step.missing .igny8-system-step-status-text { - color: var(--text-dim); -} - -.igny8-system-step.completed .igny8-system-step-status-icon { - color: var(--green); -} - -.igny8-system-step.in_progress .igny8-system-step-status-icon { - color: var(--amber); -} - -.igny8-system-step.missing .igny8-system-step-status-icon { - color: var(--text-dim); -} - -.igny8-system-step-data { - font-size: 10px; - color: var(--text-dim); - margin-bottom: 10px; - line-height: 1.4; -} - -.igny8-system-step-action { - margin-top: 8px; -} - -.igny8-system-step-action .igny8-btn { - font-size: 10px; - padding: 4px 8px; - border-radius: 3px; - width: 100%; - text-align: center; -} - -.igny8-system-step-connector { - position: absolute; - top: 50%; - right: -7px; - width: 14px; - height: 2px; - background: var(--stroke); - transform: translateY(-50%); - z-index: 1; -} - -.igny8-system-step:last-child .igny8-system-step-connector { - display: none; -} - -.igny8-system-step.completed + .igny8-system-step .igny8-system-step-connector { - background: var(--green); -} - -.igny8-system-step.in_progress + .igny8-system-step .igny8-system-step-connector { - background: var(--amber); -} - -/* System workflow responsive adjustments */ -@media (max-width: 1200px) { - .igny8-system-step { - min-width: 140px; - max-width: 160px; - } -} - -@media (max-width: 768px) { - .igny8-system-step { - min-width: 120px; - max-width: 140px; - padding: 12px; - } - - .igny8-system-steps-container { - gap: 8px; - } - - .igny8-system-step-number { - width: 24px; - height: 24px; - font-size: 11px; - } - - .igny8-system-step-title { - font-size: 12px; - } -} -.workflow-steps { - display: flex; -} -/* === WORDPRESS ADMIN STYLES === */ -/* Ensure WordPress admin styles are available for cron pages */ -.wp-list-table { - border: 1px solid #c3c4c7; - border-spacing: 0; - width: 100%; - clear: both; - margin: 0; -} - -.wp-list-table.widefat { - border-collapse: collapse; -} - -.wp-list-table.fixed { - table-layout: fixed; -} - -.wp-list-table.striped tbody tr:nth-child(odd) { - background-color: #f6f7f7; -} - -.wp-list-table.striped tbody tr:nth-child(even) { - background-color: #fff; -} - -.wp-list-table th, -.wp-list-table td { - border-bottom: 1px solid #c3c4c7; - padding: 8px 10px; - text-align: left; - vertical-align: top; -} - -.wp-list-table th { - background-color: #f1f1f1; - font-weight: 600; - color: #1d2327; -} - -.wp-list-table tbody tr:hover { - background-color: #f0f6fc; -} - - - - - - -/* WordPress admin wrap styles */ -.wrap { - margin: 0 20px 0 2px; -} - -.wrap h1 { - margin: 0 0 20px; - padding: 0; - font-size: 23px; - font-weight: 400; - line-height: 1.3; - color: #1d2327; -} - - - - - - -/* WordPress admin notice styles */ -.notice { - background: #fff; - border-left: 4px solid #fff; - box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); - margin: 5px 15px 2px; - padding: 1px 12px; -} - -.notice.notice-success { - border-left-color: #00a32a; -} - -.notice p { - margin: .5em 0; - padding: 2px; -} - -/* WordPress admin submit styles */ -.submit { - padding: 0; - margin: 0; -} - - -.ai-integration, .new-content-status { - border-right: 3px solid #ccc; - margin-right: 25px; - padding-right: 25px; -} -#igny8-ai-integration-form .igny8-form-group h4 {margin-bottom: 35px;} -.new-content-status .igny8-form-group h4 {margin-bottom: 20px;} - - -.igny8-flex-row { - display: flex; - align-items: center; - align-content: center; - -} - -/* Workflow section styling */ -.igny8-workflow-section { - margin-top: 30px; -} - -.igny8-step-card { - border-left: 4px solid #e5e7eb; - transition: all 0.3s ease; -} - -.igny8-step-card.completed { - border-left-color: #10b981; - background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%); -} - -.igny8-step-card.current { - border-left-color: #3b82f6; - background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); -} - -.igny8-step-card.pending { - border-left-color: #f59e0b; - background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); -} - -.igny8-step-header { - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 15px; -} - -.igny8-step-number { - background: #6b7280; - color: white; - width: 35px; - height: 35px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - font-size: 16px; - flex-shrink: 0; -} - -.igny8-step-card.completed .igny8-step-number { - background: #10b981; -} - -.igny8-step-card.current .igny8-step-number { - background: #3b82f6; -} - -.igny8-step-card.pending .igny8-step-number { - background: #f59e0b; -} - -.igny8-step-title { - font-size: 18px; - font-weight: 600; - color: #1f2937; - margin: 0; -} - -.igny8-step-status { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; -} - -.igny8-step-status-icon { - font-size: 16px; -} - -.igny8-step-status-text { - font-size: 14px; - font-weight: 500; -} - -.igny8-step-card.completed .igny8-step-status-text { - color: #10b981; -} - -.igny8-step-card.current .igny8-step-status-text { - color: #3b82f6; -} - -.igny8-step-card.pending .igny8-step-status-text { - color: #f59e0b; -} - -.igny8-step-data { - color: #6b7280; - font-size: 14px; - margin-bottom: 15px; -} - -.igny8-step-action { - margin-top: 15px; -} - -.igny8-grid-4 { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 20px; -} - -/* Card layout optimization for settings cards */ -.igny8-flex-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - width: 100%; -} - -.igny8-card-header-content { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - gap: 20px; -} - -.igny8-card-title-text { - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; - min-width: 0; -} - -.igny8-card-title-text h3 { - margin: 0 0 5px 0; - font-size: 18px; - font-weight: 600; - color: #1f2937; -} - -.igny8-card-title-text .igny8-card-subtitle { - margin: 0; - font-size: 14px; - color: #6b7280; -} - -.igny8-flex-row form { - display: flex; - align-items: flex-end; - gap: 20px; - flex: 1; - justify-content: flex-end; - flex-direction: column; - align-content: flex-end; -} - -.igny8-form-group {display: flex;align-items: flex-start;gap: 5px;flex: 1;flex-direction: column;} - -/* Editor Type Selection Styles */ -.igny8-editor-option { - display: block; - margin-bottom: 15px; - padding: 15px; - border: 2px solid #e1e5e9; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s ease; - background: #fff; -} - -.igny8-editor-option:hover { - border-color: #0073aa; - background-color: #f8f9fa; -} - -.igny8-editor-option.selected { - border-color: #0073aa; - background-color: #f0f8ff; - box-shadow: 0 2px 8px rgba(0, 115, 170, 0.1); -} - -.igny8-editor-option input[type="radio"] { - margin-right: 10px; - transform: scale(1.2); -} - -.igny8-editor-option-content { - display: inline-block; - vertical-align: top; - width: calc(100% - 30px); -} - -.igny8-editor-option-title { - font-size: 16px; - font-weight: 600; - color: #333; - margin: 0 0 5px 0; -} - -.igny8-editor-option-description { - margin: 5px 0 0 0; - color: #666; - font-size: 14px; - line-height: 1.4; -} - -.igny8-form-actions { - display: flex; - align-items: center; - flex-shrink: 0; - justify-content: flex-end; -} - -.igny8-card-body { - padding: 20px; -} - -.igny8-mode-toggle-label, -.igny8-radio-group { - display: flex; - align-items: center; - gap: 10px; - white-space: nowrap; -} - -.igny8-mode-toggle-label { - gap: 15px; -} - - - -/* Responsive adjustments */ -@media (max-width: 768px) { - .igny8-grid-4 { - grid-template-columns: 1fr; - } - - .igny8-step-header { - flex-direction: column; - text-align: center; - gap: 10px; - } - - .igny8-flex-row { - flex-direction: column; - align-items: stretch; - gap: 15px; - } - - .igny8-card-header-content { - flex-direction: column; - align-items: stretch; - } - - .igny8-flex-row form { - flex-direction: column; - align-items: stretch; - } - - .igny8-form-group { - justify-content: flex-start; - } -} - -#igny8-new-content-form .igny8-flex-row .igny8-form-actions, #igny8-ai-integration-form .igny8-flex-row .igny8-form-actions {margin-top: 0;} - - -.igny8-card .igny8-standard-header .igny8-card-title-text h3 { - font-size: 20px !important; -} - -.igny8-error-log { - width: 800px; -} -.igny8-form-group select{min-width: 200px;} -.igny8-form-group textarea {width: 80%;} - -/* Title with Badge Layout */ -.igny8-title-with-badge { - display: flex; - align-items: center; - gap: 8px; -} - -.igny8-title-actions { - display: flex; - align-items: center; - gap: 4px; - margin-left: auto; -} - -.igny8-title-text { - flex: 1; -} - -.igny8-menu-toggle { - padding: 8px; - border: none; - background: transparent; - cursor: pointer; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} - -.igny8-menu-toggle:hover { - background: rgba(0, 0, 0, 0.05); - transform: scale(1.1); -} - -.igny8-hamburger { - display: flex; - flex-direction: column; - gap: 3px; - width: 16px; - height: 14px; -} - -.igny8-hamburger span { - display: block; - width: 100%; - height: 2px; - background: var(--blue); - border-radius: 1px; - transition: all 0.2s ease; -} - -.igny8-menu-toggle:hover .igny8-hamburger span { - background: var(--blue-dark); -} - -/* Expandable Description Row */ -.igny8-description-row { - display: none; - background: var(--panel-2); - border-top: 1px solid var(--border); -} - -.igny8-description-row.expanded { - display: table-row; -} - -.igny8-description-content-cell { - padding: 16px; - color: var(--text); - line-height: 1.5; -} - -.igny8-description-content { - background: var(--panel-1); - border-radius: 6px; - padding: 12px; - border: 1px solid var(--border); -} - -.igny8-description-content p { - margin: 0 0 8px 0; -} - -.igny8-description-content p:last-child { - margin-bottom: 0; -} - -/* Description Section Styling */ -.description-section { - margin-bottom: 16px; - padding: 12px; - background: var(--panel-2); - border-radius: 6px; - border-left: 4px solid var(--blue); -} - -.description-section:last-child { - margin-bottom: 0; -} - -.section-heading { - margin: 0 0 8px 0; - color: var(--blue); - font-size: 14px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.section-content { - position: relative; -} - -.content-type-badge { - display: inline-block; - background: var(--blue); - color: white; - padding: 2px 8px; - border-radius: 12px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 8px; -} - -.content-details { - color: var(--text); - line-height: 1.5; - font-size: 13px; -} - -.description-item { - margin-bottom: 8px; - padding: 8px; - background: var(--panel-2); - border-radius: 4px; - border-left: 3px solid var(--blue); -} - -.description-item:last-child { - margin-bottom: 0; -} - -.description-item strong { - color: var(--blue); - display: block; - margin-bottom: 4px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.description-text { - color: var(--text); - line-height: 1.5; - white-space: pre-wrap; -} - -/* Image Prompts Toggle Styles */ -.igny8-image-prompts-display { - display: flex; - align-items: center; - gap: 8px; -} - -.igny8-image-icon { - font-size: 16px; - display: inline-block; - width: 16px; - height: 16px; - line-height: 1; -} - -.igny8-image-prompts-toggle { - background: none; - border: none; - padding: 8px; - margin-bottom: 3px; - cursor: pointer; - border-radius: 4px; - transition: all 0.2s ease; - -} - -.igny8-image-prompts-toggle:hover { - background: rgba(0, 0, 0, 0.05); - transform: scale(1.1); -} - -/* Expandable Image Prompts Row */ -.igny8-image-prompts-row { - display: none; - background: var(--panel-2); - border-top: 1px solid var(--border); -} - -.igny8-image-prompts-row.expanded { - display: table-row; -} - -.igny8-image-prompts-content-cell { - padding: 16px; - color: var(--text); - line-height: 1.5; -} - -.igny8-image-prompts-content { - background: var(--panel-1); - border-radius: 6px; - padding: 12px; - border: 1px solid var(--border); -} - -.igny8-image-prompts-content .prompt-item { - margin-bottom: 8px; - padding: 8px; - background: var(--panel-2); - border-radius: 4px; - border-left: 3px solid var(--blue); -} - -.igny8-image-prompts-content .prompt-item:last-child { - margin-bottom: 0; -} - -.igny8-image-prompts-content .prompt-item strong { - color: var(--blue); - display: block; - margin-bottom: 4px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.igny8-image-prompts-content .prompt-item:not(:last-child) { - margin-bottom: 12px; -} - -/* Ensure dashicons are properly styled */ -.igny8-image-icon.dashicons { - font-family: dashicons; - font-size: 16px; - color: var(--blue); - vertical-align: middle; -} - -.igny8-image-icon.dashicons:hover { - color: var(--blue-dark); -} - -/* Status with Badge Layout */ -.igny8-status-with-badge { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.igny8-status-text { - flex: 1; - min-width: 0; -} - -.igny8-status-with-badge .igny8-badge { - font-size: 10px; - padding: 2px 6px; - border-radius: 8px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Static image size options styling */ -.igny8-size-options-static { - display: flex; - gap: 10px; - margin-bottom: 10px; -} - -.igny8-size-static { - flex: 1; - padding: 12px 8px; - border: 2px solid var(--border-light); - border-radius: 8px; - text-align: center; - min-height: 60px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - opacity: 0.7; -} - -.igny8-size-static .size-label { - font-weight: 600; - font-size: 14px; - margin-bottom: 4px; -} - -.igny8-size-static .size-dimensions { - font-size: 12px; - opacity: 0.8; -} - -/* Different colors for each size option */ -/* DALL-E sizes */ -.igny8-size-square { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-color: #667eea; -} - -.igny8-size-portrait { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - color: white; - border-color: #f093fb; -} - -.igny8-size-landscape { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - color: white; - border-color: #4facfe; -} - -/* Runware sizes */ -.igny8-size-featured { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-color: #667eea; -} - -.igny8-size-desktop { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - color: white; - border-color: #f093fb; -} - -.igny8-size-mobile { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - color: white; - border-color: #4facfe; -} - -/* Image provider styling */ -.igny8-provider-info { - margin-bottom: 10px; -} - -.igny8-provider-badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - background: var(--bg-light); - border: 1px solid var(--border-light); - border-radius: 8px; - font-weight: 500; - color: var(--text-primary); -} - -.igny8-provider-badge .dashicons { - color: var(--blue); - font-size: 16px; -} -.igny8-card.igny8-prompt-section { - display: flex; - flex-direction: row; -} - -.igny8-card.igny8-prompt-section .igny8-dashboard-section { - width: 100%; -} - -/* Image Size Checkbox and Quantity Input Styling */ -.igny8-size-checkbox-container { - display: flex; - flex-direction: column; - gap: 15px; - margin-top: 10px; -} - -.igny8-size-option { - display: flex; - align-items: center; - padding: 15px; - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 8px; - transition: all 0.2s ease; - justify-content: space-between; -} - -.igny8-size-option:hover { - background: #f1f5f9; - border-color: #cbd5e1; -} - -.igny8-checkbox-label { - display: flex; - align-items: center; - cursor: pointer; - font-weight: 500; - color: #374151; - margin-right: 15px; -} - -.igny8-checkbox-label input[type="checkbox"] { - margin-right: 8px; - width: 16px; - height: 16px; - accent-color: var(--blue); -} - -.igny8-checkbox-text { - font-size: 14px; - font-weight: 500; -} - -.igny8-quantity-input { - display: flex; - align-items: center; - gap: 5px; -} - -.igny8-quantity-input label { - font-size: 12px; - color: #6b7280; - font-weight: 500; -} - -.igny8-quantity-input input[type="number"] { - padding: 4px 8px; - border: 1px solid #d1d5db; - border-radius: 4px; - font-size: 12px; - text-align: center; -} - -.igny8-quantity-input input[type="number"]:disabled { - background-color: #f3f4f6; - color: #9ca3af; - cursor: not-allowed; -} - -.igny8-size-info { - - font-size: 12px; - color: #6b7280; - background: #e5e7eb; - padding: 4px 8px; - border-radius: 4px; - font-weight: 500; -} - -/* Featured Image Row Styling */ -.igny8-featured-image-row { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-color: #667eea; -} - -.igny8-featured-image-row .igny8-size-info { - background: rgba(255, 255, 255, 0.2); - color: white; - font-weight: 600; -} - -.igny8-featured-image-row .igny8-size-info:first-child { - font-size: 14px; - font-weight: 700; -} \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/assets/css/image-injection.css b/igny8-ai-seo-wp-plugin/assets/css/image-injection.css deleted file mode 100644 index 1c1e3d3a..00000000 --- a/igny8-ai-seo-wp-plugin/assets/css/image-injection.css +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Igny8 Image Injection CSS - * - * Responsive image display styles for marker-based image injection - * - * @package Igny8 - * @version 1.0.0 - */ - -/* Desktop and larger screens (769px+) */ -@media (min-width: 769px) { - .igny8-article-image-desktop { - display: block !important; - } - .igny8-article-image-mobile { - display: none !important; - } -} - -/* Mobile and smaller screens (768px and below) */ -@media (max-width: 768px) { - .igny8-article-image-desktop { - display: none !important; - } - .igny8-article-image-mobile { - display: block !important; - } -} - -/* Image wrapper styling */ -.igny8-image-wrapper { - margin: 20px 0; - text-align: center; -} - -.igny8-image-wrapper img { - max-width: 100%; - height: auto; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -/* Loading state */ -.igny8-image-wrapper img[loading="lazy"] { - opacity: 0; - transition: opacity 0.3s ease; -} - -.igny8-image-wrapper img[loading="lazy"].loaded { - opacity: 1; -} diff --git a/igny8-ai-seo-wp-plugin/assets/js/core.js b/igny8-ai-seo-wp-plugin/assets/js/core.js deleted file mode 100644 index aab49bea..00000000 --- a/igny8-ai-seo-wp-plugin/assets/js/core.js +++ /dev/null @@ -1,4961 +0,0 @@ -/** - * IGNY8 UNIFIED JAVASCRIPT - PRODUCTION READY - * ============================================ - * - * This file contains ALL JavaScript functionality for the Igny8 plugin. - * Cleaned and optimized for production use. - * - * @package Igny8Compact - * @since 1.0.0 - */ - -/* ========================================= - Planner Settings - Sector Selection - ========================================= */ - -window.initializePlannerSettings = function() { - // Only initialize if we're on the planner home page - if (!window.IGNY8_PAGE || window.IGNY8_PAGE.module !== 'planner' || window.IGNY8_PAGE.submodule !== 'home') { - return; - } - - const parentSelect = document.getElementById('igny8-parent-sector'); - const lockButton = document.getElementById('igny8-lock-parent'); - const childSelection = document.getElementById('igny8-child-selection'); - const childCheckboxes = document.getElementById('igny8-child-checkboxes'); - const saveButton = document.getElementById('igny8-save-selection'); - const finalSelection = document.getElementById('igny8-final-selection'); - const selectedDisplay = document.getElementById('igny8-selected-sectors-display'); - const editButton = document.getElementById('igny8-edit-selection'); - - if (!parentSelect || !lockButton || !childSelection || !childCheckboxes || !saveButton || !finalSelection || !selectedDisplay || !editButton) { - return; - } - - let selectedParent = null; - let selectedChildren = []; - - // Load parent sectors - loadParentSectors(); - - // Load saved selection if exists - loadSavedSelection(); - - // Sample data creation button - const createSampleButton = document.getElementById('igny8-create-sample-sectors'); - if (createSampleButton) { - createSampleButton.addEventListener('click', createSampleSectors); - } - - // Parent sector change handler - parentSelect.addEventListener('change', function() { - if (this.value) { - lockButton.style.display = 'inline-block'; - } else { - lockButton.style.display = 'none'; - } - }); - - // Lock parent selection - lockButton.addEventListener('click', function() { - selectedParent = parentSelect.value; - if (selectedParent) { - parentSelect.disabled = true; - this.style.display = 'none'; - loadChildSectors(selectedParent); - childSelection.style.display = 'block'; - } - }); - - // Save selection - saveButton.addEventListener('click', function() { - const checkedBoxes = childCheckboxes.querySelectorAll('input[type="checkbox"]:checked'); - selectedChildren = Array.from(checkedBoxes).map(cb => { - const label = document.querySelector(`label[for="${cb.id}"]`); - return { - id: cb.value, - name: label ? label.textContent.trim() : cb.value - }; - }); - - console.log('Selected children:', selectedChildren); // Debug log - - if (selectedChildren.length === 0) { - alert('Please select at least one child sector.'); - return; - } - - saveSectorSelection(selectedParent, selectedChildren); - }); - - // Edit selection - editButton.addEventListener('click', function() { - resetSelection(); - }); - - function loadParentSectors() { - const formData = new FormData(); - formData.append('action', 'igny8_get_parent_sectors'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - parentSelect.innerHTML = ''; - data.data.forEach(sector => { - const option = document.createElement('option'); - option.value = sector.id; - option.textContent = sector.name; - parentSelect.appendChild(option); - }); - } - }) - .catch(error => console.error('Error loading parent sectors:', error)); - } - - function loadChildSectors(parentId) { - const formData = new FormData(); - formData.append('action', 'igny8_get_child_sectors'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('parent_id', parentId); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - childCheckboxes.innerHTML = ''; - data.data.forEach(sector => { - const checkboxContainer = document.createElement('div'); - checkboxContainer.className = 'igny8-checkbox-item'; - - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.id = 'child-sector-' + sector.id; - checkbox.value = sector.id; - - const label = document.createElement('label'); - label.htmlFor = 'child-sector-' + sector.id; - label.textContent = sector.name; - - checkboxContainer.appendChild(checkbox); - checkboxContainer.appendChild(label); - childCheckboxes.appendChild(checkboxContainer); - }); - } - }) - .catch(error => console.error('Error loading child sectors:', error)); - } - - function saveSectorSelection(parentId, children) { - console.log('Saving sector selection:', { parentId, children }); // Debug log - - const formData = new FormData(); - formData.append('action', 'igny8_save_sector_selection'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('parent_id', parentId); - formData.append('children_count', children.length); - - // Add each child as separate form fields - children.forEach((child, index) => { - formData.append(`child_${index}_id`, child.id); - formData.append(`child_${index}_name`, child.name); - }); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - displayFinalSelection(data.data.parent, data.data.children); - } else { - let errorMessage = 'Unknown error'; - if (data.data) { - if (typeof data.data === 'string') { - errorMessage = data.data; - } else if (data.data.message) { - errorMessage = data.data.message; - } else { - errorMessage = JSON.stringify(data.data); - } - } - alert('Error saving selection: ' + errorMessage); - } - }) - .catch(error => { - console.error('Error saving selection:', error); - alert('Error saving selection. Please try again.'); - }); - } - - function loadSavedSelection() { - const formData = new FormData(); - formData.append('action', 'igny8_get_saved_sector_selection'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success && data.data) { - displayFinalSelection(data.data.parent, data.data.children); - } - }) - .catch(error => console.error('Error loading saved selection:', error)); - } - - function displayFinalSelection(parent, children) { - selectedParent = parent.id; - selectedChildren = children; - - // Hide all selection steps - document.getElementById('igny8-parent-selection').style.display = 'none'; - childSelection.style.display = 'none'; - - // Show final selection - selectedDisplay.innerHTML = ` -
      - Parent Sector: ${parent.name} -
      -
      - Child Sectors: -
        - ${children.map(child => `
      • ${child.name}
      • `).join('')} -
      -
      - `; - finalSelection.style.display = 'block'; - } - - function resetSelection() { - selectedParent = null; - selectedChildren = []; - - // Show parent selection, hide others - document.getElementById('igny8-parent-selection').style.display = 'block'; - childSelection.style.display = 'none'; - finalSelection.style.display = 'none'; - - // Reset form - parentSelect.disabled = false; - parentSelect.value = ''; - lockButton.style.display = 'none'; - childCheckboxes.innerHTML = ''; - } - - function createSampleSectors() { - const button = document.getElementById('igny8-create-sample-sectors'); - if (!button) return; - - const originalText = button.textContent; - button.textContent = 'Creating...'; - button.disabled = true; - - const formData = new FormData(); - formData.append('action', 'igny8_create_sample_sectors'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert('Sample sectors created successfully! Refreshing the page...'); - location.reload(); - } else { - alert('Error: ' + (data.data || 'Unknown error')); - } - }) - .catch(error => { - console.error('Error creating sample sectors:', error); - alert('Error creating sample sectors. Please try again.'); - }) - .finally(() => { - button.textContent = originalText; - button.disabled = false; - }); - } -}; - -/* ========================================= - AI Integration Form - ========================================= */ - -window.initializeAIIntegrationForm = function() { - // Only initialize if we're on the planner home page - if (!window.IGNY8_PAGE || window.IGNY8_PAGE.module !== 'planner' || window.IGNY8_PAGE.submodule !== 'home') { - return; - } - - const form = document.getElementById('igny8-ai-integration-form'); - if (!form) { - return; - } - - // Handle mode change to show/hide AI features and prompts section - const modeRadios = form.querySelectorAll('input[name="igny8_planner_mode"]'); - const aiFeatures = document.getElementById('igny8-ai-features'); - const promptsSection = document.querySelector('.igny8-planner-prompts'); - - modeRadios.forEach(radio => { - radio.addEventListener('change', function() { - if (this.value === 'ai') { - aiFeatures.style.display = ''; - if (promptsSection) { - promptsSection.style.display = ''; - } - } else { - aiFeatures.style.display = 'none'; - if (promptsSection) { - promptsSection.style.display = 'none'; - } - } - }); - }); - - form.addEventListener('submit', function(e) { - e.preventDefault(); - - const formData = new FormData(form); - - // Determine the correct action based on the current page - const currentPage = window.location.href; - if (currentPage.includes('writer')) { - formData.append('action', 'igny8_save_writer_ai_settings'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - } else { - formData.append('action', 'igny8_save_ai_integration_settings'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - } - - // Show loading state - const submitBtn = form.querySelector('button[type="submit"]'); - const originalText = submitBtn.textContent; - submitBtn.textContent = 'Saving...'; - submitBtn.disabled = true; - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Show success message - igny8GlobalNotification('AI Integration settings saved successfully!', 'success'); - // Reload page to update AI buttons - setTimeout(() => { - window.location.reload(); - }, 1500); - } else { - // Show error message - const errorMsg = data.data?.message || 'Error saving settings'; - igny8GlobalNotification(errorMsg, 'error'); - } - }) - .catch(error => { - console.error('Error saving AI integration settings:', error); - igny8GlobalNotification('Error saving settings. Please try again.', 'error'); - }) - .finally(() => { - // Reset button state - submitBtn.textContent = originalText; - submitBtn.disabled = false; - }); - }); -} - -// Initialize AI action buttons for tables -window.initializeAIActionButtons = function() { - // Only initialize if we're on a planner or writer submodule page - if (!window.IGNY8_PAGE || !window.IGNY8_PAGE.submodule) { - return; - } - - // Only initialize for planner and writer modules - if (window.IGNY8_PAGE.module !== 'planner' && window.IGNY8_PAGE.module !== 'writer') { - return; - } - - const tableId = window.IGNY8_PAGE.tableId; - if (!tableId) return; - - // AI Clustering button - const clusterBtn = document.getElementById(`${tableId}_ai_cluster_btn`); - if (clusterBtn) { - clusterBtn.addEventListener('click', function() { - const selectedIds = getSelectedRowIds(tableId); - if (selectedIds.length === 0) { - igny8GlobalNotification('Please select keywords to cluster', 'error'); - return; - } - - if (selectedIds.length > 20) { - igny8GlobalNotification('Maximum 20 keywords allowed for clustering', 'error'); - return; - } - - // Check if sector is selected before clustering - checkSectorSelectionBeforeClustering(selectedIds); - }); - } - - // AI Ideas button - const ideasBtn = document.getElementById(`${tableId}_ai_ideas_btn`); - if (ideasBtn) { - ideasBtn.addEventListener('click', function() { - const selectedIds = getSelectedRowIds(tableId); - if (selectedIds.length === 0) { - igny8GlobalNotification('Please select clusters to generate ideas', 'error'); - return; - } - - if (selectedIds.length > 5) { - igny8GlobalNotification('Maximum 5 clusters allowed for idea generation', 'error'); - return; - } - - processAIIdeas(selectedIds); - }); - } - - // AI Generate Images button - const generateImagesBtn = document.getElementById(`${tableId}_generate_images_btn`); - if (generateImagesBtn) { - console.log('Igny8: Generate Images button found for table:', tableId); - generateImagesBtn.addEventListener('click', function(e) { - console.log('Igny8: Generate Images button clicked'); - e.preventDefault(); - e.stopPropagation(); - - const selectedIds = getSelectedRowIds(tableId); - console.log('Igny8: Selected IDs:', selectedIds); - console.log('Igny8: Post IDs being sent for image generation:', selectedIds); - - if (selectedIds.length === 0) { - igny8GlobalNotification('Please select posts to generate images', 'error'); - return; - } - - if (selectedIds.length > 10) { - igny8GlobalNotification('Maximum 10 posts allowed for image generation', 'error'); - return; - } - - // Only for drafts table - console.log('Igny8: Calling processAIImageGenerationDrafts with', selectedIds.length, 'posts'); - processAIImageGenerationDrafts(selectedIds); - }); - } else { - console.log('Igny8: Generate Images button NOT found for table:', tableId); - } - - // Queue to Writer button is now handled by the bulk selection system - // No need for separate event listener as it's integrated into updateStates() -} - -// Get selected row IDs from table -function getSelectedRowIds(tableId) { - const checkboxes = document.querySelectorAll(`#table-${tableId}-body input[type="checkbox"]:checked`); - const ids = []; - console.log(`Igny8: Found ${checkboxes.length} checked checkboxes for table ${tableId}`); - - checkboxes.forEach((checkbox, index) => { - const row = checkbox.closest('tr'); - const rowId = row.getAttribute('data-id'); - console.log(`Igny8: Checkbox ${index + 1} - Row ID: ${rowId}`); - - if (rowId && !isNaN(rowId)) { - ids.push(parseInt(rowId)); - console.log(`Igny8: Added Post ID ${rowId} to selection`); - } - }); - - console.log(`Igny8: Final selected Post IDs: ${ids.join(', ')}`); - return ids; -} - -// Process AI Clustering -function processAIClustering(keywordIds) { - const formData = new FormData(); - formData.append('action', 'igny8_ai_cluster_keywords'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('keyword_ids', JSON.stringify(keywordIds)); - - // Show progress modal - showProgressModal('Auto Clustering', keywordIds.length); - - // Log client-side event start - console.log('Igny8 AI: Starting clustering process for', keywordIds.length, 'keywords'); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(data => { - if (data.success) { - console.log('Igny8 AI: Clustering completed successfully', data.data); - - // Show success modal - showSuccessModal('Auto Clustering Complete', data.data.clusters_created || keywordIds.length, data.data.message); - - // Reload table data - try { - if (window.loadTableData && window.IGNY8_PAGE?.tableId) { - window.loadTableData(window.IGNY8_PAGE.tableId); - } - } catch (error) { - console.error('Error reloading table data:', error); - // Don't show error to user since clustering was successful - } - } else { - console.error('Igny8 AI: Clustering failed', data.data); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification(data.data?.message || 'AI clustering failed', 'error'); - } - }) - .catch(error => { - console.error('Error processing AI clustering:', error); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification('Error processing AI clustering', 'error'); - }); -} - -// Process AI Ideas Generation -function processAIIdeas(clusterIds) { - const formData = new FormData(); - formData.append('action', 'igny8_ai_generate_ideas'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('cluster_ids', JSON.stringify(clusterIds)); - - // Show progress modal - showProgressModal('Generate Ideas', clusterIds.length); - - // Log client-side event start - console.log('Igny8 AI: Starting ideas generation process for', clusterIds.length, 'clusters'); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - console.log('Igny8 AI: Ideas generation completed successfully', data.data); - - // Show success modal - showSuccessModal('Ideas Generated', data.data.ideas_created || clusterIds.length, data.data.message); - - // Reload table data - try { - if (window.loadTableData && window.IGNY8_PAGE?.tableId) { - window.loadTableData(window.IGNY8_PAGE.tableId); - } - } catch (error) { - console.error('Error reloading table data:', error); - // Don't show error to user since ideas generation was successful - } - } else { - console.error('Igny8 AI: Ideas generation failed', data.data); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification(data.data?.message || 'AI ideas generation failed', 'error'); - } - }) - .catch(error => { - console.error('Error processing AI ideas:', error); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification('Error processing AI ideas', 'error'); - }); -} - -// Process AI Image Generation for Drafts (Sequential Image Processing) -function processAIImageGenerationDrafts(postIds) { - console.log('Igny8: processAIImageGenerationDrafts called with postIds:', postIds); - - // Get image generation settings from saved options (passed via wp_localize_script) - const desktopEnabled = window.IGNY8_PAGE?.imageSettings?.desktop_enabled || false; - const mobileEnabled = window.IGNY8_PAGE?.imageSettings?.mobile_enabled || false; - const maxInArticleImages = window.IGNY8_PAGE?.imageSettings?.max_in_article_images || 1; - - // Calculate total images per post - let imagesPerPost = 1; // Featured image (always 1) - let imageTypes = ['featured']; - - if (desktopEnabled) { - imagesPerPost += maxInArticleImages; - for (let i = 1; i <= maxInArticleImages; i++) { - imageTypes.push('desktop'); - } - } - - if (mobileEnabled) { - imagesPerPost += maxInArticleImages; - for (let i = 1; i <= maxInArticleImages; i++) { - imageTypes.push('mobile'); - } - } - - const totalImages = postIds.length * imagesPerPost; - - console.log('Igny8: Image generation settings:', { - desktopEnabled, - mobileEnabled, - maxInArticleImages, - imagesPerPost, - totalImages, - imageTypes - }); - - // Show progress modal with calculated total - showProgressModal('Generate Images', totalImages, 'images'); - updateProgressModal(0, totalImages, 'processing', 'Preparing to generate images...'); - - let totalImagesGenerated = 0; - let totalImagesFailed = 0; - const allResults = { - generated: [], - failed: [] - }; - - // Process each post sequentially - function processNextPost(postIndex) { - if (postIndex >= postIds.length) { - // All posts processed - show final results - console.log('Igny8: All posts processed', allResults); - - if (allResults.generated.length > 0) { - updateProgressModal(totalImages, totalImages, 'completed'); - - setTimeout(() => { - showSuccessModal( - 'Images Generated', - allResults.generated.length, - `Successfully generated ${allResults.generated.length} images for ${postIds.length} posts` - ); - - // Reload table - if (window.loadTableData && window.IGNY8_PAGE?.tableId) { - window.loadTableData(window.IGNY8_PAGE.tableId); - } - }, 500); - } else { - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification('No images were generated', 'error'); - } - return; - } - - const postId = postIds[postIndex]; - - // Update progress for current post - const currentProgress = postIndex * imagesPerPost; - updateProgressModal(currentProgress, totalImages, 'processing', `Post ${postIndex + 1} of ${postIds.length}`); - - // Generate all images for this post (one at a time) - generateAllImagesForPost(postId, postIndex + 1, function(postResults) { - // Post complete - add results - allResults.generated.push(...postResults.generated); - allResults.failed.push(...postResults.failed); - totalImagesGenerated += postResults.generated.length; - totalImagesFailed += postResults.failed.length; - - console.log(`✓ Post ${postId} complete: ${postResults.generated.length} images generated, ${postResults.failed.length} failed`); - - // Move to next post - setTimeout(() => processNextPost(postIndex + 1), 100); - }); - } - - // Generate all images for a single post (featured + in-article) - function generateAllImagesForPost(postId, postNumber, callback) { - // Use the existing action but with single post - const formData = new FormData(); - formData.append('action', 'igny8_ai_generate_images_drafts'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('post_ids', JSON.stringify([postId])); - formData.append('desktop_enabled', desktopEnabled ? '1' : '0'); - formData.append('mobile_enabled', mobileEnabled ? '1' : '0'); - formData.append('max_in_article_images', maxInArticleImages); - - console.log('Igny8: Sending AJAX request to:', window.IGNY8_PAGE.ajaxUrl); - console.log('Igny8: AJAX request data:', { - action: 'igny8_ai_generate_images_drafts', - post_ids: JSON.stringify([postId]), - desktop_enabled: desktopEnabled ? '1' : '0', - mobile_enabled: mobileEnabled ? '1' : '0', - max_in_article_images: maxInArticleImages, - nonce: window.IGNY8_PAGE.nonce - }); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => { - console.log('Igny8: AJAX response received:', response.status, response.statusText); - return response.json(); - }) - .then(data => { - if (data.success) { - callback({ - generated: data.data.generated_images || [], - failed: data.data.failed_images || [] - }); - } else { - callback({ - generated: [], - failed: [{ - post_id: postId, - error: data.data?.message || 'Unknown error' - }] - }); - } - }) - .catch(error => { - console.error(`✗ Post ${postId} - Exception:`, error); - callback({ - generated: [], - failed: [{ - post_id: postId, - error: error.message - }] - }); - }); - } - - // Start processing first post - processNextPost(0); -} - -// Process AI Content Generation -function processAIContentGeneration(taskId) { - const formData = new FormData(); - formData.append('action', 'igny8_ai_generate_content'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('task_id', taskId); - - // Show progress modal - showProgressModal('Generate Draft', 1); - - // Log client-side event start - console.log('Igny8 AI: Starting content generation for task', taskId); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - console.log('Igny8 AI: Content generation completed successfully', data.data); - - // Show success modal - showSuccessModal('Draft Generated', 1, data.data.message); - - // Reload table data - try { - if (window.loadTableData && window.IGNY8_PAGE?.tableId) { - window.loadTableData(window.IGNY8_PAGE.tableId); - } - } catch (error) { - console.error('Error reloading table data:', error); - // Don't show error to user since content generation was successful - } - } else { - console.error('Igny8 AI: Content generation failed', data.data); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification(data.data?.message || 'AI content generation failed', 'error'); - } - }) - .catch(error => { - console.error('Error processing AI content generation:', error); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8GlobalNotification('Error processing AI content generation', 'error'); - }); -} - - -// Unified Global Notification System -function igny8GlobalNotification(message, type = 'info') { - // Debug logging - console.log('igny8GlobalNotification called:', message, type, new Date().toLocaleTimeString()); - - // Get or create global notification container - let container = document.getElementById('igny8-global-notification'); - if (!container) { - container = document.createElement('div'); - container.id = 'igny8-global-notification'; - document.body.appendChild(container); - } - - // Clear any existing timeouts to prevent conflicts - if (container._hideTimeout) { - clearTimeout(container._hideTimeout); - console.log('Cleared existing hide timeout'); - } - if (container._removeTimeout) { - clearTimeout(container._removeTimeout); - console.log('Cleared existing remove timeout'); - } - - // Clear any existing classes and content - container.className = ''; - container.textContent = message; - container.style.display = 'block'; - - // Add type class and show - container.classList.add(type); - container.classList.add('show'); - - console.log('Notification shown, will hide in 4 seconds'); - - // Auto-hide after 4 seconds (4000ms) - container._hideTimeout = setTimeout(() => { - console.log('Starting hide animation'); - container.classList.remove('show'); - container.classList.add('hide'); - - // Remove from DOM after animation completes - container._removeTimeout = setTimeout(() => { - console.log('Removing notification from DOM'); - container.classList.remove('hide'); - container.style.display = 'none'; - }, 300); - }, 4000); -} - -// Legacy function for backward compatibility - -/* ========================================= - Debug Toggle Functionality - ========================================= */ - -window.initializeDebugToggle = function() { - const saveButton = document.getElementById('save-debug-setting'); - const enabledRadio = document.getElementById('debug-enabled'); - const disabledRadio = document.getElementById('debug-disabled'); - - if (!saveButton || !enabledRadio || !disabledRadio) return; - - saveButton.addEventListener('click', function() { - const isEnabled = enabledRadio.checked; - - console.log('DEBUG: Save button clicked, isEnabled:', isEnabled); - - // Show loading state - const originalText = this.innerHTML; - this.innerHTML = ' Saving...'; - this.disabled = true; - - // Send AJAX request to update the setting - const formData = new FormData(); - formData.append('action', 'igny8_toggle_debug_monitoring'); - formData.append('nonce', igny8_ajax.nonce); - formData.append('is_enabled', isEnabled ? '1' : '0'); - - console.log('DEBUG: Sending AJAX with is_enabled:', isEnabled ? '1' : '0'); - - fetch(igny8_ajax.ajax_url, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Show success notification - if (typeof igny8ShowNotification === 'function') { - igny8ShowNotification( - isEnabled ? 'Debug monitoring enabled' : 'Debug monitoring disabled', - 'success' - ); - } - - // Update module debug visibility on submodule pages - updateModuleDebugVisibility(isEnabled); - - // Update button to show success - this.innerHTML = ' Saved'; - setTimeout(() => { - this.innerHTML = originalText; - }, 2000); - } else { - // Show error notification - if (typeof igny8ShowNotification === 'function') { - igny8ShowNotification( - data.data?.message || 'Failed to update debug monitoring setting', - 'error' - ); - } - } - }) - .catch(error => { - // Show error notification - if (typeof igny8ShowNotification === 'function') { - igny8ShowNotification('Network error occurred', 'error'); - } - }) - .finally(() => { - // Restore button state - this.disabled = false; - if (this.innerHTML.includes('Saving...')) { - this.innerHTML = originalText; - } - }); - }); -}; - -window.updateModuleDebugVisibility = function(isEnabled) { - // Only update on submodule pages (pages with module debug container) - const moduleDebugContainer = document.getElementById('igny8-module-debug-container'); - if (!moduleDebugContainer) return; - - if (isEnabled) { - moduleDebugContainer.style.display = 'block'; - } else { - moduleDebugContainer.style.display = 'none'; - } -}; - -// Initialize module debug visibility on page load -window.initializeModuleDebugVisibility = function() { - const enabledRadio = document.getElementById('debug-enabled'); - const moduleDebugContainer = document.getElementById('igny8-module-debug-container'); - - if (enabledRadio && moduleDebugContainer) { - const isEnabled = enabledRadio.checked; - updateModuleDebugVisibility(isEnabled); - } -}; - -/* ========================================= - Igny8 Core Initialization & Dropdowns - ========================================= */ - -jQuery(document).ready(function ($) { - initializeDropdowns($); - initializeDebugSystem(); - initializeDebugToggle(); - initializeModuleDebugVisibility(); - // Removed initializeDelegatedEvents() - handled in DOMContentLoaded -}); - -/* ========================================= - Dropdown Functionality - ========================================= */ - -window.initializeDropdowns = function ($) { - // Toggle dropdown - $(document).off('click.dropdown').on('click.dropdown', '.select-btn', function (e) { - e.preventDefault(); - e.stopPropagation(); - - const $select = $(this).closest('.select'); - if (!$select.find('.select-list').length) return; - - $('.select').not($select).removeClass('open'); - $select.toggleClass('open'); - }); - - // Select item - $(document).off('click.dropdown-item').on('click.dropdown-item', '.select-item', function (e) { - e.stopPropagation(); - - const $item = $(this); - const $select = $item.closest('.select'); - const $btn = $select.find('.select-btn'); - const value = $item.data('value'); - const text = $item.text(); - - $btn.attr('data-value', value).data('value', value) - .find('.select-text').text(text); - - $select.removeClass('open'); - $select[0].dispatchEvent(new CustomEvent('change', { detail: { value, text } })); - }); - - // Close on outside click - $(document).off('click.dropdown-outside').on('click.dropdown-outside', function (e) { - if (!$(e.target).closest('.select').length) $('.select').removeClass('open'); - }); -}; - -/* ========================================= - Igny8 Debug System – Toggle & Monitoring - ========================================= */ - -function initializeDebugSystem() { - const toggle = document.getElementById('igny8-debug-toggle'); - const statusText = document.getElementById('igny8-module-debug-status'); - const container = document.getElementById('igny8-module-debug-container'); - const widgets = document.getElementById('igny8-module-debug-widgets'); - - // ---- Main Debug Toggle ---- - if (toggle) { - setDebugUI(toggle.checked); - toggle.addEventListener('change', () => { - const enabled = toggle.checked; - setDebugUI(enabled); - saveDebugState(enabled); - }); - } - - // ---- Module Debug Widget Toggle ---- - window.igny8ToggleModuleDebug = () => { - if (!widgets) return; - const hidden = widgets.classList.contains('hidden') || widgets.style.display === 'none'; - widgets.style.display = hidden ? 'block' : 'none'; - widgets.classList.toggle('hidden', !hidden); - }; - - // ---- Auto-show Debug Panel on DOM Ready ---- - document.addEventListener('DOMContentLoaded', () => { - if (container && widgets) { - container.style.display = 'block'; - widgets.style.display = 'block'; - widgets.classList.remove('hidden'); - } - }); - - // ---- Helper: UI Update ---- - function setDebugUI(enabled) { - if (container) container.style.display = enabled ? 'block' : 'none'; - if (statusText) statusText.textContent = enabled ? 'Enabled' : 'Disabled'; - } - - // ---- Helper: Save State via AJAX ---- - function saveDebugState(enabled) { - if (!igny8_ajax?.ajax_url) return; - fetch(igny8_ajax.ajax_url, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - action: 'igny8_toggle_debug', - nonce: igny8_ajax.nonce, - enabled: enabled ? '1' : '0' - }) - }) - .then(r => r.json()) - .then(d => { if (!d.success) {} }) - .catch(err => {}); - } -} - - - -/* ========================================= - Igny8 Utility – Number, Currency, Date - ========================================= */ - -window.igny8FormatNumber = (num, decimals = 0) => - new Intl.NumberFormat(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(num); - -window.igny8FormatCurrency = (amount, currency = 'USD') => - new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount); - -window.igny8FormatDate = (date, options = {}) => { - const base = { year: 'numeric', month: 'short', day: 'numeric' }; - return new Intl.DateTimeFormat('en-US', { ...base, ...options }).format(new Date(date)); -}; - - -// ---- Update Debug Grid Cell State ---- -window.igny8DebugState = function (stage, ok, msg = '') { - const cell = document.querySelector(`[data-debug-stage="${stage}"]`); - if (!cell) return; - - cell.classList.toggle('ok', !!ok); - cell.classList.toggle('fail', !ok); - cell.classList.toggle('neutral', !ok && !ok); - if (msg) cell.title = msg; -}; - -// ---- Centralized Event Logger ---- -window.igny8LogEvent = function (eventType, status, details = '') { - const timestamp = new Date().toLocaleTimeString(); - const logEntry = `[${timestamp}] ${eventType}: ${status}${details ? ' - ' + details : ''}`; - - const colorMap = { - SUCCESS: '#28a745', FOUND: '#28a745', EXISTS: '#28a745', WORKING: '#28a745', - MISSING: '#dc3545', BROKEN: '#dc3545', ERROR: '#dc3545', NOT_: '#dc3545', - WARNING: '#ffc107', PENDING: '#ffc107' - }; - const color = colorMap[status] || '#6c757d'; - const method = (status === 'MISSING' || status === 'BROKEN' || status === 'ERROR' || status === 'NOT_') ? 'error' : - (status === 'WARNING' || status === 'PENDING') ? 'warn' : 'log'; - console[method](`%c${logEntry}`, `color: ${color}; font-weight: bold;`); - - window.igny8EventLog = window.igny8EventLog || []; - window.igny8EventLog.push(logEntry); - if (window.igny8EventLog.length > 100) window.igny8EventLog = window.igny8EventLog.slice(-100); -}; - - -/* ========================================= - Igny8 Filters – Inputs, Dropdowns, Ranges - ========================================= */ - -// ---- Initialize All Filter Listeners ---- -function initializeFilters() { - const filters = document.querySelectorAll('.igny8-filters [data-filter]'); - let boundCount = 0; - - filters.forEach(el => { - if (el.tagName === 'INPUT' && el.type === 'text') { - // Debounced search input - let timer; - el.addEventListener('input', () => { - clearTimeout(timer); - timer = setTimeout(() => { - const tableId = el.closest('.igny8-filters')?.dataset.table; - if (tableId) handleFilterChange(tableId); - }, 400); - }); - boundCount++; - - } else if (el.classList.contains('select')) { - // Dropdown select - el.querySelectorAll('.select-item').forEach(item => { - item.addEventListener('click', () => { - const val = item.dataset.value || ''; - const txt = item.textContent.trim(); - const btn = el.querySelector('.select-btn'); - const label = el.querySelector('.select-text'); - if (btn && label) { - btn.dataset.value = val; - label.textContent = txt; - } - el.classList.remove('open'); - }); - }); - boundCount++; - } - }); - - // ---- Range OK & Clear ---- - document.querySelectorAll('[id$="_ok"]').forEach(btn => { - btn.addEventListener('click', () => { - const range = btn.closest('.select'); - if (!range) return; - - const min = range.querySelector('input[id$="_min"]')?.value.trim(); - const max = range.querySelector('input[id$="_max"]')?.value.trim(); - const label = range.querySelector('.select-text'); - - if (label) { - label.textContent = min && max ? `${min} - ${max}` : - min ? `${min} - ∞` : - max ? `0 - ${max}` : 'Volume'; - } - range.classList.remove('open'); - }); - boundCount++; - }); - - document.querySelectorAll('[id$="_clear"]').forEach(btn => { - btn.addEventListener('click', () => { - const range = btn.closest('.select'); - if (!range) return; - - range.querySelectorAll('input[type="number"]').forEach(i => i.value = ''); - const label = range.querySelector('.select-text'); - if (label) label.textContent = 'Volume'; - range.classList.remove('open'); - }); - boundCount++; - }); - - // ---- Apply & Reset ---- - document.querySelectorAll('[id$="_filter_apply_btn"]').forEach(btn => { - btn.addEventListener('click', () => applyFilters(btn.id.replace('_filter_apply_btn', ''))); - boundCount++; - }); - - document.querySelectorAll('[id$="_filter_reset_btn"]').forEach(btn => { - btn.addEventListener('click', () => resetFilters(btn.id.replace('_filter_reset_btn', ''))); - boundCount++; - }); -} - -// ---- Collect Filter Values ---- -function collectFilters(tableId) { - const container = document.querySelector(`[data-table="${tableId}"].igny8-filters`); - if (!container) return {}; - - const payload = {}; - - container.querySelectorAll('input[data-filter]').forEach(i => { - const val = i.value.trim(); - if (val) payload[i.dataset.filter] = val; - }); - - container.querySelectorAll('.select[data-filter]').forEach(s => { - const val = s.querySelector('.select-btn')?.dataset.value; - if (val) payload[s.dataset.filter] = val; - }); - - container.querySelectorAll('input[type="number"]').forEach(i => { - const val = i.value.trim(); - if (val) { - if (i.id.includes('search_volume_min')) payload['search_volume-min'] = val; - if (i.id.includes('search_volume_max')) payload['search_volume-max'] = val; - } - }); - - return payload; -} - -// ---- Trigger Filter Change ---- -function handleFilterChange(tableId) { - const payload = collectFilters(tableId); - loadTableData(tableId, payload, 1); -} - -// ---- Apply Filters ---- -function applyFilters(tableId) { - const payload = collectFilters(tableId); - const perPage = getSessionPerPage(tableId) || getDefaultPerPage(); - loadTableData(tableId, payload, 1, perPage); -} - -// ---- Reset Filters ---- -function resetFilters(tableId) { - const container = document.querySelector(`[data-table="${tableId}"].igny8-filters`); - if (!container) return; - - container.querySelectorAll('input[type="text"], input[type="number"]').forEach(i => i.value = ''); - - container.querySelectorAll('.select').forEach(select => { - const btn = select.querySelector('.select-btn'); - const label = select.querySelector('.select-text'); - if (btn && label) { - btn.dataset.value = ''; - const field = select.dataset.filter; - label.textContent = field ? getFilterLabel(tableId, field) : 'All'; - } - select.classList.remove('open'); - }); - - const perPage = getSessionPerPage(tableId) || getDefaultPerPage(); - loadTableData(tableId, {}, 1, perPage); -} - -// ---- Retrieve Original Filter Label ---- -function getFilterLabel(tableId, field) { - if (window.IGNY8_PAGE?.filtersConfig?.[tableId]?.[field]) { - return IGNY8_PAGE.filtersConfig[tableId][field].label || field; - } - - const el = document.querySelector(`[data-table="${tableId}"].igny8-filters [data-filter="${field}"]`); - const allText = el?.querySelector('.select-item[data-value=""]')?.textContent.trim(); - const match = allText?.match(/^All\s+(.+)$/); - return match ? match[1] : field.charAt(0).toUpperCase() + field.slice(1); -} - -/* ========================================= - Igny8 Table Actions – Bulk & Row Controls - ========================================= */ - -function initializeTableActions(tableId) { - const exportBtn = document.getElementById(`${tableId}_export_btn`); - const deleteBtn = document.getElementById(`${tableId}_delete_btn`); - const publishBtn = document.getElementById(`${tableId}_publish_btn`); - const importBtn = document.getElementById(`${tableId}_import_btn`); - const addBtn = document.getElementById(`${tableId}_add_btn`); - const countSpan = document.getElementById(`${tableId}_count`); - - // ---- Button Handlers ---- - exportBtn?.addEventListener('click', () => { - const selectedIds = getSelectedRows(tableId); - if (selectedIds.length) { - igny8ShowExportModal(tableId, selectedIds); - } else { - igny8ShowNotification('Please select records to export', 'warning'); - } - }); - - deleteBtn?.addEventListener('click', () => { - const rows = getSelectedRows(tableId); - if (rows.length) deleteSelectedData(tableId, rows); - else igny8ShowNotification('Please select records to delete', 'warning'); - }); - - // Publish button handler (for drafts) - publishBtn?.addEventListener('click', () => { - const rows = getSelectedRows(tableId); - if (rows.length) { - handleBulkPublishDrafts(rows, tableId); - } else { - igny8ShowNotification('Please select drafts to publish', 'warning'); - } - }); - - importBtn?.addEventListener('click', () => igny8ShowImportModal(tableId)); - addBtn?.addEventListener('click', () => openAddNewModal?.(tableId)); - - // ---- Selection Count & Button State ---- - const updateStates = () => { - const count = getSelectedRows(tableId).length; - if (countSpan) { - countSpan.textContent = `${count} selected`; - countSpan.classList.toggle('igny8-count-hidden', count === 0); - } - if (exportBtn) exportBtn.disabled = !count; - if (deleteBtn) deleteBtn.disabled = !count; - if (publishBtn) publishBtn.disabled = !count; - - // Update AI action buttons - const clusterBtn = document.getElementById(`${tableId}_ai_cluster_btn`); - const ideasBtn = document.getElementById(`${tableId}_ai_ideas_btn`); - const mappingBtn = document.getElementById(`${tableId}_ai_mapping_btn`); - const queueWriterBtn = document.getElementById(`${tableId}_queue_writer_btn`); - const generateContentBtn = document.getElementById(`${tableId}_generate_content_btn`); - const generateImagesBtn = document.getElementById(`${tableId}_generate_images_btn`); - - if (clusterBtn) clusterBtn.disabled = !count; - if (ideasBtn) ideasBtn.disabled = !count; - if (mappingBtn) mappingBtn.disabled = !count; - if (queueWriterBtn) queueWriterBtn.disabled = !count; - if (generateContentBtn) generateContentBtn.disabled = !count; - if (generateImagesBtn) generateImagesBtn.disabled = !count; - }; - - document.addEventListener('rowSelectionChanged', e => { - if (e.detail.tableId === tableId) updateStates(); - }); - - // Initial state update - updateStates(); -} - -// ---- Helpers ---- -function getSelectedRows(tableId) { - return [...document.querySelectorAll(`#table-${tableId}-body input[type="checkbox"]:checked`)] - .map(cb => { - const row = cb.closest('tr'); - return row ? row.getAttribute('data-id') : null; - }) - .filter(id => id !== null); -} - -function exportSelectedData(tableId, rows) { - loadTableData(tableId, {}, 1); -} - -function deleteSelectedData(tableId, rows) { - if (!rows.length) return igny8ShowNotification('No records selected for deletion', 'warning'); - - // Get the actual record data for the selected rows - const data = rows.map(rowId => { - const row = document.querySelector(`#table-${tableId}-body tr[data-id="${rowId}"]`); - if (!row) return null; - - // Extract record data from the row - const cells = row.querySelectorAll('td'); - const record = { - id: rowId, - table_id: tableId // CRITICAL FIX: Include table_id in record data - }; - - // Get the first text cell as the title/name - if (cells.length > 1) { - const titleCell = cells[1]; // Skip checkbox column - record.name = titleCell.textContent.trim(); - record.keyword = titleCell.textContent.trim(); // For keywords - record.idea_title = titleCell.textContent.trim(); // For ideas - } - - return record; - }).filter(record => record !== null); - - igny8ShowDeleteDialog('bulk', data); -} - -function deleteRow(rowId) { - const row = document.querySelector(`tr[data-id="${rowId}"]`); - const tableId = row?.closest('table')?.id.replace('_table', ''); - if (!tableId) return; - - const rowData = getUniversalRowData(tableId, rowId); - if (rowData) igny8ShowDeleteDialog('single', [rowData]); -} - -/* ========================================= - Igny8 Add/Edit Row Handlers - ========================================= */ - -function openAddNewRow(tableId) { - document.querySelector('tr.igny8-inline-form-row')?.remove(); - - const formData = new FormData(); - formData.append('action', 'igny8_render_form_row'); - formData.append('nonce', igny8_ajax.nonce); - formData.append('table_id', tableId); - formData.append('mode', 'add'); - formData.append('record_data', '{}'); - - fetch(ajaxurl, { method: 'POST', body: formData }) - .then(r => r.json()) - .then(result => { - if (!result.success) throw new Error(result.data); - const tableBody = document.querySelector(`#table-${tableId}-body`); - if (tableBody) tableBody.insertAdjacentHTML('afterbegin', result.data); - }) - .catch(err => igny8ShowNotification(`Add form error: ${err.message}`, 'error')); -} - -function editRow(rowId, tableId) { - document.querySelector('tr.igny8-inline-form-row')?.remove(); - - const formData = new FormData(); - formData.append('action', 'igny8_get_row_data'); - formData.append('nonce', igny8_ajax.nonce); - formData.append('table_id', tableId); - formData.append('row_id', rowId); - - fetch(ajaxurl, { method: 'POST', body: formData }) - .then(r => r.json()) - .then(result => { - if (!result.success) throw new Error(result.data); - - const editFormData = new FormData(); - editFormData.append('action', 'igny8_render_form_row'); - editFormData.append('nonce', igny8_ajax.nonce); - editFormData.append('table_id', tableId); - editFormData.append('mode', 'edit'); - editFormData.append('record_data', JSON.stringify(result.data)); - - return fetch(ajaxurl, { method: 'POST', body: editFormData }); - }) - .then(r => r.json()) - .then(formResult => { - if (!formResult.success) throw new Error(formResult.data); - const row = document.querySelector(`tr[data-id="${rowId}"]`); - if (row) row.outerHTML = formResult.data; - else igny8ShowNotification('Target row not found for editing', 'error'); - }) - .catch(err => igny8ShowNotification(`Edit form error: ${err.message}`, 'error')); -} - -/* ========================================= - Igny8 Delegated Event Handlers - ========================================= */ - -function initializeDelegatedEvents() { - // Single delegated click handler to prevent conflicts - document.addEventListener('click', e => { - // Prevent multiple event handlers from interfering - if (e.defaultPrevented) return; - - const addBtn = e.target.closest('[data-action="addRow"]'); - const editBtn = e.target.closest('[data-action="editRow"]'); - const queueBtn = e.target.closest('[data-action="queueToWriter"]'); - const bulkQueueBtn = e.target.closest('[data-action="bulkQueueToWriter"]'); - const deleteBtn = e.target.closest('[data-action="deleteRow"]'); - const personalizeBtn = e.target.closest('#igny8-launch'); - const personalizeForm = e.target.closest('#igny8-form'); - const saveBtn = e.target.closest('.igny8-save-btn'); - const cronBtn = e.target.closest('button[data-action]'); - const descriptionToggle = e.target.closest('.igny8-description-toggle'); - const imagePromptsToggle = e.target.closest('.igny8-image-prompts-toggle'); - - if (addBtn) { - e.preventDefault(); - e.stopPropagation(); - openAddNewRow(addBtn.dataset.tableId); - return; - } - - if (editBtn) { - e.preventDefault(); - e.stopPropagation(); - editRow(editBtn.dataset.rowId, editBtn.dataset.tableId); - return; - } - - if (queueBtn) { - e.preventDefault(); - e.stopPropagation(); - const ideaId = queueBtn.dataset.ideaId; - if (ideaId) { - igny8QueueIdeaToWriter(ideaId); - } - return; - } - - if (bulkQueueBtn) { - e.preventDefault(); - e.stopPropagation(); - const tableId = bulkQueueBtn.closest('[data-table]')?.dataset?.table; - if (tableId) { - const selectedRows = getSelectedRows(tableId); - if (selectedRows.length > 0) { - igny8BulkQueueIdeasToWriter(selectedRows); - } else { - igny8ShowNotification('Please select ideas to queue to Writer', 'warning'); - } - } - return; - } - - if (deleteBtn) { - e.preventDefault(); - e.stopPropagation(); - const tableId = deleteBtn.closest('[data-table]').dataset.table; - const rowId = deleteBtn.dataset.rowId; - const rowData = getUniversalRowData(tableId, rowId); - if (rowData) igny8ShowDeleteDialog('single', [rowData]); - return; - } - - // Handle Queue to Writer button click (for Ideas table) - const queueWriterBtn = e.target.closest(`[id$="_queue_writer_btn"]`); - if (queueWriterBtn) { - e.preventDefault(); - e.stopPropagation(); - const tableId = queueWriterBtn.closest('[data-table]')?.dataset?.table; - if (tableId) { - const selectedRows = getSelectedRows(tableId); - if (selectedRows.length > 0) { - if (selectedRows.length > 50) { - igny8ShowNotification('Maximum 50 ideas allowed for queuing', 'error'); - return; - } - igny8BulkQueueIdeasToWriter(selectedRows); - } else { - igny8ShowNotification('Please select ideas to queue to Writer', 'warning'); - } - } - } - - // Handle Generate Content button click (for Writer Queue table) - const generateContentBtn = e.target.closest(`[id$="_generate_content_btn"]`); - if (generateContentBtn) { - e.preventDefault(); - e.stopPropagation(); - const tableId = generateContentBtn.closest('[data-table]')?.dataset?.table; - if (tableId) { - const selectedRows = getSelectedRows(tableId); - if (selectedRows.length > 0) { - if (selectedRows.length > 5) { - igny8ShowNotification('Maximum 5 tasks allowed for content generation', 'error'); - return; - } - igny8BulkGenerateContent(selectedRows); - } else { - igny8ShowNotification('Please select tasks to generate content', 'warning'); - } - } - return; - } - - // Handle personalization button clicks - if (personalizeBtn) { - e.preventDefault(); - e.stopPropagation(); - handlePersonalizeClick(personalizeBtn); - return; - } - - // Handle personalization form submissions - if (personalizeForm) { - e.preventDefault(); - e.stopPropagation(); - handlePersonalizeFormSubmit(personalizeForm); - return; - } - - // Handle save content button - if (saveBtn) { - e.preventDefault(); - e.stopPropagation(); - handleSaveContent(saveBtn); - return; - } - - // Handle cron job buttons - if (cronBtn) { - e.preventDefault(); - e.stopPropagation(); - const action = cronBtn.getAttribute('data-action'); - const hook = cronBtn.getAttribute('data-hook'); - - if (action === 'runNow') { - handleIconRunNow(cronBtn, hook); - } else if (action === 'openInNewWindow') { - handleOpenInNewWindow(cronBtn, hook); - } - return; - } - - // Handle description toggle clicks - if (descriptionToggle) { - e.preventDefault(); - e.stopPropagation(); - // Description toggle logic would go here - return; - } - - // Handle image prompts toggle clicks - if (imagePromptsToggle) { - e.preventDefault(); - e.stopPropagation(); - // Image prompts toggle logic would go here - return; - } - }); -} - -// === Planner → Writer Bridge Functions === - -/** - * Queue a single idea to Writer - */ -function igny8QueueIdeaToWriter(ideaId) { - const data = new FormData(); - data.append('action', 'igny8_create_task_from_idea'); - data.append('nonce', igny8_ajax.nonce); - data.append('idea_id', ideaId); - - fetch(igny8_ajax.ajax_url, { - method: 'POST', - body: data - }) - .then(r => r.json()) - .then(resp => { - if (resp.success) { - igny8ShowNotification(resp.message || 'Task created', 'success'); - } else { - igny8ShowNotification(resp.message || 'Failed to create task', 'error'); - } - // Reload both tables to show updated data - if (typeof igny8ReloadTable === 'function') { - igny8ReloadTable('planner_ideas'); - igny8ReloadTable('writer_drafts'); - } - }) - .catch(error => { - console.error('Queue to Writer error:', error); - igny8ShowNotification('Failed to queue idea to Writer', 'error'); - }); -} - -/** - * Bulk generate content for tasks - */ -function igny8BulkGenerateContent(selectedIds) { - let completed = 0; - let failed = 0; - const total = selectedIds.length; - - igny8ShowNotification(`Starting content generation for ${total} tasks...`, 'info'); - - selectedIds.forEach((taskId, index) => { - // Add a small delay between requests to avoid overwhelming the API - setTimeout(() => { - const data = new FormData(); - data.append('action', 'igny8_ai_generate_content'); - data.append('nonce', window.IGNY8_PAGE?.nonce || igny8_ajax.nonce); - data.append('task_id', taskId); - - fetch(window.IGNY8_PAGE?.ajaxUrl || igny8_ajax.ajax_url, { - method: 'POST', - body: data - }) - .then(r => { - if (!r.ok) { - throw new Error(`HTTP error! status: ${r.status}`); - } - return r.text().then(text => { - try { - return JSON.parse(text); - } catch (e) { - console.error('Invalid JSON response:', text); - throw new Error('Invalid JSON response from server'); - } - }); - }) - .then(resp => { - completed++; - if (resp.success) { - const data = resp.data; - console.log(`Content generated for task ${taskId}:`, data); - - // Show detailed success message - if (data.post_id) { - console.log(`✅ WordPress post created: Post ID ${data.post_id}`); - if (data.post_edit_url) { - console.log(`📝 Edit post: ${data.post_edit_url}`); - } - } - if (data.task_status) { - console.log(`📋 Task status updated to: ${data.task_status}`); - } - } else { - failed++; - console.error(`Failed to generate content for task ${taskId}:`, resp.data); - } - - // Check if all requests are complete - if (completed + failed === total) { - if (failed === 0) { - igny8ShowNotification(`Content generated successfully for all ${total} tasks. Check WordPress Posts for drafts.`, 'success'); - } else { - igny8ShowNotification(`Content generation completed: ${completed} successful, ${failed} failed`, 'warning'); - } - - // Reload tables to show updated data - if (typeof igny8ReloadTable === 'function') { - igny8ReloadTable('writer_queue'); - igny8ReloadTable('writer_drafts'); - } - } - }) - .catch(error => { - failed++; - console.error(`Content generation error for task ${taskId}:`, error); - - // Check if all requests are complete - if (completed + failed === total) { - igny8ShowNotification(`Content generation completed: ${completed} successful, ${failed} failed`, 'warning'); - - // Reload tables to show updated data - if (typeof igny8ReloadTable === 'function') { - igny8ReloadTable('writer_queue'); - igny8ReloadTable('writer_drafts'); - } - } - }); - }, index * 1000); // 1 second delay between requests - }); -} - -/** - * Bulk queue ideas to Writer - */ -function igny8BulkQueueIdeasToWriter(selectedIds) { - const data = new FormData(); - data.append('action', 'igny8_bulk_create_tasks_from_ideas'); - data.append('nonce', igny8_ajax.nonce); - selectedIds.forEach(id => data.append('idea_ids[]', id)); - - // Show progress modal - showProgressModal('Queue to Writer', selectedIds.length); - - fetch(igny8_ajax.ajax_url, { - method: 'POST', - body: data - }) - .then(r => { - if (!r.ok) { - throw new Error(`HTTP error! status: ${r.status}`); - } - return r.text().then(text => { - try { - return JSON.parse(text); - } catch (e) { - console.error('Invalid JSON response:', text); - throw new Error('Invalid JSON response from server'); - } - }); - }) - .then(resp => { - if (resp.success) { - // Show success modal - showSuccessModal('Queue to Writer Complete', selectedIds.length, resp.data?.message || 'Ideas queued to Writer successfully'); - } else { - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8ShowNotification(resp.data?.message || 'Failed to queue ideas to Writer', 'error'); - } - // Reload both tables to show updated data - if (typeof igny8ReloadTable === 'function') { - igny8ReloadTable('planner_ideas'); - igny8ReloadTable('writer_queue'); - } - }) - .catch(error => { - console.error('Bulk queue to Writer error:', error); - // Close progress modal and show error - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - igny8ShowNotification('Failed to bulk queue ideas to Writer: ' + error.message, 'error'); - }); -} - -// === Bulk & Row Actions === - -/** - * Handle bulk actions (delete, map, unmap) - */ -function handleBulkAction(action, btn) { - const tableId = btn.closest('[data-table]')?.dataset?.table; - if (!tableId) { - igny8ShowNotification('Table not found', 'error'); - return; - } - - // Get selected row IDs - const selectedRows = getSelectedRows(tableId); - if (selectedRows.length === 0) { - igny8ShowNotification('Please select records to perform this action', 'warning'); - return; - } - - // Handle different bulk actions - if (action === 'bulk_delete_keywords') { - handleBulkDelete(selectedRows, tableId); - } else if (action === 'bulk_map_keywords') { - handleBulkMap(selectedRows, tableId); - } else if (action === 'bulk_unmap_keywords') { - handleBulkUnmap(selectedRows, tableId); - } else if (action === 'bulk_publish_drafts') { - handleBulkPublishDrafts(selectedRows, tableId); - } -} - -/** - * Handle row actions (view, map, create draft) - */ -function handleRowAction(action, btn) { - const rowId = btn.dataset.rowId; - const tableId = btn.closest('[data-table]')?.dataset?.table; - - if (!rowId || !tableId) { - igny8ShowNotification('Row or table not found', 'error'); - return; - } - - // Handle different row actions - if (action === 'view_cluster_keywords') { - handleViewClusterKeywords(rowId, tableId); - } else if (action === 'map_cluster_to_keywords') { - handleMapClusterToKeywords(rowId, tableId); - } else if (action === 'create_draft_from_idea') { - handleCreateDraftFromIdea(rowId, tableId); - } -} - -/** - * Handle bulk delete - */ -function handleBulkDelete(keywordIds, tableId) { - if (!confirm(`Are you sure you want to delete ${keywordIds.length} selected keywords?`)) { - return; - } - - sendBulkAction('igny8_bulk_delete_keywords', { keyword_ids: keywordIds }, tableId); -} - -/** - * Handle bulk map to cluster - */ -function handleBulkMap(keywordIds, tableId) { - // Get cluster selection (could be from a dropdown or modal) - const clusterSelect = document.querySelector(`[data-table="${tableId}"] .cluster-select`); - if (!clusterSelect) { - igny8ShowNotification('No cluster selection found', 'error'); - return; - } - - const clusterId = clusterSelect.value; - if (!clusterId) { - igny8ShowNotification('Please select a cluster to map keywords to', 'warning'); - return; - } - - sendBulkAction('igny8_bulk_map_keywords', { keyword_ids: keywordIds, cluster_id: clusterId }, tableId); -} - -/** - * Handle bulk unmap from clusters - */ -function handleBulkUnmap(keywordIds, tableId) { - if (!confirm(`Are you sure you want to unmap ${keywordIds.length} selected keywords from their clusters?`)) { - return; - } - - sendBulkAction('igny8_bulk_unmap_keywords', { keyword_ids: keywordIds }, tableId); -} - -/** - * Handle view cluster keywords (modal) - */ -function handleViewClusterKeywords(clusterId, tableId) { - sendRowAction('igny8_view_cluster_keywords', { cluster_id: clusterId }, (response) => { - if (response.success) { - showClusterKeywordsModal(response.cluster_name, response.keywords); - } - }); -} - -/** - * Handle map cluster to keywords - */ -function handleMapClusterToKeywords(clusterId, tableId) { - // Get selected keyword IDs from checkboxes - const selectedRows = getSelectedRows(tableId); - if (selectedRows.length === 0) { - igny8ShowNotification('Please select keywords to map to this cluster', 'warning'); - return; - } - - sendRowAction('igny8_map_cluster_to_keywords', { - cluster_id: clusterId, - keyword_ids: selectedRows - }, tableId); -} - -/** - * Handle create draft from idea - */ -function handleCreateDraftFromIdea(ideaId, tableId) { - sendRowAction('igny8_create_draft_from_idea', { idea_id: ideaId }, (response) => { - if (response.success) { - igny8ShowNotification(`Draft created successfully (ID: ${response.draft_id})`, 'success'); - // Optionally refresh the table to show the new draft - loadTableData(tableId, {}, 1); - } - }); -} - -/** - * Handle bulk publish drafts action - */ -function handleBulkPublishDrafts(selectedRows, tableId) { - if (selectedRows.length === 0) { - igny8ShowNotification('Please select drafts to publish', 'warning'); - return; - } - - // Show confirmation dialog - if (!confirm(`Are you sure you want to publish ${selectedRows.length} draft(s)? This will make them live on your website.`)) { - return; - } - - const formData = new FormData(); - formData.append('action', 'igny8_bulk_publish_drafts'); - formData.append('nonce', window.IGNY8_PAGE?.nonce || igny8_ajax.nonce); - - // Add task IDs - selectedRows.forEach(id => { - formData.append('task_ids[]', id); - }); - - // Show progress notification - igny8ShowNotification('Publishing drafts...', 'info'); - - fetch(window.IGNY8_PAGE?.ajaxUrl || igny8_ajax.ajax_url, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - igny8ShowNotification(data.data.message, 'success'); - // Refresh table to show updated data - if (window.loadTableData && tableId) { - window.loadTableData(tableId); - } - } else { - igny8ShowNotification(data.data?.message || 'Failed to publish drafts', 'error'); - } - }) - .catch(error => { - console.error('Bulk publish error:', error); - igny8ShowNotification('Network error occurred while publishing', 'error'); - }); -} - -/** - * Send bulk action AJAX request - */ -function sendBulkAction(action, data, tableId) { - const formData = new FormData(); - formData.append('action', action); - formData.append('nonce', igny8_ajax.nonce); - - // Add data fields - Object.keys(data).forEach(key => { - if (Array.isArray(data[key])) { - data[key].forEach(value => { - formData.append(`${key}[]`, value); - }); - } else { - formData.append(key, data[key]); - } - }); - - fetch(igny8_ajax.ajax_url, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - igny8ShowNotification(data.data.message, 'success'); - - // Show workflow automation results if available - if (data.data.workflow_message) { - igny8ShowNotification(data.data.workflow_message, 'info'); - } - - // Refresh table to show updated data - loadTableData(tableId, {}, 1); - } else { - igny8ShowNotification(data.data || 'Action failed', 'error'); - } - }) - .catch(error => { - igny8ShowNotification('Network error occurred', 'error'); - }); -} - -/** - * Send row action AJAX request - */ -function sendRowAction(action, data, tableId, callback) { - const formData = new FormData(); - formData.append('action', action); - formData.append('nonce', igny8_ajax.nonce); - - // Add data fields - Object.keys(data).forEach(key => { - formData.append(key, data[key]); - }); - - fetch(igny8_ajax.ajax_url, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - if (callback) { - callback(data); - } else { - igny8ShowNotification(data.data.message, 'success'); - - // Show workflow automation results if available - if (data.data.workflow_message) { - igny8ShowNotification(data.data.workflow_message, 'info'); - } - - // Refresh table to show updated data - if (tableId) { - loadTableData(tableId, {}, 1); - } - } - } else { - igny8ShowNotification(data.data || 'Action failed', 'error'); - } - }) - .catch(error => { - igny8ShowNotification('Network error occurred', 'error'); - }); -} - -/** - * Show cluster keywords modal - */ -function showClusterKeywordsModal(clusterName, keywords) { - // Remove existing modal if present - document.getElementById('cluster-keywords-modal')?.remove(); - - const modal = document.createElement('div'); - modal.id = 'cluster-keywords-modal'; - modal.className = 'igny8-modal'; - modal.innerHTML = ` -
      -
      -

      Keywords in "${clusterName}"

      - -
      -
      - ${keywords.length > 0 ? - `
        ${keywords.map(k => `
      • ${k.keyword} (${k.search_volume} vol, ${k.difficulty} diff)
      • `).join('')}
      ` : - '

      No keywords found in this cluster.

      ' - } -
      - -
      - `; - - document.body.appendChild(modal); - modal.classList.add('open'); -} - -/* ========================================= - Igny8 Pagination Component - ========================================= */ - -function initializePagination(tableId) { - const container = document.querySelector(`[data-table="${tableId}"].igny8-pagination`); - if (!container) { - return; - } - - // --- Page Navigation --- - container.addEventListener('click', e => { - const btn = e.target.closest('.igny8-page-btn'); - if (!btn) return; - - const page = parseInt(btn.dataset.page); - if (!page || page === getCurrentPage(tableId)) return; - - const filters = collectFilters(tableId); - const perPage = getSessionPerPage(tableId) || getDefaultPerPage(); - loadTableData(tableId, filters, page, perPage); - }); - - // --- Per-Page Selection --- - const perPageSelect = document.querySelector(`#${tableId} .igny8-per-page-select`); - if (perPageSelect) { - perPageSelect.addEventListener('change', () => { - const perPage = parseInt(perPageSelect.value); - const filters = collectFilters(tableId); - filters.per_page = perPage; - loadTableData(tableId, filters, 1); - }); - } -} - -function getCurrentPage(tableId) { - return parseInt(document - .querySelector(`[data-table="${tableId}"].igny8-pagination`) - ?.dataset.currentPage || 1); -} - -function loadPage(tableId, page, perPage = null) { - const filters = collectFilters(tableId); - filters.per_page = perPage || getCurrentPerPage(tableId); - loadTableData(tableId, filters, page); -} - -function getCurrentPerPage(tableId) { - return parseInt(document.querySelector(`#${tableId} .igny8-per-page-select`)?.value || 10); -} - -/** - * Update pagination controls - * @param {string} tableId - * @param {Object} p - Pagination data - */ -function updatePagination(tableId, p) { - const container = document.querySelector(`[data-table="${tableId}"].igny8-pagination`); - if (!container) return; - - container.dataset.currentPage = p.current_page || 1; - container.dataset.totalItems = p.total_items || 0; - container.dataset.perPage = p.per_page || 10; - - container.innerHTML = ''; - - if (!p.total_pages) return; - - const { current_page, total_pages, total_items, per_page } = p; - - // --- Prev Button --- - if (current_page > 1) addPageBtn(container, '‹ Previous', current_page - 1); - - // --- First Pages --- - for (let i = 1; i <= Math.min(2, total_pages); i++) - addPageBtn(container, i, i, i === current_page); - - // --- Ellipsis before middle --- - if (total_pages > 4 && current_page > 3) addEllipsis(container); - - // --- Middle Page --- - if (current_page > 2 && current_page < total_pages - 1) - addPageBtn(container, current_page, current_page, true); - - // --- Ellipsis before last pages --- - if (total_pages > 4 && current_page < total_pages - 2) addEllipsis(container); - - // --- Last Pages --- - for (let i = Math.max(3, total_pages - 1); i <= total_pages; i++) - if (i > 2) addPageBtn(container, i, i, i === current_page); - - // --- Next Button --- - if (current_page < total_pages) addPageBtn(container, 'Next ›', current_page + 1); - - // --- Info Text --- - const start = (current_page - 1) * per_page + 1; - const end = Math.min(current_page * per_page, total_items); - const info = document.createElement('span'); - info.textContent = `Showing ${start}-${end} of ${total_items} items`; - Object.assign(info.style, { marginLeft: '12px', fontSize: '12px', color: '#666' }); - container.appendChild(info); -} - -// --- Helpers --- -function addPageBtn(container, label, page, isActive = false) { - const btn = document.createElement('button'); - btn.textContent = label; - btn.dataset.page = page; - btn.className = `igny8-btn igny8-btn-sm igny8-page-btn ${isActive ? 'igny8-btn-primary' : 'igny8-btn-outline'}`; - container.appendChild(btn); -} - -function addEllipsis(container) { - const span = document.createElement('span'); - span.textContent = '...'; - Object.assign(span.style, { margin: '0 8px', color: '#666' }); - container.appendChild(span); -} - -/* ========================================= - Igny8 Delete Dialog & Notifications - ========================================= */ - -function igny8ShowDeleteDialog(type, records) { - // Remove existing modal if present - document.getElementById('igny8-delete-modal')?.remove(); - - const modal = document.createElement('div'); - modal.id = 'igny8-delete-modal'; - modal.className = 'igny8-modal'; - - const headerTitle = type === 'single' ? 'Delete Record' : 'Delete Multiple Records'; - const bodyHTML = type === 'single' - ? getSingleDeleteBody(records[0]) - : getBulkDeleteBody(records); - - modal.innerHTML = ` -
      -
      -

      ${headerTitle}

      - -
      -
      ${bodyHTML}
      - -
      `; - - document.body.appendChild(modal); - modal.classList.add('open'); - - // Store delete context - window.igny8DeleteRecords = records; - window.igny8DeleteType = type; -} - -// ---- Helper to build single-record body ---- -function getSingleDeleteBody(record) { - const title = record.keyword || record.name || record.idea_title || 'Unknown'; - return ` -

      Are you sure you want to delete this record?

      -

      ${title}

      -

      This action cannot be undone.

      `; -} - -// ---- Helper to build bulk delete body ---- -function getBulkDeleteBody(records) { - const total = records.length; - const previewCount = Math.min(5, total); - const remaining = total - previewCount; - - const previewItems = records.slice(0, previewCount) - .map(r => `
    1. ${r.keyword || r.name || r.idea_title || 'Unknown'}
    2. `) - .join(''); - - const moreText = remaining > 0 ? `
    3. ... and ${remaining} more records
    4. ` : ''; - - return ` -

      Are you sure you want to delete ${total} records?

      -
        ${previewItems}${moreText}
      -

      This action cannot be undone.

      `; -} - -// ---- Confirm Deletion ---- -async function igny8ConfirmDelete() { - const { igny8DeleteRecords: records, igny8DeleteType: type } = window; - if (!records || !type) return; - - const tableId = records[0].table_id || 'planner_keywords'; - - try { - const formData = new FormData(); - formData.append('nonce', igny8_ajax.nonce); - formData.append('table_id', tableId); - - if (type === 'single') { - formData.append('action', 'igny8_delete_single_record'); - formData.append('record_id', records[0].id); - } else { - formData.append('action', 'igny8_delete_bulk_records'); - records.forEach(r => formData.append('record_ids[]', r.id)); - } - - const res = await fetch(igny8_ajax.ajax_url, { method: 'POST', body: formData }); - const data = await res.json(); - - if (data.success) { - igny8ShowNotification(data.data.message || 'Record(s) deleted successfully', 'success'); - igny8CancelDelete(); - loadTableData(tableId, {}, 1); - if (type === 'bulk') updateBulkActionStates(tableId); - } else { - igny8ShowNotification(data.data || 'Delete failed', 'error'); - } - } catch (err) { - igny8ShowNotification('Delete failed due to a server error.', 'error'); - } -} - -// ---- Cancel & Close Modal ---- -function igny8CancelDelete() { - document.getElementById('igny8-delete-modal')?.remove(); - window.igny8DeleteRecords = null; - window.igny8DeleteType = null; -} - -// ---- Universal Bulk Action State Update ---- -function updateBulkActionStates(tableId) { - document.querySelectorAll(`[data-table="${tableId}"] .igny8-checkbox`) - .forEach(cb => cb.checked = false); - - const selectAll = document.querySelector(`[data-table="${tableId}"] .igny8-select-all`); - if (selectAll) { - selectAll.checked = false; - selectAll.indeterminate = false; - } - - const deleteBtn = document.getElementById(`${tableId}_delete_btn`); - const exportBtn = document.getElementById(`${tableId}_export_btn`); - if (deleteBtn) deleteBtn.disabled = true; - if (exportBtn) exportBtn.disabled = true; - - const countDisplay = document.querySelector(`[data-table="${tableId}"] .igny8-selected-count`); - if (countDisplay) countDisplay.textContent = '0 selected'; -} - -// ---- Unified Notification System ---- -function igny8ShowNotification(message, type = 'info', tableId = null) { - // Use the unified global notification system - igny8GlobalNotification(message, type); -} - -// =================================================================== -// TABLE COMPONENT -// =================================================================== - -/* ---------------------------- - Table Body Update ------------------------------ */ -function updateTableBody(tableId, tableBodyHtml) { - const tbody = document.querySelector(`#table-${tableId}-body`); - if (!tbody) return; - - tbody.innerHTML = ''; - - if (tableBodyHtml) { - const template = document.createElement('template'); - template.innerHTML = tableBodyHtml; - - template.content.querySelectorAll('tr').forEach(row => { - tbody.appendChild(row.cloneNode(true)); - }); - } -} - -/* ---------------------------- - Loading State ------------------------------ */ -function showTableLoadingState(tableId) { - const tbody = document.querySelector(`#table-${tableId}-body`); - if (!tbody) return; - - tbody.innerHTML = ''; - const loadingRow = document.createElement('tr'); - loadingRow.innerHTML = `Loading...`; - tbody.appendChild(loadingRow); -} - -/* ---------------------------- - Detect Current Table ------------------------------ */ -function detectCurrentTableId() { - return ( - document.querySelector('.igny8-table[data-table]')?.dataset.table || - document.querySelector('.igny8-filters[data-table]')?.dataset.table || - document.querySelector('.igny8-pagination[data-table]')?.dataset.table || - null - ); -} - -/* ---------------------------- - Unified AJAX Loader ------------------------------ */ -async function loadTableData(tableId, filters = {}, page = 1, perPage = null) { - try { - showTableLoadingState(tableId); - - const recordsPerPage = perPage || getSessionPerPage(tableId) || getDefaultPerPage(); - const body = new URLSearchParams({ - action: 'igny8_get_table_data', - nonce: igny8_ajax.nonce, - table: tableId, - filters: JSON.stringify(filters), - page, - per_page: recordsPerPage - }); - - const response = await fetch(igny8_ajax.ajax_url, { method: 'POST', body }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - - const data = await response.json(); - if (!data.success) throw new Error(data.data || 'Unknown error'); - - // Update DOM - if (data.data.table_body_html) updateTableBody(tableId, data.data.table_body_html); - if (data.data.pagination) updatePagination(tableId, data.data.pagination); - } catch (err) { - igny8ShowNotification('Failed to load table data', 'error'); - } -} - -/* ---------------------------- - Universal Table Initialization ------------------------------ */ -function initializeTableWithAJAX(tableId, module, submodule) { - initializeFilters(); - initializeTableActions(tableId); - initializePagination(tableId); - initializeTableSelection(tableId); - loadTableData(tableId, {}, 1); -} - -/* ---------------------------- - Row Selection Handling ------------------------------ */ -function initializeTableSelection(tableId) { - const table = document.getElementById(tableId); - if (!table) return; - - const selectAll = table.querySelector('thead input[type="checkbox"]'); - - if (selectAll) { - selectAll.addEventListener('change', () => { - document.querySelectorAll(`#table-${tableId}-body input[type="checkbox"]`).forEach(cb => { - cb.checked = selectAll.checked; - }); - dispatchSelectionChange(tableId); - }); - } - - table.addEventListener('change', e => { - if (e.target.matches('#table-' + tableId + '-body input[type="checkbox"]')) { - const all = document.querySelectorAll(`#table-${tableId}-body input[type="checkbox"]`); - const checked = document.querySelectorAll(`#table-${tableId}-body input[type="checkbox"]:checked`); - - if (selectAll) { - selectAll.checked = checked.length === all.length; - selectAll.indeterminate = checked.length > 0 && checked.length < all.length; - } - dispatchSelectionChange(tableId); - } - }); -} - -function dispatchSelectionChange(tableId) { - document.dispatchEvent(new CustomEvent('rowSelectionChanged', { detail: { tableId } })); -} - -/* ---------------------------- - Universal Row Data Extraction ------------------------------ */ -function getUniversalRowData(tableId, rowId) { - const row = document.querySelector(`[data-table="${tableId}"] tr[data-id="${rowId}"]`); - if (!row) return null; - - const headers = row.closest('table').querySelectorAll('thead th'); - const cells = row.querySelectorAll('td'); - const record = { id: rowId, table_id: tableId }; - - // Map headers to cell values (skip checkbox + actions) - for (let i = 1; i < headers.length - 1; i++) { - const field = headers[i].textContent.trim().toLowerCase().replace(/\s+/g, '_'); - record[field] = cells[i]?.textContent.trim() || ''; - } - - if (tableId === 'planner_keywords') { - const map = { volume: 'search_volume', cluster: 'cluster_id' }; - return Object.keys(record).reduce((acc, key) => { - acc[map[key] || key] = record[key]; - return acc; - }, { id: rowId, table_id: tableId }); - } - return record; -} - -/* ---------------------------- - Per-Page Handling ------------------------------ */ -function initializePerPageSelectors() { - const defaultPP = getDefaultPerPage(); - - document.querySelectorAll('.igny8-per-page-select').forEach(select => { - const tableId = select.dataset.table; - select.value = getSessionPerPage(tableId) || defaultPP; - - select.addEventListener('change', e => { - const perPage = parseInt(e.target.value); - setSessionPerPage(tableId, perPage); - loadTableData(tableId, {}, 1, perPage); - }); - }); -} - -const getDefaultPerPage = () => 20; -const getSessionPerPage = id => sessionStorage.getItem(`igny8_per_page_${id}`); -const setSessionPerPage = (id, val) => sessionStorage.setItem(`igny8_per_page_${id}`, val); - -/* ---------------------------- - Prompts Functionality ------------------------------ */ -window.initializePromptsFunctionality = function() { - // Only initialize if we're on the planner home page - if (!window.IGNY8_PAGE || window.IGNY8_PAGE.module !== 'planner' || window.IGNY8_PAGE.submodule !== 'home') { - return; - } - - const savePromptsBtn = document.getElementById('igny8-save-prompts'); - const resetPromptsBtn = document.getElementById('igny8-reset-prompts'); - - if (savePromptsBtn) { - savePromptsBtn.addEventListener('click', function() { - const formData = new FormData(); - formData.append('action', 'igny8_save_ai_prompts'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - // Get prompt values - const clusteringPrompt = document.querySelector('textarea[name="igny8_clustering_prompt"]').value; - const ideasPrompt = document.querySelector('textarea[name="igny8_ideas_prompt"]').value; - - formData.append('igny8_clustering_prompt', clusteringPrompt); - formData.append('igny8_ideas_prompt', ideasPrompt); - - // Show loading state - const originalText = savePromptsBtn.textContent; - savePromptsBtn.textContent = 'Saving...'; - savePromptsBtn.disabled = true; - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - igny8GlobalNotification('Prompts saved successfully!', 'success'); - } else { - const errorMsg = data.data?.message || 'Error saving prompts'; - igny8GlobalNotification(errorMsg, 'error'); - } - }) - .catch(error => { - console.error('Error saving prompts:', error); - igny8GlobalNotification('Error saving prompts. Please try again.', 'error'); - }) - .finally(() => { - // Reset button state - savePromptsBtn.textContent = originalText; - savePromptsBtn.disabled = false; - }); - }); - } - - if (resetPromptsBtn) { - resetPromptsBtn.addEventListener('click', function() { - if (confirm('Are you sure you want to reset all prompts to their default values? This action cannot be undone.')) { - const formData = new FormData(); - formData.append('action', 'igny8_reset_ai_prompts'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - // Show loading state - const originalText = resetPromptsBtn.textContent; - resetPromptsBtn.textContent = 'Resetting...'; - resetPromptsBtn.disabled = true; - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Reload the page to show default prompts - window.location.reload(); - } else { - const errorMsg = data.data?.message || 'Error resetting prompts'; - igny8GlobalNotification(errorMsg, 'error'); - } - }) - .catch(error => { - console.error('Error resetting prompts:', error); - igny8GlobalNotification('Error resetting prompts. Please try again.', 'error'); - }) - .finally(() => { - // Reset button state - resetPromptsBtn.textContent = originalText; - resetPromptsBtn.disabled = false; - }); - } - }); - } -} - -/* ---------------------------- - DOM Ready ------------------------------ */ -document.addEventListener('DOMContentLoaded', () => { - initializePerPageSelectors(); - - // Initialize planner settings - initializePlannerSettings(); - - // Initialize AI integration form - initializeAIIntegrationForm(); - - // Initialize AI action buttons - initializeAIActionButtons(); - - // Initialize prompts functionality - initializePromptsFunctionality(); - - // Initialize Writer AI settings - initializeWriterAISettings(); - - // Only initialize table functionality on submodule pages that have tableId - if (typeof IGNY8_PAGE !== 'undefined' && IGNY8_PAGE.submodule && IGNY8_PAGE.tableId) { - initializeTableWithAJAX(IGNY8_PAGE.tableId, IGNY8_PAGE.module, IGNY8_PAGE.submodule); - } - // No fallback initialization - tables should only be initialized on submodule pages - - // Initialize all delegated events in one place to prevent conflicts - initializeDelegatedEvents(); - - // Initialize personalization functionality - initializePersonalization(); -}); - -// =================================================================== -// Form functionality -// =================================================================== - -/** - * Client-side validation function for form data - * Performs lightweight validation before AJAX submission - * - * @param {HTMLElement} formRow The form row element - * @param {string} tableId The table ID for validation rules - * @returns {Object} Validation result with valid boolean and error message - */ -function igny8ValidateFormData(formRow, tableId) { - // Define validation rules for each table - const validationRules = { - 'planner_keywords': { - 'keyword': { required: true, maxLength: 255, noHtml: true }, - 'search_volume': { required: false, type: 'numeric', min: 0 }, - 'difficulty': { required: false, type: 'numeric_or_text', min: 0, max: 100, textOptions: ['Very Easy', 'Easy', 'Medium', 'Hard', 'Very Hard'] }, - 'cpc': { required: false, type: 'decimal', min: 0 }, - 'intent': { required: false, enum: ['informational', 'navigational', 'transactional', 'commercial'] }, - 'status': { required: true, enum: ['unmapped', 'mapped', 'queued', 'published'] }, - 'cluster_id': { required: false, type: 'integer' } - }, - 'planner_clusters': { - 'cluster_name': { required: true, maxLength: 255, noHtml: true }, - 'sector_id': { required: false, type: 'integer' }, - 'status': { required: true, enum: ['active', 'inactive', 'archived'] } - }, - 'planner_ideas': { - 'idea_title': { required: true, maxLength: 255, noHtml: true }, - 'idea_description': { required: false, noHtml: true }, - 'content_structure': { required: true, enum: ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] }, - 'content_type': { required: true, enum: ['post', 'product', 'page', 'CPT'] }, - 'keyword_cluster_id': { required: false, type: 'integer' }, - 'status': { required: true, enum: ['new', 'scheduled', 'published'] }, - 'estimated_word_count': { required: false, type: 'integer', min: 0 }, - 'target_keywords': { required: false, type: 'text', noHtml: false }, - 'mapped_post_id': { required: false, type: 'integer' } - }, - 'writer_tasks': { - 'title': { required: true, maxLength: 255, noHtml: true }, - 'description': { required: false, noHtml: true }, - 'status': { required: true, enum: ['pending', 'in_progress', 'completed', 'cancelled', 'draft', 'queued', 'review', 'published'] }, - 'priority': { required: true, enum: ['high', 'medium', 'low', 'urgent'] }, - 'content_type': { required: false, enum: ['blog_post', 'landing_page', 'product_page', 'guide_tutorial', 'news_article', 'review', 'comparison', 'email', 'social_media'] }, - 'cluster_id': { required: false, type: 'integer' }, - 'keywords': { required: false, type: 'text', noHtml: false }, - 'word_count': { required: false, type: 'integer', min: 0 }, - 'idea_id': { required: false, type: 'integer' }, - 'due_date': { required: false, type: 'datetime' }, - 'schedule_at': { required: false, type: 'datetime' }, - 'assigned_post_id': { required: false, type: 'integer' }, - 'ai_writer': { required: false, enum: ['ai', 'human'] } - }, - }; - - const rules = validationRules[tableId]; - if (!rules) { - return { valid: true }; // No validation rules defined, allow submission - } - - // Get form inputs - const inputs = formRow.querySelectorAll('input, textarea'); - const selects = formRow.querySelectorAll('.select-btn'); - - // Validate each field - for (const [fieldName, fieldRules] of Object.entries(rules)) { - let value = ''; - - // Get value from input or select - const input = Array.from(inputs).find(i => i.name === fieldName); - if (input) { - value = input.value.trim(); - } else { - const select = Array.from(selects).find(s => s.name === fieldName); - if (select) { - value = select.getAttribute('data-value') || ''; - } - } - - // Required field validation - if (fieldRules.required && (!value || value === '')) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} is required` }; - } - - // Skip further validation if field is empty and not required - if (!value || value === '') { - continue; - } - - // Length validation - if (fieldRules.maxLength && value.length > fieldRules.maxLength) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} cannot exceed ${fieldRules.maxLength} characters` }; - } - - // HTML content validation - if (fieldRules.noHtml && value !== value.replace(/<[^>]*>/g, '')) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} cannot contain HTML` }; - } - - // Numeric validation - if (fieldRules.type === 'numeric' || fieldRules.type === 'integer' || fieldRules.type === 'decimal' || fieldRules.type === 'numeric_or_text') { - let numValue; - - // Handle numeric_or_text type (like difficulty) - if (fieldRules.type === 'numeric_or_text') { - if (fieldRules.textOptions && fieldRules.textOptions.includes(value)) { - // Valid text option, convert to numeric for range validation - const difficultyMap = { - 'Very Easy': 10, - 'Easy': 30, - 'Medium': 50, - 'Hard': 70, - 'Very Hard': 90 - }; - numValue = difficultyMap[value] || 0; - } else if (isNaN(value) || value === '') { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} must be a number or valid difficulty level` }; - } else { - numValue = parseFloat(value); - } - } else { - if (isNaN(value) || value === '') { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} must be a number` }; - } - numValue = parseFloat(value); - } - - // Range validation - if (fieldRules.min !== undefined && numValue < fieldRules.min) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} must be at least ${fieldRules.min}` }; - } - - if (fieldRules.max !== undefined && numValue > fieldRules.max) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} must be at most ${fieldRules.max}` }; - } - - // Integer validation - if ((fieldRules.type === 'integer') && !Number.isInteger(numValue)) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} must be a whole number` }; - } - } - - // Enum validation - if (fieldRules.enum && !fieldRules.enum.includes(value)) { - return { valid: false, error: `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} must be one of: ${fieldRules.enum.join(', ')}` }; - } - } - - return { valid: true }; -} - -document.addEventListener('click', function (e) { - const saveBtn = e.target.closest('.igny8-form-save'); - const cancelBtn = e.target.closest('.igny8-form-cancel'); - - // Save action - if (saveBtn) { - const formRow = saveBtn.closest('tr.igny8-inline-form-row'); - if (!formRow) return; - - const tableId = saveBtn.dataset.tableId; - const nonce = saveBtn.dataset.nonce; - const mode = formRow.dataset.mode; - const recordId = formRow.dataset.id || ''; - - // Client-side validation before AJAX submit - const validationResult = igny8ValidateFormData(formRow, tableId); - if (!validationResult.valid) { - if (typeof igny8ShowNotification === 'function') { - igny8ShowNotification(validationResult.error, 'error'); - } - return; - } - - const formData = new FormData(); - formData.append('action', 'igny8_save_form_record'); - formData.append('nonce', nonce); - formData.append('table_id', tableId); - formData.append('action_type', mode); - if (recordId) formData.append('record_id', recordId); - - formRow.querySelectorAll('input, textarea').forEach(input => { - if (input.name && input.name !== 'record_id') { - formData.append(input.name, input.value); - } - }); - - formRow.querySelectorAll('.select-btn').forEach(btn => { - if (btn.name) formData.append(btn.name, btn.getAttribute('data-value') || ''); - }); - - fetch(ajaxurl, { method: 'POST', body: formData }) - .then(res => res.json()) - .then(result => { - if (result.success) { - if (typeof igny8ShowNotification === 'function') { - igny8ShowNotification('Record saved successfully!', 'success', tableId); - - // Show workflow automation results if available - if (result.data.workflow_message) { - igny8ShowNotification(result.data.workflow_message, 'info', tableId); - } - } - formRow.style.transition = 'all 0.3s ease-out'; - formRow.style.opacity = '0'; - formRow.style.transform = 'translateX(-20px)'; - setTimeout(() => { - formRow.remove(); - if (typeof loadTableData === 'function') { - loadTableData(tableId, {}, 1); - } else { - location.reload(); - } - }, 300); - } else { - igny8ShowNotification(`Error saving record: ${result.data?.message || result.data || 'Unknown error'}`, 'error', tableId); - } - }) - .catch(err => { - igny8ShowNotification(`Error saving record: ${err.message}`, 'error', tableId); - }); - } - - // Cancel action - if (cancelBtn) { - const formRow = cancelBtn.closest('tr.igny8-inline-form-row'); - if (!formRow) return; - formRow.style.transition = 'all 0.3s ease-out'; - formRow.style.opacity = '0'; - formRow.style.transform = 'translateX(20px)'; - setTimeout(() => formRow.remove(), 300); - } -}); - -/* ========================================= - Personalization Module Functionality - ========================================= */ - -/** - * Initialize personalization functionality - */ -function initializePersonalization() { - // Personalization click handlers moved to main delegated events handler - - // Handle auto mode initialization - const autoContainer = document.getElementById('igny8-auto-content'); - if (autoContainer) { - initializeAutoMode(autoContainer); - } - - // Handle inline mode initialization - const inlineContainer = document.getElementById('igny8-inline-form'); - if (inlineContainer) { - initializeInlineMode(inlineContainer); - } -} - -/** - * Handle personalization button click - */ -function handlePersonalizeClick(button) { - const ajaxUrl = button.dataset.ajaxUrl; - const postId = button.dataset.postId; - const formFields = button.dataset.formFields || ''; - - // Get all data attributes for context - const contextData = {}; - for (const [key, value] of Object.entries(button.dataset)) { - if (key !== 'ajaxUrl' && key !== 'postId' && key !== 'formFields') { - contextData[key] = value; - } - } - - // Build URL with context data - let url = `${ajaxUrl}?action=igny8_get_fields&post_id=${postId}`; - if (formFields) { - url += `&form_fields=${encodeURIComponent(formFields)}`; - } - - // Add nonce for security - if (window.igny8_ajax?.nonce) { - url += `&nonce=${encodeURIComponent(window.igny8_ajax.nonce)}`; - } - - // Add context data as query parameters - for (const [key, value] of Object.entries(contextData)) { - url += `&${key}=${encodeURIComponent(value)}`; - } - - // Show loading state - const originalContent = button.parentElement.innerHTML; - button.parentElement.innerHTML = '
      Loading personalization form...
      '; - - // Load form fields - fetch(url, { - method: 'GET', - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - button.parentElement.innerHTML = data.data; - // Re-initialize form handlers for the new content - initializePersonalization(); - } else { - throw new Error(data.data || 'Failed to load form'); - } - }) - .catch(error => { - console.error('Error loading personalization form:', error); - button.parentElement.innerHTML = originalContent; - igny8ShowNotification('Error loading personalization form: ' + error.message, 'error'); - }); -} - -/** - * Handle personalization form submission - */ -function handlePersonalizeFormSubmit(form) { - const ajaxUrl = form.closest('[data-ajax-url]')?.dataset.ajaxUrl || - document.querySelector('#igny8-launch')?.dataset.ajaxUrl || - window.igny8_ajax?.ajax_url; - const postId = form.closest('[data-post-id]')?.dataset.postId || - document.querySelector('#igny8-launch')?.dataset.postId; - - if (!ajaxUrl || !postId) { - igny8ShowNotification('Missing configuration for personalization', 'error'); - return; - } - - // Collect form data - const formData = new FormData(); - formData.append('action', 'igny8_generate_custom'); - formData.append('nonce', window.igny8_ajax?.nonce || ''); - formData.append('post_id', postId); - - // Add all form fields - const inputs = form.querySelectorAll('input, select, textarea'); - inputs.forEach(input => { - if (input.name && input.name !== 'submit') { - formData.append(input.name, input.value); - } - }); - - // Show loading state - const outputContainer = document.getElementById('igny8-generated-content') || form.parentElement; - if (outputContainer) { - outputContainer.innerHTML = '
      Generating personalized content...
      '; - } - - // Submit form - fetch(ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - if (outputContainer) { - outputContainer.innerHTML = ` -
      -

      Your Personalized Content

      -
      - ${data.data} -
      -
      - -
      -
      - `; - } - igny8ShowNotification('Content personalized successfully!', 'success'); - } else { - throw new Error(data.data || 'Failed to generate content'); - } - }) - .catch(error => { - console.error('Error generating content:', error); - if (outputContainer) { - outputContainer.innerHTML = '
      Error generating personalized content: ' + error.message + '
      '; - } - igny8ShowNotification('Error generating personalized content', 'error'); - }); -} - -/** - * Handle save content button - */ -function handleSaveContent(button) { - const contentContainer = button.closest('.igny8-content-container'); - if (!contentContainer) { - igny8ShowNotification('No content to save', 'error'); - return; - } - - const content = contentContainer.querySelector('.igny8-final-content')?.innerHTML; - const postId = document.querySelector('#igny8-launch')?.dataset.postId || - document.querySelector('[data-post-id]')?.dataset.postId; - - if (!content || !postId) { - igny8ShowNotification('Missing content or post ID', 'error'); - return; - } - - // Get field inputs from the form - const form = document.getElementById('igny8-form'); - const fieldInputs = {}; - if (form) { - const inputs = form.querySelectorAll('input, select, textarea'); - inputs.forEach(input => { - if (input.name && input.name !== 'submit' && input.name !== 'PageContent') { - fieldInputs[input.name] = input.value; - } - }); - } - - // Show loading state - const originalText = button.innerHTML; - button.innerHTML = ' Saving...'; - button.disabled = true; - - // Save content - const formData = new FormData(); - formData.append('action', 'igny8_save_content_manual'); - formData.append('nonce', window.igny8_ajax?.nonce || ''); - formData.append('content', content); - formData.append('post_id', postId); - formData.append('field_inputs', JSON.stringify(fieldInputs)); - - fetch(window.igny8_ajax?.ajax_url || '/wp-admin/admin-ajax.php', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - igny8ShowNotification(data.data?.message || 'Content saved successfully!', 'success'); - // Update the content container with saved status - const statusElement = contentContainer.querySelector('.igny8-content-status'); - if (statusElement) { - statusElement.textContent = '✅ Content saved'; - } - } else { - igny8ShowNotification(data.data?.message || 'Error saving content', 'error'); - } - }) - .catch(error => { - console.error('Error saving content:', error); - igny8ShowNotification('Error saving content', 'error'); - }) - .finally(() => { - button.innerHTML = originalText; - button.disabled = false; - }); -} - -/** - * Initialize auto mode - */ -function initializeAutoMode(container) { - const ajaxUrl = container.dataset.ajaxUrl; - const postId = container.dataset.postId; - const formFields = container.dataset.formFields || ''; - - // Get all data attributes for context - const contextData = {}; - for (const [key, value] of Object.entries(container.dataset)) { - if (key !== 'ajaxUrl' && key !== 'postId' && key !== 'formFields') { - contextData[key] = value; - } - } - - // Build URL with context data - let url = `${ajaxUrl}?action=igny8_get_fields&post_id=${postId}`; - if (formFields) { - url += `&form_fields=${encodeURIComponent(formFields)}`; - } - - // Add context data as query parameters - for (const [key, value] of Object.entries(contextData)) { - url += `&${key}=${encodeURIComponent(value)}`; - } - - // Load form and auto-submit - fetch(url) - .then(response => response.text()) - .then(html => { - const formContainer = document.createElement('div'); - formContainer.innerHTML = html; - const form = formContainer.querySelector('#igny8-form'); - - if (form) { - // Auto-submit the form - setTimeout(() => { - form.dispatchEvent(new Event('submit')); - }, 1000); - } - }) - .catch(error => { - console.error('Error in auto mode:', error); - container.querySelector('.igny8-loading').textContent = 'Error loading personalization form'; - }); -} - -/** - * Initialize inline mode - */ -function initializeInlineMode(container) { - const ajaxUrl = document.querySelector('#igny8-launch')?.dataset.ajaxUrl || - window.igny8_ajax?.ajax_url; - const postId = document.querySelector('#igny8-launch')?.dataset.postId || - document.querySelector('[data-post-id]')?.dataset.postId; - const formFields = document.querySelector('#igny8-launch')?.dataset.formFields || ''; - - if (!ajaxUrl || !postId) { - console.error('Missing AJAX URL or post ID for inline mode'); - return; - } - - // Get all data attributes for context - const launchButton = document.querySelector('#igny8-launch'); - const contextData = {}; - if (launchButton) { - for (const [key, value] of Object.entries(launchButton.dataset)) { - if (key !== 'ajaxUrl' && key !== 'postId' && key !== 'formFields') { - contextData[key] = value; - } - } - } - - // Build URL with context data - let url = `${ajaxUrl}?action=igny8_get_fields&post_id=${postId}`; - if (formFields) { - url += `&form_fields=${encodeURIComponent(formFields)}`; - } - - // Add context data as query parameters - for (const [key, value] of Object.entries(contextData)) { - url += `&${key}=${encodeURIComponent(value)}`; - } - - // Load form fields - const formContainer = container.querySelector('#igny8-form-container'); - if (formContainer) { - fetch(url) - .then(response => response.text()) - .then(html => { - formContainer.innerHTML = html; - // Re-initialize form handlers for the new content - initializePersonalization(); - }) - .catch(error => { - console.error('Error loading inline form:', error); - formContainer.innerHTML = '

      Error loading form fields.

      '; - }); - } -} - -/** - * Global function for manual save (called from onclick) - */ -window.igny8_save_content_manual = function(button) { - handleSaveContent(button); -}; - -/** - * Initialize Writer AI Settings - */ -function initializeWriterAISettings() { - // Only initialize if we're on the writer home page - if (!window.IGNY8_PAGE || window.IGNY8_PAGE.module !== 'writer' || window.IGNY8_PAGE.submodule !== 'home') { - return; - } - - // Writer Mode Toggle - const writerModeRadios = document.querySelectorAll('input[name="igny8_writer_mode"]'); - writerModeRadios.forEach(radio => { - radio.addEventListener('change', function() { - const aiFeatures = document.getElementById('igny8-writer-ai-features'); - if (aiFeatures) { - aiFeatures.style.display = this.value === 'ai' ? 'block' : 'none'; - } - }); - }); - - // Save Writer AI Settings - const saveWriterAIBtn = document.getElementById('igny8-save-writer-ai-settings'); - if (saveWriterAIBtn) { - saveWriterAIBtn.addEventListener('click', function() { - saveWriterAISettings(); - }); - } - - // Save Content Prompt - const saveContentPromptBtn = document.getElementById('igny8-save-content-prompt'); - if (saveContentPromptBtn) { - saveContentPromptBtn.addEventListener('click', function() { - saveContentPrompt(); - }); - } - - // Reset Content Prompt - const resetContentPromptBtn = document.getElementById('igny8-reset-content-prompt'); - if (resetContentPromptBtn) { - resetContentPromptBtn.addEventListener('click', function() { - resetContentPrompt(); - }); - } - - // Save Content Decision button - const saveContentDecisionBtn = document.getElementById('igny8-save-content-decision'); - console.log('Save Content Decision button found:', saveContentDecisionBtn); - if (saveContentDecisionBtn) { - console.log('Adding click event listener to Save Content Decision button'); - saveContentDecisionBtn.addEventListener('click', function() { - console.log('Save Content Decision button clicked!'); - saveContentDecision(); - }); - } else { - console.log('Save Content Decision button NOT found!'); - } -} - -/** - * Save Content Decision - */ -function saveContentDecision() { - console.log('saveContentDecision function called'); - - const formData = new FormData(); - formData.append('action', 'igny8_save_new_content_decision'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - const newContentAction = document.querySelector('input[name="new_content_action"]:checked')?.value || 'draft'; - console.log('Selected content action:', newContentAction); - console.log('All radio buttons:', document.querySelectorAll('input[name="new_content_action"]')); - formData.append('new_content_action', newContentAction); - - console.log('Sending AJAX request...'); - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => { - console.log('Response received:', response); - return response.json(); - }) - .then(data => { - console.log('Response data:', data); - if (data.success) { - igny8ShowNotification('New content decision saved successfully', 'success'); - } else { - igny8ShowNotification(data.data?.message || 'Failed to save content decision', 'error'); - } - }) - .catch(error => { - console.error('Error saving content decision:', error); - igny8ShowNotification('Error saving content decision', 'error'); - }); -} - -/** - * Save Writer AI Settings - */ -function saveWriterAISettings() { - const formData = new FormData(); - formData.append('action', 'igny8_save_writer_ai_settings'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - const writerMode = document.querySelector('input[name="igny8_writer_mode"]:checked')?.value || 'manual'; - const contentGeneration = document.querySelector('input[name="igny8_content_generation"]:checked')?.value || 'enabled'; - - formData.append('igny8_writer_mode', writerMode); - formData.append('igny8_content_generation', contentGeneration); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - igny8GlobalNotification(data.data.message, 'success'); - } else { - igny8GlobalNotification(data.data?.message || 'Failed to save Writer AI settings', 'error'); - } - }) - .catch(error => { - console.error('Error saving Writer AI settings:', error); - igny8GlobalNotification('Error saving Writer AI settings', 'error'); - }); -} - -/** - * Save Content Generation Prompt - */ -function saveContentPrompt() { - const promptTextarea = document.querySelector('textarea[name="igny8_content_generation_prompt"]'); - if (!promptTextarea) return; - - const formData = new FormData(); - formData.append('action', 'igny8_save_content_prompt'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('igny8_content_generation_prompt', promptTextarea.value); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - igny8GlobalNotification(data.data.message, 'success'); - } else { - igny8GlobalNotification(data.data?.message || 'Failed to save content prompt', 'error'); - } - }) - .catch(error => { - console.error('Error saving content prompt:', error); - igny8GlobalNotification('Error saving content prompt', 'error'); - }); -} - -/** - * Reset Content Generation Prompt - */ -function resetContentPrompt() { - if (confirm('Are you sure you want to reset the content generation prompt to default?')) { - // This would need to be implemented with a default prompt endpoint - igny8GlobalNotification('Reset to default functionality coming soon', 'info'); - } -} - -// Personalization initialization moved to main DOMContentLoaded handler - -// ========================================= -// Import/Export Functionality -// ========================================= - -window.initializeImportExport = function() { - console.log('Initializing Import/Export functionality...'); - - // Only initialize if we're on the import-export page - if (!window.IGNY8_IMPORT_EXPORT) { - console.log('IGNY8_IMPORT_EXPORT not found, skipping initialization'); - return; - } - - console.log('IGNY8_IMPORT_EXPORT found:', window.IGNY8_IMPORT_EXPORT); - - const importForm = document.getElementById('igny8-import-form'); - const exportForm = document.getElementById('igny8-export-form'); - const settingsForm = document.getElementById('igny8-settings-form'); - const downloadTemplateBtns = document.querySelectorAll('.download-template'); - - console.log('Forms found:', { - importForm: !!importForm, - exportForm: !!exportForm, - settingsForm: !!settingsForm, - downloadTemplateBtns: downloadTemplateBtns.length - }); - - // Template download handlers - downloadTemplateBtns.forEach(btn => { - btn.addEventListener('click', function() { - const templateType = this.getAttribute('data-type'); - downloadTemplate(templateType); - }); - }); - - // Import form handler - if (importForm) { - importForm.addEventListener('submit', function(e) { - if (!runImport()) { - e.preventDefault(); - } - }); - } - - // Export form handler - if (exportForm) { - exportForm.addEventListener('submit', function(e) { - if (!runExport()) { - e.preventDefault(); - } - }); - } - - // Settings form handler - if (settingsForm) { - settingsForm.addEventListener('submit', function(e) { - if (!saveImportExportSettings()) { - e.preventDefault(); - } - }); - } -}; - -// Download CSV template -function downloadTemplate(templateType) { - console.log('Downloading template:', templateType); - - // Create a form to submit to the AJAX handler - const form = document.createElement('form'); - form.method = 'POST'; - form.action = window.IGNY8_IMPORT_EXPORT.ajaxUrl; - form.style.display = 'none'; - - // Add form fields - const actionInput = document.createElement('input'); - actionInput.type = 'hidden'; - actionInput.name = 'action'; - actionInput.value = 'igny8_download_template'; - form.appendChild(actionInput); - - const nonceInput = document.createElement('input'); - nonceInput.type = 'hidden'; - nonceInput.name = 'nonce'; - nonceInput.value = window.IGNY8_IMPORT_EXPORT.nonce; - form.appendChild(nonceInput); - - const tableIdInput = document.createElement('input'); - tableIdInput.type = 'hidden'; - tableIdInput.name = 'table_id'; - tableIdInput.value = 'planner_' + templateType; - form.appendChild(tableIdInput); - - // Add form to document and submit - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); - - console.log('Template download initiated'); - igny8GlobalNotification('Downloading template...', 'info'); -} - -// Run CSV import -function runImport() { - console.log('Starting import process...'); - - const importForm = document.getElementById('igny8-import-form'); - const importFile = document.getElementById('import-file'); - const importType = document.getElementById('import-type'); - const autoCluster = document.getElementById('auto-cluster-import'); - const resultsDiv = document.getElementById('import-results'); - const submitBtn = importForm.querySelector('button[type="submit"]'); - - console.log('Import form elements found:', { - importForm: !!importForm, - importFile: !!importFile, - importType: !!importType, - autoCluster: !!autoCluster, - resultsDiv: !!resultsDiv, - submitBtn: !!submitBtn - }); - - if (!importFile.files.length) { - console.log('No file selected'); - igny8GlobalNotification('Please select a CSV file to import', 'error'); - return false; - } - - if (!importType.value) { - console.log('No import type selected'); - igny8GlobalNotification('Please select an import type', 'error'); - return false; - } - - console.log('Import validation passed, submitting form...'); - igny8GlobalNotification('Starting import process...', 'info'); - - // Let the form submit naturally - return true; - -} - -// Run CSV export -function runExport() { - console.log('Starting export process...'); - - const exportForm = document.getElementById('igny8-export-form'); - const exportType = document.getElementById('export-type'); - const includeMetrics = document.getElementById('include-metrics'); - const includeRelationships = document.getElementById('include-relationships'); - const includeTimestamps = document.getElementById('include-timestamps'); - const submitBtn = exportForm.querySelector('button[type="submit"]'); - - console.log('Export form elements found:', { - exportForm: !!exportForm, - exportType: !!exportType, - includeMetrics: !!includeMetrics, - includeRelationships: !!includeRelationships, - includeTimestamps: !!includeTimestamps, - submitBtn: !!submitBtn - }); - - if (!exportType.value) { - console.log('No export type selected'); - igny8GlobalNotification('Please select an export type', 'error'); - return false; - } - - console.log('Export validation passed, submitting form...'); - igny8GlobalNotification('Starting export process...', 'info'); - - // Let the form submit naturally - return true; -} - -// Save import/export settings -function saveImportExportSettings() { - console.log('Saving import/export settings...'); - igny8GlobalNotification('Saving settings...', 'info'); - - // Let the form submit naturally - return true; -} - -// Display import results -function displayImportResults(data, resultsDiv, success = true) { - if (!resultsDiv) return; - - let html = '
      '; - - html += '

      Import Results

      '; - html += `

      Status: ${data.message}

      `; - - if (data.imported !== undefined) { - html += `

      Imported: ${data.imported} records

      `; - } - - if (data.skipped !== undefined) { - html += `

      Skipped: ${data.skipped} records

      `; - } - - if (data.details) { - html += `

      Details: ${data.details}

      `; - } - - html += '
      '; - - resultsDiv.innerHTML = html; - resultsDiv.style.display = 'block'; -} - -// Display export results -function displayExportResults(exportType, success = true) { - const resultsDiv = document.getElementById('export-results'); - if (!resultsDiv) return; - - let html = '
      '; - - html += '

      Export Results

      '; - html += `

      Status: ${success ? 'Export completed successfully' : 'Export failed'}

      `; - html += `

      Type: ${exportType}

      `; - html += `

      File: igny8_export_${exportType}_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.csv

      `; - - html += '
      '; - - resultsDiv.innerHTML = html; - resultsDiv.style.display = 'block'; -} - -// =================================================================== -// IMPORT/EXPORT MODAL FUNCTIONALITY -// =================================================================== - -/** - * Show Import Modal - * - * @param {string} tableId The table ID for configuration - */ -function igny8ShowImportModal(tableId) { - // Remove existing modal if present - const existingModal = document.getElementById('igny8-import-export-modal'); - if (existingModal) { - existingModal.remove(); - } - - // Call PHP function to get modal HTML - const formData = new FormData(); - formData.append('action', 'igny8_get_import_modal'); - formData.append('nonce', window.igny8_ajax?.nonce || ''); - formData.append('table_id', tableId); - - fetch(window.igny8_ajax?.ajax_url || '/wp-admin/admin-ajax.php', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - document.body.insertAdjacentHTML('beforeend', result.data); - const modal = document.getElementById('igny8-import-export-modal'); - - // Set the nonce in the form after modal is created - const nonceInput = modal.querySelector('input[name="nonce"]'); - if (nonceInput && window.igny8_ajax?.nonce) { - nonceInput.value = window.igny8_ajax.nonce; - } - - modal.classList.add('open'); - } else { - igny8ShowNotification('Failed to load import modal', 'error'); - } - }) - .catch(error => { - igny8ShowNotification('Error loading import modal', 'error'); - }); -} - -/** - * Show Export Modal - * - * @param {string} tableId The table ID for configuration - * @param {Array} selectedIds Array of selected row IDs (for export selected) - */ -function igny8ShowExportModal(tableId, selectedIds = []) { - // Remove existing modal if present - const existingModal = document.getElementById('igny8-import-export-modal'); - if (existingModal) { - existingModal.remove(); - } - - // Call PHP function to get modal HTML - const formData = new FormData(); - formData.append('action', 'igny8_get_export_modal'); - formData.append('nonce', window.igny8_ajax?.nonce || ''); - formData.append('table_id', tableId); - formData.append('selected_ids', JSON.stringify(selectedIds)); - - fetch(window.igny8_ajax?.ajax_url || '/wp-admin/admin-ajax.php', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - document.body.insertAdjacentHTML('beforeend', result.data); - const modal = document.getElementById('igny8-import-export-modal'); - - // Set the nonce in the form after modal is created - const nonceInput = modal.querySelector('input[name="nonce"]'); - if (nonceInput && window.igny8_ajax?.nonce) { - nonceInput.value = window.igny8_ajax.nonce; - } - - modal.classList.add('open'); - } else { - igny8ShowNotification('Failed to load export modal', 'error'); - } - }) - .catch(error => { - igny8ShowNotification('Error loading export modal', 'error'); - }); -} - -/** - * Close Import/Export Modal - */ -function igny8CloseImportExportModal() { - const modal = document.getElementById('igny8-import-export-modal'); - if (modal) { - modal.remove(); - } -} - - -/** - * Submit Import Form - */ -async function igny8SubmitImportForm() { - const form = document.getElementById('igny8-modal-import-form'); - const fileInput = document.getElementById('import-file'); - - if (!fileInput.files.length) { - igny8ShowNotification('Please select a CSV file', 'error'); - return; - } - - const formData = new FormData(form); - formData.append('import_file', fileInput.files[0]); - - // Debug logging - console.log('Igny8 Import Debug - Nonce being sent:', window.igny8_ajax?.nonce); - console.log('Igny8 Import Debug - AJAX URL:', window.igny8_ajax?.ajax_url); - console.log('Igny8 Import Debug - Form data:', Object.fromEntries(formData.entries())); - - try { - const response = await fetch(window.igny8_ajax?.ajax_url || '/wp-admin/admin-ajax.php', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - if (result.success) { - igny8ShowNotification(result.data.message || 'Import completed successfully', 'success'); - igny8CloseImportExportModal(); - - // Get current table ID from the modal context or page - let currentTableId = window.igny8_current_table_id; - - // If not set globally, try to get from the modal - if (!currentTableId) { - const modal = document.getElementById('igny8-import-export-modal'); - if (modal) { - // Try to extract table ID from modal data attributes or other context - currentTableId = modal.dataset.tableId || 'planner_keywords'; - } else { - currentTableId = 'planner_keywords'; // fallback - } - } - - // Reload table data - if (typeof loadTableData === 'function') { - loadTableData(currentTableId, {}, 1); - } else if (typeof igny8LoadTableData === 'function') { - igny8LoadTableData(currentTableId, {}, 1); - } else { - // Fallback: reload the page - location.reload(); - } - } else { - const errorMessage = typeof result.data === 'object' ? - (result.data.message || JSON.stringify(result.data)) : - (result.data || 'Import failed'); - igny8ShowNotification(errorMessage, 'error'); - } - } catch (error) { - igny8ShowNotification('Import failed due to server error', 'error'); - } -} - -/** - * Submit Export Form - */ -async function igny8SubmitExportForm() { - const form = document.getElementById('igny8-modal-export-form'); - const includeMetrics = document.getElementById('include-metrics')?.checked || false; - const includeRelationships = document.getElementById('include-relationships')?.checked || false; - const includeTimestamps = document.getElementById('include-timestamps')?.checked || false; - - const formData = new FormData(form); - formData.append('include_metrics', includeMetrics ? '1' : '0'); - formData.append('include_relationships', includeRelationships ? '1' : '0'); - formData.append('include_timestamps', includeTimestamps ? '1' : '0'); - - // Debug logging - console.log('Igny8 Export Debug - Form data:', Object.fromEntries(formData.entries())); - console.log('Igny8 Export Debug - Action:', formData.get('action')); - console.log('Igny8 Export Debug - Export type:', formData.get('export_type')); - console.log('Igny8 Export Debug - Selected IDs:', formData.get('selected_ids')); - - try { - const response = await fetch(window.igny8_ajax?.ajax_url || '/wp-admin/admin-ajax.php', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - if (result.success) { - // Download the CSV file - const blob = new Blob([result.data.csv_content], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = result.data.filename || 'export.csv'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - - igny8ShowNotification(`Exported ${result.data.count} records successfully`, 'success'); - igny8CloseImportExportModal(); - } else { - const errorMessage = typeof result.data === 'object' ? - (result.data.message || JSON.stringify(result.data)) : - (result.data || 'Export failed'); - igny8ShowNotification(errorMessage, 'error'); - } - } catch (error) { - igny8ShowNotification('Export failed due to server error', 'error'); - } -} - -/** - * Download Template - */ -function igny8DownloadTemplate(tableId) { - // Create form for template download - const form = document.createElement('form'); - form.method = 'POST'; - form.action = window.igny8_ajax?.ajax_url || '/wp-admin/admin-ajax.php'; - form.style.display = 'none'; - - const actionInput = document.createElement('input'); - actionInput.type = 'hidden'; - actionInput.name = 'action'; - actionInput.value = 'igny8_download_template'; - form.appendChild(actionInput); - - const nonceInput = document.createElement('input'); - nonceInput.type = 'hidden'; - nonceInput.name = 'nonce'; - nonceInput.value = window.igny8_ajax?.nonce || ''; - form.appendChild(nonceInput); - - const typeInput = document.createElement('input'); - typeInput.type = 'hidden'; - typeInput.name = 'table_id'; - typeInput.value = tableId; - form.appendChild(typeInput); - - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); - - igny8ShowNotification('Template downloaded', 'success'); -} - - -// =================================================================== -// PROGRESS MODAL SYSTEM -// =================================================================== - -// Global progress modal instance -let currentProgressModal = null; - -// Show progress modal for AI operations -function showProgressModal(title, totalItems, itemType = 'items') { - // Remove existing modal if present - if (currentProgressModal) { - currentProgressModal.remove(); - } - - currentProgressModal = document.createElement('div'); - currentProgressModal.id = 'igny8-progress-modal'; - currentProgressModal.className = 'igny8-modal'; - currentProgressModal.setAttribute('data-item-type', itemType); - currentProgressModal.setAttribute('data-total', totalItems); - currentProgressModal.innerHTML = ` -
      -
      -

      ${title}

      -
      -
      -
      -
      - Starting... -
      -
      - Preparing to process ${totalItems} ${itemType} -
      -
      -
      -
      0%
      -
      -
      -
      -
      0 Completed
      -
      0 Processing
      -
      ${totalItems} Remaining
      -
      -
      -
      - `; - - document.body.appendChild(currentProgressModal); - currentProgressModal.classList.add('open'); - - return currentProgressModal; -} - -// Update progress modal with live stats -function updateProgressModal(current, total, status = 'processing', currentItem = '') { - if (!currentProgressModal) return; - - const itemType = currentProgressModal.getAttribute('data-item-type') || 'items'; - const progressText = currentProgressModal.querySelector('#progress-text'); - const progressSubtext = currentProgressModal.querySelector('#progress-subtext'); - const progressBar = currentProgressModal.querySelector('#progress-bar'); - const progressPercentage = currentProgressModal.querySelector('#progress-percentage'); - const completedCount = currentProgressModal.querySelector('#completed-count'); - const processingCount = currentProgressModal.querySelector('#processing-count'); - const remainingCount = currentProgressModal.querySelector('#remaining-count'); - const progressIcon = currentProgressModal.querySelector('#progress-icon'); - - const percentage = Math.round((current / total) * 100); - const remaining = Math.max(0, total - current); - - // Update main text - if (progressText) { - if (status === 'completed') { - progressText.textContent = `✓ Completed ${current} of ${total} ${itemType}`; - if (progressIcon) progressIcon.textContent = '✅'; - } else { - progressText.textContent = `Processing ${current} of ${total} ${itemType}`; - } - } - - // Update subtext - if (progressSubtext) { - if (currentItem) { - progressSubtext.textContent = `Current: ${currentItem}`; - } else if (status === 'completed') { - progressSubtext.textContent = `All ${itemType} processed successfully!`; - } else { - progressSubtext.textContent = `Working on ${itemType}...`; - } - } - - // Update progress bar - if (progressBar) { - progressBar.style.width = percentage + '%'; - } - - if (progressPercentage) { - progressPercentage.textContent = percentage + '%'; - } - - // Update stats - if (completedCount) completedCount.textContent = current; - if (processingCount) processingCount.textContent = status === 'processing' ? '1' : '0'; - if (remainingCount) remainingCount.textContent = remaining; -} - -// Show success modal -function showSuccessModal(title, completedCount, message = '') { - // Remove progress modal - if (currentProgressModal) { - currentProgressModal.remove(); - currentProgressModal = null; - } - - const modal = document.createElement('div'); - modal.id = 'igny8-success-modal'; - modal.className = 'igny8-modal'; - modal.innerHTML = ` -
      -
      -

      ${title}

      -
      -
      -
      -

      - Done! ${completedCount} items completed. -

      - ${message ? `

      ${message}

      ` : ''} -
      - -
      - `; - - document.body.appendChild(modal); - modal.classList.add('open'); -} - -// Close success modal -function closeSuccessModal() { - const modal = document.getElementById('igny8-success-modal'); - if (modal) { - modal.classList.remove('open'); - setTimeout(() => modal.remove(), 300); - } -} - -// =================================================================== -// SECTOR SELECTION ENFORCEMENT -// =================================================================== - -// Check sector selection before clustering -function checkSectorSelectionBeforeClustering(keywordIds) { - // Use existing AJAX call to get sector options - const formData = new FormData(); - formData.append('action', 'igny8_get_saved_sector_selection'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success && data.data && data.data.children && data.data.children.length > 0) { - // Sector is selected, proceed with clustering - processAIClustering(keywordIds); - } else { - // No sector selected, show modal using existing modal system - showSectorRequiredModal(); - } - }) - .catch(error => { - console.error('Error checking sector selection:', error); - igny8GlobalNotification('Error checking sector selection', 'error'); - }); -} - -// Show sector required modal using existing modal system -function showSectorRequiredModal() { - const modal = document.createElement('div'); - modal.className = 'igny8-modal'; - modal.innerHTML = ` -
      -
      -

      Sector Selection Required

      - -
      -
      -
      -

      You must select a Sector before performing Auto Clustering.

      -

      Please go to the Planner dashboard and select your sectors in the "Planner Settings" section.

      -
      - -
      - `; - - document.body.appendChild(modal); - modal.classList.add('open'); -} - -// Close sector required modal -function closeSectorRequiredModal() { - const modal = document.querySelector('.igny8-modal'); - if (modal) { - modal.classList.remove('open'); - setTimeout(() => modal.remove(), 300); - } -} - -// Go to planner settings -function goToPlannerSettings() { - closeSectorRequiredModal(); - window.location.href = window.location.origin + window.location.pathname + '?page=igny8-planner'; -} - -// =================================================================== -// CRON SCHEDULE SETTINGS MODAL -// =================================================================== - -// REMOVED: showCronScheduleModal() - Now handled by Smart Automation System -function showCronScheduleModal_DEPRECATED() { - const modal = document.createElement('div'); - modal.id = 'igny8-cron-schedule-modal'; - modal.className = 'igny8-modal'; - modal.innerHTML = ` -
      -
      -

      Cron Schedule Settings

      - -
      -
      -

      Use these URLs to trigger automation manually or set up external cron jobs. These URLs use wp-load.php structure and are secured with authentication keys.

      - -
      -
      -
      - Auto Cluster (Daily) - ${getAutomationStatus('auto_cluster_enabled')} -
      -
      - Loading... - -
      -
      - -
      -
      - Auto Generate Ideas (Hourly) - ${getAutomationStatus('auto_generate_ideas_enabled')} -
      -
      - Loading... - -
      -
      - -
      -
      - Auto Queue (Hourly) - ${getAutomationStatus('auto_queue_enabled')} -
      -
      - Loading... - -
      -
      -
      - -
      -

      Security Key

      -

      Your security key: ${getSecurityKey()}

      - -

      - Keep this key secure. It's required to trigger automation externally. The key is automatically generated and stored securely. URL structure: /wp-load.php?import_key=[KEY]&import_id=igny8_cron&action=[ACTION] -

      -
      -
      - -
      - `; - - document.body.appendChild(modal); - modal.classList.add('open'); - - // Populate URLs after modal is created to ensure key is available - setTimeout(() => { - const autoClusterUrl = getCronUrl('igny8_auto_cluster_cron'); - const autoIdeasUrl = getCronUrl('igny8_auto_generate_ideas_cron'); - const autoQueueUrl = getCronUrl('igny8_auto_queue_cron'); - - document.getElementById('cron-url-auto-cluster').textContent = autoClusterUrl; - document.getElementById('cron-url-auto-ideas').textContent = autoIdeasUrl; - document.getElementById('cron-url-auto-queue').textContent = autoQueueUrl; - - // Update copy button onclick handlers with actual URLs - const clusterCopyBtn = document.querySelector('#cron-url-auto-cluster').nextElementSibling; - const ideasCopyBtn = document.querySelector('#cron-url-auto-ideas').nextElementSibling; - const queueCopyBtn = document.querySelector('#cron-url-auto-queue').nextElementSibling; - - clusterCopyBtn.onclick = () => copyToClipboard(autoClusterUrl); - ideasCopyBtn.onclick = () => copyToClipboard(autoIdeasUrl); - queueCopyBtn.onclick = () => copyToClipboard(autoQueueUrl); - }, 100); -} - -// Close cron schedule modal -function closeCronScheduleModal() { - const modal = document.getElementById('igny8-cron-schedule-modal'); - if (modal) { - modal.classList.remove('open'); - setTimeout(() => modal.remove(), 300); - } -} - -// Get automation status -function getAutomationStatus(setting) { - const enabled = document.querySelector(`input[name="igny8_${setting}"]`)?.checked; - return enabled ? '● Enabled' : '● Disabled'; -} - -// Get cron URL - Updated for wp-load.php endpoint structure (v3.3.0) -function getCronUrl(action) { - const securityKey = getSecurityKey(); - - // Return null if no CRON key (page doesn't need CRON functionality) - if (!securityKey) { - return null; - } - - const baseUrl = window.location.origin; - const wpLoadPath = '/wp-load.php'; - - // Map internal action names to external action names - const actionMap = { - 'igny8_auto_cluster_cron': 'auto_cluster', - 'igny8_auto_generate_ideas_cron': 'auto_ideas', - 'igny8_auto_queue_cron': 'auto_queue', - 'igny8_auto_drafts_cron': 'auto_content', - 'igny8_auto_generate_content_cron': 'auto_content', - 'igny8_auto_publish_drafts_cron': 'auto_publish', - 'igny8_auto_optimizer_cron': 'auto_optimizer', - 'igny8_trigger_recalc': 'auto_recalc' - }; - - const externalAction = actionMap[action] || action; - const fullUrl = `${baseUrl}${wpLoadPath}?import_key=${securityKey}&import_id=igny8_cron&action=${externalAction}`; - - return fullUrl; -} - -// Get security key (retrieve from server) -function getSecurityKey() { - // Get the secure CRON key from server-side localization - const key = window.IGNY8_PAGE?.cronKey; - - // Return null if no CRON key (page doesn't need CRON functionality) - if (!key || key === null) { - return null; - } - - return key; -} - -// Copy to clipboard -function copyToClipboard(text) { - navigator.clipboard.writeText(text).then(() => { - igny8GlobalNotification('URL copied to clipboard', 'success'); - }).catch(() => { - igny8GlobalNotification('Failed to copy URL', 'error'); - }); -} - -// Regenerate CRON key -function regenerateCronKey() { - if (confirm('Are you sure you want to regenerate the CRON key? This will invalidate all existing CRON URLs.')) { - // Show loading state - const button = event.target.closest('button'); - const originalText = button.innerHTML; - button.innerHTML = ' Regenerating...'; - button.disabled = true; - - // Make AJAX request to regenerate key - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - action: 'igny8_regenerate_cron_key', - nonce: window.IGNY8_PAGE.nonce - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // Update the key in the page data - window.IGNY8_PAGE.cronKey = data.data.new_key; - - // Update display elements - const keyDisplays = document.querySelectorAll('#cron-key-display, #writer-cron-key-display'); - keyDisplays.forEach(display => { - display.textContent = data.data.new_key; - }); - - // Update all CRON URLs in the modal - const urlElements = document.querySelectorAll('.igny8-cron-url code'); - urlElements.forEach(element => { - const currentUrl = element.textContent; - const newUrl = currentUrl.replace(/import_key=[^&]+/, `import_key=${data.data.new_key}`); - element.textContent = newUrl; - }); - - igny8GlobalNotification('CRON key regenerated successfully', 'success'); - } else { - igny8GlobalNotification('Failed to regenerate CRON key: ' + (data.data?.message || 'Unknown error'), 'error'); - } - }) - .catch(error => { - console.error('Error regenerating CRON key:', error); - igny8GlobalNotification('Failed to regenerate CRON key', 'error'); - }) - .finally(() => { - // Restore button state - button.innerHTML = originalText; - button.disabled = false; - }); - } -} - -// REMOVED: showWriterCronScheduleModal() - Now handled by Smart Automation System -function showWriterCronScheduleModal_DEPRECATED() { - const modal = document.createElement('div'); - modal.id = 'igny8-writer-cron-schedule-modal'; - modal.className = 'igny8-modal'; - modal.innerHTML = ` -
      -
      -

      Writer Cron Schedule Settings

      - -
      -
      -

      Use these URLs to trigger Writer automation manually or set up external cron jobs. These URLs use wp-load.php structure and are secured with authentication keys.

      - -
      -
      -
      - Generate Content (Hourly) - ${getWriterAutomationStatus('auto_generate_content_enabled')} -
      -
      - Loading... - -
      -
      - -
      -
      - Publish Drafts (Daily) - ${getWriterAutomationStatus('auto_publish_drafts_enabled')} -
      -
      - Loading... - -
      -
      -
      - -
      -

      Security Key

      -

      Your security key: ${getSecurityKey()}

      - -

      - Keep this key secure. It's required to trigger automation externally. The key is automatically generated and stored securely. URL structure: /wp-load.php?import_key=[KEY]&import_id=igny8_cron&action=[ACTION] -

      -
      -
      - -
      - `; - - document.body.appendChild(modal); - modal.classList.add('open'); - - // Populate URLs after modal is created to ensure key is available - setTimeout(() => { - const autoContentUrl = getCronUrl('igny8_auto_generate_content_cron'); - const autoPublishUrl = getCronUrl('igny8_auto_publish_drafts_cron'); - - document.getElementById('writer-cron-url-auto-content').textContent = autoContentUrl; - document.getElementById('writer-cron-url-auto-publish').textContent = autoPublishUrl; - - // Update copy button onclick handlers with actual URLs - const contentCopyBtn = document.querySelector('#writer-cron-url-auto-content').nextElementSibling; - const publishCopyBtn = document.querySelector('#writer-cron-url-auto-publish').nextElementSibling; - - contentCopyBtn.onclick = () => copyToClipboard(autoContentUrl); - publishCopyBtn.onclick = () => copyToClipboard(autoPublishUrl); - }, 100); -} - -// Close Writer cron schedule modal -function closeWriterCronScheduleModal() { - const modal = document.getElementById('igny8-writer-cron-schedule-modal'); - if (modal) { - modal.classList.remove('open'); - setTimeout(() => modal.remove(), 300); - } -} - -// Get Writer automation status -function getWriterAutomationStatus(setting) { - const enabled = document.querySelector(`input[name="igny8_${setting}"]`)?.checked; - return enabled ? '● Enabled' : '● Disabled'; -} - -// =================================================================== -// SMART AUTOMATION - RUN NOW AJAX FUNCTIONALITY -// =================================================================== - -/** - * Handle Run Now button clicks for cron jobs - */ -function handleRunNowClick(event) { - event.preventDefault(); - - const button = event.target.closest('button[name="manual_run"]'); - if (!button) return; - - const form = button.closest('form'); - const hook = form.querySelector('input[name="hook"]').value; - const originalText = button.innerHTML; - - // Show loading state - button.disabled = true; - button.innerHTML = ' Running...'; - - // Make AJAX request - jQuery.ajax({ - url: ajaxurl, - type: 'POST', - data: { - action: 'igny8_cron_manual_run', - hook: hook, - nonce: jQuery('#_wpnonce').val() - }, - success: function(response) { - if (response.success) { - // Show success message - igny8GlobalNotification('Job executed successfully: ' + response.data.message, 'success'); - - // Refresh the page after a short delay to show updated status - setTimeout(function() { - location.reload(); - }, 1500); - } else { - igny8GlobalNotification('Error: ' + (response.data || 'Unknown error'), 'error'); - button.disabled = false; - button.innerHTML = originalText; - } - }, - error: function(xhr, status, error) { - igny8GlobalNotification('AJAX Error: ' + error, 'error'); - button.disabled = false; - button.innerHTML = originalText; - } - }); -} - -/** - * Show notification message - */ -function igny8GlobalNotification(message, type) { - const notification = document.createElement('div'); - notification.className = 'notice notice-' + (type === 'success' ? 'success' : 'error') + ' is-dismissible'; - notification.style.position = 'fixed'; - notification.style.top = '32px'; - notification.style.right = '20px'; - notification.style.zIndex = '9999'; - notification.style.maxWidth = '400px'; - notification.innerHTML = '

      ' + message + '

      '; - - document.body.appendChild(notification); - - // Auto-dismiss after 5 seconds - setTimeout(function() { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }, 5000); - - // Handle manual dismiss - notification.querySelector('.notice-dismiss').addEventListener('click', function() { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }); -} - -// Cron job click handlers moved to main delegated events handler - -/** - * Handle Run Now icon button click - */ - -/** - * Handle Open in New Window icon button click - */ -function handleOpenInNewWindow(button, hook) { - // Get the security key from the page - let securityKey = ''; - - // Try to find the security key in various locations on the page - const keyInput = document.querySelector('input[name="igny8_secure_cron_key"]'); - if (keyInput) { - securityKey = keyInput.value; - } else { - // Try to find it in a hidden field or data attribute - const keyElement = document.querySelector('[data-cron-key]'); - if (keyElement) { - securityKey = keyElement.getAttribute('data-cron-key'); - } else { - // Try to get it from the page content (if displayed) - const keyDisplay = document.querySelector('.igny8-cron-key-display'); - if (keyDisplay) { - securityKey = keyDisplay.textContent.trim(); - } - } - } - - // If still no key found, show error - if (!securityKey) { - igny8GlobalNotification('Security key not found. Please check the cron settings page.', 'error'); - return; - } - - // Map hook names to action names for the external URL - const actionMap = { - 'igny8_auto_cluster_cron': 'auto_cluster', - 'igny8_auto_generate_ideas_cron': 'auto_ideas', - 'igny8_auto_queue_cron': 'auto_queue', - 'igny8_auto_generate_content_cron': 'auto_content', - 'igny8_auto_generate_images_cron': 'auto_images', - 'igny8_auto_publish_drafts_cron': 'auto_publish', - 'igny8_auto_optimizer_cron': 'auto_optimizer', - 'igny8_auto_recalc_cron': 'auto_recalc', - 'igny8_health_check_cron': 'health_check' - }; - - const action = actionMap[hook] || 'master_scheduler'; - const baseUrl = window.location.origin; - const cronUrl = baseUrl + '/wp-load.php?import_key=' + encodeURIComponent(securityKey) + '&import_id=igny8_cron&action=' + action; - - // Open in new window - window.open(cronUrl, '_blank', 'width=800,height=600,scrollbars=yes,resizable=yes'); -} - -// Add CSS for spin animation -const style = document.createElement('style'); -style.textContent = ` - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } -`; -document.head.appendChild(style); - -// =================================================================== -// Dynamic Image Size Selector -// =================================================================== - -// Image size options for different providers -const imageSizeOptions = { - openai: [ - { value: '1024x768', label: 'Landscape – 1024 × 768', width: 1024, height: 768 }, - { value: '1024x1024', label: 'Square – 1024 × 1024', width: 1024, height: 1024 }, - { value: '720x1280', label: 'Social Portrait – 720 × 1280', width: 720, height: 1280 } - ], - runware: [ - { value: '1280x832', label: 'Landscape – 1280 × 832', width: 1280, height: 832 }, - { value: '1024x1024', label: 'Square – 1024 × 1024', width: 1024, height: 1024 }, - { value: '960x1280', label: 'Social Portrait – 960 × 1280', width: 960, height: 1280 } - ] -}; - -// Initialize image size selector when page loads -document.addEventListener('DOMContentLoaded', function() { - const providerSelect = document.getElementById('image_provider'); - const sizeSelect = document.getElementById('igny8_image_size_selector'); - const formatSelect = document.getElementById('igny8_image_format_selector'); - const dimensionsDisplay = document.getElementById('igny8-selected-dimensions'); - const formatDisplay = document.getElementById('igny8-selected-format'); - const widthInput = document.getElementById('image_width'); - const heightInput = document.getElementById('image_height'); - - if (providerSelect && sizeSelect && widthInput && heightInput) { - // Function to update size options based on provider - function updateSizeOptions() { - const selectedProvider = providerSelect.value; - const options = imageSizeOptions[selectedProvider] || imageSizeOptions.openai; - - // Clear existing options - sizeSelect.innerHTML = ''; - - // Add new options - options.forEach((option, index) => { - const optionElement = document.createElement('option'); - optionElement.value = option.value; - optionElement.textContent = option.label; - if (index === 0) optionElement.selected = true; // Select first option by default - sizeSelect.appendChild(optionElement); - }); - - // Update dimensions and hidden fields - updateDimensions(); - } - - // Function to update dimensions display and hidden fields - function updateDimensions() { - const selectedSize = sizeSelect.value; - const selectedProvider = providerSelect.value; - const options = imageSizeOptions[selectedProvider] || imageSizeOptions.openai; - const selectedOption = options.find(opt => opt.value === selectedSize); - - if (selectedOption) { - widthInput.value = selectedOption.width; - heightInput.value = selectedOption.height; - - if (dimensionsDisplay) { - dimensionsDisplay.textContent = `Selected: ${selectedOption.width} × ${selectedOption.height} (${selectedOption.label.split(' – ')[0]})`; - } - } - } - - // Function to update format display - function updateFormatDisplay() { - if (formatSelect && formatDisplay) { - const selectedFormat = formatSelect.value.toUpperCase(); - formatDisplay.textContent = `Selected format: ${selectedFormat}`; - } - } - - // Event listeners - providerSelect.addEventListener('change', updateSizeOptions); - sizeSelect.addEventListener('change', updateDimensions); - if (formatSelect) { - formatSelect.addEventListener('change', updateFormatDisplay); - } - - // Initialize on page load - updateSizeOptions(); - updateFormatDisplay(); - } -}); - -// =================================================================== -// Test Runware API Connection -// =================================================================== - -// Test Runware API Connection button handler -document.addEventListener('click', function(event) { - if (event.target && event.target.id === 'igny8-test-runware-btn') { - event.preventDefault(); - - const button = event.target; - const resultDiv = document.getElementById('igny8-runware-test-result'); - - // Disable button and show loading state - button.disabled = true; - button.textContent = 'Testing...'; - - // Clear previous results - if (resultDiv) { - resultDiv.innerHTML = ''; - } - - // Make AJAX request - fetch(igny8_ajax.ajax_url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - action: 'igny8_test_runware_connection', - nonce: igny8_ajax.nonce - }) - }) - .then(response => response.json()) - .then(data => { - // Re-enable button - button.disabled = false; - button.textContent = 'Test Runware Connection'; - - // Show result - if (resultDiv) { - if (data.success) { - resultDiv.innerHTML = '

      ' + data.data.message + '

      '; - } else { - resultDiv.innerHTML = '

      ' + data.data.message + '

      '; - } - } - }) - .catch(error => { - // Re-enable button - button.disabled = false; - button.textContent = 'Test Runware Connection'; - - // Show error - if (resultDiv) { - resultDiv.innerHTML = '

      ❌ Connection failed: Network error

      '; - } - - console.error('Runware API test error:', error); - }); - } -}); - -// =================================================================== -// DESCRIPTION TOGGLE FUNCTIONALITY -// =================================================================== - -// Handle description toggle clicks -document.addEventListener('click', function(e) { - const toggleBtn = e.target.closest('.igny8-description-toggle'); - if (toggleBtn) { - e.preventDefault(); - e.stopPropagation(); - - const rowId = toggleBtn.dataset.rowId; - const description = toggleBtn.dataset.description; - const tableRow = toggleBtn.closest('tr'); - - // Check if description row already exists - let descriptionRow = document.querySelector(`tr.igny8-description-row[data-parent-id="${rowId}"]`); - - if (descriptionRow && descriptionRow.classList.contains('expanded')) { - // Close existing description row - descriptionRow.classList.remove('expanded'); - setTimeout(() => { - if (!descriptionRow.classList.contains('expanded')) { - descriptionRow.remove(); - } - }, 300); - } else { - // Remove any existing description rows for this table - const existingRows = document.querySelectorAll(`tr.igny8-description-row[data-parent-id="${rowId}"]`); - existingRows.forEach(row => row.remove()); - - // Parse and format description (handle both JSON and plain text) - let formattedDescription = ''; - try { - // Try to parse as JSON first - const descriptionData = JSON.parse(description); - if (descriptionData && typeof descriptionData === 'object') { - formattedDescription = '
      '; - - // Handle H2 sections if they exist - if (descriptionData.H2 && Array.isArray(descriptionData.H2)) { - descriptionData.H2.forEach((section, index) => { - if (section.heading && section.content_type && section.details) { - formattedDescription += ` -
      -

      ${section.heading}

      -
      -
      ${section.content_type.replace('_', ' ').toUpperCase()}
      -
      ${section.details}
      -
      -
      - `; - } - }); - } else { - // If it's JSON but not the expected format, show as structured data - Object.keys(descriptionData).forEach(key => { - if (descriptionData[key]) { - const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - formattedDescription += `
      ${label}: ${descriptionData[key]}
      `; - } - }); - } - formattedDescription += '
      '; - } else { - formattedDescription = '
      Invalid description format
      '; - } - } catch (error) { - // If JSON parsing fails, treat as plain text - formattedDescription = ` -
      -
      ${description}
      -
      - `; - } - - // Create new description row - const newRow = document.createElement('tr'); - newRow.className = 'igny8-description-row expanded'; - newRow.setAttribute('data-parent-id', rowId); - - const cellCount = tableRow.cells.length; - newRow.innerHTML = ` - - ${formattedDescription} - - `; - - // Insert after the current row - tableRow.parentNode.insertBefore(newRow, tableRow.nextSibling); - } - } -}); - -// Handle image prompts toggle clicks -document.addEventListener('click', function(e) { - const toggleBtn = e.target.closest('.igny8-image-prompts-toggle'); - if (toggleBtn) { - e.preventDefault(); - e.stopPropagation(); - - const rowId = toggleBtn.dataset.rowId; - const imagePrompts = toggleBtn.dataset.imagePrompts; - const tableRow = toggleBtn.closest('tr'); - - // Check if image prompts row already exists - let imagePromptsRow = document.querySelector(`tr.igny8-image-prompts-row[data-parent-id="${rowId}"]`); - - if (imagePromptsRow && imagePromptsRow.classList.contains('expanded')) { - // Close existing image prompts row - imagePromptsRow.classList.remove('expanded'); - setTimeout(() => { - if (!imagePromptsRow.classList.contains('expanded')) { - imagePromptsRow.remove(); - } - }, 300); - } else { - // Remove any existing image prompts rows for this table - const existingRows = document.querySelectorAll(`tr.igny8-image-prompts-row[data-parent-id="${rowId}"]`); - existingRows.forEach(row => row.remove()); - - // Parse and format image prompts - let formattedPrompts = ''; - try { - if (!imagePrompts || imagePrompts.trim() === '') { - formattedPrompts = '
      No image prompts available
      '; - } else { - const prompts = JSON.parse(imagePrompts); - if (prompts && typeof prompts === 'object') { - formattedPrompts = '
      '; - const promptKeys = Object.keys(prompts); - - if (promptKeys.length === 0) { - formattedPrompts += '
      No prompts found in data
      '; - } else { - promptKeys.forEach(key => { - if (prompts[key] && prompts[key].trim() !== '') { - const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - formattedPrompts += `
      ${label}: ${prompts[key]}
      `; - } - }); - } - formattedPrompts += '
      '; - } else { - formattedPrompts = '
      Invalid prompts data format
      '; - } - } - } catch (error) { - console.error('Error parsing image prompts:', error); - formattedPrompts = '
      Error parsing image prompts: ' + error.message + '
      '; - } - - // Create new image prompts row - const newRow = document.createElement('tr'); - newRow.className = 'igny8-image-prompts-row expanded'; - newRow.setAttribute('data-parent-id', rowId); - - const cellCount = tableRow.cells.length; - newRow.innerHTML = ` - - ${formattedPrompts} - - `; - - // Insert after the current row - tableRow.parentNode.insertBefore(newRow, tableRow.nextSibling); - } - } -}); - -// Handle click outside to close description and image prompts rows -document.addEventListener('click', function(e) { - // Check if click is outside any description toggle button - if (!e.target.closest('.igny8-description-toggle') && !e.target.closest('.igny8-description-row')) { - // Close all expanded description rows - const expandedRows = document.querySelectorAll('.igny8-description-row.expanded'); - expandedRows.forEach(row => { - row.classList.remove('expanded'); - setTimeout(() => { - if (!row.classList.contains('expanded')) { - row.remove(); - } - }, 300); - }); - } - - // Check if click is outside any image prompts toggle button - if (!e.target.closest('.igny8-image-prompts-toggle') && !e.target.closest('.igny8-image-prompts-row')) { - // Close all expanded image prompts rows - const expandedImageRows = document.querySelectorAll('.igny8-image-prompts-row.expanded'); - expandedImageRows.forEach(row => { - row.classList.remove('expanded'); - setTimeout(() => { - if (!row.classList.contains('expanded')) { - row.remove(); - } - }, 300); - }); - } -}); - - -// =================================================================== -// END OF UNIFIED JAVASCRIPT -// =================================================================== diff --git a/igny8-ai-seo-wp-plugin/assets/js/image-queue-processor.js b/igny8-ai-seo-wp-plugin/assets/js/image-queue-processor.js deleted file mode 100644 index d3dae4ce..00000000 --- a/igny8-ai-seo-wp-plugin/assets/js/image-queue-processor.js +++ /dev/null @@ -1,436 +0,0 @@ -/** - * Igny8 Image Queue Processor - * Sequential image generation with individual progress tracking - */ - -// Process AI Image Generation for Drafts (Sequential Image Processing with Queue Modal) -function processAIImageGenerationDrafts(postIds) { - console.log('Igny8: processAIImageGenerationDrafts called with postIds:', postIds); - - // Event 1: Generate Images button clicked - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('INFO', 'Generate Images button clicked', { - postIds: postIds, - timestamp: new Date().toISOString() - }); - } - - // Get image generation settings from saved options (passed via wp_localize_script) - const desktopEnabled = window.IGNY8_PAGE?.imageSettings?.desktop_enabled || false; - const mobileEnabled = window.IGNY8_PAGE?.imageSettings?.mobile_enabled || false; - const maxInArticleImages = window.IGNY8_PAGE?.imageSettings?.max_in_article_images || 1; - - // Event 2: Settings retrieved - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('SUCCESS', 'Settings retrieved', { - desktop: desktopEnabled, - mobile: mobileEnabled, - maxImages: maxInArticleImages - }); - } - - // Build image queue based on settings - const imageQueue = []; - - postIds.forEach((postId, postIndex) => { - // Featured image (always) - imageQueue.push({ - post_id: postId, - post_number: postIndex + 1, - type: 'featured', - device: '', - label: 'Featured Image', - post_title: `Post ${postIndex + 1}` - }); - - // Desktop in-article images - if (desktopEnabled) { - for (let i = 1; i <= maxInArticleImages; i++) { - imageQueue.push({ - post_id: postId, - post_number: postIndex + 1, - type: 'article', - device: 'desktop', - index: i, - label: `desktop-${i}`, - section: i, - post_title: `Post ${postIndex + 1}` - }); - } - } - - // Mobile in-article images - if (mobileEnabled) { - for (let i = 1; i <= maxInArticleImages; i++) { - imageQueue.push({ - post_id: postId, - post_number: postIndex + 1, - type: 'article', - device: 'mobile', - index: i, - label: `mobile-${i}`, - section: i, - post_title: `Post ${postIndex + 1}` - }); - } - } - }); - - console.log('Igny8: Image queue built:', imageQueue); - - // Show queue modal - showImageQueueModal(imageQueue, imageQueue.length); - - // Start processing queue - processImageQueue(imageQueue, 0); -} - -// Show modal with image queue and individual progress bars -function showImageQueueModal(queue, totalImages) { - if (window.currentProgressModal) { - window.currentProgressModal.remove(); - } - - const modal = document.createElement('div'); - modal.id = 'igny8-image-queue-modal'; - modal.className = 'igny8-modal'; - - let queueHTML = ''; - queue.forEach((item, index) => { - const itemId = `queue-item-${index}`; - queueHTML += ` -
      -
      -
      -
      - ${index + 1} - ${item.label} - ${item.post_title} - ⏳ Pending -
      -
      -
      -
      0%
      -
      - -
      -
      - No image -
      -
      -
      - `; - }); - - modal.innerHTML = ` -
      -
      -

      🎨 Generating Images

      -

      Total: ${totalImages} images in queue

      -
      -
      -
      - ${queueHTML} -
      -
      -
      - - `; - - document.body.appendChild(modal); - modal.classList.add('open'); - window.currentProgressModal = modal; -} - -// Process image queue sequentially with progressive loading -function processImageQueue(queue, currentIndex) { - if (currentIndex >= queue.length) { - // All done - console.log('Igny8: All images processed'); - - // Log to Image Generation Debug - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('SUCCESS', 'All images processed', { - total: queue.length, - timestamp: new Date().toISOString() - }); - } - - setTimeout(() => { - if (window.currentProgressModal) { - window.currentProgressModal.remove(); - window.currentProgressModal = null; - } - showNotification('Image generation complete!', 'success'); - - // Reload table - if (window.loadTableData && window.IGNY8_PAGE?.tableId) { - window.loadTableData(window.IGNY8_PAGE.tableId); - } - }, 2000); - return; - } - - const item = queue[currentIndex]; - const itemElement = document.getElementById(`queue-item-${currentIndex}`); - - if (!itemElement) { - console.error('Queue item element not found:', currentIndex); - - // Log to Image Generation Debug - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('ERROR', 'Queue item element not found', { - index: currentIndex, - itemId: `queue-item-${currentIndex}` - }); - } - - setTimeout(() => processImageQueue(queue, currentIndex + 1), 100); - return; - } - - // Update UI to processing - itemElement.setAttribute('data-status', 'processing'); - itemElement.querySelector('.queue-status').textContent = '⏳ Generating...'; - - const progressFill = itemElement.querySelector('.queue-progress-fill'); - const progressText = itemElement.querySelector('.queue-progress-text'); - - // Log to Image Generation Debug - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('INFO', `Processing ${item.label}`, { - postId: item.post_id, - type: item.type, - device: item.device || 'N/A', - index: item.index || 1, - queuePosition: `${currentIndex + 1}/${queue.length}` - }); - } - - // Progressive loading: 50% in 7s, 75% in next 5s, then 5% every second until 95% - let currentProgress = 0; - let phase = 1; - let phaseStartTime = Date.now(); - - const progressInterval = setInterval(() => { - const elapsed = Date.now() - phaseStartTime; - - if (phase === 1 && currentProgress < 50) { - // Phase 1: 0% to 50% in 7 seconds (7.14% per second) - currentProgress += 0.714; - if (currentProgress >= 50 || elapsed >= 7000) { - currentProgress = 50; - phase = 2; - phaseStartTime = Date.now(); - } - } else if (phase === 2 && currentProgress < 75) { - // Phase 2: 50% to 75% in 5 seconds (5% per second) - currentProgress += 0.5; - if (currentProgress >= 75 || elapsed >= 5000) { - currentProgress = 75; - phase = 3; - phaseStartTime = Date.now(); - } - } else if (phase === 3 && currentProgress < 95) { - // Phase 3: 75% to 95% - 5% every second - if (elapsed >= 1000) { - currentProgress = Math.min(95, currentProgress + 5); - phaseStartTime = Date.now(); - } - } - - progressFill.style.width = currentProgress + '%'; - progressText.textContent = Math.round(currentProgress) + '%'; - }, 100); - - // Generate single image - const formData = new FormData(); - formData.append('action', 'igny8_ai_generate_single_image'); - formData.append('nonce', window.IGNY8_PAGE.nonce); - formData.append('post_id', item.post_id); - formData.append('type', item.type); - formData.append('device', item.device || ''); - formData.append('index', item.index || 1); - // Add meta box integration fields - formData.append('image_label', item.label || ''); - formData.append('section', item.section || ''); - - fetch(window.IGNY8_PAGE.ajaxUrl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - // Stop progressive loading - clearInterval(progressInterval); - - if (data.success) { - // Success - complete to 100% - progressFill.style.width = '100%'; - progressText.textContent = '100%'; - itemElement.setAttribute('data-status', 'completed'); - itemElement.querySelector('.queue-status').textContent = '✅ Complete'; - - // Display thumbnail if image URL is available - if (data.data?.image_url) { - const thumbnailDiv = itemElement.querySelector('.queue-thumbnail'); - if (thumbnailDiv) { - thumbnailDiv.innerHTML = `Generated image`; - } - } - - console.log(`✓ Image ${currentIndex + 1} generated successfully`); - - // Log to Image Generation Debug - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('SUCCESS', `${item.label} generated successfully`, { - postId: item.post_id, - attachmentId: data.data?.attachment_id, - provider: data.data?.provider, - queuePosition: `${currentIndex + 1}/${queue.length}` - }); - } - - // Process next image after short delay - setTimeout(() => processImageQueue(queue, currentIndex + 1), 500); - } else { - // Error - show at 90% - progressFill.style.width = '90%'; - progressText.textContent = 'Failed'; - itemElement.setAttribute('data-status', 'failed'); - itemElement.querySelector('.queue-status').textContent = '❌ Failed'; - - const errorDiv = itemElement.querySelector('.queue-error'); - errorDiv.textContent = data.data?.message || 'Unknown error'; - errorDiv.style.display = 'block'; - - console.error(`✗ Image ${currentIndex + 1} failed:`, data.data?.message); - - // Log to Image Generation Debug - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('ERROR', `${item.label} generation failed`, { - postId: item.post_id, - error: data.data?.message || 'Unknown error', - queuePosition: `${currentIndex + 1}/${queue.length}` - }); - } - - // Continue to next image despite error - setTimeout(() => processImageQueue(queue, currentIndex + 1), 1000); - } - }) - .catch(error => { - // Exception - stop progressive loading - clearInterval(progressInterval); - - progressFill.style.width = '90%'; - progressText.textContent = 'Error'; - itemElement.setAttribute('data-status', 'failed'); - itemElement.querySelector('.queue-status').textContent = '❌ Error'; - - const errorDiv = itemElement.querySelector('.queue-error'); - errorDiv.textContent = 'Exception: ' + error.message; - errorDiv.style.display = 'block'; - - console.error(`✗ Image ${currentIndex + 1} exception:`, error); - - // Log to Image Generation Debug - if (window.addImageGenDebugLog) { - window.addImageGenDebugLog('ERROR', `${item.label} request exception`, { - postId: item.post_id, - error: error.message, - queuePosition: `${currentIndex + 1}/${queue.length}` - }); - } - - // Continue to next image despite error - setTimeout(() => processImageQueue(queue, currentIndex + 1), 1000); - }); -} diff --git a/igny8-ai-seo-wp-plugin/assets/shortcodes/_README.php b/igny8-ai-seo-wp-plugin/assets/shortcodes/_README.php deleted file mode 100644 index f669a0fd..00000000 --- a/igny8-ai-seo-wp-plugin/assets/shortcodes/_README.php +++ /dev/null @@ -1,14 +0,0 @@ - ''], $atts); - $post_id = get_the_ID(); - - if (empty($post_id)) { - return ''; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images)) { - return ''; - } - - // Display specific image by ID - if (!empty($atts['id']) && isset($images[$atts['id']])) { - $image_data = $images[$atts['id']]; - - // Device detection - only show if device matches - $is_mobile = wp_is_mobile(); - $is_desktop = !$is_mobile; - - // Check if image should be displayed based on device - $should_display = false; - if (strpos($atts['id'], 'desktop-') === 0 && $is_desktop) { - $should_display = true; - } elseif (strpos($atts['id'], 'mobile-') === 0 && $is_mobile) { - $should_display = true; - } - - if (!$should_display) { - return ''; - } - - $attachment_id = intval($image_data['attachment_id']); - - if ($attachment_id > 0) { - return wp_get_attachment_image($attachment_id, 'large', false, [ - 'class' => 'igny8-inarticle-image', - 'data-image-id' => esc_attr($atts['id']), - 'data-device' => esc_attr($image_data['device']), - 'alt' => esc_attr($image_data['label']) - ]); - } - } - - return ''; -}); - -/** - * Display all in-article images - * - * Usage: [igny8-images] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-images', function($atts) { - $atts = shortcode_atts([ - 'device' => '', // Filter by device type (desktop/mobile) - 'size' => 'large', // Image size - 'class' => 'igny8-image-gallery' // CSS class - ], $atts); - - $post_id = get_the_ID(); - - if (empty($post_id)) { - return ''; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images) || empty($images)) { - return ''; - } - - $output = '
      '; - $output .= '

      This is coming from shortcode

      '; - - foreach ($images as $label => $image_data) { - // Filter by device if specified - if (!empty($atts['device']) && $image_data['device'] !== $atts['device']) { - continue; - } - - $attachment_id = intval($image_data['attachment_id']); - - if ($attachment_id > 0) { - $output .= wp_get_attachment_image($attachment_id, $atts['size'], false, [ - 'class' => 'igny8-inarticle-image', - 'data-image-id' => esc_attr($label), - 'data-device' => esc_attr($image_data['device']), - 'alt' => esc_attr($image_data['label']) - ]); - } - } - - $output .= '
      '; - - return $output; -}); - -/** - * Display desktop images only - * - * Usage: [igny8-desktop-images] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-desktop-images', function($atts) { - $atts = shortcode_atts([ - 'size' => 'large', - 'class' => 'igny8-desktop-gallery' - ], $atts); - - return do_shortcode('[igny8-images device="desktop" size="' . $atts['size'] . '" class="' . $atts['class'] . '"]'); -}); - -/** - * Display mobile images only - * - * Usage: [igny8-mobile-images] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-mobile-images', function($atts) { - $atts = shortcode_atts([ - 'size' => 'large', - 'class' => 'igny8-mobile-gallery' - ], $atts); - - return do_shortcode('[igny8-images device="mobile" size="' . $atts['size'] . '" class="' . $atts['class'] . '"]'); -}); - -/** - * Display image count - * - * Usage: [igny8-image-count] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-image-count', function($atts) { - $atts = shortcode_atts(['device' => ''], $atts); - - $post_id = get_the_ID(); - - if (empty($post_id)) { - return '0'; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images)) { - return '0'; - } - - if (!empty($atts['device'])) { - $count = 0; - foreach ($images as $image_data) { - if ($image_data['device'] === $atts['device']) { - $count++; - } - } - return (string) $count; - } - - return (string) count($images); -}); - -/** - * Display image gallery with responsive design - * - * Usage: [igny8-responsive-gallery] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-responsive-gallery', function($atts) { - $atts = shortcode_atts([ - 'desktop_size' => 'large', - 'mobile_size' => 'medium', - 'class' => 'igny8-responsive-gallery' - ], $atts); - - $post_id = get_the_ID(); - - if (empty($post_id)) { - return ''; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images) || empty($images)) { - return ''; - } - - $output = '
      '; - - // Desktop images - $desktop_images = array_filter($images, function($img) { - return $img['device'] === 'desktop'; - }); - - if (!empty($desktop_images)) { - $output .= '
      '; - foreach ($desktop_images as $label => $image_data) { - $attachment_id = intval($image_data['attachment_id']); - if ($attachment_id > 0) { - $output .= wp_get_attachment_image($attachment_id, $atts['desktop_size'], false, [ - 'class' => 'igny8-desktop-image', - 'data-image-id' => esc_attr($label) - ]); - } - } - $output .= '
      '; - } - - // Mobile images - $mobile_images = array_filter($images, function($img) { - return $img['device'] === 'mobile'; - }); - - if (!empty($mobile_images)) { - $output .= ''; - } - - $output .= '
      '; - - // Add responsive CSS - $output .= ''; - - return $output; -}); diff --git a/igny8-ai-seo-wp-plugin/assets/templates/igny8_clusters_template.csv b/igny8-ai-seo-wp-plugin/assets/templates/igny8_clusters_template.csv deleted file mode 100644 index 5dbfffc3..00000000 --- a/igny8-ai-seo-wp-plugin/assets/templates/igny8_clusters_template.csv +++ /dev/null @@ -1,4 +0,0 @@ -cluster_name,sector_id,status,keyword_count,total_volume,avg_difficulty,mapped_pages_count -"Car Interior Accessories",1,"active",25,45000,42,0 -"Car Storage Solutions",1,"active",18,32000,38,0 -"Car Beverage Holders",1,"active",12,18000,35,0 diff --git a/igny8-ai-seo-wp-plugin/assets/templates/igny8_ideas_template.csv b/igny8-ai-seo-wp-plugin/assets/templates/igny8_ideas_template.csv deleted file mode 100644 index a165a451..00000000 --- a/igny8-ai-seo-wp-plugin/assets/templates/igny8_ideas_template.csv +++ /dev/null @@ -1,4 +0,0 @@ -idea_title,idea_description,content_structure,content_type,keyword_cluster_id,target_keywords,status,estimated_word_count -"Top 10 Car Interior Accessories for 2024","A comprehensive list of the best car interior accessories available this year, including reviews and recommendations.","review","post",1,"car accessories, car storage solutions, car interior accessories","new",1200 -"How to Organize Your Car Interior Like a Pro","Step-by-step guide to organizing your car interior for maximum efficiency and comfort.","guide_tutorial","post",2,"car organization, car storage tips, car interior organization","new",1500 -"DIY Car Storage Solutions That Actually Work","Creative and practical DIY storage solutions you can make at home for your car.","guide_tutorial","post",2,"DIY car storage, car storage solutions, car organization tips","new",800 diff --git a/igny8-ai-seo-wp-plugin/assets/templates/igny8_keywords_template.csv b/igny8-ai-seo-wp-plugin/assets/templates/igny8_keywords_template.csv deleted file mode 100644 index 7a5b750d..00000000 --- a/igny8-ai-seo-wp-plugin/assets/templates/igny8_keywords_template.csv +++ /dev/null @@ -1,4 +0,0 @@ -keyword,search_volume,difficulty,cpc,intent,status,sector_id,cluster_id -"car accessories",12000,45,2.50,"commercial","unmapped",1,0 -"car storage solutions",8500,38,1.80,"informational","unmapped",1,0 -"car interior accessories",15000,52,3.20,"commercial","unmapped",1,0 diff --git a/igny8-ai-seo-wp-plugin/core/_README.php b/igny8-ai-seo-wp-plugin/core/_README.php deleted file mode 100644 index 313ed5a2..00000000 --- a/igny8-ai-seo-wp-plugin/core/_README.php +++ /dev/null @@ -1,14 +0,0 @@ - bool, 'keywords' => array, 'message' => string] - */ -function igny8_view_cluster_keywords($cluster_id) { - global $wpdb; - - if (empty($cluster_id) || !is_numeric($cluster_id)) { - return ['success' => false, 'keywords' => [], 'message' => 'Invalid cluster ID provided']; - } - - $cluster_id = intval($cluster_id); - - // Verify cluster exists - $cluster = $wpdb->get_row($wpdb->prepare( - "SELECT cluster_name FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!$cluster) { - return ['success' => false, 'keywords' => [], 'message' => 'Cluster not found']; - } - - // Get keywords in this cluster - $keywords = $wpdb->get_results($wpdb->prepare( - "SELECT id, keyword, search_volume, difficulty, intent, status - FROM {$wpdb->prefix}igny8_keywords - WHERE cluster_id = %d - ORDER BY keyword ASC", - $cluster_id - ), ARRAY_A); - - return [ - 'success' => true, - 'keywords' => $keywords, - 'cluster_name' => $cluster->cluster_name, - 'message' => "Found " . count($keywords) . " keywords in cluster" - ]; -} - -/** - * Create draft from idea - * - * @param int $idea_id Idea ID to create draft from - * @return array ['success' => bool, 'draft_id' => int, 'message' => string] - */ -function igny8_create_draft_from_idea($idea_id) { - global $wpdb; - - if (empty($idea_id) || !is_numeric($idea_id)) { - return ['success' => false, 'draft_id' => 0, 'message' => 'Invalid idea ID provided']; - } - - $idea_id = intval($idea_id); - - // Get idea details - $idea = $wpdb->get_row($wpdb->prepare( - "SELECT idea_title, idea_description, content_structure, content_type, keyword_cluster_id, estimated_word_count - FROM {$wpdb->prefix}igny8_content_ideas - WHERE id = %d", - $idea_id - )); - - if (!$idea) { - return ['success' => false, 'draft_id' => 0, 'message' => 'Idea not found']; - } - - // Create draft record in wp_igny8_tasks (which serves as drafts) - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_tasks', - [ - 'title' => $idea->idea_title, - 'description' => $idea->idea_description, - 'status' => 'draft', - 'content_structure' => $idea->content_structure, - 'content_type' => $idea->content_type, - 'cluster_id' => $idea->keyword_cluster_id, - 'keywords' => json_encode([]), // Will be populated from cluster if needed - 'schedule_at' => null, - 'assigned_post_id' => null, - 'created_at' => current_time('mysql'), - 'updated_at' => current_time('mysql') - ], - ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s', '%s'] - ); - - if ($result === false) { - return ['success' => false, 'draft_id' => 0, 'message' => 'Failed to create draft from idea']; - } - - $draft_id = $wpdb->insert_id; - - return [ - 'success' => true, - 'draft_id' => $draft_id, - 'message' => "Successfully created draft from idea: {$idea->idea_title}" - ]; -} - -/** - * ============================================= - * UNIFIED DATA VALIDATION LAYER - * ============================================= - */ - -/** - * Unified record validation function for all Planner module tables - * - * @param string $table_id Table ID (e.g., 'planner_keywords', 'planner_clusters') - * @param array $data Array of field data to validate - * @return array ['valid' => bool, 'error' => string|null] - */ -function igny8_validate_record($table_id, $data) { - global $wpdb; - - // Define validation rules for each table - $validation_rules = igny8_get_validation_rules($table_id); - - if (!$validation_rules) { - return ['valid' => false, 'error' => 'Invalid table ID provided']; - } - - // Validate each field - foreach ($validation_rules as $field => $rules) { - $value = $data[$field] ?? ''; - - // Skip validation if field is not provided and not required - if (empty($value) && !$rules['required']) { - continue; - } - - // Required field validation - if ($rules['required'] && empty($value)) { - return ['valid' => false, 'error' => ucfirst($field) . ' is required']; - } - - // Skip further validation if field is empty and not required - if (empty($value)) { - continue; - } - - // Type-specific validations - if (isset($rules['type'])) { - $validation_result = igny8_validate_field_by_type($field, $value, $rules); - if (!$validation_result['valid']) { - return $validation_result; - } - } - - // Enum validation - if (isset($rules['enum'])) { - if (!in_array($value, $rules['enum'])) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be one of: ' . implode(', ', $rules['enum'])]; - } - } - - // Range validation - if (isset($rules['min']) && is_numeric($value)) { - if (floatval($value) < $rules['min']) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be at least ' . $rules['min']]; - } - } - - if (isset($rules['max']) && is_numeric($value)) { - if (floatval($value) > $rules['max']) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be at most ' . $rules['max']]; - } - } - - // Length validation - if (isset($rules['max_length'])) { - if (strlen($value) > $rules['max_length']) { - return ['valid' => false, 'error' => ucfirst($field) . ' cannot exceed ' . $rules['max_length'] . ' characters']; - } - } - - // Foreign key validation - if (isset($rules['foreign_key'])) { - $fk_result = igny8_validate_foreign_key($rules['foreign_key'], $value); - if (!$fk_result['valid']) { - return $fk_result; - } - } - } - - return ['valid' => true]; -} - -/** - * Get validation rules for a specific table - * - * @param string $table_id Table ID - * @return array|null Validation rules array or null if not found - */ -function igny8_get_validation_rules($table_id) { - $rules = [ - 'planner_keywords' => [ - 'keyword' => [ - 'required' => true, - 'type' => 'text', - 'max_length' => 255, - 'no_html' => true - ], - 'search_volume' => [ - 'required' => false, - 'type' => 'numeric', - 'min' => 0 - ], - 'difficulty' => [ - 'required' => false, - 'type' => 'numeric_or_text', - 'min' => 0, - 'max' => 100, - 'text_options' => ['Very Easy', 'Easy', 'Medium', 'Hard', 'Very Hard'] - ], - 'cpc' => [ - 'required' => false, - 'type' => 'decimal', - 'min' => 0 - ], - 'intent' => [ - 'required' => false, - 'enum' => ['informational', 'navigational', 'transactional', 'commercial'] - ], - 'status' => [ - 'required' => true, - 'enum' => ['unmapped', 'mapped', 'queued', 'published'] - ], - 'cluster_id' => [ - 'required' => false, - 'type' => 'integer', - 'foreign_key' => [ - 'table' => 'igny8_clusters', - 'column' => 'id' - ] - ] - ], - 'planner_clusters' => [ - 'cluster_name' => [ - 'required' => true, - 'type' => 'text', - 'max_length' => 255, - 'no_html' => true - ], - 'sector_id' => [ - 'required' => false, - 'type' => 'integer' - ], - 'status' => [ - 'required' => true, - 'enum' => ['active', 'inactive', 'archived'] - ] - ], - 'planner_ideas' => [ - 'idea_title' => [ - 'required' => true, - 'type' => 'text', - 'max_length' => 255, - 'no_html' => true - ], - 'idea_description' => [ - 'required' => false, - 'type' => 'text', - 'no_html' => true - ], - 'content_structure' => [ - 'required' => true, - 'enum' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'required' => true, - 'enum' => ['post', 'product', 'page', 'CPT'] - ], - 'keyword_cluster_id' => [ - 'required' => false, - 'type' => 'integer', - 'foreign_key' => [ - 'table' => 'igny8_clusters', - 'column' => 'id' - ] - ], - 'status' => [ - 'required' => true, - 'enum' => ['new', 'scheduled', 'published'] - ], - 'estimated_word_count' => [ - 'required' => false, - 'type' => 'integer', - 'min' => 0 - ], - 'source' => [ - 'required' => true, - 'enum' => ['AI', 'Manual'] - ], - 'target_keywords' => [ - 'required' => false, - 'type' => 'text', - 'no_html' => false - ], - 'tasks_count' => [ - 'required' => false, - 'type' => 'integer', - 'min' => 0 - ] - ], - 'writer_tasks' => [ - 'title' => [ - 'required' => true, - 'type' => 'text', - 'max_length' => 255, - 'no_html' => true - ], - 'description' => [ - 'required' => false, - 'type' => 'text', - 'no_html' => true - ], - 'status' => [ - 'required' => true, - 'enum' => ['pending', 'in_progress', 'completed', 'cancelled', 'draft', 'queued', 'review', 'published'] - ], - 'priority' => [ - 'required' => true, - 'enum' => ['high', 'medium', 'low'] - ], - 'content_structure' => [ - 'required' => false, - 'enum' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'required' => false, - 'enum' => ['post', 'product', 'page', 'CPT'] - ], - 'cluster_id' => [ - 'required' => false, - 'type' => 'integer', - 'foreign_key' => [ - 'table' => 'igny8_clusters', - 'column' => 'id' - ] - ], - 'idea_id' => [ - 'required' => false, - 'type' => 'integer', - 'foreign_key' => [ - 'table' => 'igny8_content_ideas', - 'column' => 'id' - ] - ], - 'keywords' => [ - 'required' => false, - 'type' => 'text' - ], - 'word_count' => [ - 'required' => false, - 'type' => 'integer', - 'min' => 0 - ], - 'due_date' => [ - 'required' => false, - 'type' => 'datetime' - ], - 'schedule_at' => [ - 'required' => false, - 'type' => 'datetime' - ], - 'assigned_post_id' => [ - 'required' => false, - 'type' => 'integer', - 'foreign_key' => [ - 'table' => 'wp_posts', - 'column' => 'ID' - ] - ], - 'ai_writer' => [ - 'required' => false, - 'enum' => ['ai', 'human'] - ], - ] - ]; - - return $rules[$table_id] ?? null; -} - -/** - * Validate field by type - * - * @param string $field Field name - * @param mixed $value Field value - * @param array $rules Validation rules - * @return array ['valid' => bool, 'error' => string|null] - */ -function igny8_validate_field_by_type($field, $value, $rules) { - switch ($rules['type']) { - case 'text': - // Special handling for target_keywords field - if ($field === 'target_keywords') { - // Allow any format for target_keywords - arrays, strings, etc. - if (is_array($value)) { - $value = implode(', ', array_filter($value)); - } elseif (!is_string($value)) { - $value = (string) $value; - } - // No HTML validation for target_keywords - return ['valid' => true]; - } - - // Convert to string if not already - if (!is_string($value)) { - $value = (string) $value; - } - - // Check for HTML content if not allowed - if (isset($rules['no_html']) && $rules['no_html'] && strip_tags($value) !== $value) { - return ['valid' => false, 'error' => ucfirst($field) . ' cannot contain HTML']; - } - break; - - case 'numeric': - case 'integer': - if (!is_numeric($value)) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be a number']; - } - if ($rules['type'] === 'integer' && intval($value) != $value) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be a whole number']; - } - break; - - case 'decimal': - if (!is_numeric($value)) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be a decimal number']; - } - break; - - case 'numeric_or_text': - // Handle difficulty field that can be text or numeric - if (isset($rules['text_options']) && in_array($value, $rules['text_options'])) { - // Valid text option, convert to numeric for range validation - $difficulty_map = [ - 'Very Easy' => 10, - 'Easy' => 30, - 'Medium' => 50, - 'Hard' => 70, - 'Very Hard' => 90 - ]; - $value = $difficulty_map[$value] ?? 0; - } elseif (!is_numeric($value)) { - return ['valid' => false, 'error' => ucfirst($field) . ' must be a number or valid difficulty level']; - } - break; - - default: - // Unknown type, skip validation - break; - } - - return ['valid' => true]; -} - -/** - * Validate foreign key reference - * - * @param array $fk_config Foreign key configuration - * @param mixed $value Value to validate - * @return array ['valid' => bool, 'error' => string|null] - */ -function igny8_validate_foreign_key($fk_config, $value) { - global $wpdb; - - if (empty($value) || !is_numeric($value)) { - return ['valid' => false, 'error' => 'Invalid reference ID']; - } - - $table = $fk_config['table']; - $column = $fk_config['column']; - - // Handle WordPress posts table - if ($table === 'posts') { - $table = $wpdb->posts; - } else { - $table = $wpdb->prefix . $table; - } - - $exists = $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM `{$table}` WHERE `{$column}` = %d", - intval($value) - )); - - if (!$exists) { - return ['valid' => false, 'error' => 'Referenced record does not exist']; - } - - return ['valid' => true]; -} -require_once plugin_dir_path(__FILE__) . '../../ai/prompts-library.php'; - -// Include monitor helpers for diagnostic tracking -require_once plugin_dir_path(__FILE__) . '../../debug/monitor-helpers.php'; - -// Unified AJAX handler for all table data loading -add_action('wp_ajax_igny8_get_table_data', 'igny8_get_table_data'); - -// AJAX handler for setting global flags (for debugging) -add_action('wp_ajax_igny8_set_global_flag', 'igny8_set_global_flag'); - -// AJAX handler for refreshing debug panel - -// AJAX handler for toggling debug monitoring -add_action('wp_ajax_igny8_toggle_debug_monitoring', 'igny8_toggle_debug_monitoring'); - -// AJAX handlers for save and delete operations -add_action('wp_ajax_igny8_save_table_record', 'igny8_save_table_record'); -add_action('wp_ajax_igny8_delete_table_records', 'igny8_delete_table_records'); - -// AJAX handlers for inline forms -add_action('wp_ajax_igny8_save_form_record', 'igny8_save_form_record'); -add_action('wp_ajax_igny8_delete_single_record', 'igny8_delete_single_record'); -add_action('wp_ajax_igny8_delete_bulk_records', 'igny8_delete_bulk_records'); - -// MOVED TO: flows/sync-ajax.php -// AJAX handler for keyword imports with workflow automation -// add_action('wp_ajax_igny8_import_keywords', 'igny8_ajax_import_keywords'); - -// MOVED TO: flows/sync-ajax.php -// AJAX handlers for Planner → Writer Bridge -// add_action('wp_ajax_igny8_create_task_from_idea', 'igny8_create_task_from_idea_ajax'); -// add_action('wp_ajax_igny8_bulk_create_tasks_from_ideas', 'igny8_bulk_create_tasks_from_ideas_ajax'); - -// AJAX handlers for Personalization Module -// igny8_get_fields actions moved to ai/openai-api.php -// Personalization AJAX actions moved to ai/openai-api.php -add_action('wp_ajax_igny8_test_ajax', 'igny8_test_ajax_callback'); -add_action('wp_ajax_nopriv_igny8_test_ajax', 'igny8_test_ajax_callback'); - -// Simple test handler for debug -function igny8_test_ajax_callback() { - wp_send_json_success('AJAX is working!'); -} - -// AJAX handler for saving text input (debug) -add_action('wp_ajax_igny8_save_ajax_text', 'igny8_ajax_save_ajax_text'); - -function igny8_ajax_save_ajax_text() { - try { - // Check if all required data is present - if (!isset($_POST['ajax_text_input'])) { - wp_send_json_error('Missing text input'); - return; - } - - if (!isset($_POST['ajax_text_nonce'])) { - wp_send_json_error('Missing security token'); - return; - } - - // Verify nonce - if (!wp_verify_nonce($_POST['ajax_text_nonce'], 'ajax_text_action')) { - wp_send_json_error('Security check failed'); - return; - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - return; - } - - // Get and sanitize form data - $text_input = sanitize_text_field($_POST['ajax_text_input']); - - // Save text input to WordPress options - $result = update_option('igny8_ajax_test_text', $text_input); - - if ($result === false) { - wp_send_json_error('Failed to save to database'); - return; - } - - wp_send_json_success('Text saved successfully: ' . $text_input); - - } catch (Exception $e) { - wp_send_json_error('Server error: ' . $e->getMessage()); - } -} - -// AJAX handler for testing Runware API connection -add_action('wp_ajax_igny8_test_runware_connection', 'igny8_ajax_test_runware_connection'); - -// Legacy AJAX handlers (redirected to unified handler) -add_action('wp_ajax_igny8_planner_keywords_ajax', 'igny8_ajax_load_table_data'); -add_action('wp_ajax_igny8_planner_clusters_ajax', 'igny8_ajax_load_table_data'); -add_action('wp_ajax_igny8_planner_ideas_ajax', 'igny8_ajax_load_table_data'); -add_action('wp_ajax_igny8_writer_tasks_ajax', 'igny8_ajax_load_table_data'); -add_action('wp_ajax_igny8_writer_drafts_ajax', 'igny8_ajax_load_table_data'); -add_action('wp_ajax_igny8_writer_published_ajax', 'igny8_ajax_load_table_data'); - -/** - * AJAX handler for bulk publishing drafts - */ -add_action('wp_ajax_igny8_bulk_publish_drafts', 'igny8_ajax_bulk_publish_drafts'); - -/** - * Save New Content Decision Setting - */ -add_action('wp_ajax_igny8_save_new_content_decision', 'igny8_ajax_save_new_content_decision'); -function igny8_ajax_save_new_content_decision() { - try { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $new_content_action = sanitize_text_field($_POST['new_content_action'] ?? 'draft'); - - // Validate the value - if (!in_array($new_content_action, ['draft', 'publish'])) { - wp_send_json_error(['message' => 'Invalid content action']); - } - - // Save the setting - update_option('igny8_new_content_action', $new_content_action); - - // Debug logging - error_log('Igny8 DEBUG: Saving new content action: ' . $new_content_action); - $saved_value = get_option('igny8_new_content_action', 'not_saved'); - error_log('Igny8 DEBUG: Verified saved value: ' . $saved_value); - error_log('Igny8 DEBUG: All WordPress options with igny8: ' . print_r(get_option('igny8_new_content_action'), true)); - - wp_send_json_success([ - 'message' => 'New content decision saved successfully', - 'action' => $new_content_action, - 'debug_saved' => $saved_value - ]); - - } catch (Exception $e) { - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} -function igny8_ajax_bulk_publish_drafts() { - try { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $task_ids = $_POST['task_ids'] ?? []; - if (empty($task_ids) || !is_array($task_ids)) { - wp_send_json_error(['message' => 'No tasks selected']); - } - - global $wpdb; - $published = 0; - $failed = 0; - - foreach ($task_ids as $task_id) { - $task_id = intval($task_id); - - // Get task details - $task = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", - $task_id - )); - - if (!$task || !$task->assigned_post_id) { - $failed++; - continue; - } - - // Update WordPress post status to publish - $result = wp_update_post([ - 'ID' => $task->assigned_post_id, - 'post_status' => 'publish' - ]); - - if (!is_wp_error($result) && $result) { - // Update task status to completed - $wpdb->update( - $wpdb->prefix . 'igny8_tasks', - ['status' => 'completed'], - ['id' => $task_id], - ['%s'], - ['%d'] - ); - - // Trigger keyword status update - do_action('igny8_post_published', $task->assigned_post_id); - - $published++; - } else { - $failed++; - } - } - - wp_send_json_success([ - 'message' => "Published {$published} drafts, {$failed} failed", - 'published' => $published, - 'failed' => $failed - ]); - } catch (Exception $e) { - error_log('Igny8 Bulk Publish Error: ' . $e->getMessage()); - wp_send_json_error(['message' => 'Error publishing drafts: ' . $e->getMessage()]); - } -} -add_action('wp_ajax_igny8_writer_templates_ajax', 'igny8_ajax_load_table_data'); -add_action('wp_ajax_igny8_render_form_row', 'igny8_render_form_row'); -add_action('wp_ajax_igny8_get_row_data', 'igny8_get_row_data'); -add_action('wp_ajax_igny8_test_ajax', 'igny8_test_ajax'); - -/** - * Secure AJAX endpoint for table data loading - * Phase-1: Security Backbone & Table Skeletons - */ -function igny8_get_table_data() { - // Note: TABLE_AJAX_REQUEST_SENT is set by JavaScript when request is initiated - - // Verify nonce using check_ajax_referer - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - if (function_exists('igny8_debug_state')) { - igny8_debug_state('AJAX_NONCE_VALIDATED', false, 'Nonce verification failed'); - } - wp_send_json_error('Security check failed'); - } - - // Debug state: Nonce validated - if (function_exists('igny8_debug_state')) { - igny8_debug_state('AJAX_NONCE_VALIDATED', true, 'Nonce verification passed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - if (function_exists('igny8_debug_state')) { - igny8_debug_state('USER_CAPABILITY_OK', false, 'User lacks manage_options capability'); - } - wp_send_json_error('Insufficient permissions'); - } - - // Debug state: User capability OK - if (function_exists('igny8_debug_state')) { - igny8_debug_state('USER_CAPABILITY_OK', true, 'User has required capabilities'); - } - - // Get and sanitize parameters - $tableId = sanitize_text_field($_POST['table'] ?? ''); - $filters_raw = $_POST['filters'] ?? []; - $page = intval($_POST['page'] ?? 1); - $per_page = intval($_POST['per_page'] ?? get_option('igny8_records_per_page', 20)); - - // Decode filters if it's a JSON string - if (is_string($filters_raw)) { - // First try to decode as-is - $filters = json_decode($filters_raw, true); - if (json_last_error() !== JSON_ERROR_NONE) { - error_log("DEBUG: JSON decode error: " . json_last_error_msg()); - error_log("DEBUG: Raw string was: " . $filters_raw); - - // Try to fix escaped quotes and decode again - $fixed_string = stripslashes($filters_raw); - error_log("DEBUG: Fixed string: " . $fixed_string); - $filters = json_decode($fixed_string, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - error_log("DEBUG: JSON decode error after fix: " . json_last_error_msg()); - $filters = []; - } - } - if ($filters === null) { - $filters = []; - } - } else { - $filters = $filters_raw; - } - - - // Validate required parameters - if (empty($tableId)) { - wp_send_json_error('Required parameter missing: table'); - } - - // Extract submodule name from tableId for debug tracking - // tableId format: module_submodule (e.g., planner_keywords, writer_templates) - $submodule_name = ''; - if (strpos($tableId, '_') !== false) { - $parts = explode('_', $tableId, 2); - $submodule_name = $parts[1] ?? ''; - } - - // Store the actual submodule name for debug detection - $GLOBALS['igny8_ajax_submodule_name'] = $submodule_name; - - // Mark that AJAX request was actually sent - $GLOBALS['igny8_ajax_request_sent'] = true; - - // Debug state: AJAX response OK - // Get table data using the data fetching function - $table_data = igny8_fetch_table_data($tableId, $filters, $page, $per_page); - - // Store AJAX response for event detection - $GLOBALS['igny8_last_ajax_response'] = [ - 'data' => $table_data, - 'table_id' => $tableId, - 'timestamp' => time() - ]; - - if (function_exists('igny8_debug_state')) { - igny8_debug_state('TABLE_AJAX_RESPONSE_OK', true, 'AJAX response prepared successfully'); - } - - // Add debug query to table data - $table_data['debug_query'] = $GLOBALS['igny8_debug_query'] ?? 'No query executed'; - - // Return actual table data - wp_send_json_success($table_data); -} - - -/** - * Legacy AJAX handler for loading table data (redirected to new endpoint) - */ -function igny8_ajax_load_table_data() { - // Debug state: AJAX request sent - if (function_exists('igny8_debug_state')) { - igny8_debug_state('TABLE_AJAX_REQUEST_SENT', true, 'AJAX request initiated'); - } - - // Verify nonce - if (!wp_verify_nonce($_POST['security'], 'igny8_ajax_nonce')) { - if (function_exists('igny8_debug_state')) { - igny8_debug_state('AJAX_NONCE_VALIDATED', false, 'Nonce verification failed'); - } - wp_send_json_error('Security check failed'); - return; - } - - // Debug state: Nonce validated - if (function_exists('igny8_debug_state')) { - igny8_debug_state('AJAX_NONCE_VALIDATED', true, 'Nonce verification passed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - if (function_exists('igny8_debug_state')) { - igny8_debug_state('USER_CAPABILITY_OK', false, 'User lacks manage_options capability'); - } - wp_send_json_error('Insufficient permissions'); - return; - } - - // Debug state: User capability OK - if (function_exists('igny8_debug_state')) { - igny8_debug_state('USER_CAPABILITY_OK', true, 'User has required capabilities'); - } - - // Get parameters - $module = sanitize_text_field($_POST['module'] ?? ''); - $tab = sanitize_text_field($_POST['tab'] ?? ''); - $page = intval($_POST['page'] ?? 1); - $per_page = intval($_POST['per_page'] ?? 20); - - // Prepare response - $response = [ - 'success' => true, - 'data' => [ - 'items' => [], - 'total' => 0, - 'page' => $page, - 'per_page' => $per_page, - 'total_pages' => 0 - ] - ]; - - // Debug state: AJAX response OK - if (function_exists('igny8_debug_state')) { - igny8_debug_state('TABLE_AJAX_RESPONSE_OK', true, 'AJAX response prepared successfully'); - } - - // Return JSON response - wp_send_json_success($response); -} - - - -/** - * Save table record (insert/update) - */ -function igny8_save_table_record() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - global $wpdb; - - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $record_data = $_POST['record_data'] ?? []; - $record_id = intval($_POST['record_id'] ?? 0); - - if (empty($table_id)) { - wp_send_json_error('Table ID is required'); - } - - // Get actual table name - try { - $table_name = igny8_get_table_name($table_id); - } catch (InvalidArgumentException $e) { - wp_send_json_error('Invalid table ID: ' . $e->getMessage()); - } - - // Sanitize record data - $sanitized_data = []; - foreach ($record_data as $key => $value) { - $sanitized_data[sanitize_key($key)] = sanitize_text_field($value); - } - - if (empty($sanitized_data)) { - wp_send_json_error('No data provided'); - } - - // Validate record data before database operations - $validation = igny8_validate_record($table_id, $sanitized_data); - if (!$validation['valid']) { - wp_send_json_error(['message' => $validation['error']]); - } - - // Add timestamps - $sanitized_data['updated_at'] = current_time('mysql'); - - try { - if ($record_id > 0) { - // Update existing record - $result = $wpdb->update( - $table_name, - $sanitized_data, - ['id' => $record_id], - array_fill(0, count($sanitized_data), '%s'), - ['%d'] - ); - - if ($result === false) { - wp_send_json_error('Failed to update record'); - } - - // Trigger taxonomy term update after cluster is updated - if ($table_id === 'planner_clusters' && $action_type === 'edit' && $record_id > 0) { - do_action('igny8_cluster_updated', $record_id); - error_log("Igny8: Triggered igny8_cluster_updated for cluster ID $record_id"); - } - - $message = 'Record updated successfully'; - } else { - // Insert new record - $sanitized_data['created_at'] = current_time('mysql'); - - $result = $wpdb->insert( - $table_name, - $sanitized_data, - array_fill(0, count($sanitized_data), '%s') - ); - - if ($result === false) { - wp_send_json_error('Failed to insert record'); - } - - $record_id = $wpdb->insert_id; - - // Trigger taxonomy term creation after new cluster is saved - if ($table_id === 'planner_clusters' && $action_type === 'add' && $record_id > 0) { - do_action('igny8_cluster_added', $record_id); - error_log("Igny8: Triggered igny8_cluster_added for cluster ID $record_id"); - } - - $message = 'Record created successfully'; - } - - // Trigger workflow automation after successful save - $workflow_result = null; - if ($record_id > 0) { - switch ($table_id) { - case 'planner_clusters': - // Trigger auto-idea generation when cluster is created - if ($record_id > 0 && isset($sanitized_data['created_at'])) { // New record created - $workflow_result = igny8_workflow_triggers('cluster_created', ['cluster_id' => $record_id]); - } - break; - - } - } - - // Prepare response with workflow result - $response = [ - 'message' => $message, - 'record_id' => $record_id - ]; - - if ($workflow_result && is_array($workflow_result) && isset($workflow_result['success']) && $workflow_result['success']) { - $response['workflow_message'] = $workflow_result['message']; - if (isset($workflow_result['clusters_created'])) { - $response['workflow_data'] = [ - 'clusters_created' => $workflow_result['clusters_created'], - 'cluster_ids' => $workflow_result['cluster_ids'] ?? [] - ]; - } - if (isset($workflow_result['ideas_created'])) { - $response['workflow_data'] = [ - 'ideas_created' => $workflow_result['ideas_created'] - ]; - } - if (isset($workflow_result['suggestions'])) { - $response['workflow_data'] = [ - 'suggestions' => $workflow_result['suggestions'] - ]; - } - } - - wp_send_json_success($response); - - } catch (Exception $e) { - wp_send_json_error('Database error: ' . $e->getMessage()); - } -} - -/** - * Delete table records - */ -function igny8_delete_table_records() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - global $wpdb; - - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $record_ids = $_POST['record_ids'] ?? []; - - if (empty($table_id)) { - wp_send_json_error('Table ID is required'); - } - - if (empty($record_ids) || !is_array($record_ids)) { - wp_send_json_error('No records selected for deletion'); - } - - // Get actual table name - $table_name = igny8_get_table_name($table_id); - if (!$table_name) { - wp_send_json_error('Invalid table ID'); - } - - // Sanitize record IDs - $sanitized_ids = array_map('intval', $record_ids); - $sanitized_ids = array_filter($sanitized_ids, function($id) { - return $id > 0; - }); - - if (empty($sanitized_ids)) { - wp_send_json_error('No valid record IDs provided'); - } - - try { - // Build placeholders for IN clause - $placeholders = implode(',', array_fill(0, count($sanitized_ids), '%d')); - - $result = $wpdb->query($wpdb->prepare( - "DELETE FROM `{$table_name}` WHERE id IN ({$placeholders})", - $sanitized_ids - )); - - if ($result === false) { - wp_send_json_error('Failed to delete records'); - } - - wp_send_json_success([ - 'message' => "Successfully deleted {$result} record(s)", - 'deleted_count' => $result - ]); - - } catch (Exception $e) { - wp_send_json_error('Database error: ' . $e->getMessage()); - } -} - -/** - * AJAX handler for saving form records (add/edit) - */ -function igny8_save_form_record() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $action_type = sanitize_text_field($_POST['action_type'] ?? 'add'); - $record_id = intval($_POST['record_id'] ?? 0); - - if (empty($table_id)) { - wp_send_json_error('Table ID required'); - } - - // Get table name - try { - $table_name = igny8_get_table_name($table_id); - } catch (InvalidArgumentException $e) { - wp_send_json_error('Invalid table ID: ' . $e->getMessage()); - } - - // Get form configuration - require_once plugin_dir_path(__FILE__) . '../../modules/config/forms-config.php'; - $config = igny8_get_form_config($table_id); - if (!$config) { - wp_send_json_error('Form configuration not found'); - } - - // Collect form data - $form_data = []; - foreach ($config['fields'] as $field) { - $field_name = $field['name']; - $field_type = $field['type']; - - if ($field_type === 'multiselect') { - $values = $_POST[$field_name] ?? []; - $form_data[$field_name] = is_array($values) ? implode(',', $values) : ''; - } else { - $form_data[$field_name] = sanitize_text_field($_POST[$field_name] ?? ''); - } - } - - // Handle special field processing - if (isset($form_data['difficulty']) && !is_numeric($form_data['difficulty'])) { - // Convert difficulty text to numeric - $difficulty_map = [ - 'Very Easy' => 10, - 'Easy' => 30, - 'Medium' => 50, - 'Hard' => 70, - 'Very Hard' => 90 - ]; - $form_data['difficulty'] = $difficulty_map[$form_data['difficulty']] ?? 0; - } - - // Handle target_keywords field - store as comma-separated text - if (isset($form_data['target_keywords']) && !empty($form_data['target_keywords'])) { - if (is_array($form_data['target_keywords'])) { - $keywords = array_map('trim', $form_data['target_keywords']); - $keywords = array_filter($keywords); // Remove empty values - $form_data['target_keywords'] = implode(', ', $keywords); - } else { - $keywords = array_map('trim', explode(',', $form_data['target_keywords'])); - $keywords = array_filter($keywords); // Remove empty values - $form_data['target_keywords'] = implode(', ', $keywords); - } - } elseif (isset($form_data['target_keywords'])) { - $form_data['target_keywords'] = null; // Set to null if empty - } - - // Validate form data before database operations - $validation = igny8_validate_record($table_id, $form_data); - if (!$validation['valid']) { - wp_send_json_error(['message' => $validation['error']]); - } - - global $wpdb; - - if ($action_type === 'add') { - // Insert new record - $result = $wpdb->insert($table_name, $form_data); - - if ($result === false) { - wp_send_json_error('Failed to add record'); - } - - $new_id = $wpdb->insert_id; - - // Update cluster metrics if this is a keywords record - if ($table_id === 'planner_keywords' && isset($form_data['cluster_id'])) { - do_action('igny8_keyword_added', $new_id, $form_data['cluster_id']); - } - - // Trigger cluster-added automation for new clusters (same as test page) - if ($table_id === 'planner_clusters') { - do_action('igny8_cluster_added', $new_id); - } - - wp_send_json_success([ - 'message' => 'Record added successfully', - 'id' => $new_id - ]); - - } elseif ($action_type === 'edit') { - if (!$record_id) { - wp_send_json_error('Record ID required for edit'); - } - - // Update existing record - $result = $wpdb->update($table_name, $form_data, ['id' => $record_id]); - - if ($result === false) { - wp_send_json_error('Failed to update record'); - } - - // Update cluster metrics if this is a keywords record - if ($table_id === 'planner_keywords' && isset($form_data['cluster_id'])) { - do_action('igny8_keyword_updated', $record_id, $form_data['cluster_id']); - } - - // Trigger cluster taxonomy term update for cluster updates - if ($table_id === 'planner_clusters') { - do_action('igny8_cluster_updated', $record_id); - } - - wp_send_json_success([ - 'message' => 'Record updated successfully', - 'id' => $record_id - ]); - - } else { - wp_send_json_error('Invalid action type'); - } -} - -/** - * AJAX handler for deleting single record - */ -function igny8_delete_single_record() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $record_id = intval($_POST['record_id'] ?? 0); - - if (empty($table_id) || !$record_id) { - wp_send_json_error('Table ID and Record ID required'); - } - - // Get table name - $table_name = igny8_get_table_name($table_id); - if (!$table_name) { - wp_send_json_error('Invalid table ID'); - } - - global $wpdb; - - // Handle cluster deletion - clean up keyword relationships - if ($table_id === 'planner_clusters') { - // Before deleting cluster, unmap all keywords from this cluster - $unmapped_count = $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->prefix}igny8_keywords - SET cluster_id = NULL, status = 'unmapped', updated_at = CURRENT_TIMESTAMP - WHERE cluster_id = %d", - $record_id - )); - - if ($unmapped_count !== false) { - // Log the unmapping - error_log("Igny8: Unmapped {$unmapped_count} keywords from deleted cluster ID {$record_id}"); - } - } - - // Get cluster_id before deleting for metrics update - $cluster_id = null; - if ($table_id === 'planner_keywords') { - $cluster_id = $wpdb->get_var($wpdb->prepare( - "SELECT cluster_id FROM {$table_name} WHERE id = %d", - $record_id - )); - } - - // Delete the record - $result = $wpdb->delete($table_name, ['id' => $record_id]); - - if ($result === false) { - wp_send_json_error('Failed to delete record'); - } - - // Update cluster metrics if this was a keywords or mapping record - if ($cluster_id) { - if ($table_id === 'planner_keywords') { - do_action('igny8_keyword_deleted', $record_id, $cluster_id); - } - } - - wp_send_json_success([ - 'message' => 'Record deleted successfully', - 'id' => $record_id - ]); -} - -/** - * MOVED TO: flows/sync-ajax.php - * AJAX handler for bulk deleting records - */ -/* -function igny8_delete_bulk_records() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $record_ids = $_POST['record_ids'] ?? []; - - if (empty($table_id) || empty($record_ids) || !is_array($record_ids)) { - wp_send_json_error('Table ID and Record IDs required'); - } - - // Get table name - $table_name = igny8_get_table_name($table_id); - if (!$table_name) { - wp_send_json_error('Invalid table ID'); - } - - // Sanitize IDs - $record_ids = array_map('intval', $record_ids); - $record_ids = array_filter($record_ids, function($id) { return $id > 0; }); - - if (empty($record_ids)) { - wp_send_json_error('No valid record IDs provided'); - } - - global $wpdb; - - // Build placeholders for IN clause - $placeholders = implode(',', array_fill(0, count($record_ids), '%d')); - - // Delete the records - $result = $wpdb->query($wpdb->prepare( - "DELETE FROM `{$table_name}` WHERE id IN ({$placeholders})", - $record_ids - )); - - if ($result === false) { - wp_send_json_error('Failed to delete records'); - } - - wp_send_json_success([ - 'message' => $result . ' records deleted successfully', - 'deleted_count' => $result - ]); -} -*/ - -/** - * AJAX handler for rendering form rows - */ -function igny8_render_form_row() { - // Error reporting disabled for clean JSON responses - - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $mode = sanitize_text_field($_POST['mode'] ?? 'add'); - $record_data_json = wp_unslash($_POST['record_data'] ?? '{}'); - - if (empty($table_id)) { - wp_send_json_error('Table ID required'); - } - - // Parse record data - $record_data = json_decode($record_data_json, true) ?? []; - - // Include forms config first - $forms_config_path = plugin_dir_path(__FILE__) . '../../modules/config/forms-config.php'; - if (!file_exists($forms_config_path)) { - wp_send_json_error('Forms config not found: ' . $forms_config_path); - } - require_once $forms_config_path; - - // Include forms template - $forms_template_path = plugin_dir_path(__FILE__) . '../../modules/components/forms-tpl.php'; - if (!file_exists($forms_template_path)) { - wp_send_json_error('Forms template not found: ' . $forms_template_path); - } - require_once $forms_template_path; - - // Render form row - try { - $form_html = igny8_render_inline_form_row($table_id, $mode, $record_data); - - if ($form_html) { - wp_send_json_success($form_html); - } else { - wp_send_json_error('Failed to render form row'); - } - } catch (Exception $e) { - wp_send_json_error('PHP Error: ' . $e->getMessage()); - } -} - -/** - * Debug AJAX handler - Toggle debug monitoring - */ - -/** - * AJAX handler for getting row data for edit form - */ -function igny8_get_row_data() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $row_id = intval($_POST['row_id'] ?? 0); - - if (empty($table_id) || !$row_id) { - wp_send_json_error('Table ID and Row ID required'); - } - - // Get table name - $table_name = igny8_get_table_name($table_id); - if (!$table_name) { - wp_send_json_error('Invalid table ID'); - } - - global $wpdb; - - // Check if table exists - $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'"); - if (!$table_exists) { - wp_send_json_error("Table $table_name does not exist"); - } - - // Get row data - $row_data = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$table_name} WHERE id = %d", - $row_id - ), ARRAY_A); - - if (!$row_data) { - wp_send_json_error('Record not found'); - } - - // =========================================================== - // FIX: Convert numeric difficulty value into text label for edit form - // =========================================================== - // This ensures that when editing a record, the "Difficulty" dropdown - // correctly pre-selects the current label (e.g., "Hard") instead of - // defaulting to "Select Difficulty". - if (isset($row_data['difficulty']) && is_numeric($row_data['difficulty'])) { - if (function_exists('igny8_get_difficulty_range_name')) { - $row_data['difficulty'] = igny8_get_difficulty_range_name($row_data['difficulty']); - } - } - - // =========================================================== - // covered_keywords is already in comma-separated format, no conversion needed - - wp_send_json_success($row_data); -} - -/** - * AJAX handler for toggling debug monitoring - */ -function igny8_toggle_debug_monitoring() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - $is_enabled = isset($_POST['is_enabled']) ? (bool) $_POST['is_enabled'] : false; - - // Debug logging - error_log("DEBUG: Toggle AJAX - is_enabled: " . ($is_enabled ? 'true' : 'false')); - error_log("DEBUG: Toggle AJAX - POST data: " . print_r($_POST, true)); - - // Update the option in the database - $result = update_option('igny8_debug_enabled', $is_enabled); - error_log("DEBUG: Toggle AJAX - update_option result: " . ($result ? 'success' : 'no change')); - - wp_send_json_success([ - 'message' => 'Debug monitoring status updated.', - 'is_enabled' => $is_enabled - ]); -} - -/** - * Test AJAX endpoint - */ -function igny8_test_ajax() { - wp_send_json_success('AJAX is working!'); -} - -/** - * AJAX handler for viewing cluster keywords (modal display) - */ -add_action('wp_ajax_igny8_view_cluster_keywords', 'igny8_ajax_view_cluster_keywords'); -function igny8_ajax_view_cluster_keywords() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - // Get parameters - $cluster_id = intval($_POST['cluster_id'] ?? 0); - - if (empty($cluster_id)) { - wp_send_json_error(['message' => 'No cluster ID provided.']); - } - - // Call view function - $result = igny8_view_cluster_keywords($cluster_id); - - if ($result['success']) { - wp_send_json_success([ - 'keywords' => $result['keywords'], - 'cluster_name' => $result['cluster_name'], - 'message' => $result['message'] - ]); - } else { - wp_send_json_error(['message' => $result['message']]); - } -} - -/** - * AJAX handler for creating draft from idea - */ -add_action('wp_ajax_igny8_create_draft_from_idea', 'igny8_ajax_create_draft_from_idea'); -function igny8_ajax_create_draft_from_idea() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - // Get parameters - $idea_id = intval($_POST['idea_id'] ?? 0); - - if (empty($idea_id)) { - wp_send_json_error(['message' => 'No idea ID provided.']); - } - - // Call create draft function - $result = igny8_create_draft_from_idea($idea_id); - - if ($result['success']) { - wp_send_json_success([ - 'message' => $result['message'], - 'draft_id' => $result['draft_id'] - ]); - } else { - wp_send_json_error(['message' => $result['message']]); - } -} - -/** - * AJAX handler for mapping cluster to keywords - */ -add_action('wp_ajax_igny8_map_cluster_to_keywords', 'igny8_ajax_map_cluster_to_keywords'); -function igny8_ajax_map_cluster_to_keywords() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - // Get parameters - $cluster_id = intval($_POST['cluster_id'] ?? 0); - $keyword_ids = $_POST['keyword_ids'] ?? []; - - if (empty($cluster_id)) { - wp_send_json_error(['message' => 'No cluster ID provided.']); - } - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - wp_send_json_error(['message' => 'No keywords provided for mapping.']); - } - - // Call map function - $result = igny8_map_cluster_to_keywords($cluster_id, $keyword_ids); - - if ($result['success']) { - wp_send_json_success([ - 'message' => $result['message'], - 'mapped_count' => $result['mapped_count'] - ]); - } else { - wp_send_json_error(['message' => $result['message']]); - } -} - -// ========================================= -// Planner Settings AJAX Handlers -// ========================================= - -/** - * Get parent sectors (sectors with no parent) - */ -add_action('wp_ajax_igny8_get_parent_sectors', 'igny8_ajax_get_parent_sectors'); -function igny8_ajax_get_parent_sectors() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Check if sectors taxonomy exists, if not try to register it - if (!taxonomy_exists('sectors')) { - // Try to register the taxonomy - if (function_exists('igny8_register_taxonomies')) { - igny8_register_taxonomies(); - } - - // Check again - if (!taxonomy_exists('sectors')) { - wp_send_json_error(['message' => 'Sectors taxonomy not registered. Please create sample sectors first.']); - } - } - - // Get parent sectors (terms with parent = 0) - $parent_sectors = get_terms([ - 'taxonomy' => 'sectors', - 'parent' => 0, - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC' - ]); - - if (is_wp_error($parent_sectors)) { - wp_send_json_error(['message' => 'Error retrieving parent sectors: ' . $parent_sectors->get_error_message()]); - } - - $sectors_data = []; - foreach ($parent_sectors as $sector) { - $sectors_data[] = [ - 'id' => $sector->term_id, - 'name' => $sector->name, - 'slug' => $sector->slug - ]; - } - - wp_send_json_success($sectors_data); -} - -/** - * Get child sectors for a specific parent - */ -add_action('wp_ajax_igny8_get_child_sectors', 'igny8_ajax_get_child_sectors'); -function igny8_ajax_get_child_sectors() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $parent_id = intval($_POST['parent_id']); - if (!$parent_id) { - wp_send_json_error(['message' => 'Invalid parent ID']); - } - - // Check if sectors taxonomy exists, if not try to register it - if (!taxonomy_exists('sectors')) { - // Try to register the taxonomy - if (function_exists('igny8_register_taxonomies')) { - igny8_register_taxonomies(); - } - - // Check again - if (!taxonomy_exists('sectors')) { - wp_send_json_error(['message' => 'Sectors taxonomy not registered. Please create sample sectors first.']); - } - } - - // Get child sectors - $child_sectors = get_terms([ - 'taxonomy' => 'sectors', - 'parent' => $parent_id, - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC' - ]); - - if (is_wp_error($child_sectors)) { - wp_send_json_error(['message' => 'Error retrieving child sectors: ' . $child_sectors->get_error_message()]); - } - - $sectors_data = []; - foreach ($child_sectors as $sector) { - $sectors_data[] = [ - 'id' => $sector->term_id, - 'name' => $sector->name, - 'slug' => $sector->slug - ]; - } - - wp_send_json_success($sectors_data); -} - -/** - * Save sector selection - */ -add_action('wp_ajax_igny8_save_sector_selection', 'igny8_ajax_save_sector_selection'); -function igny8_ajax_save_sector_selection() { - try { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $parent_id = intval($_POST['parent_id']); - $children_count = intval($_POST['children_count']); - - if (!$parent_id || $children_count <= 0) { - wp_send_json_error(['message' => 'Invalid data provided']); - } - - // Build children array from individual form fields - $children = []; - for ($i = 0; $i < $children_count; $i++) { - $child_id = intval($_POST["child_{$i}_id"]); - $child_name = sanitize_text_field($_POST["child_{$i}_name"]); - - if ($child_id && $child_name) { - $children[] = [ - 'id' => $child_id, - 'name' => $child_name - ]; - } - } - - if (empty($children)) { - wp_send_json_error(['message' => 'No valid children data received']); - } - - // Check if sectors taxonomy exists, if not try to register it - if (!taxonomy_exists('sectors')) { - // Try to register the taxonomy - if (function_exists('igny8_register_taxonomies')) { - igny8_register_taxonomies(); - } - - // Check again - if (!taxonomy_exists('sectors')) { - wp_send_json_error(['message' => 'Sectors taxonomy not registered. Please create sample sectors first.']); - } - } - - // Get parent sector info - $parent_sector = get_term($parent_id, 'sectors'); - if (is_wp_error($parent_sector) || !$parent_sector) { - $error_msg = is_wp_error($parent_sector) ? $parent_sector->get_error_message() : 'Sector not found'; - wp_send_json_error(['message' => 'Invalid parent sector: ' . $error_msg]); - } - - // Validate children sectors - $validated_children = []; - foreach ($children as $child) { - if (isset($child['id']) && isset($child['name'])) { - $child_term = get_term($child['id'], 'sectors'); - - if (!is_wp_error($child_term) && $child_term && $child_term->parent == $parent_id) { - $validated_children[] = [ - 'id' => $child['id'], - 'name' => $child['name'] - ]; - } - } - } - - if (empty($validated_children)) { - wp_send_json_error(['message' => 'No valid child sectors selected. Please ensure the selected sectors exist and belong to the parent sector.']); - } - - // Save to user options - $selection_data = [ - 'parent' => [ - 'id' => $parent_sector->term_id, - 'name' => $parent_sector->name - ], - 'children' => $validated_children, - 'saved_at' => current_time('mysql') - ]; - - $user_id = get_current_user_id(); - update_user_meta($user_id, 'igny8_planner_sector_selection', $selection_data); - - wp_send_json_success($selection_data); - - } catch (Exception $e) { - error_log('DEBUG: Exception in save_sector_selection: ' . $e->getMessage()); - wp_send_json_error(['message' => 'Server error: ' . $e->getMessage()]); - } -} - -/** - * Get saved sector selection - */ -add_action('wp_ajax_igny8_get_saved_sector_selection', 'igny8_ajax_get_saved_sector_selection'); -function igny8_ajax_get_saved_sector_selection() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $user_id = get_current_user_id(); - $saved_selection = get_user_meta($user_id, 'igny8_planner_sector_selection', true); - - if (empty($saved_selection)) { - wp_send_json_success(null); - } - - // Validate that the saved sectors still exist - $parent_id = $saved_selection['parent']['id']; - $parent_sector = get_term($parent_id, 'sectors'); - - if (is_wp_error($parent_sector) || !$parent_sector) { - // Parent sector no longer exists, clear the selection - delete_user_meta($user_id, 'igny8_planner_sector_selection'); - wp_send_json_success(null); - } - - // Validate children - $valid_children = []; - foreach ($saved_selection['children'] as $child) { - $child_term = get_term($child['id'], 'sectors'); - if (!is_wp_error($child_term) && $child_term && $child_term->parent == $parent_id) { - $valid_children[] = $child; - } - } - - if (empty($valid_children)) { - // No valid children, clear the selection - delete_user_meta($user_id, 'igny8_planner_sector_selection'); - wp_send_json_success(null); - } - - // Update with validated data - $saved_selection['children'] = $valid_children; - update_user_meta($user_id, 'igny8_planner_sector_selection', $saved_selection); - - wp_send_json_success($saved_selection); -} - -/** - * Get AI operation progress - */ -add_action('wp_ajax_igny8_get_ai_progress', 'igny8_ajax_get_ai_progress'); -function igny8_ajax_get_ai_progress() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $operation = sanitize_text_field($_POST['operation'] ?? ''); - $session_id = sanitize_text_field($_POST['session_id'] ?? ''); - - if (empty($operation) || empty($session_id)) { - wp_send_json_error(['message' => 'Missing operation or session ID']); - } - - // Get recent AI logs for this operation - $ai_logs = get_option('igny8_ai_logs', []); - $operation_logs = array_filter($ai_logs, function($log) use ($operation, $session_id) { - return isset($log['action']) && $log['action'] === $operation && - isset($log['details']) && strpos($log['details'], $session_id) !== false; - }); - - // Count completed items based on success logs - $completed = 0; - $total = 0; - $current_message = 'Processing...'; - - foreach ($operation_logs as $log) { - if (isset($log['event'])) { - if (strpos($log['event'], 'Complete') !== false || strpos($log['event'], 'Created') !== false) { - $completed++; - } - if (strpos($log['event'], 'Initiated') !== false) { - // Extract total count from initiation log - if (preg_match('/(\d+)\s+(keywords|clusters|ideas|tasks)/', $log['details'], $matches)) { - $total = intval($matches[1]); - } - } - } - } - - wp_send_json_success([ - 'completed' => $completed, - 'total' => $total, - 'message' => $current_message, - 'is_complete' => $completed >= $total && $total > 0 - ]); -} - -/** - * Save AI Integration Settings - */ -add_action('wp_ajax_igny8_save_ai_integration_settings', 'igny8_ajax_save_ai_integration_settings'); -function igny8_ajax_save_ai_integration_settings() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Get and sanitize form data - $planner_mode = sanitize_text_field($_POST['igny8_planner_mode'] ?? 'manual'); - $clustering = sanitize_text_field($_POST['igny8_ai_clustering'] ?? 'enabled'); - $ideas = sanitize_text_field($_POST['igny8_ai_ideas'] ?? 'enabled'); - $mapping = sanitize_text_field($_POST['igny8_ai_mapping'] ?? 'enabled'); - - // Automation settings - $auto_cluster = sanitize_text_field($_POST['igny8_auto_cluster_enabled'] ?? 'disabled'); - $auto_generate_ideas = sanitize_text_field($_POST['igny8_auto_generate_ideas_enabled'] ?? 'disabled'); - $auto_queue = sanitize_text_field($_POST['igny8_auto_queue_enabled'] ?? 'disabled'); - - // Validate values - $valid_modes = ['manual', 'ai']; - $valid_values = ['enabled', 'disabled']; - - if (!in_array($planner_mode, $valid_modes)) { - wp_send_json_error(['message' => 'Invalid planner mode']); - } - - if (!in_array($clustering, $valid_values) || !in_array($ideas, $valid_values) || !in_array($mapping, $valid_values)) { - wp_send_json_error(['message' => 'Invalid setting values']); - } - - if (!in_array($auto_cluster, $valid_values) || !in_array($auto_generate_ideas, $valid_values) || !in_array($auto_queue, $valid_values)) { - wp_send_json_error(['message' => 'Invalid automation setting values']); - } - - // Save settings using new AI settings system - igny8_update_ai_setting('planner_mode', $planner_mode); - igny8_update_ai_setting('clustering', $clustering); - igny8_update_ai_setting('ideas', $ideas); - igny8_update_ai_setting('mapping', $mapping); - - // Save automation settings - igny8_update_ai_setting('auto_cluster_enabled', $auto_cluster); - igny8_update_ai_setting('auto_generate_ideas_enabled', $auto_generate_ideas); - igny8_update_ai_setting('auto_queue_enabled', $auto_queue); - - // Schedule/unschedule automation based on settings - igny8_manage_automation_schedules(); - - wp_send_json_success(['message' => 'AI Integration settings saved successfully']); -} - -/** - * AI Integration Settings - Save Writer AI settings - */ -add_action('wp_ajax_igny8_save_writer_ai_settings', 'igny8_ajax_save_writer_ai_settings'); - -/** - * Regenerate CRON key - */ -add_action('wp_ajax_igny8_regenerate_cron_key', 'igny8_ajax_regenerate_cron_key'); -function igny8_ajax_regenerate_cron_key() { - try { - // Verify nonce (use planner nonce since it's the same for both modules) - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings') && !wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Generate new key - $new_key = wp_generate_password(32, false, false); - update_option('igny8_secure_cron_key', $new_key); - - wp_send_json_success(['new_key' => $new_key, 'message' => 'CRON key regenerated successfully']); - } catch (Exception $e) { - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} - -function igny8_ajax_save_writer_ai_settings() { - try { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Get and sanitize form data - $writer_mode = sanitize_text_field($_POST['igny8_writer_mode'] ?? 'manual'); - $content_generation = sanitize_text_field($_POST['igny8_content_generation'] ?? 'enabled'); - - // Writer automation settings - $auto_generate_content = sanitize_text_field($_POST['igny8_auto_generate_content_enabled'] ?? 'disabled'); - $auto_publish_drafts = sanitize_text_field($_POST['igny8_auto_publish_drafts_enabled'] ?? 'disabled'); - - // Validate values - $valid_modes = ['manual', 'ai']; - $valid_values = ['enabled', 'disabled']; - - if (!in_array($writer_mode, $valid_modes)) { - wp_send_json_error(['message' => 'Invalid writer mode']); - } - - if (!in_array($content_generation, $valid_values)) { - wp_send_json_error(['message' => 'Invalid content generation setting']); - } - - if (!in_array($auto_generate_content, $valid_values) || !in_array($auto_publish_drafts, $valid_values)) { - wp_send_json_error(['message' => 'Invalid automation setting values']); - } - - // Save settings using new AI settings system - igny8_update_ai_setting('writer_mode', $writer_mode); - igny8_update_ai_setting('content_generation', $content_generation); - - // Save Writer automation settings - igny8_update_ai_setting('auto_generate_content_enabled', $auto_generate_content); - igny8_update_ai_setting('auto_publish_drafts_enabled', $auto_publish_drafts); - - // Schedule/unschedule Writer automation based on settings - igny8_manage_writer_automation_schedules(); - - wp_send_json_success(['message' => 'Writer AI settings saved successfully']); - } catch (Exception $e) { - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} - -/** - * Save Writer Content Generation Prompt - */ -add_action('wp_ajax_igny8_save_content_prompt', 'igny8_ajax_save_content_prompt'); -function igny8_ajax_save_content_prompt() { - try { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Get and sanitize prompt data - $content_prompt = wp_unslash($_POST['igny8_content_generation_prompt'] ?? ''); - - // Save prompt using AI settings system - igny8_update_ai_setting('content_generation_prompt', $content_prompt); - - wp_send_json_success(['message' => 'Content generation prompt saved successfully']); - } catch (Exception $e) { - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} - -/** - * AI Content Generation - Generate content from idea and keywords - */ -add_action('wp_ajax_igny8_ai_generate_content', 'igny8_ajax_ai_generate_content'); -function igny8_ajax_ai_generate_content() { - try { - // Debug logging for CRON context - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Starting content generation process"); - } - - // Verify nonce - accept multiple nonce types for compatibility - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - - if (!$nonce_valid) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Security check failed - invalid nonce"); - } - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Insufficient permissions"); - } - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Check if AI mode is enabled - if (igny8_get_ai_setting('writer_mode', 'manual') !== 'ai') { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: AI mode is not enabled"); - } - wp_send_json_error(['message' => 'AI mode is not enabled']); - } - - // Get task ID - $task_id = absint($_POST['task_id'] ?? 0); - if (!$task_id) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Invalid task ID"); - } - wp_send_json_error(['message' => 'Invalid task ID']); - } - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Processing task ID: " . $task_id); - } - - global $wpdb; - - // Get task details - $task = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", - $task_id - )); - - if (!$task) { - wp_send_json_error(['message' => 'Task not found']); - } - - // Get idea details if available - $idea = null; - if ($task->idea_id) { - $idea = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}igny8_content_ideas WHERE id = %d", - $task->idea_id - )); - } - - // Get cluster details if available - $cluster = null; - if ($task->cluster_id) { - $cluster = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $task->cluster_id - )); - } - - // Get keywords - prefer target_keywords from idea, fallback to cluster keywords - $keywords = []; - if ($idea && !empty($idea->target_keywords)) { - // Use target_keywords from the idea (comma-separated format) - $target_keywords_array = array_map('trim', explode(',', $idea->target_keywords)); - foreach ($target_keywords_array as $keyword) { - if (!empty($keyword)) { - $keywords[] = (object)['keyword' => $keyword]; - } - } - } elseif ($task->cluster_id) { - // Fallback to all keywords from cluster - $keywords = $wpdb->get_results($wpdb->prepare( - "SELECT keyword FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id = %d", - $task->cluster_id - )); - } - - // Get desktop quantity setting for image generation - $desktop_quantity = get_option('igny8_desktop_quantity', 1); - - // Prepare data for AI processing - $ai_data = [ - 'idea' => $idea, - 'cluster' => $cluster, - 'keywords' => $keywords, - 'task_id' => $task_id, - 'desktop_quantity' => $desktop_quantity - ]; - - // Generate session ID for progress tracking - $session_id = 'content_' . time() . '_' . wp_generate_password(8, false); - - // Log AI request initiation - igny8_log_ai_event('AI Content Generation Initiated', 'writer', 'content_generation', 'info', 'Starting content generation for task', 'Task ID: ' . $task_id . ', Session: ' . $session_id); - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: AI data prepared - Idea: " . ($idea ? 'Yes' : 'No') . ", Cluster: " . ($cluster ? 'Yes' : 'No') . ", Keywords: " . count($keywords)); - } - - // Get content generation prompt from database (same as prompts page) - $prompt_template = igny8_get_ai_setting('content_generation_prompt', igny8_content_generation_prompt()); - - // Check if prompt is loaded from database field - $db_prompt = igny8_get_ai_setting('content_generation_prompt', ''); - if (empty($db_prompt)) { - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Wrong prompt detected - database field is empty"); - } - wp_send_json_error(['message' => 'Wrong prompt detected - database field is empty']); - } - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Prompt template loaded from database, length: " . strlen($prompt_template)); - } - - // Process with AI - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: Calling igny8_process_ai_request..."); - } - $ai_result = igny8_process_ai_request('content_generation', $ai_data, $prompt_template); - - if (defined('DOING_CRON') && DOING_CRON) { - error_log("Igny8 AI Generation: AI result received: " . ($ai_result ? 'Success' : 'Failed')); - if ($ai_result) { - error_log("Igny8 AI Generation: AI result type: " . gettype($ai_result)); - if (is_array($ai_result)) { - error_log("Igny8 AI Generation: AI result keys: " . implode(', ', array_keys($ai_result))); - } - } else { - error_log("Igny8 AI Generation: AI result is false/null - checking why..."); - } - } - - if (!$ai_result) { - igny8_log_ai_event('AI Content Generation Failed', 'writer', 'content_generation', 'error', 'AI processing returned false', 'Check OpenAI API configuration'); - wp_send_json_error(['message' => 'AI content generation failed - no result']); - } - - // Parse and validate AI response - if (!isset($ai_result['content']) || !isset($ai_result['title'])) { - igny8_log_ai_event('AI Content Generation Failed', 'writer', 'content_generation', 'error', 'AI returned invalid response structure', 'Missing content or title fields'); - wp_send_json_error(['message' => 'AI returned invalid content structure']); - } - - // Save raw AI response content to tasks table - $wpdb->update( - $wpdb->prefix . 'igny8_tasks', - ['raw_ai_response' => $ai_result['content']], - ['id' => $task_id], - ['%s'], - ['%d'] - ); - - // Get new content decision setting (will be used after successful post creation) - $new_content_action = get_option('igny8_new_content_action', 'draft'); - - // Debug logging - error_log('Igny8 DEBUG: Content decision setting: ' . $new_content_action); - - // Prepare update data for task (status will be set after post creation) - $update_data = [ - 'description' => $ai_result['content'], - 'updated_at' => current_time('mysql') - ]; - - // Add title if provided - if (!empty($ai_result['title'])) { - $update_data['title'] = $ai_result['title']; - } - - // Add meta fields if provided - if (!empty($ai_result['meta_title'])) { - $update_data['meta_title'] = $ai_result['meta_title']; - } - if (!empty($ai_result['meta_description'])) { - $update_data['meta_description'] = $ai_result['meta_description']; - } - if (!empty($ai_result['keywords'])) { - $update_data['keywords'] = $ai_result['keywords']; - } - if (!empty($ai_result['word_count'])) { - $update_data['word_count'] = $ai_result['word_count']; - } - - // Add task_id to AI result for taxonomy association - $ai_result['task_id'] = $task_id; - - // Create WordPress post from AI response - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN AJAX.PHP - About to call igny8_create_post_from_ai_response()"); - $post_id = igny8_create_post_from_ai_response($ai_result); - error_log("IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN AJAX.PHP - igny8_create_post_from_ai_response() returned: " . ($post_id ? $post_id : 'false')); - - // Check taxonomy association results - $cluster_success = false; - $sector_success = false; - - if ($post_id) { - // Determine task status based on content decision setting AFTER successful post creation - $task_status = ($new_content_action === 'publish') ? 'completed' : 'draft'; - error_log('Igny8 DEBUG: Task status set to: ' . $task_status . ' after successful post creation'); - - // Link the WordPress post to the task - update_post_meta($post_id, '_igny8_task_id', $task_id); - - // Update task with WordPress post ID and mark as completed - $wpdb->update( - $wpdb->prefix . 'igny8_tasks', - [ - 'assigned_post_id' => $post_id, - 'status' => $task_status, - 'updated_at' => current_time('mysql') - ], - ['id' => $task_id], - ['%d', '%s', '%s'], - ['%d'] - ); - - // Update task with generated content - $wpdb->update( - $wpdb->prefix . 'igny8_tasks', - $update_data, - ['id' => $task_id], - ['%s', '%s', '%s', '%s'], - ['%d'] - ); - - // Check if taxonomies were actually associated - $cluster_terms = wp_get_object_terms($post_id, 'clusters'); - $sector_terms = wp_get_object_terms($post_id, 'sectors'); - $cluster_success = !empty($cluster_terms) && !is_wp_error($cluster_terms); - $sector_success = !empty($sector_terms) && !is_wp_error($sector_terms); - - igny8_log_ai_event('WordPress Post Created', 'writer', 'content_generation', 'success', 'WordPress post created from AI content', 'Post ID: ' . $post_id . ', Task ID: ' . $task_id); - igny8_log_ai_event('AI Content Generation Complete', 'writer', 'content_generation', 'success', 'Content generated and saved to task', 'Task ID: ' . $task_id . ', Word count: ' . ($ai_result['word_count'] ?? 'unknown')); - } else { - // Log failure but DO NOT change task status - keep it as draft - igny8_log_ai_event('WordPress Post Creation Failed', 'writer', 'content_generation', 'error', 'Failed to create WordPress post from AI content', 'Task ID: ' . $task_id); - igny8_log_ai_event('AI Content Generation Failed', 'writer', 'content_generation', 'error', 'Content generation failed - post creation unsuccessful', 'Task ID: ' . $task_id); - } - - // Check if this is a CRON request (no AJAX) - if (defined('DOING_CRON') || (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'wp-load.php') !== false)) { - // For CRON requests, don't send JSON, just return success - echo "Igny8 CRON: Content generation completed for task " . $task_id . "
      "; - return; - } - - wp_send_json_success([ - 'message' => 'Content generated successfully', - 'content' => 'original post content', - 'title' => $ai_result['title'] ?? $task->title, - 'word_count' => $ai_result['word_count'] ?? 'unknown', - 'meta_description' => $ai_result['meta_description'] ?? '', - 'seo_score' => $ai_result['seo_score'] ?? 'unknown', - 'post_id' => $post_id ?? null, - 'post_edit_url' => $post_id ? get_edit_post_link($post_id) : null, - 'task_status' => $task_status ?? 'failed', - 'session_id' => $session_id - ]); - - } catch (Exception $e) { - igny8_log_ai_event('AI Content Generation Error', 'writer', 'content_generation', 'error', 'Exception during content generation', $e->getMessage()); - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} - -/** - * AI Clustering - Process keywords into clusters - */ -add_action('wp_ajax_igny8_ai_cluster_keywords', 'igny8_ajax_ai_cluster_keywords'); -function igny8_ajax_ai_cluster_keywords() { - // Add detailed logging for cron debugging - if (defined('DOING_AJAX') && DOING_AJAX) { - error_log('Igny8 AJAX: Function started - AJAX context detected'); - } else { - error_log('Igny8 AJAX: Function started - Non-AJAX context (cron)'); - } - - // Ensure we're in an AJAX context and prevent HTML output - // Skip AJAX check for cron context (external cron URLs) - if (!wp_doing_ajax() && !defined('DOING_AJAX')) { - error_log('Igny8 AJAX: wp_doing_ajax() returned false, calling wp_die'); - wp_die('Invalid request'); - } - - // If we're in cron context, we're good to proceed - if (defined('DOING_AJAX')) { - error_log('Igny8 AJAX: Cron context detected, bypassing AJAX validation'); - } - - error_log('Igny8 AJAX: Passed wp_doing_ajax() check'); - - try { - error_log('Igny8 AJAX: Starting nonce validation'); - // Verify nonce - accept multiple nonce types for compatibility - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - - error_log('Igny8 AJAX: Nonce validation result: ' . ($nonce_valid ? 'VALID' : 'INVALID')); - - if (!$nonce_valid) { - error_log('Igny8 AJAX: Nonce validation failed, sending error'); - wp_send_json_error(['message' => 'Security check failed']); - } - - error_log('Igny8 AJAX: Starting user permission check'); - // Check user permissions - $user_can = current_user_can('manage_options'); - error_log('Igny8 AJAX: User permission check result: ' . ($user_can ? 'HAS PERMISSION' : 'NO PERMISSION')); - - if (!$user_can) { - error_log('Igny8 AJAX: User permission check failed, sending error'); - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - error_log('Igny8 AJAX: Passed user permission check'); - - // Check if AI mode is enabled - error_log('Igny8 AJAX: Checking AI mode'); - $planner_mode = igny8_get_ai_setting('planner_mode', 'manual'); - error_log('Igny8 AJAX: Planner mode: ' . $planner_mode); - - if ($planner_mode !== 'ai') { - error_log('Igny8 AJAX: AI mode check failed, sending error'); - wp_send_json_error(['message' => 'AI mode is not enabled']); - } - - error_log('Igny8 AJAX: Passed AI mode check'); - - // Check if clustering is enabled - error_log('Igny8 AJAX: Checking clustering setting'); - $clustering_enabled = igny8_get_ai_setting('clustering', 'enabled'); - error_log('Igny8 AJAX: Clustering setting: ' . $clustering_enabled); - - if ($clustering_enabled !== 'enabled') { - error_log('Igny8 AJAX: Clustering check failed, sending error'); - wp_send_json_error(['message' => 'Clustering feature is disabled']); - } - - error_log('Igny8 AJAX: Passed clustering check'); - - // Check if sector is selected (reuse existing function) - error_log('Igny8 AJAX: Checking sector options'); - $sector_options = igny8_get_sector_options(); - error_log('Igny8 AJAX: Sector options count: ' . count($sector_options)); - - if (empty($sector_options)) { - error_log('Igny8 AJAX: No sectors found, sending error'); - wp_send_json_error(['message' => 'You must select a Sector before performing Auto Clustering.']); - } - - error_log('Igny8 AJAX: Passed sector check'); - - // Handle keyword_ids - it comes as JSON string from JavaScript - error_log('Igny8 AJAX: Processing keyword IDs'); - $keyword_ids_raw = $_POST['keyword_ids'] ?? []; - error_log('Igny8 AJAX: Raw keyword IDs: ' . print_r($keyword_ids_raw, true)); - - if (is_string($keyword_ids_raw)) { - $keyword_ids = json_decode($keyword_ids_raw, true) ?: []; - error_log('Igny8 AJAX: Decoded keyword IDs: ' . print_r($keyword_ids, true)); - } else { - $keyword_ids = $keyword_ids_raw; - } - $keyword_ids = array_map('intval', $keyword_ids); - error_log('Igny8 AJAX: Final keyword IDs: ' . print_r($keyword_ids, true)); - - if (empty($keyword_ids)) { - error_log('Igny8 AJAX: No keywords found, sending error'); - wp_send_json_error(['message' => 'No keywords selected']); - } - - error_log('Igny8 AJAX: Passed keyword validation'); - - // Limit to 20 keywords max - if (count($keyword_ids) > 20) { - wp_send_json_error(['message' => 'Maximum 20 keywords allowed for clustering']); - } - - // Get keywords data and check if they already have clusters assigned - global $wpdb; - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $keywords = $wpdb->get_results($wpdb->prepare(" - SELECT * FROM {$wpdb->prefix}igny8_keywords - WHERE id IN ({$placeholders}) - ", $keyword_ids)); - - if (empty($keywords)) { - wp_send_json_error(['message' => 'No valid keywords found']); - } - - // Check if keywords already have clusters assigned - $keywords_with_clusters = array_filter($keywords, function($keyword) { - return !empty($keyword->cluster_id) && $keyword->cluster_id > 0; - }); - - if (!empty($keywords_with_clusters)) { - $keyword_names = array_column($keywords_with_clusters, 'keyword'); - wp_send_json_error(['message' => 'Keywords already have clusters assigned: ' . implode(', ', array_slice($keyword_names, 0, 3)) . (count($keyword_names) > 3 ? '...' : '')]); - } - - // Get clustering prompt - $prompt_template = wp_unslash(igny8_get_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt())); - - // Generate session ID for progress tracking - $session_id = 'clustering_' . time() . '_' . wp_generate_password(8, false); - - // Log AI request initiation - igny8_log_ai_event('AI Request Initiated', 'planner', 'clustering', 'info', 'Starting AI clustering process', 'Keywords: ' . count($keyword_ids) . ', Session: ' . $session_id); - - // Log data preparation - igny8_log_ai_event('Data Preparation', 'planner', 'clustering', 'info', 'Preparing keywords data for AI', 'Keywords count: ' . count($keywords) . ', Prompt length: ' . strlen($prompt_template)); - - // Process with AI - error_log('Igny8 AI: Starting AI processing with ' . count($keywords) . ' keywords'); - error_log('Igny8 AJAX: About to call igny8_process_ai_request'); - $ai_result = igny8_process_ai_request('clustering', $keywords, $prompt_template); - error_log('Igny8 AJAX: AI processing completed, result type: ' . gettype($ai_result)); - - // Log detailed AI processing result - if ($ai_result === false) { - igny8_log_ai_event('AI Processing Failed', 'planner', 'clustering', 'error', 'AI processing returned false', 'Check OpenAI API configuration'); - } elseif (is_array($ai_result) && isset($ai_result['clusters'])) { - igny8_log_ai_event('AI Processing Complete', 'planner', 'clustering', 'success', 'AI returned ' . count($ai_result['clusters']) . ' clusters', 'Clusters: ' . json_encode(array_column($ai_result['clusters'], 'name'))); - } elseif (is_array($ai_result)) { - igny8_log_ai_event('AI Processing Failed', 'planner', 'clustering', 'error', 'AI returned array but missing clusters key', 'Result keys: ' . json_encode(array_keys($ai_result))); - } else { - igny8_log_ai_event('AI Processing Failed', 'planner', 'clustering', 'error', 'AI returned invalid data type', 'Type: ' . gettype($ai_result) . ', Value: ' . json_encode($ai_result)); - } - - if (!$ai_result) { - error_log('Igny8 AI: AI processing returned false'); - wp_send_json_error(['message' => 'AI processing failed - no result']); - } - - if (!isset($ai_result['clusters'])) { - error_log('Igny8 AI: AI result missing clusters array. Result: ' . print_r($ai_result, true)); - wp_send_json_error(['message' => 'AI processing failed - invalid result format']); - } - - // Log database operations start - igny8_log_ai_event('Database Operations Started', 'planner', 'clustering', 'info', 'Starting to create clusters in database', 'Clusters to create: ' . count($ai_result['clusters'])); - - // Get sector options for assignment logic - $sector_options = igny8_get_sector_options(); - $sector_count = count($sector_options); - - // Create clusters in database - $created_clusters = []; - foreach ($ai_result['clusters'] as $cluster_data) { - // Determine sector_id based on sector count - $sector_id = 1; // Default fallback - - if ($sector_count == 1) { - // Only 1 sector: assign all clusters to that sector - $sector_id = $sector_options[0]['value']; - } elseif ($sector_count > 1) { - // Multiple sectors: use AI response sector assignment - if (isset($cluster_data['sector']) && !empty($cluster_data['sector'])) { - // Find sector ID by matching sector name from AI response - foreach ($sector_options as $sector) { - if (strtolower(trim($sector['label'])) === strtolower(trim($cluster_data['sector']))) { - $sector_id = $sector['value']; - break; - } - } - } - // If no match found or no sector in AI response, use first sector as fallback - if ($sector_id == 1 && !isset($cluster_data['sector'])) { - $sector_id = $sector_options[0]['value']; - } - } - - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_clusters', - [ - 'cluster_name' => sanitize_text_field($cluster_data['name']), - 'sector_id' => $sector_id, - 'status' => 'active', - 'keyword_count' => count($cluster_data['keywords']), - 'total_volume' => 0, - 'avg_difficulty' => 0, - 'mapped_pages_count' => 0, - 'created_at' => current_time('mysql') - ], - ['%s', '%d', '%s', '%d', '%d', '%f', '%d', '%s'] - ); - - if ($result) { - $cluster_id = $wpdb->insert_id; - $created_clusters[] = $cluster_id; - - // Trigger taxonomy term creation for AI-generated cluster - do_action('igny8_cluster_added', $cluster_id); - igny8_log_ai_event('Cluster Taxonomy Triggered', 'planner', 'clustering', 'info', 'Triggered igny8_cluster_added action', "Cluster: {$cluster_data['name']} (ID: {$cluster_id})"); - - // Log cluster creation - igny8_log_ai_event('Cluster Created', 'planner', 'clustering', 'success', 'Cluster created successfully', "Cluster: {$cluster_data['name']} (ID: {$cluster_id})"); - - // Update keywords with cluster_id - foreach ($cluster_data['keywords'] as $keyword_name) { - $update_result = $wpdb->update( - $wpdb->prefix . 'igny8_keywords', - ['cluster_id' => $cluster_id], - ['keyword' => $keyword_name], - ['%d'], - ['%s'] - ); - - // Log if keyword update failed - if ($update_result === false) { - error_log("Igny8 AI: Failed to update keyword '{$keyword_name}' with cluster_id {$cluster_id}. Error: " . $wpdb->last_error); - } - } - - // Log keyword updates - igny8_log_ai_event('Keywords Updated', 'planner', 'clustering', 'success', 'Keywords assigned to cluster', "Cluster: {$cluster_data['name']}, Keywords: " . count($cluster_data['keywords'])); - - // Update cluster metrics - try { - $metrics_result = igny8_update_cluster_metrics($cluster_id); - if ($metrics_result) { - igny8_log_ai_event('Metrics Updated', 'planner', 'clustering', 'success', 'Cluster metrics calculated', "Cluster: {$cluster_data['name']}"); - } else { - igny8_log_ai_event('Metrics Update Failed', 'planner', 'clustering', 'warning', 'Failed to update cluster metrics', "Cluster: {$cluster_data['name']}"); - } - } catch (Exception $e) { - igny8_log_ai_event('Metrics Update Error', 'planner', 'clustering', 'error', 'Exception during metrics update', "Cluster: {$cluster_data['name']}, Error: " . $e->getMessage()); - error_log("Igny8 AI: Metrics update error for cluster {$cluster_id}: " . $e->getMessage()); - } - } else { - // Log cluster creation failure - igny8_log_ai_event('Cluster Creation Failed', 'planner', 'clustering', 'error', 'Failed to create cluster in database', "Cluster: {$cluster_data['name']}, Error: " . $wpdb->last_error); - } - } - - // Log completion - igny8_log_ai_event('AI Clustering Complete', 'planner', 'clustering', 'success', 'AI clustering process completed successfully', "Clusters created: " . count($created_clusters)); - - wp_send_json_success([ - 'message' => 'Successfully created ' . count($created_clusters) . ' clusters', - 'clusters_created' => count($created_clusters), - 'session_id' => $session_id - ]); - } catch (Exception $e) { - // Log error - igny8_log_ai_event('AI Clustering Error', 'planner', 'clustering', 'error', 'Exception during AI clustering process', $e->getMessage()); - - error_log('Igny8 AI Clustering Error: ' . $e->getMessage()); - wp_send_json_error(['message' => 'Error processing AI clustering: ' . $e->getMessage()]); - } -} - -/** - * Save Prompt - AJAX handler for saving prompts - */ -add_action('wp_ajax_igny8_save_prompt', 'igny8_ajax_save_prompt'); -function igny8_ajax_save_prompt() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_thinker_settings')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $prompt_type = sanitize_text_field($_POST['prompt_type'] ?? ''); - $prompt_value = sanitize_textarea_field($_POST['prompt_value'] ?? ''); - - if (empty($prompt_type) || empty($prompt_value)) { - wp_send_json_error('Prompt type and value are required'); - } - - // Validate prompt type - $valid_prompt_types = [ - 'clustering_prompt', - 'ideas_prompt', - 'content_generation_prompt', - 'image_prompt_template', - 'negative_prompt', - ]; - - if (!in_array($prompt_type, $valid_prompt_types)) { - wp_send_json_error('Invalid prompt type'); - } - - // Save the prompt using appropriate method - if (in_array($prompt_type, ['image_prompt_template', 'negative_prompt'])) { - // Image prompts are stored as regular WordPress options - update_option('igny8_' . $prompt_type, $prompt_value); - } else { - // AI prompts use the AI settings system - igny8_update_ai_setting($prompt_type, $prompt_value); - } - - wp_send_json_success('Prompt saved successfully'); -} - -/** - * Reset Individual Prompt - AJAX handler for resetting individual prompts - */ -add_action('wp_ajax_igny8_reset_prompt', 'igny8_ajax_reset_prompt'); -function igny8_ajax_reset_prompt() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_thinker_settings')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $prompt_type = sanitize_text_field($_POST['prompt_type'] ?? ''); - - if (empty($prompt_type)) { - wp_send_json_error('Prompt type is required'); - } - - // Validate prompt type - $valid_prompt_types = [ - 'clustering_prompt', - 'ideas_prompt', - 'content_generation_prompt', - ]; - - if (!in_array($prompt_type, $valid_prompt_types)) { - wp_send_json_error('Invalid prompt type'); - } - - // Get default prompt value - $default_function = 'igny8_get_default_' . $prompt_type; - if (!function_exists($default_function)) { - wp_send_json_error('Default prompt function not found'); - } - - $default_value = $default_function(); - - // Reset the prompt using AI settings system - igny8_update_ai_setting($prompt_type, $default_value); - - wp_send_json_success('Prompt reset to default successfully'); -} - -/** - * Reset Multiple Prompts - AJAX handler for resetting multiple prompts at once - */ -add_action('wp_ajax_igny8_reset_multiple_prompts', 'igny8_ajax_reset_multiple_prompts'); -function igny8_ajax_reset_multiple_prompts() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_thinker_settings')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $prompt_types = $_POST['prompt_types'] ?? []; - - if (empty($prompt_types) || !is_array($prompt_types)) { - wp_send_json_error('Prompt types are required'); - } - - // Validate prompt types - $valid_prompt_types = [ - 'clustering_prompt', - 'ideas_prompt', - 'content_generation_prompt', - ]; - - $reset_data = []; - - foreach ($prompt_types as $prompt_type) { - $prompt_type = sanitize_text_field($prompt_type); - - if (!in_array($prompt_type, $valid_prompt_types)) { - wp_send_json_error('Invalid prompt type: ' . $prompt_type); - } - - // Get default prompt value - if ($prompt_type === 'content_generation_prompt') { - $default_value = igny8_content_generation_prompt(); - } else { - $default_function = 'igny8_get_default_' . $prompt_type; - if (!function_exists($default_function)) { - wp_send_json_error('Default prompt function not found for: ' . $prompt_type); - } - $default_value = $default_function(); - } - - // Reset the prompt using AI settings system - igny8_update_ai_setting($prompt_type, $default_value); - - // Store the reset value for response - $reset_data[$prompt_type] = $default_value; - } - - wp_send_json_success([ - 'message' => 'All prompts reset to default successfully', - 'data' => $reset_data - ]); -} - -/** - * AI Logs - Get AI event logs - */ -add_action('wp_ajax_igny8_get_ai_logs', 'igny8_ajax_get_ai_logs'); -function igny8_ajax_get_ai_logs() { - // Verify nonce - try multiple nonce types - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_debug_nonce') || - wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings') || - wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - } - - if (!$nonce_valid) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Get AI logs from options (last 50 events) - $ai_logs = get_option('igny8_ai_logs', []); - - // Sort by timestamp (newest first) - usort($ai_logs, function($a, $b) { - return strtotime($b['timestamp']) - strtotime($a['timestamp']); - }); - - // Limit to last 50 events - $ai_logs = array_slice($ai_logs, 0, 50); - - wp_send_json_success($ai_logs); -} - -/** - * AI Logs - Clear AI event logs - */ -add_action('wp_ajax_igny8_clear_ai_logs', 'igny8_ajax_clear_ai_logs'); -function igny8_ajax_clear_ai_logs() { - // Verify nonce - try multiple nonce types - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_debug_nonce') || - wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings') || - wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - } - - if (!$nonce_valid) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Clear AI logs - delete_option('igny8_ai_logs'); - - wp_send_json_success(['message' => 'AI logs cleared successfully']); -} - -/** - * AI Ideas Generation - Generate content ideas from clusters - */ -add_action('wp_ajax_igny8_ai_generate_ideas', 'igny8_ajax_ai_generate_ideas'); -function igny8_ajax_ai_generate_ideas() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Check if AI mode is enabled - if (igny8_get_ai_setting('planner_mode', 'manual') !== 'ai') { - wp_send_json_error(['message' => 'AI mode is not enabled']); - } - - // Check if ideas generation is enabled - if (igny8_get_ai_setting('ideas', 'enabled') !== 'enabled') { - wp_send_json_error(['message' => 'Ideas generation feature is disabled']); - } - - // Handle cluster_ids - it comes as JSON string from JavaScript - $cluster_ids_raw = $_POST['cluster_ids'] ?? []; - if (is_string($cluster_ids_raw)) { - $cluster_ids = json_decode($cluster_ids_raw, true) ?: []; - } else { - $cluster_ids = $cluster_ids_raw; - } - $cluster_ids = array_map('intval', $cluster_ids); - - if (empty($cluster_ids)) { - wp_send_json_error(['message' => 'No clusters selected']); - } - - // Limit to 5 clusters max - if (count($cluster_ids) > 5) { - wp_send_json_error(['message' => 'Maximum 5 clusters allowed for idea generation']); - } - - // Get clusters data with their keywords and check if they already have ideas - global $wpdb; - $placeholders = implode(',', array_fill(0, count($cluster_ids), '%d')); - $clusters = $wpdb->get_results($wpdb->prepare(" - SELECT c.*, - GROUP_CONCAT(k.keyword SEPARATOR ', ') as keywords_list - FROM {$wpdb->prefix}igny8_clusters c - LEFT JOIN {$wpdb->prefix}igny8_keywords k ON c.id = k.cluster_id - WHERE c.id IN ({$placeholders}) - GROUP BY c.id - ", $cluster_ids)); - - if (empty($clusters)) { - wp_send_json_error(['message' => 'No valid clusters found']); - } - - // Check if clusters already have associated ideas - $clusters_with_ideas = []; - foreach ($clusters as $cluster) { - $idea_count = $wpdb->get_var($wpdb->prepare(" - SELECT COUNT(*) FROM {$wpdb->prefix}igny8_content_ideas - WHERE keyword_cluster_id = %d - ", $cluster->id)); - - if ($idea_count > 0) { - $clusters_with_ideas[] = $cluster->name; - } - } - - if (!empty($clusters_with_ideas)) { - wp_send_json_error(['message' => 'Clusters already have associated ideas: ' . implode(', ', array_slice($clusters_with_ideas, 0, 3)) . (count($clusters_with_ideas) > 3 ? '...' : '')]); - } - - // Generate session ID for progress tracking - $session_id = 'ideas_' . time() . '_' . wp_generate_password(8, false); - - // Log AI request initiation - igny8_log_ai_event('AI Request Initiated', 'planner', 'ideas', 'info', 'Starting AI ideas generation process', 'Clusters: ' . count($cluster_ids) . ', Session: ' . $session_id); - - // Get ideas prompt - $prompt_template = wp_unslash(igny8_get_ai_setting('ideas_prompt', igny8_get_default_ideas_prompt())); - - // Log data preparation - igny8_log_ai_event('Data Preparation', 'planner', 'ideas', 'info', 'Preparing clusters data for AI', 'Clusters count: ' . count($clusters) . ', Prompt length: ' . strlen($prompt_template)); - - // Process with AI - error_log('Igny8 AI: Starting AI processing with ' . count($clusters) . ' clusters'); - $ai_result = igny8_process_ai_request('ideas', $clusters, $prompt_template); - - // Log detailed AI processing result - if ($ai_result === false) { - igny8_log_ai_event('AI Processing Failed', 'planner', 'ideas', 'error', 'AI processing returned false', 'Check OpenAI API configuration'); - } elseif (is_array($ai_result) && isset($ai_result['ideas'])) { - igny8_log_ai_event('AI Processing Complete', 'planner', 'ideas', 'success', 'AI returned ' . count($ai_result['ideas']) . ' ideas', 'Ideas: ' . json_encode(array_column($ai_result['ideas'], 'title'))); - } elseif (is_array($ai_result)) { - igny8_log_ai_event('AI Processing Failed', 'planner', 'ideas', 'error', 'AI returned array but missing ideas key', 'Result keys: ' . json_encode(array_keys($ai_result))); - } else { - igny8_log_ai_event('AI Processing Failed', 'planner', 'ideas', 'error', 'AI returned invalid data type', 'Type: ' . gettype($ai_result) . ', Value: ' . json_encode($ai_result)); - } - - if (!$ai_result) { - error_log('Igny8 AI: AI processing returned false'); - wp_send_json_error(['message' => 'AI processing failed - no result']); - } - - // Handle different AI response formats - if (!$ai_result) { - wp_send_json_error(['message' => 'AI processing failed']); - } - - // Check if response is wrapped in 'ideas' key or direct array - $ideas = null; - if (isset($ai_result['ideas'])) { - $ideas = $ai_result['ideas']; - } elseif (is_array($ai_result) && !isset($ai_result['ideas'])) { - // AI returned direct array of ideas - $ideas = $ai_result; - igny8_log_ai_event('Response Format Adjusted', 'planner', 'ideas', 'info', 'AI returned direct array, wrapped for processing', 'Ideas count: ' . count($ideas)); - } - - if (!$ideas || !is_array($ideas)) { - igny8_log_ai_event('AI Processing Failed', 'planner', 'ideas', 'error', 'No valid ideas found in AI response', 'Response type: ' . gettype($ai_result) . ', Keys: ' . json_encode(array_keys($ai_result))); - wp_send_json_error(['message' => 'AI processing failed - no valid ideas']); - } - - // Log database operations start - igny8_log_ai_event('Database Operations Started', 'planner', 'ideas', 'info', 'Starting to create ideas in database', 'Ideas to create: ' . count($ideas)); - - // Create ideas in database - $created_ideas = []; - foreach ($ideas as $idea_data) { - // Validate required fields - if (empty($idea_data['title']) || empty($idea_data['description']) || empty($idea_data['cluster_id'])) { - igny8_log_ai_event('Idea Validation Failed', 'planner', 'ideas', 'error', 'Missing required fields in AI response', 'Title: ' . ($idea_data['title'] ?? 'missing') . ', Description: ' . (empty($idea_data['description']) ? 'missing' : 'present') . ', Cluster ID: ' . ($idea_data['cluster_id'] ?? 'missing')); - continue; - } - - // Debug: Log the idea data being processed - error_log('Igny8 Debug: Processing idea - Title: ' . $idea_data['title'] . ', Cluster ID: ' . $idea_data['cluster_id'] . ', Content Structure: ' . ($idea_data['content_structure'] ?? 'missing') . ', Content Type: ' . ($idea_data['content_type'] ?? 'missing')); - - // Validate content structure and type - if (empty($idea_data['content_structure'])) { - $idea_data['content_structure'] = 'cluster_hub'; // Default fallback - } - if (empty($idea_data['content_type'])) { - $idea_data['content_type'] = 'post'; // Default fallback - } - - // Handle target_keywords field - store as comma-separated text - $target_keywords = null; - if (isset($idea_data['covered_keywords']) && !empty($idea_data['covered_keywords'])) { - // Handle both array and string formats - if (is_array($idea_data['covered_keywords'])) { - $keywords = array_map('trim', $idea_data['covered_keywords']); - } else { - $keywords = array_map('trim', explode(',', $idea_data['covered_keywords'])); - } - $keywords = array_filter($keywords); // Remove empty values - $target_keywords = implode(', ', $keywords); - } - - // Handle image_prompts field - store as JSON string - $image_prompts = null; - if (isset($idea_data['image_prompts']) && !empty($idea_data['image_prompts'])) { - // Ensure it's properly formatted JSON - if (is_array($idea_data['image_prompts'])) { - $image_prompts = json_encode($idea_data['image_prompts']); - } else { - $image_prompts = sanitize_text_field($idea_data['image_prompts']); - } - } - - // Handle description field - store as JSON string for structured content - $description = null; - if (isset($idea_data['description']) && !empty($idea_data['description'])) { - if (is_array($idea_data['description'])) { - // If it's already structured JSON, encode it - $description = json_encode($idea_data['description']); - } else { - // If it's a string, store as is (for backward compatibility) - $description = sanitize_textarea_field($idea_data['description']); - } - } - - // Debug: Log what we're trying to insert - error_log('Igny8 Debug: Inserting idea with target_keywords: ' . ($target_keywords ?: 'NULL') . ', image_prompts: ' . ($image_prompts ? 'Present' : 'NULL') . ', description length: ' . strlen($description ?: 'NULL')); - - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_content_ideas', - [ - 'idea_title' => sanitize_text_field($idea_data['title']), - 'idea_description' => $description ?: sanitize_textarea_field($idea_data['description']), - 'content_structure' => sanitize_text_field($idea_data['content_structure'] ?? 'cluster_hub'), - 'content_type' => sanitize_text_field($idea_data['content_type'] ?? 'post'), - 'keyword_cluster_id' => intval($idea_data['cluster_id']), - 'status' => 'new', - 'estimated_word_count' => intval($idea_data['estimated_word_count']), - 'target_keywords' => $target_keywords ?: null, - 'image_prompts' => $image_prompts ?: null, - 'source' => 'AI', - 'mapped_post_id' => null, - 'tasks_count' => 0 - ], - ['%s', '%s', '%s', '%s', '%d', '%s', '%d', '%s', '%s', '%s', '%d', '%d'] - ); - - if ($result) { - $created_ideas[] = $wpdb->insert_id; - igny8_log_ai_event('Idea Created', 'planner', 'ideas', 'success', 'Content idea created successfully', 'Title: ' . $idea_data['title']); - } else { - error_log('Igny8 Debug: Database insert failed - Error: ' . $wpdb->last_error); - igny8_log_ai_event('Idea Creation Failed', 'planner', 'ideas', 'error', 'Failed to create content idea', 'Title: ' . $idea_data['title'] . ', Error: ' . $wpdb->last_error); - } - } - - // Log completion - igny8_log_ai_event('AI Ideas Generation Complete', 'planner', 'ideas', 'success', 'AI ideas generation process completed successfully', 'Ideas created: ' . count($created_ideas)); - - wp_send_json_success([ - 'message' => 'Successfully created ' . count($created_ideas) . ' content ideas', - 'ideas_created' => count($created_ideas), - 'session_id' => $session_id - ]); -} - -/** - * AI Image Generation - Generate featured images for ideas - */ - - -/** - * Get image prompt counts for posts (preview before generation) - */ -add_action('wp_ajax_igny8_get_image_counts', 'igny8_ajax_get_image_counts'); -function igny8_ajax_get_image_counts() { - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $task_ids_raw = $_POST['post_ids'] ?? []; - if (is_string($task_ids_raw)) { - $task_ids = json_decode($task_ids_raw, true) ?: []; - } else { - $task_ids = $task_ids_raw; - } - $task_ids = array_map('intval', $task_ids); - - if (empty($task_ids)) { - wp_send_json_error(['message' => 'No tasks selected']); - } - - // Get image generation settings from request - $desktop_enabled = sanitize_text_field($_POST['desktop_enabled'] ?? '0') === '1'; - $mobile_enabled = sanitize_text_field($_POST['mobile_enabled'] ?? '0') === '1'; - $max_in_article_images = intval($_POST['max_in_article_images'] ?? 1); - - error_log('Igny8: Image counts settings - Desktop: ' . ($desktop_enabled ? 'enabled' : 'disabled') . - ', Mobile: ' . ($mobile_enabled ? 'enabled' : 'disabled') . - ', Max In-Article: ' . $max_in_article_images); - - global $wpdb; - $placeholders = implode(',', array_fill(0, count($task_ids), '%d')); - $tasks = $wpdb->get_results($wpdb->prepare( - "SELECT id, title, assigned_post_id FROM {$wpdb->prefix}igny8_tasks WHERE id IN ($placeholders)", - ...$task_ids - )); - - if (empty($tasks)) { - wp_send_json_error(['message' => 'No valid tasks found']); - } - - $image_queue = []; - - foreach ($tasks as $task) { - if (empty($task->assigned_post_id)) continue; - - $post_title = get_the_title($task->assigned_post_id) ?: $task->title; - - // Check for featured image prompt - $featured_prompt = get_post_meta($task->assigned_post_id, '_igny8_featured_image_prompt', true); - if (!empty($featured_prompt)) { - $image_queue[] = [ - 'post_id' => $task->assigned_post_id, - 'task_id' => $task->id, - 'post_title' => $post_title, - 'type' => 'featured', - 'label' => 'Featured Image', - 'prompt' => $featured_prompt - ]; - } - - // Check for in-article image prompts - handle both formats - // Only add in-article images if desktop or mobile is enabled - if ($desktop_enabled || $mobile_enabled) { - // Format 1: New format with array (from _igny8_article_images_data) - $article_images_data = get_post_meta($task->assigned_post_id, '_igny8_article_images_data', true); - if (!empty($article_images_data)) { - $article_images = json_decode($article_images_data, true); - if (json_last_error() !== JSON_ERROR_NONE) { - error_log("IGNY8 IMAGE QUEUE: JSON decode error for _igny8_article_images_data: " . json_last_error_msg()); - error_log("IGNY8 IMAGE QUEUE: Raw data: " . substr($article_images_data, 0, 200) . "..."); - - // Try to clean the data by stripping HTML tags - $cleaned_data = wp_strip_all_tags($article_images_data); - $article_images = json_decode($cleaned_data, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - error_log("IGNY8 IMAGE QUEUE: Still invalid JSON after cleaning: " . json_last_error_msg()); - $article_images = null; // Skip this format - } else { - error_log("IGNY8 IMAGE QUEUE: Successfully cleaned and parsed JSON"); - } - } - if (is_array($article_images)) { - $image_count = 0; - foreach ($article_images as $index => $image_data) { - // Find the prompt key (prompt-img-1, prompt-img-2, etc.) - $prompt_key = null; - $prompt_value = null; - foreach ($image_data as $key => $value) { - if (strpos($key, 'prompt-img-') === 0) { - $prompt_key = $key; - $prompt_value = $value; - break; - } - } - - if (!empty($prompt_value) && $image_count < $max_in_article_images) { - // Desktop version (if enabled) - if ($desktop_enabled) { - $image_queue[] = [ - 'post_id' => $task->assigned_post_id, - 'task_id' => $task->id, - 'post_title' => $post_title, - 'type' => 'article', - 'device' => 'desktop', - 'section' => $image_data['section'] ?? "Section " . ($index + 1), - 'label' => "Article " . ($index + 1) . " - Desktop", - 'prompt' => $prompt_value, - 'index' => $index - ]; - } - - // Mobile version (if enabled) - if ($mobile_enabled) { - $image_queue[] = [ - 'post_id' => $task->assigned_post_id, - 'task_id' => $task->id, - 'post_title' => $post_title, - 'type' => 'article', - 'device' => 'mobile', - 'section' => $image_data['section'] ?? "Section " . ($index + 1), - 'label' => "Article " . ($index + 1) . " - Mobile", - 'prompt' => $prompt_value, - 'index' => $index - ]; - } - - $image_count++; - } - } - } - } - } - - // Format 2: Old format with in_article_image_1, in_article_image_2, etc (from _igny8_image_prompts) - if (($desktop_enabled || $mobile_enabled) && (empty($image_queue) || count($image_queue) == 1)) { // Only featured found, check old format - $image_prompts_json = get_post_meta($task->assigned_post_id, '_igny8_image_prompts', true); - if (!empty($image_prompts_json)) { - $image_prompts = json_decode($image_prompts_json, true); - if (is_array($image_prompts)) { - $article_index = 0; - foreach ($image_prompts as $key => $prompt) { - if (strpos($key, 'in_article_image_') === 0 && !empty($prompt) && $article_index < $max_in_article_images) { - // Extract section name from key or use generic name - $section = "Section " . ($article_index + 1); - - // Desktop version (if enabled) - if ($desktop_enabled) { - $image_queue[] = [ - 'post_id' => $task->assigned_post_id, - 'task_id' => $task->id, - 'post_title' => $post_title, - 'type' => 'article', - 'device' => 'desktop', - 'section' => $section, - 'label' => "Article " . ($article_index + 1) . " - Desktop", - 'prompt' => $prompt, - 'index' => $article_index - ]; - } - - // Mobile version (if enabled) - if ($mobile_enabled) { - $image_queue[] = [ - 'post_id' => $task->assigned_post_id, - 'task_id' => $task->id, - 'post_title' => $post_title, - 'type' => 'article', - 'device' => 'mobile', - 'section' => $section, - 'label' => "Article " . ($article_index + 1) . " - Mobile", - 'prompt' => $prompt, - 'index' => $article_index - ]; - } - - $article_index++; - } - } - } - } - } - } - - wp_send_json_success([ - 'total_images' => count($image_queue), - 'queue' => $image_queue - ]); -} - -/** - * Generate single image from queue - */ -add_action('wp_ajax_igny8_generate_single_image_queue', 'igny8_ajax_generate_single_image_queue'); -function igny8_ajax_generate_single_image_queue() { - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $post_id = intval($_POST['post_id'] ?? 0); - $type = sanitize_text_field($_POST['type'] ?? ''); - $device = sanitize_text_field($_POST['device'] ?? ''); - $prompt = sanitize_textarea_field($_POST['prompt'] ?? ''); - $section = sanitize_text_field($_POST['section'] ?? ''); - $index = intval($_POST['index'] ?? 0); - - if (empty($post_id) || empty($type) || empty($prompt)) { - wp_send_json_error(['message' => 'Missing required parameters']); - } - - // Get image generation settings - $image_type = get_option('igny8_image_type', 'realistic'); - $image_provider = get_option('igny8_image_provider', 'runware'); - $image_format = get_option('igny8_image_format', 'jpg'); - $negative_prompt = get_option('igny8_negative_prompt', ''); - - try { - if ($type === 'featured') { - // Generate featured image - $result = igny8_generate_single_image( - $post_id, - $prompt, - 'featured', - $image_provider, - $image_format, - $negative_prompt, - ['type' => 'featured'] - ); - - if ($result['success']) { - set_post_thumbnail($post_id, $result['attachment_id']); - - wp_send_json_success([ - 'attachment_id' => $result['attachment_id'], - 'image_url' => $result['image_url'], - 'type' => 'featured' - ]); - } else { - wp_send_json_error(['message' => $result['error']]); - } - - } elseif ($type === 'article') { - // Generate article image (desktop or mobile) - // Use prompt as-is if it's already detailed, otherwise enhance it - $full_prompt = $prompt; - if (strlen($prompt) < 50 || strpos($prompt, 'Create') !== 0) { - // Only enhance if prompt is short or doesn't start with "Create" - $full_prompt = "Create a high-quality {$image_type} image for the section titled '{$section}'. {$prompt}"; - } - - $size_type = ($device === 'mobile') ? 'mobile' : 'desktop'; - - $result = igny8_generate_single_image( - $post_id, - $full_prompt, - $size_type, - $image_provider, - $image_format, - $negative_prompt, - [ - 'section' => $section, - 'index' => $index, - 'device' => $device - ] - ); - - if ($result['success']) { - wp_send_json_success([ - 'attachment_id' => $result['attachment_id'], - 'image_url' => $result['image_url'], - 'type' => 'article', - 'device' => $device, - 'section' => $section, - 'index' => $index - ]); - } else { - wp_send_json_error(['message' => $result['error']]); - } - } - - } catch (Exception $e) { - wp_send_json_error(['message' => 'Exception: ' . $e->getMessage()]); - } -} - -/** - * AI Generate Images for Drafts - Generate images from post meta prompts - */ -add_action('wp_ajax_igny8_ai_generate_images_drafts', 'igny8_ajax_ai_generate_images_drafts'); -function igny8_ajax_ai_generate_images_drafts() { - error_log('Igny8: AJAX HANDLER CALLED - igny8_ajax_ai_generate_images_drafts'); - error_log('Igny8: POST data received: ' . print_r($_POST, true)); - - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - error_log('Igny8: NONCE VERIFICATION FAILED'); - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Handle post_ids - these are actually task IDs from igny8_tasks table - $task_ids_raw = $_POST['post_ids'] ?? []; - if (is_string($task_ids_raw)) { - $task_ids = json_decode($task_ids_raw, true) ?: []; - } else { - $task_ids = $task_ids_raw; - } - $task_ids = array_map('intval', $task_ids); - - if (empty($task_ids)) { - wp_send_json_error(['message' => 'No tasks selected']); - } - - // Limit to 10 tasks max (image generation is expensive) - if (count($task_ids) > 10) { - wp_send_json_error(['message' => 'Maximum 10 tasks allowed for image generation']); - } - - // Get image generation settings from request - $desktop_enabled = sanitize_text_field($_POST['desktop_enabled'] ?? '0') === '1'; - $mobile_enabled = sanitize_text_field($_POST['mobile_enabled'] ?? '0') === '1'; - $max_in_article_images = intval($_POST['max_in_article_images'] ?? 1); - - error_log('Igny8: Image generation settings - Desktop: ' . ($desktop_enabled ? 'enabled' : 'disabled') . - ', Mobile: ' . ($mobile_enabled ? 'enabled' : 'disabled') . - ', Max In-Article: ' . $max_in_article_images); - - global $wpdb; - - // Event 3: Task IDs validated - error_log('Igny8: IMAGE_GEN_EVENT_3 - Task IDs validated: ' . implode(', ', $task_ids)); - $debug_events[] = ['event' => 'Task IDs validated', 'level' => 'INFO', 'data' => ['taskIds' => $task_ids]]; - - // Get WordPress post IDs from tasks table - $placeholders = implode(',', array_fill(0, count($task_ids), '%d')); - $tasks = $wpdb->get_results($wpdb->prepare( - "SELECT id, title, assigned_post_id FROM {$wpdb->prefix}igny8_tasks WHERE id IN ($placeholders)", - ...$task_ids - )); - - if (empty($tasks)) { - error_log('Igny8: IMAGE_GEN_EVENT_3_ERROR - No valid tasks found'); - wp_send_json_error(['message' => 'No valid tasks found']); - } - - // Event 4: WordPress post IDs retrieved - $post_ids_retrieved = array_map(function($task) { return $task->assigned_post_id; }, $tasks); - $task_to_post_map = []; - foreach ($tasks as $t) { - $task_to_post_map[$t->id] = $t->assigned_post_id; - } - error_log('Igny8: IMAGE_GEN_EVENT_4 - WordPress post IDs retrieved: ' . implode(', ', $post_ids_retrieved)); - $debug_events[] = ['event' => 'WordPress post IDs retrieved', 'level' => 'SUCCESS', 'data' => ['mapping' => $task_to_post_map]]; - - // Process each task - $generated_images = []; - $failed_images = []; - - // Generate session ID for progress tracking - $session_id = 'image_gen_drafts_' . time() . '_' . wp_generate_password(8, false); - - // Log AI request initiation - igny8_log_ai_event('AI Image Generation Initiated', 'writer', 'image_generation', 'info', 'Starting AI image generation for drafts', 'Tasks: ' . count($tasks) . ', Session: ' . $session_id); - - foreach ($tasks as $task) { - $task_id = $task->id; - $post_id = $task->assigned_post_id; - $task_title = $task->title; - - error_log('Igny8: IMAGE_GEN - Processing Task ID: ' . $task_id . ' -> WordPress Post ID: ' . $post_id); - - if (!$post_id) { - error_log('Igny8: IMAGE_GEN_ERROR - Task ' . $task_id . ' has no assigned WordPress post'); - $failed_images[] = [ - 'task_id' => $task_id, - 'task_title' => $task_title, - 'error' => 'No WordPress post assigned to this task' - ]; - continue; - } - - $post = get_post($post_id); - if (!$post) { - error_log('Igny8: IMAGE_GEN_ERROR - WordPress post ' . $post_id . ' not found for task ' . $task_id); - $failed_images[] = [ - 'task_id' => $task_id, - 'task_title' => $task_title, - 'post_id' => $post_id, - 'error' => 'WordPress post not found' - ]; - continue; - } - - error_log('Igny8: IMAGE_GEN - WordPress post found: ' . $post->post_title . ' (Post ID: ' . $post_id . ', Task ID: ' . $task_id . ')'); - - // Event 5: Image prompts loaded - $featured_prompt = get_post_meta($post_id, '_igny8_featured_image_prompt', true); - $article_images = get_post_meta($post_id, '_igny8_article_images_data', true); - error_log('Igny8: IMAGE_GEN_EVENT_5 - Image prompts loaded for post: ' . $post_id); - $debug_events[] = ['event' => 'Image prompts loaded', 'level' => 'INFO', 'data' => ['postId' => $post_id, 'postTitle' => $post->post_title]]; - - // Event 6: Featured image generation initiated - error_log('Igny8: IMAGE_GEN_EVENT_6 - Featured image generation initiated for post: ' . $post_id); - $debug_events[] = ['event' => 'Featured image generation started', 'level' => 'INFO', 'data' => ['postId' => $post_id]]; - $featured_result = igny8_generate_featured_image_for_post($post_id); - - error_log('Igny8: Step 4 - WordPress post retrieved: ' . ($post ? 'Post found: ' . $post->post_title . ' (ID: ' . $post->ID . ')' : 'Post not found')); - - // Generate featured image (always) - if ($featured_result['success']) { - $generated_images[] = [ - 'task_id' => $task_id, - 'post_id' => $post_id, - 'post_title' => $post->post_title, - 'type' => 'featured', - 'attachment_id' => $featured_result['attachment_id'], - 'image_url' => $featured_result['image_url'], - 'provider' => $featured_result['provider'] - ]; - - // Event 9: Image saved successfully - $debug_events[] = ['event' => 'Featured image saved', 'level' => 'SUCCESS', 'data' => ['postId' => $post_id, 'attachmentId' => $featured_result['attachment_id'], 'provider' => $featured_result['provider']]]; - - // Log success - igny8_log_ai_event('Featured Image Generated', 'writer', 'image_generation', 'success', 'Featured image generated and set for post', 'Post: ' . $post->post_title . ', Post ID: ' . $post_id . ', Attachment ID: ' . $featured_result['attachment_id']); - } else { - $failed_images[] = [ - 'task_id' => $task_id, - 'post_id' => $post_id, - 'post_title' => $post->post_title, - 'type' => 'featured', - 'error' => $featured_result['error'] - ]; - - // Event: Image generation failed - $debug_events[] = ['event' => 'Featured image failed', 'level' => 'ERROR', 'data' => ['postId' => $post_id, 'error' => $featured_result['error']]]; - } - - // Safety check: Calculate safe image quantity based on post content - error_log('Igny8: Step 5 - Calculating safe image quantity for post_id: ' . $post_id . ', content length: ' . strlen($post->post_content)); - $safe_max_images = igny8_calculate_safe_image_quantity($post->post_content, $max_in_article_images); - error_log('Igny8: Step 5 - Safe max images calculated: ' . $safe_max_images); - - // Generate desktop in-article images if enabled - if ($desktop_enabled) { - error_log('Igny8: Step 6 - Generating desktop images for post_id: ' . $post_id . ', count: ' . $safe_max_images); - for ($i = 1; $i <= $safe_max_images; $i++) { - error_log('Igny8: Step 6 - Generating desktop image ' . $i . ' for post_id: ' . $post_id); - $desktop_result = igny8_generate_single_article_image($post_id, 'desktop', $i); - error_log('Igny8: Step 6 - Desktop image ' . $i . ' result: ' . print_r($desktop_result, true)); - - if ($desktop_result['success']) { - $generated_images[] = [ - 'task_id' => $task_id, - 'post_id' => $post_id, - 'post_title' => $post->post_title, - 'type' => 'desktop', - 'index' => $i, - 'attachment_id' => $desktop_result['attachment_id'], - 'image_url' => $desktop_result['image_url'], - 'provider' => $desktop_result['provider'] - ]; - } else { - $failed_images[] = [ - 'task_id' => $task_id, - 'post_id' => $post_id, - 'post_title' => $post->post_title, - 'type' => 'desktop', - 'index' => $i, - 'error' => $desktop_result['error'] - ]; - } - } - } - - // Generate mobile in-article images if enabled - if ($mobile_enabled) { - error_log('Igny8: Step 7 - Generating mobile images for post_id: ' . $post_id . ', count: ' . $safe_max_images); - for ($i = 1; $i <= $safe_max_images; $i++) { - error_log('Igny8: Step 7 - Generating mobile image ' . $i . ' for post_id: ' . $post_id); - $mobile_result = igny8_generate_single_article_image($post_id, 'mobile', $i); - error_log('Igny8: Step 7 - Mobile image ' . $i . ' result: ' . print_r($mobile_result, true)); - - if ($mobile_result['success']) { - $generated_images[] = [ - 'task_id' => $task_id, - 'post_id' => $post_id, - 'post_title' => $post->post_title, - 'type' => 'mobile', - 'index' => $i, - 'attachment_id' => $mobile_result['attachment_id'], - 'image_url' => $mobile_result['image_url'], - 'provider' => $mobile_result['provider'] - ]; - } else { - $failed_images[] = [ - 'task_id' => $task_id, - 'post_id' => $post_id, - 'post_title' => $post->post_title, - 'type' => 'mobile', - 'index' => $i, - 'error' => $mobile_result['error'] - ]; - } - } - } - } - - // Log completion - igny8_log_ai_event('AI Image Generation Complete', 'writer', 'image_generation', 'success', 'Image generation completed', 'Success: ' . count($generated_images) . ', Failed: ' . count($failed_images)); - - wp_send_json_success([ - 'message' => 'Generated ' . count($generated_images) . ' images' . (count($failed_images) > 0 ? ', ' . count($failed_images) . ' failed' : ''), - 'images_generated' => count($generated_images), - 'images_failed' => count($failed_images), - 'generated_images' => $generated_images, - 'failed_images' => $failed_images, - 'session_id' => $session_id, - 'debug_events' => $debug_events - ]); -} - -/** - * AI Generate Single Image - Generate one image at a time for queue processing - */ -add_action('wp_ajax_igny8_ai_generate_single_image', 'igny8_ajax_ai_generate_single_image'); -function igny8_ajax_ai_generate_single_image() { - // Verify nonce - if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'igny8_writer_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('edit_posts')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Get parameters - post_id is actually task_id from igny8_tasks table - $task_id = intval($_POST['post_id'] ?? 0); - $type = sanitize_text_field($_POST['type'] ?? 'featured'); - $device = sanitize_text_field($_POST['device'] ?? ''); - $index = intval($_POST['index'] ?? 1); - - if (!$task_id) { - wp_send_json_error(['message' => 'Invalid task ID']); - } - - // Get WordPress post ID from task - global $wpdb; - $task = $wpdb->get_row($wpdb->prepare( - "SELECT id, assigned_post_id, title FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", - $task_id - )); - - if (!$task || !$task->assigned_post_id) { - wp_send_json_error(['message' => 'Task not found or no post assigned (Task ID: ' . $task_id . ')']); - } - - $post_id = $task->assigned_post_id; - - // Verify WordPress post exists - $post = get_post($post_id); - if (!$post) { - wp_send_json_error(['message' => 'WordPress post not found (Post ID: ' . $post_id . ', Task ID: ' . $task_id . ')']); - } - - error_log('Igny8: IMAGE_GEN_SINGLE - Processing Task ID: ' . $task_id . ' -> WordPress Post ID: ' . $post_id . ' (' . $post->post_title . ')'); - - // Generate image based on type - if ($type === 'featured') { - $result = igny8_generate_featured_image_for_post($post_id); - } else { - $result = igny8_generate_single_article_image($post_id, $device, $index); - } - - if ($result['success']) { - // For in-article images, add to meta box - if ($type !== 'featured') { - $image_label = sanitize_text_field($_POST['image_label'] ?? ''); - $device = sanitize_text_field($_POST['device'] ?? 'desktop'); - $section = isset($_POST['section']) ? intval($_POST['section']) : null; - - if (!empty($image_label)) { - igny8_add_inarticle_image_meta($post_id, $result['attachment_id'], $image_label, $device, $section); - } - } - - wp_send_json_success([ - 'message' => 'Image generated successfully', - 'attachment_id' => $result['attachment_id'], - 'image_url' => $result['image_url'], - 'provider' => $result['provider'] - ]); - } else { - wp_send_json_error([ - 'message' => $result['error'] ?? 'Failed to generate image' - ]); - } -} - -/** - * Create sample sectors for testing (development only) - */ -add_action('wp_ajax_igny8_create_sample_sectors', 'igny8_ajax_create_sample_sectors'); -function igny8_ajax_create_sample_sectors() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Only create if no sectors exist - $existing_sectors = get_terms([ - 'taxonomy' => 'sectors', - 'hide_empty' => false - ]); - - if (!empty($existing_sectors) && !is_wp_error($existing_sectors)) { - wp_send_json_error(['message' => 'Sectors already exist']); - } - - // Create parent sectors - $parent_sectors = [ - 'Technology' => [ - 'Software Development', - 'Artificial Intelligence', - 'Cybersecurity', - 'Cloud Computing' - ], - 'Healthcare' => [ - 'Medical Devices', - 'Pharmaceuticals', - 'Telemedicine', - 'Mental Health' - ], - 'Finance' => [ - 'Banking', - 'Insurance', - 'Investment', - 'Fintech' - ], - 'Education' => [ - 'Online Learning', - 'Educational Technology', - 'Professional Development', - 'Research' - ] - ]; - - $created_count = 0; - $errors = []; - - foreach ($parent_sectors as $parent_name => $children) { - // Create parent sector - $parent_result = wp_insert_term($parent_name, 'sectors'); - - if (is_wp_error($parent_result)) { - $errors[] = "Failed to create parent sector: {$parent_name}"; - continue; - } - - $parent_id = $parent_result['term_id']; - $created_count++; - - // Create child sectors - foreach ($children as $child_name) { - $child_result = wp_insert_term($child_name, 'sectors', [ - 'parent' => $parent_id - ]); - - if (is_wp_error($child_result)) { - $errors[] = "Failed to create child sector: {$child_name}"; - } else { - $created_count++; - } - } - } - - if ($created_count > 0) { - wp_send_json_success([ - 'message' => "Created {$created_count} sectors successfully", - 'created_count' => $created_count, - 'errors' => $errors - ]); - } else { - wp_send_json_error([ - 'message' => 'Failed to create any sectors', - 'errors' => $errors - ]); - } -} - -/** - * AJAX handler for saving AI prompts - */ -add_action('wp_ajax_igny8_save_ai_prompts', 'igny8_ajax_save_ai_prompts'); -function igny8_ajax_save_ai_prompts() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Get and sanitize prompt data - $clustering_prompt = sanitize_textarea_field($_POST['igny8_clustering_prompt'] ?? ''); - $ideas_prompt = sanitize_textarea_field($_POST['igny8_ideas_prompt'] ?? ''); - - // Save prompts using AI settings system - igny8_update_ai_setting('clustering_prompt', $clustering_prompt); - igny8_update_ai_setting('ideas_prompt', $ideas_prompt); - - wp_send_json_success(['message' => 'AI prompts saved successfully']); -} - -/** - * AJAX handler for resetting AI prompts to defaults - */ -add_action('wp_ajax_igny8_reset_ai_prompts', 'igny8_ajax_reset_ai_prompts'); -function igny8_ajax_reset_ai_prompts() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_planner_settings')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Reset prompts to defaults - igny8_update_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt()); - igny8_update_ai_setting('ideas_prompt', igny8_get_default_ideas_prompt()); - - wp_send_json_success(['message' => 'AI prompts reset to defaults successfully']); -} - -// =================================================================== -// IMPORT/EXPORT AJAX HANDLERS -// =================================================================== - -/** - * Download CSV template - */ -add_action('wp_ajax_igny8_download_template', 'igny8_ajax_download_template'); -function igny8_ajax_download_template() { - // Verify nonce - check both POST and GET - $nonce = $_POST['nonce'] ?? $_GET['nonce'] ?? ''; - $nonce_valid = wp_verify_nonce($nonce, 'igny8_import_export_nonce') || - wp_verify_nonce($nonce, 'igny8_ajax_nonce') || - wp_verify_nonce($nonce, 'igny8_admin_nonce'); - - if (!$nonce_valid) { - wp_die('Security check failed - Invalid nonce'); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_die('Insufficient permissions'); - } - - $template_type = sanitize_text_field($_POST['template_type'] ?? $_GET['template_type'] ?? ''); - if (empty($template_type)) { - wp_die('Template type is required'); - } - - $plugin_root = plugin_dir_path(dirname(dirname(__FILE__))); - $template_path = $plugin_root . "assets/templates/igny8_{$template_type}_template.csv"; - - if (!file_exists($template_path)) { - wp_die('Template file not found: ' . $template_path); - } - - // Set headers for file download - header('Content-Type: text/csv'); - header('Content-Disposition: attachment; filename="igny8_' . $template_type . '_template.csv"'); - header('Pragma: no-cache'); - header('Expires: 0'); - - // Output file content - readfile($template_path); - exit; -} - -/** - * Run CSV import - */ -add_action('wp_ajax_igny8_run_import', 'igny8_ajax_run_import'); -function igny8_ajax_run_import() { - // Debug logging - error_log('Igny8 Import Debug - POST data: ' . print_r($_POST, true)); - error_log('Igny8 Import Debug - Nonce received: ' . ($_POST['nonce'] ?? 'NOT SET')); - - // Verify nonce - check both possible nonce actions - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_import_export = wp_verify_nonce($_POST['nonce'], 'igny8_import_export_nonce'); - $nonce_ajax = wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - $nonce_valid = $nonce_import_export || $nonce_ajax; - - error_log('Igny8 Import Debug - Import/Export nonce valid: ' . ($nonce_import_export ? 'YES' : 'NO')); - error_log('Igny8 Import Debug - AJAX nonce valid: ' . ($nonce_ajax ? 'YES' : 'NO')); - error_log('Igny8 Import Debug - Overall valid: ' . ($nonce_valid ? 'YES' : 'NO')); - } - - if (!$nonce_valid) { - wp_send_json_error(['message' => 'Security check failed - Invalid nonce. Received: ' . ($_POST['nonce'] ?? 'NOT SET')]); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $import_type = sanitize_text_field($_POST['import_type'] ?? ''); - - $allowed_types = ['keywords', 'clusters', 'ideas', 'mapping', 'tasks', 'templates', 'audits', 'suggestions', 'backlinks', 'campaigns', 'rewrites', 'tones', 'personalization_data', 'variations']; - if (!in_array($import_type, $allowed_types)) { - wp_send_json_error(['message' => 'Invalid import type']); - } - - // Handle file upload - if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) { - wp_send_json_error(['message' => 'No file uploaded or upload error']); - } - - $uploaded_file = $_FILES['import_file']; - $file_extension = strtolower(pathinfo($uploaded_file['name'], PATHINFO_EXTENSION)); - - if ($file_extension !== 'csv') { - wp_send_json_error(['message' => 'Only CSV files are allowed']); - } - - // Read and parse CSV - $csv_data = []; - $handle = fopen($uploaded_file['tmp_name'], 'r'); - - if ($handle === false) { - wp_send_json_error(['message' => 'Could not read uploaded file']); - } - - // Read headers - $headers = fgetcsv($handle); - if ($headers === false) { - fclose($handle); - wp_send_json_error(['message' => 'Invalid CSV format - no headers found']); - } - - // Read data rows - while (($row = fgetcsv($handle)) !== false) { - if (count($row) === count($headers)) { - $csv_data[] = array_combine($headers, $row); - } - } - fclose($handle); - - if (empty($csv_data)) { - wp_send_json_error(['message' => 'No data rows found in CSV']); - } - - // Import data based on type - $result = igny8_import_data($import_type, $csv_data, $headers); - - // Log import activity - igny8_log_import_export('import', $import_type, $result['success'], $result['message'], $result['details']); - - if ($result['success']) { - - wp_send_json_success($result); - } else { - wp_send_json_error($result); - } -} - -/** - * Run CSV export - */ -add_action('wp_ajax_igny8_run_export', 'igny8_ajax_run_export'); -function igny8_ajax_run_export() { - // Verify nonce - check both possible nonce actions - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_import_export_nonce') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - } - - if (!$nonce_valid) { - wp_send_json_error(['message' => 'Security check failed - Invalid nonce']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $export_type = sanitize_text_field($_POST['export_type'] ?? ''); - $include_metrics = isset($_POST['include_metrics']) && $_POST['include_metrics'] === 'on'; - $include_relationships = isset($_POST['include_relationships']) && $_POST['include_relationships'] === 'on'; - $include_timestamps = isset($_POST['include_timestamps']) && $_POST['include_timestamps'] === 'on'; - - $allowed_types = ['keywords', 'clusters', 'ideas', 'mapping', 'tasks', 'templates', 'audits', 'suggestions', 'backlinks', 'campaigns', 'rewrites', 'tones', 'personalization_data', 'variations']; - if (!in_array($export_type, $allowed_types)) { - wp_send_json_error(['message' => 'Invalid export type']); - } - - // Generate CSV data - $csv_data = igny8_export_data($export_type, [ - 'include_metrics' => $include_metrics, - 'include_relationships' => $include_relationships, - 'include_timestamps' => $include_timestamps - ]); - - if (!$csv_data || !is_array($csv_data)) { - wp_send_json_error(['message' => 'Export failed - No data returned from export function']); - } - - // Handle empty results - if ($csv_data['count'] == 0) { - wp_send_json_error(['message' => 'No records found to export']); - } - - // Generate filename - $date = date('Y-m-d_H-i-s'); - $filename = "igny8_export_{$export_type}_{$date}.csv"; - - // Log export activity - igny8_log_import_export('export', $export_type, true, 'Export completed successfully', "Records exported: {$csv_data['count']}"); - - // Set headers for file download - header('Content-Type: text/csv'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); - header('Pragma: no-cache'); - header('Expires: 0'); - - // Output CSV content directly - echo $csv_data['content']; - exit; -} - -/** - * Save import/export settings - */ -add_action('wp_ajax_igny8_save_import_export_settings', 'igny8_ajax_save_import_export_settings'); -function igny8_ajax_save_import_export_settings() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_import_export_nonce')) { - wp_send_json_error(['message' => 'Security check failed']); - } - - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - $settings = [ - 'default_format' => sanitize_text_field($_POST['default_format'] ?? 'csv'), - 'overwrite_existing' => isset($_POST['overwrite_existing']) && $_POST['overwrite_existing'] === 'on', - 'include_metrics' => isset($_POST['include_metrics']) && $_POST['include_metrics'] === 'on', - 'file_naming_pattern' => sanitize_text_field($_POST['file_naming_pattern'] ?? 'igny8_export_[date].csv') - ]; - - update_option('igny8_import_export_settings', $settings); - wp_send_json_success(['message' => 'Import/Export settings saved successfully']); -} - -/** - * Import data helper function - */ -function igny8_import_data($type, $data, $headers) { - global $wpdb; - $imported = 0; - $skipped = 0; - $errors = []; - - switch ($type) { - case 'keywords': - $table = $wpdb->prefix . 'igny8_keywords'; - $required_fields = ['keyword']; - break; - case 'clusters': - $table = $wpdb->prefix . 'igny8_clusters'; - $required_fields = ['cluster_name']; - break; - case 'ideas': - $table = $wpdb->prefix . 'igny8_content_ideas'; - $required_fields = ['idea_title']; - break; - case 'mapping': - $table = $wpdb->prefix . 'igny8_mapping'; - $required_fields = ['source_id', 'target_id']; - break; - case 'tasks': - $table = $wpdb->prefix . 'igny8_tasks'; - $required_fields = ['title']; - break; - case 'templates': - $table = $wpdb->prefix . 'igny8_templates'; - $required_fields = ['template_name']; - break; - case 'audits': - $table = $wpdb->prefix . 'igny8_audits'; - $required_fields = ['page_id']; - break; - case 'suggestions': - $table = $wpdb->prefix . 'igny8_suggestions'; - $required_fields = ['audit_id']; - break; - case 'backlinks': - $table = $wpdb->prefix . 'igny8_backlinks'; - $required_fields = ['source_url', 'target_url']; - break; - case 'campaigns': - $table = $wpdb->prefix . 'igny8_campaigns'; - $required_fields = ['campaign_name']; - break; - case 'rewrites': - $table = $wpdb->prefix . 'igny8_rewrites'; - $required_fields = ['post_id']; - break; - case 'tones': - $table = $wpdb->prefix . 'igny8_tones'; - $required_fields = ['tone_name']; - break; - case 'personalization_data': - $table = $wpdb->prefix . 'igny8_personalization_data'; - $required_fields = ['data_key']; - break; - case 'variations': - $table = $wpdb->prefix . 'igny8_variations'; - $required_fields = ['post_id', 'field_name']; - break; - default: - return ['success' => false, 'message' => 'Invalid import type']; - } - - foreach ($data as $row) { - // Validate required fields - $missing_fields = []; - foreach ($required_fields as $field) { - if (empty($row[$field])) { - $missing_fields[] = $field; - } - } - - if (!empty($missing_fields)) { - $skipped++; - $errors[] = "Row skipped: Missing required fields: " . implode(', ', $missing_fields); - continue; - } - - // Prepare data for insertion - $insert_data = []; - $format = []; - - foreach ($headers as $header) { - if (isset($row[$header]) && $row[$header] !== '') { - $insert_data[$header] = $row[$header]; - - // Determine format based on field type - if (in_array($header, ['search_volume', 'difficulty', 'cpc', 'keyword_count', 'total_volume', 'avg_difficulty', 'mapped_pages_count', 'estimated_word_count', 'source_id', 'target_id', 'sector_id', 'cluster_id'])) { - $format[] = '%d'; - } else { - $format[] = '%s'; - } - } - } - - // Add default fields - if (!isset($insert_data['status'])) { - $insert_data['status'] = 'active'; - $format[] = '%s'; - } - - if (!isset($insert_data['created_at'])) { - $insert_data['created_at'] = current_time('mysql'); - $format[] = '%s'; - } - - // Insert into database - $result = $wpdb->insert($table, $insert_data, $format); - - if ($result === false) { - $skipped++; - $errors[] = "Failed to insert row: " . $wpdb->last_error; - } else { - $imported++; - - // Trigger cluster_added action for imported clusters to create taxonomy terms - if ($type === 'clusters') { - $cluster_id = $wpdb->insert_id; - do_action('igny8_cluster_added', $cluster_id); - } - } - } - - $message = "Import completed. Imported: {$imported}, Skipped: {$skipped}"; - $details = !empty($errors) ? implode('; ', array_slice($errors, 0, 5)) : ''; - - return [ - 'success' => true, - 'message' => $message, - 'details' => $details, - 'imported' => $imported, - 'skipped' => $skipped, - 'errors' => $errors - ]; -} - -/** - * Export data helper function - */ -function igny8_export_data($type, $options = []) { - global $wpdb; - - switch ($type) { - case 'keywords': - $table = $wpdb->prefix . 'igny8_keywords'; - $columns = ['keyword', 'search_volume', 'difficulty', 'cpc', 'intent', 'status', 'sector_id', 'cluster_id']; - break; - case 'clusters': - $table = $wpdb->prefix . 'igny8_clusters'; - $columns = ['cluster_name', 'sector_id', 'status', 'keyword_count', 'total_volume', 'avg_difficulty', 'mapped_pages_count']; - break; - case 'ideas': - $table = $wpdb->prefix . 'igny8_content_ideas'; - $columns = ['idea_title', 'idea_description', 'content_structure', 'content_type', 'keyword_cluster_id', 'status', 'estimated_word_count']; - break; - case 'mapping': - $table = $wpdb->prefix . 'igny8_mapping'; - $columns = ['source_type', 'source_id', 'target_type', 'target_id', 'relevance_score']; - break; - case 'tasks': - $table = $wpdb->prefix . 'igny8_tasks'; - $columns = ['title', 'description', 'content_type', 'cluster_id', 'priority', 'status', 'keywords', 'schedule_at', 'assigned_post_id']; - break; - case 'templates': - $table = $wpdb->prefix . 'igny8_templates'; - $columns = ['template_name', 'prompt_type', 'system_prompt', 'user_prompt', 'is_active']; - break; - case 'audits': - $table = $wpdb->prefix . 'igny8_audits'; - $columns = ['page_id', 'audit_status', 'seo_score', 'issues_found', 'recommendations']; - break; - case 'suggestions': - $table = $wpdb->prefix . 'igny8_suggestions'; - $columns = ['audit_id', 'suggestion_type', 'priority', 'status', 'impact_level']; - break; - case 'backlinks': - $table = $wpdb->prefix . 'igny8_backlinks'; - $columns = ['source_url', 'target_url', 'anchor_text', 'domain_authority', 'link_type', 'status']; - break; - case 'campaigns': - $table = $wpdb->prefix . 'igny8_campaigns'; - $columns = ['campaign_name', 'target_url', 'status', 'backlink_count', 'live_links_count']; - break; - case 'rewrites': - $table = $wpdb->prefix . 'igny8_rewrites'; - $columns = ['post_id', 'tone_id', 'variation_content', 'created_at']; - break; - case 'tones': - $table = $wpdb->prefix . 'igny8_tones'; - $columns = ['tone_name', 'tone_type', 'description', 'status', 'usage_count']; - break; - case 'personalization_data': - $table = $wpdb->prefix . 'igny8_personalization_data'; - $columns = ['data_key', 'data_value', 'data_type', 'created_at']; - break; - case 'variations': - $table = $wpdb->prefix . 'igny8_variations'; - $columns = ['post_id', 'field_name', 'variation_content', 'tone_id']; - break; - default: - return false; - } - - // Add optional columns - if ($options['include_timestamps']) { - $columns[] = 'created_at'; - $columns[] = 'updated_at'; - } - - // Build query with optional filters - $query = "SELECT " . implode(', ', $columns) . " FROM {$table}"; - $query_params = []; - - // Add WHERE clause for selected IDs if provided - if (!empty($options['selected_ids'])) { - $placeholders = implode(',', array_fill(0, count($options['selected_ids']), '%d')); - $query .= " WHERE id IN ({$placeholders})"; - $query_params = $options['selected_ids']; - } else { - $query_params = []; - } - - // Debug logging - error_log('Igny8 Export Debug - Table: ' . $table); - error_log('Igny8 Export Debug - Query: ' . $query); - error_log('Igny8 Export Debug - Query params: ' . print_r($query_params, true)); - error_log('Igny8 Export Debug - Selected IDs: ' . print_r($options['selected_ids'] ?? 'NOT SET', true)); - - // Check if table exists - $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table}'"); - error_log('Igny8 Export Debug - Table exists: ' . ($table_exists ? 'YES' : 'NO')); - - if (!$table_exists) { - error_log('Igny8 Export Debug - Table does not exist: ' . $table); - return [ - 'content' => '', - 'count' => 0, - 'message' => 'Table does not exist: ' . $table - ]; - } - - // Check total records in table - $total_records = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); - error_log('Igny8 Export Debug - Total records in table: ' . $total_records); - - // Check if columns exist in table - $existing_columns = $wpdb->get_col("SHOW COLUMNS FROM {$table}"); - error_log('Igny8 Export Debug - Existing columns: ' . print_r($existing_columns, true)); - - // Filter out non-existent columns - $valid_columns = array_intersect($columns, $existing_columns); - $invalid_columns = array_diff($columns, $existing_columns); - - if (!empty($invalid_columns)) { - error_log('Igny8 Export Debug - Invalid columns removed: ' . print_r($invalid_columns, true)); - } - - if (empty($valid_columns)) { - error_log('Igny8 Export Debug - No valid columns found for export'); - return [ - 'content' => '', - 'count' => 0, - 'message' => 'No valid columns found for export' - ]; - } - - // Rebuild query with valid columns only - $query = "SELECT " . implode(', ', $valid_columns) . " FROM {$table}"; - - // Re-add WHERE clause for selected IDs if provided (after column validation) - if (!empty($options['selected_ids'])) { - $placeholders = implode(',', array_fill(0, count($options['selected_ids']), '%d')); - $query .= " WHERE id IN ({$placeholders})"; - $query_params = $options['selected_ids']; - } else { - $query_params = []; - } - - if (!empty($query_params)) { - $results = $wpdb->get_results($wpdb->prepare($query, $query_params), ARRAY_A); - } else { - $results = $wpdb->get_results($query, ARRAY_A); - } - - // Check for SQL errors - if ($wpdb->last_error) { - error_log('Igny8 Export Debug - SQL Error: ' . $wpdb->last_error); - return [ - 'content' => '', - 'count' => 0, - 'message' => 'SQL Error: ' . $wpdb->last_error - ]; - } - - error_log('Igny8 Export Debug - Results count: ' . count($results)); - error_log('Igny8 Export Debug - Results: ' . print_r($results, true)); - - if (empty($results)) { - return [ - 'content' => '', - 'count' => 0 - ]; - } - - // Generate CSV content - $output = fopen('php://temp', 'r+'); - - // Write headers - fputcsv($output, $columns); - - // Write data - foreach ($results as $row) { - fputcsv($output, $row); - } - - rewind($output); - $csv_content = stream_get_contents($output); - fclose($output); - - return [ - 'content' => $csv_content, - 'count' => count($results) - ]; -} - -/** - * Log import/export activity - */ -function igny8_log_import_export($operation, $type, $success, $message, $details = '') { - $logs = get_option('igny8_import_export_logs', []); - - $log_entry = [ - 'timestamp' => current_time('mysql'), - 'operation' => ucfirst($operation) . ' ' . ucfirst($type), - 'status' => $success ? 'success' : 'error', - 'message' => $message, - 'details' => $details - ]; - - // Add to beginning of array (newest first) - array_unshift($logs, $log_entry); - - // Keep only last 50 entries - $logs = array_slice($logs, 0, 50); - - update_option('igny8_import_export_logs', $logs); -} - -/** - * AJAX handler for exporting selected records - */ -function igny8_ajax_export_selected() { - // Debug logging - error_log('Igny8 Export Selected Debug - POST data: ' . print_r($_POST, true)); - - // Verify nonce - check both possible nonce actions - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_import_export = wp_verify_nonce($_POST['nonce'], 'igny8_import_export_nonce'); - $nonce_ajax = wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - $nonce_valid = $nonce_import_export || $nonce_ajax; - - error_log('Igny8 Export Selected Debug - Import/Export nonce valid: ' . ($nonce_import_export ? 'YES' : 'NO')); - error_log('Igny8 Export Selected Debug - AJAX nonce valid: ' . ($nonce_ajax ? 'YES' : 'NO')); - error_log('Igny8 Export Selected Debug - Overall valid: ' . ($nonce_valid ? 'YES' : 'NO')); - } - - if (!$nonce_valid) { - wp_send_json_error('Security check failed - Invalid nonce. Received: ' . ($_POST['nonce'] ?? 'NOT SET')); - } - - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - $export_type = sanitize_text_field($_POST['export_type'] ?? ''); - $selected_ids = isset($_POST['selected_ids']) ? json_decode(stripslashes($_POST['selected_ids']), true) : []; - - error_log('Igny8 Export Selected Debug - Raw selected_ids: ' . print_r($_POST['selected_ids'] ?? 'NOT SET', true)); - error_log('Igny8 Export Selected Debug - Decoded selected_ids: ' . print_r($selected_ids, true)); - - if (empty($export_type)) { - wp_send_json_error('Export type is required'); - } - - // For export all, selected_ids can be empty - if (!empty($selected_ids) && !is_array($selected_ids)) { - wp_send_json_error('Invalid selected IDs format'); - } - - // Get export options - $options = [ - 'include_timestamps' => isset($_POST['include_timestamps']) && $_POST['include_timestamps'], - 'include_relationships' => isset($_POST['include_relationships']) && $_POST['include_relationships'], - 'include_metrics' => isset($_POST['include_metrics']) && $_POST['include_metrics'] - ]; - - // Add selected IDs filter - $options['selected_ids'] = array_map('intval', $selected_ids); - - error_log('Igny8 Export Selected Debug - Export type: ' . $export_type); - error_log('Igny8 Export Selected Debug - Options: ' . print_r($options, true)); - - $result = igny8_export_data($export_type, $options); - - error_log('Igny8 Export Selected Debug - Export result: ' . print_r($result, true)); - - if (!$result || !is_array($result)) { - wp_send_json_error('Export failed - No data returned from export function'); - } - - // Handle empty results - if ($result['count'] == 0) { - wp_send_json_error('No records found to export'); - } - - // Log the export - igny8_log_import_export('export_selected', $export_type, $result['success'], $result['message'], "Records exported: {$result['count']}"); - - wp_send_json_success([ - 'csv_content' => $result['content'], - 'count' => $result['count'], - 'filename' => $export_type . '_export_' . date('Y-m-d_H-i-s') . '.csv' - ]); -} -add_action('wp_ajax_igny8_export_selected', 'igny8_ajax_export_selected'); - -/** - * AJAX handler for getting import modal HTML - */ -function igny8_ajax_get_import_modal() { - // Verify nonce - check both possible nonce actions - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_import_export_nonce') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - } - - if (!$nonce_valid) { - wp_send_json_error('Security check failed - Invalid nonce'); - } - - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - if (empty($table_id)) { - wp_send_json_error('Table ID is required'); - } - - // Get configuration for this table - $config = igny8_get_import_export_config($table_id); - if (!$config) { - wp_send_json_error('Import/Export not available for this table'); - } - - // Generate modal HTML using PHP template - $modal_html = igny8_get_import_modal_html($table_id, $config); - - wp_send_json_success($modal_html); -} -add_action('wp_ajax_igny8_get_import_modal', 'igny8_ajax_get_import_modal'); - -/** - * AJAX handler for getting export modal HTML - */ -function igny8_ajax_get_export_modal() { - // Verify nonce - check both possible nonce actions - $nonce_valid = false; - if (isset($_POST['nonce'])) { - $nonce_valid = wp_verify_nonce($_POST['nonce'], 'igny8_import_export_nonce') || - wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce'); - } - - if (!$nonce_valid) { - wp_send_json_error('Security check failed - Invalid nonce'); - } - - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - if (empty($table_id)) { - wp_send_json_error('Table ID is required'); - } - - $selected_ids = isset($_POST['selected_ids']) ? json_decode(stripslashes($_POST['selected_ids']), true) : []; - - // Get configuration for this table - $config = igny8_get_import_export_config($table_id); - if (!$config) { - wp_send_json_error('Import/Export not available for this table'); - } - - error_log('Igny8 Export Modal Debug - Table ID: ' . $table_id); - error_log('Igny8 Export Modal Debug - Config: ' . print_r($config, true)); - error_log('Igny8 Export Modal Debug - Selected IDs: ' . print_r($selected_ids, true)); - - // Generate modal HTML using PHP template - $modal_html = igny8_get_export_modal_html($table_id, $config, $selected_ids); - - wp_send_json_success($modal_html); -} -add_action('wp_ajax_igny8_get_export_modal', 'igny8_ajax_get_export_modal'); - -/** - * Get Import Modal HTML using PHP template - */ -function igny8_get_import_modal_html($table_id, $config) { - // Start output buffering - ob_start(); - - // Include the template file - define('IGNY8_INCLUDE_TEMPLATE', true); - include plugin_dir_path(dirname(__FILE__)) . '../modules/components/import-modal-tpl.php'; - - // Get the output and clean the buffer - $html = ob_get_clean(); - - return $html; -} - -/** - * Get Export Modal HTML using PHP template - */ -function igny8_get_export_modal_html($table_id, $config, $selected_ids = []) { - // Start output buffering - ob_start(); - - // Include the template file - define('IGNY8_INCLUDE_TEMPLATE', true); - include plugin_dir_path(dirname(__FILE__)) . '../modules/components/export-modal-tpl.php'; - - // Get the output and clean the buffer - $html = ob_get_clean(); - - return $html; -} - -/** - * API Logs - Get API request logs - */ -add_action('wp_ajax_igny8_get_api_logs', 'igny8_ajax_get_api_logs'); -function igny8_ajax_get_api_logs() { - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - global $wpdb; - $logs = $wpdb->get_results(" - SELECT * FROM {$wpdb->prefix}igny8_logs - WHERE source = 'openai_api' - ORDER BY created_at DESC - LIMIT 20 - "); - - $html = ''; - $total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_logs WHERE source = 'openai_api'"); - - if ($logs) { - foreach ($logs as $log) { - $context = json_decode($log->context, true); - $status_class = $log->status === 'success' ? 'success' : 'error'; - $status_icon = $log->status === 'success' ? '✅' : '❌'; - - $html .= " - " . esc_html($log->created_at) . " - {$status_icon} " . esc_html($log->status) . " - " . esc_html($context['model'] ?? 'Unknown') . " - " . intval($context['input_tokens'] ?? 0) . " / " . intval($context['output_tokens'] ?? 0) . " - " . igny8_format_cost($context['total_cost'] ?? 0) . " - " . esc_html($log->api_id ? substr($log->api_id, 0, 12) . '...' : 'N/A') . " - "; - } - } else { - $html = 'No API logs found.'; - } - - wp_send_json_success(['html' => $html, 'total' => $total]); -} - -/** - * API Logs - Clear API request logs - */ -add_action('wp_ajax_igny8_clear_api_logs', 'igny8_ajax_clear_api_logs'); -function igny8_ajax_clear_api_logs() { - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - global $wpdb; - $result = $wpdb->delete( - $wpdb->prefix . 'igny8_logs', - ['source' => 'openai_api'], - ['%s'] - ); - - if ($result !== false) { - wp_send_json_success(['message' => 'API logs cleared successfully']); - } else { - wp_send_json_error(['message' => 'Failed to clear API logs']); - } -} - -/** - * Image Logs - Get image request logs - */ -add_action('wp_ajax_igny8_get_image_logs', 'igny8_ajax_get_image_logs'); -function igny8_ajax_get_image_logs() { - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - global $wpdb; - $logs = $wpdb->get_results(" - SELECT * FROM {$wpdb->prefix}igny8_logs - WHERE source = 'openai_image' - ORDER BY created_at DESC - LIMIT 20 - "); - - $html = ''; - $total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_logs WHERE source = 'openai_image'"); - - if ($logs) { - foreach ($logs as $log) { - $context = json_decode($log->context, true); - $status_class = $log->status === 'success' ? 'success' : 'error'; - $status_icon = $log->status === 'success' ? '✅' : '❌'; - - $html .= " - " . esc_html($log->created_at) . " - {$status_icon} " . esc_html($log->status) . " - " . esc_html($context['model'] ?? 'dall-e-3') . " - " . intval($context['prompt_length'] ?? 0) . " chars - " . igny8_format_cost($context['total_cost'] ?? 0) . " - " . esc_html($context['image_size'] ?? '1024x1024') . " - " . esc_html($log->api_id ? substr($log->api_id, 0, 12) . '...' : 'N/A') . " - "; - } - } else { - $html = 'No image request logs found.'; - } - - wp_send_json_success(['html' => $html, 'total' => $total]); -} - -/** - * Image Logs - Clear image request logs - */ -add_action('wp_ajax_igny8_clear_image_logs', 'igny8_ajax_clear_image_logs'); -function igny8_ajax_clear_image_logs() { - // Check user permissions - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - global $wpdb; - $result = $wpdb->delete( - $wpdb->prefix . 'igny8_logs', - ['source' => 'openai_image'], - ['%s'] - ); - - if ($result !== false) { - wp_send_json_success(['message' => 'Image logs cleared successfully']); - } else { - wp_send_json_error(['message' => 'Failed to clear image logs']); - } -} - -// =================================================================== -// CRON HEALTH AJAX HANDLERS -// =================================================================== - - -/** - * Manual Cron Run AJAX handler - */ -add_action('wp_ajax_igny8_cron_manual_run', 'igny8_ajax_cron_manual_run'); -function igny8_ajax_cron_manual_run() { - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - $hook = sanitize_text_field($_POST['hook'] ?? ''); - - // Load master dispatcher functions - if (!function_exists('igny8_get_defined_cron_jobs')) { - include_once plugin_dir_path(__FILE__) . '../cron/igny8-cron-master-dispatcher.php'; - } - - $defined_jobs = igny8_get_defined_cron_jobs(); - - if (!isset($defined_jobs[$hook])) { - wp_send_json_error('Invalid cron job'); - } - - // Run the cron job manually with timing - $start_time = microtime(true); - do_action($hook); - $execution_time = microtime(true) - $start_time; - - // Update health status - $current_time = current_time('timestamp'); - $job_health = [ - 'last_run' => $current_time, - 'success' => true, - 'execution_time' => round($execution_time, 2), - 'error_message' => '', - 'next_run' => igny8_calculate_next_run_time($current_time, 'daily', 0) - ]; - update_option('igny8_cron_health_' . $hook, $job_health); - - // Update last run in settings - $cron_settings = get_option('igny8_cron_settings', []); - $cron_settings[$hook]['last_run'] = $current_time; - update_option('igny8_cron_settings', $cron_settings); - - wp_send_json_success([ - 'message' => "Cron job $hook executed successfully in " . round($execution_time, 2) . "s", - 'execution_time' => round($execution_time, 2) - ]); -} - -/** - * Clear Cron Locks AJAX handler - */ -add_action('wp_ajax_igny8_clear_cron_locks', 'igny8_ajax_clear_cron_locks'); -function igny8_ajax_clear_cron_locks() { - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - if (!wp_verify_nonce($_POST['nonce'], 'igny8_clear_locks')) { - wp_send_json_error('Invalid nonce'); - } - - // Clear all execution locks - global $wpdb; - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_igny8_%_processing'"); - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_igny8_%_processing'"); - - wp_send_json_success(['message' => 'All execution locks cleared successfully']); -} - -/** - * Test Runware API Connection AJAX handler - */ -function igny8_ajax_test_runware_connection() { - // Security checks - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Get Runware API key - $api_key = get_option('igny8_runware_api_key', ''); - - if (empty($api_key)) { - wp_send_json_error(['message' => 'Missing API key.']); - } - - // Prepare payload as specified - $payload = [ - [ - 'taskType' => 'authentication', - 'apiKey' => $api_key - ], - [ - 'taskType' => 'imageInference', - 'taskUUID' => wp_generate_uuid4(), - 'positivePrompt' => 'test image connection', - 'model' => 'runware:97@1', - 'width' => 128, - 'height' => 128, - 'negativePrompt' => 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title', - 'steps' => 2, - 'CFGScale' => 5, - 'numberResults' => 1 - ] - ]; - - // Make API request - $response = wp_remote_post('https://api.runware.ai/v1', [ - 'headers' => ['Content-Type' => 'application/json'], - 'body' => json_encode($payload), - 'timeout' => 30 - ]); - - if (is_wp_error($response)) { - wp_send_json_error(['message' => $response->get_error_message()]); - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - - if (isset($body['data'][0]['imageURL'])) { - wp_send_json_success(['message' => '✅ Runware API connected successfully!']); - } elseif (isset($body['errors'][0]['message'])) { - wp_send_json_error(['message' => '❌ ' . $body['errors'][0]['message']]); - } else { - wp_send_json_error(['message' => '❌ Unknown response from Runware.']); - } -} - -/** - * Save Image Generation Settings - AJAX handler for saving image generation settings - */ -add_action('wp_ajax_igny8_save_image_settings', 'igny8_ajax_save_image_settings'); -function igny8_ajax_save_image_settings() { - // Debug: Log all received data - error_log('Image settings AJAX called with data: ' . print_r($_POST, true)); - - // Verify nonce - if (!isset($_POST['generate_image_nonce'])) { - error_log('Image settings: Missing generate_image_nonce'); - wp_send_json_error('Missing security token'); - return; - } - - if (!wp_verify_nonce($_POST['generate_image_nonce'], 'generate_image')) { - error_log('Image settings: Nonce verification failed'); - wp_send_json_error('Security check failed'); - return; - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - error_log('Image settings: Insufficient permissions'); - wp_send_json_error('Insufficient permissions'); - return; - } - - // Get and sanitize form data - $image_type = sanitize_text_field($_POST['image_type'] ?? 'realistic'); - $image_provider = sanitize_text_field($_POST['image_provider'] ?? 'openai'); - $desktop_enabled = sanitize_text_field($_POST['desktop_enabled'] ?? '0'); - $mobile_enabled = sanitize_text_field($_POST['mobile_enabled'] ?? '0'); - $max_in_article_images = intval($_POST['max_in_article_images'] ?? 1); - $image_format = sanitize_text_field($_POST['image_format'] ?? 'jpg'); - $negative_prompt = sanitize_textarea_field($_POST['negative_prompt'] ?? ''); - - error_log('Image settings: Saving data - Type: ' . $image_type . ', Provider: ' . $image_provider . ', Desktop: ' . $desktop_enabled . ', Mobile: ' . $mobile_enabled . ', Max In-Article: ' . $max_in_article_images . ', Format: ' . $image_format); - - // Save image generation settings - update_option('igny8_image_type', $image_type); - update_option('igny8_image_service', $image_provider); - update_option('igny8_desktop_enabled', $desktop_enabled); - update_option('igny8_mobile_enabled', $mobile_enabled); - update_option('igny8_max_in_article_images', $max_in_article_images); - update_option('igny8_image_format', $image_format); - update_option('igny8_negative_prompt', $negative_prompt); - - error_log('Image settings: Successfully saved all options'); - - wp_send_json_success('Image settings saved successfully'); -} - -/** - * Save Image Prompt Template - AJAX handler for saving image prompt templates - */ -add_action('wp_ajax_igny8_save_image_prompt_template', 'igny8_ajax_save_image_prompt_template'); -function igny8_ajax_save_image_prompt_template() { - // Debug: Log all received data - error_log('Prompt template AJAX called with data: ' . print_r($_POST, true)); - - // Verify nonce - if (!isset($_POST['save_prompt_nonce'])) { - error_log('Prompt template: Missing save_prompt_nonce'); - wp_send_json_error('Missing security token'); - return; - } - - if (!wp_verify_nonce($_POST['save_prompt_nonce'], 'save_prompt')) { - error_log('Prompt template: Nonce verification failed'); - wp_send_json_error('Security check failed'); - return; - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - error_log('Prompt template: Insufficient permissions'); - wp_send_json_error('Insufficient permissions'); - return; - } - - // Get and sanitize prompt template - $prompt_template = sanitize_textarea_field($_POST['prompt_template'] ?? ''); - $negative_prompt = sanitize_textarea_field($_POST['negative_prompt'] ?? ''); - - if (empty($prompt_template)) { - error_log('Prompt template: Empty prompt template'); - wp_send_json_error('Prompt template is required'); - return; - } - - error_log('Prompt template: Saving template: ' . $prompt_template); - error_log('Prompt template: Saving negative prompt: ' . $negative_prompt); - - // Save image prompt template and negative prompt - update_option('igny8_image_prompt_template', $prompt_template); - update_option('igny8_negative_prompt', $negative_prompt); - - error_log('Prompt template: Successfully saved'); - - wp_send_json_success('Image prompt template saved successfully'); -} - -add_action('wp_ajax_igny8_reset_image_prompt_template', 'igny8_ajax_reset_image_prompt_template'); -function igny8_ajax_reset_image_prompt_template() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_thinker_settings')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Default image prompt template - $default_value = 'Create a high-quality {image_type} image to use as a featured photo for a blog post titled "{post_title}". The image should visually represent the theme, mood, and subject implied by the image prompt: {image_prompt}. Focus on a realistic, well-composed scene that naturally communicates the topic without text or logos. Use balanced lighting, pleasing composition, and photographic detail suitable for lifestyle or editorial web content. Avoid adding any visible or readable text, brand names, or illustrative effects. **And make sure image is not blurry.**'; - - // Reset the prompt template - update_option('igny8_image_prompt_template', $default_value); - - wp_send_json_success([ - 'message' => 'Image prompt template reset to default successfully', - 'prompt_value' => $default_value - ]); -} - -// TEST AJAX handler to verify ajax.php is loaded -add_action('wp_ajax_igny8_test_ajax_connection', 'igny8_ajax_test_connection'); -function igny8_ajax_test_connection() { - // Simple test to verify ajax.php is loaded and working - wp_send_json_success([ - 'message' => 'AJAX connection working! ajax.php is loaded.', - 'timestamp' => current_time('mysql'), - 'server_time' => date('Y-m-d H:i:s'), - 'test_data' => 'This message came from core/admin/ajax.php' - ]); -} - -// Content parsing function (temporary testing code) -function igny8_convert_to_wp_blocks($content) { - // Sanitize: keep only block-level tags - $content = strip_tags($content, '

        1. '; - } - - $row_id_attr = ($mode === 'edit' && !empty($record_data['id'])) - ? ' data-id="' . esc_attr($record_data['id']) . '"' - : ''; - - ob_start(); ?> - - > - - - - - - - - - - - - - /> - - /> - $lbl) { - if ($value == $val) $display_text = esc_html($lbl); - } - } - - ob_start(); - ?> -
          - -
          -
          Select
          - $lbl): ?> -
          - -
          -
          - - - diff --git a/igny8-ai-seo-wp-plugin/modules/components/import-modal-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/import-modal-tpl.php deleted file mode 100644 index c7777219..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/import-modal-tpl.php +++ /dev/null @@ -1,67 +0,0 @@ - - -
          -
          -
          -

          Import

          - -
          -
          -

          Import from a CSV file. Use the template below for proper format.

          - - -
          - -
          - - -
          - - - - -
          - - -

          Upload a CSV file with your . Use the template above for proper format.

          -
          - - -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/components/kpi-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/kpi-tpl.php deleted file mode 100644 index 2515500e..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/kpi-tpl.php +++ /dev/null @@ -1,140 +0,0 @@ - $kpi_info) { - if (!isset($kpi_info['query'])) { - continue; - } - - // Replace placeholders with actual values - $query = str_replace('{table_name}', $table_name, $kpi_info['query']); - $query = str_replace('{prefix}', $wpdb->prefix, $query); - - // Execute query safely - $result = $wpdb->get_var($query); - - // Store result (handle null/empty results) - $kpi_data[$kpi_key] = $result !== null ? (int) $result : 0; - } - - return $kpi_data; -} - -/** - * Get actual table name from table ID - * - * @param string $table_id The table ID (e.g., 'planner_keywords') - * @return string The actual table name (e.g., 'wp_igny8_keywords') - */ -function igny8_get_table_name($table_id) { - global $wpdb; - - // Map table IDs to actual table names - $table_mapping = [ - 'planner_home' => $wpdb->prefix . 'igny8_keywords', // Uses keywords table as base for home metrics - 'planner_keywords' => $wpdb->prefix . 'igny8_keywords', - 'planner_clusters' => $wpdb->prefix . 'igny8_clusters', - 'planner_ideas' => $wpdb->prefix . 'igny8_content_ideas', - 'writer_home' => $wpdb->prefix . 'igny8_content_ideas', // Uses ideas table as base for home metrics - 'writer_drafts' => $wpdb->prefix . 'igny8_tasks', - 'writer_published' => $wpdb->prefix . 'igny8_tasks', - 'writer_templates' => $wpdb->prefix . 'igny8_prompts', - 'writer_tasks' => $wpdb->prefix . 'igny8_tasks', - 'optimizer_audits' => $wpdb->prefix . 'igny8_logs', - 'optimizer_suggestions' => $wpdb->prefix . 'igny8_suggestions', - 'linker_backlinks' => $wpdb->prefix . 'igny8_backlinks', - 'linker_campaigns' => $wpdb->prefix . 'igny8_campaigns', - 'linker_links' => $wpdb->prefix . 'igny8_links', - 'personalize_rewrites' => $wpdb->prefix . 'igny8_variations', - 'personalize_tones' => $wpdb->prefix . 'igny8_sites', - 'personalize_data' => $wpdb->prefix . 'igny8_personalization', - 'personalize_variations' => $wpdb->prefix . 'igny8_variations', - 'personalize_records' => $wpdb->prefix . 'igny8_personalization', - 'thinker_prompts' => '' // No table needed for prompts submodule - ]; - - $table_name = $table_mapping[$table_id] ?? ''; - - // Throw error if unknown table ID (except for special cases that don't need tables) - if (empty($table_name) && !in_array($table_id, ['thinker_prompts'])) { - throw new InvalidArgumentException("Unknown table ID: {$table_id}"); - } - - return $table_name; -} - -/** - * Check if a table exists in the database - * - * @param string $table_name The table name to check - * @return bool True if table exists, false otherwise - */ -function igny8_table_exists($table_name) { - global $wpdb; - - $result = $wpdb->get_var($wpdb->prepare( - "SHOW TABLES LIKE %s", - $table_name - )); - - return $result === $table_name; -} - -/** - * Get KPI data with fallback for missing tables - * - * @param string $table_id The table ID - * @param array $kpi_config The KPI configuration - * @return array KPI data array with fallback values - */ -function igny8_get_kpi_data_safe($table_id, $kpi_config) { - $table_name = igny8_get_table_name($table_id); - - // If table doesn't exist, return empty data - if (!igny8_table_exists($table_name)) { - $fallback_data = []; - foreach ($kpi_config as $kpi_key => $kpi_info) { - $fallback_data[$kpi_key] = 0; - } - return $fallback_data; - } - - // Get real data - return igny8_get_kpi_data($table_id, $kpi_config); -} diff --git a/igny8-ai-seo-wp-plugin/modules/components/pagination-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/pagination-tpl.php deleted file mode 100644 index 955c500d..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/pagination-tpl.php +++ /dev/null @@ -1,136 +0,0 @@ - 1, - 'total_pages' => 1, - 'per_page' => 10, - 'total_items' => 0 - ], $pagination); - - // Start output buffering to capture HTML - ob_start(); - ?> - -
          - - - - 20): ?> - - - - - - - - - 4 && $current > 3): ?> - ... - - - - 2 && $current < $total - 1): ?> - - - - - 4 && $current < $total - 2): ?> - ... - - - - - 2): // Don't duplicate first 2 pages ?> - - - - - - - - -
          - - - per page -
          - - - Showing - of items - -
          - 1, - 'total_pages' => 1, - 'per_page' => 10, - 'total_items' => 0 -]; -$module = $module ?? ''; -$tab = $tab ?? ''; - -// Debug state: Pagination HTML rendered -if (function_exists('igny8_debug_state')) { - igny8_debug_state('PAGINATION_HTML_RENDERED', true, 'Pagination HTML rendered for ' . $table_id); -} -?> diff --git a/igny8-ai-seo-wp-plugin/modules/components/table-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/table-tpl.php deleted file mode 100644 index 2d5f9eda..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/table-tpl.php +++ /dev/null @@ -1,870 +0,0 @@ - [ - 'queued' => 'Queued', - 'in_progress' => 'In Progress', - 'draft' => 'Draft', - 'review' => 'Review', - 'published' => 'Published' - ], - 'priority' => [ - 'low' => 'Low', - 'medium' => 'Medium', - 'high' => 'High', - 'urgent' => 'Urgent' - ], - 'content_type' => [ - 'blog_post' => 'Blog Post', - 'landing_page' => 'Landing Page', - 'product_page' => 'Product Page', - 'guide_tutorial' => 'Guide Tutorial', - 'news_article' => 'News Article', - 'review' => 'Review', - 'comparison' => 'Comparison', - 'email' => 'Email', - 'social_media' => 'Social Media' - ] - ]; - - // Check for specific field mapping first - if (isset($field_mappings[$field_name][$value])) { - return $field_mappings[$field_name][$value]; - } - - // Fallback: convert snake_case to Title Case - return ucwords(str_replace('_', ' ', $value)); -} - -/** - * Get proper case for enum values - * - * @param string $value The snake_case value - * @return string The proper case value - */ -function igny8_get_proper_case_enum($value) { - $enum_mapping = [ - // Content Type values - 'blog_post' => 'Blog Post', - 'landing_page' => 'Landing Page', - 'product_page' => 'Product Page', - 'guide_tutorial' => 'Guide Tutorial', - 'news_article' => 'News Article', - 'review' => 'Review', - 'comparison' => 'Comparison', - 'page' => 'Page', - 'product' => 'Product', - 'product_description' => 'Product Description', - 'email' => 'Email', - 'social_media' => 'Social Media', - - // Status values - 'unmapped' => 'Unmapped', - 'mapped' => 'Mapped', - 'queued' => 'Queued', - 'published' => 'Published', - 'active' => 'Active', - 'inactive' => 'Inactive', - 'archived' => 'Archived', - 'draft' => 'Draft', - 'in_progress' => 'In Progress', - 'completed' => 'Completed', - 'cancelled' => 'Cancelled', - 'pending' => 'Pending', - 'failed' => 'Failed', - 'lost' => 'Lost', - 'approved' => 'Approved', - 'rejected' => 'Rejected', - 'needs_revision' => 'Needs Revision', - 'planning' => 'Planning', - 'paused' => 'Paused', - 'review' => 'Review', - - // Intent values - 'informational' => 'Informational', - 'navigational' => 'Navigational', - 'transactional' => 'Transactional', - 'commercial' => 'Commercial', - - // Link type values - 'dofollow' => 'Dofollow', - 'nofollow' => 'Nofollow', - 'sponsored' => 'Sponsored', - 'ugc' => 'UGC', - - // Coverage status values - 'fully_mapped' => 'Fully Mapped', - 'partially_mapped' => 'Partially Mapped', - 'not_mapped' => 'Not Mapped', - - // Suggestion type values - 'title_optimization' => 'Title Optimization', - 'meta_description' => 'Meta Description', - 'heading_structure' => 'Heading Structure', - 'content_improvement' => 'Content Improvement', - 'internal_linking' => 'Internal Linking', - - // Tone values - 'professional' => 'Professional', - 'casual' => 'Casual', - 'friendly' => 'Friendly', - 'authoritative' => 'Authoritative', - 'conversational' => 'Conversational', - - // Category values - 'business' => 'Business', - 'creative' => 'Creative', - 'technical' => 'Technical', - 'marketing' => 'Marketing', - 'educational' => 'Educational' - ]; - - return $enum_mapping[$value] ?? ucwords(str_replace('_', ' ', $value)); -} - -/** - * Apply proper case transformation to a value - * - * @param string $value The value to transform - * @param string $column_name The column name for special handling - * @return string The transformed value - */ -function igny8_apply_proper_case($value, $column_name) { - // Apply global proper case transformation to all enum fields - return igny8_get_proper_case_enum($value); -} - -/** - * Format date for created_at column (tasks table) - * Shows hours/days ago if less than 30 days, month/day if greater - */ -function igny8_format_created_date($date_string) { - if (empty($date_string)) { - return 'Never'; - } - - try { - // Use WordPress timezone - $wp_timezone = wp_timezone(); - $date = new DateTime($date_string, $wp_timezone); - $now = new DateTime('now', $wp_timezone); - $diff = $now->diff($date); - - // Calculate total days difference - $total_days = $diff->days; - - // If less than 30 days, show relative time - if ($total_days < 30) { - if ($total_days == 0) { - if ($diff->h > 0) { - $result = $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago'; - } elseif ($diff->i > 0) { - $result = $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago'; - } else { - $result = 'Just now'; - } - } else { - $result = $total_days . ' day' . ($total_days > 1 ? 's' : '') . ' ago'; - } - } else { - // If 30+ days, show month and day - $result = $date->format('M j'); - } - - return $result; - - } catch (Exception $e) { - // Fallback to original date if parsing fails - $fallback = wp_date('M j', strtotime($date_string)); - return $fallback; - } -} - -/** - * Format date for updated_at column (drafts/published tables) - * Shows hours/days ago if less than 30 days, month/day if greater - */ -function igny8_format_updated_date($date_string) { - if (empty($date_string)) { - return 'Never'; - } - - try { - // Use WordPress timezone - $wp_timezone = wp_timezone(); - $date = new DateTime($date_string, $wp_timezone); - $now = new DateTime('now', $wp_timezone); - $diff = $now->diff($date); - - // Calculate total days difference - $total_days = $diff->days; - - // If less than 30 days, show relative time - if ($total_days < 30) { - if ($total_days == 0) { - if ($diff->h > 0) { - return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago'; - } elseif ($diff->i > 0) { - return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago'; - } else { - return 'Just now'; - } - } else { - return $total_days . ' day' . ($total_days > 1 ? 's' : '') . ' ago'; - } - } else { - // If 30+ days, show month and day - return $date->format('M j'); - } - } catch (Exception $e) { - // Fallback to original date if parsing fails - return wp_date('M j', strtotime($date_string)); - } -} - -/** - * Display image prompts in a formatted way for table display - * - * @param string $image_prompts JSON string of image prompts - * @return string Formatted display string - */ -function igny8_display_image_prompts($image_prompts) { - if (empty($image_prompts)) { - return 'No prompts'; - } - - // Try to decode JSON - $prompts = json_decode($image_prompts, true); - if (!$prompts || !is_array($prompts)) { - return 'Invalid format'; - } - - $output = '
          '; - $count = 0; - - foreach ($prompts as $key => $prompt) { - if (!empty($prompt)) { - $count++; - $label = ucfirst(str_replace('_', ' ', $key)); - $truncated = strlen($prompt) > 50 ? substr($prompt, 0, 50) . '...' : $prompt; - $output .= '
          '; - $output .= '' . esc_html($label) . ': '; - $output .= '' . esc_html($truncated) . ''; - $output .= '
          '; - } - } - - if ($count === 0) { - $output = 'No prompts'; - } else { - $output .= '
          '; - } - - return $output; -} - -/** - * Format structured description for display - */ -function igny8_format_structured_description_for_display($structured_description) { - if (!is_array($structured_description)) { - return 'No structured outline available'; - } - - $formatted = "
          "; - $formatted .= "

          Content Outline

          "; - - // Handle introduction section with hook - if (!empty($structured_description['introduction'])) { - $formatted .= "
          "; - - // Add hook if it exists - if (!empty($structured_description['introduction']['hook'])) { - $formatted .= "
          Hook: " . esc_html($structured_description['introduction']['hook']) . "
          "; - } - - // Add paragraphs if they exist - if (!empty($structured_description['introduction']['paragraphs']) && is_array($structured_description['introduction']['paragraphs'])) { - $paragraph_count = 1; - foreach ($structured_description['introduction']['paragraphs'] as $paragraph) { - if (!empty($paragraph['details'])) { - $formatted .= "
          Intro Paragraph " . $paragraph_count . ": " . esc_html($paragraph['details']) . "
          "; - $paragraph_count++; - } - } - } - - $formatted .= "
          "; - } - - // Handle H2 sections if they exist - if (!empty($structured_description['H2']) && is_array($structured_description['H2'])) { - foreach ($structured_description['H2'] as $h2_section) { - $formatted .= "
          "; - $formatted .= "
          " . esc_html($h2_section['heading']) . "
          "; - - if (!empty($h2_section['subsections'])) { - $formatted .= "
            "; - foreach ($h2_section['subsections'] as $h3_section) { - $formatted .= "
          • "; - $formatted .= "" . esc_html($h3_section['subheading']) . ""; - $formatted .= " (" . esc_html($h3_section['content_type']) . ")"; - $formatted .= "
            " . esc_html($h3_section['details']) . ""; - $formatted .= "
          • "; - } - $formatted .= "
          "; - } - $formatted .= "
          "; - } - } - - $formatted .= "
          "; - return $formatted; -} - -/** - * Fetch real table data from database - * Phase-2: Real Data Loading from Config - * - * @param string $tableId The table identifier - * @param array $filters Optional filters to apply - * @param int $page Page number for pagination - * @return array Real data structure - */ -function igny8_fetch_table_data($tableId, $filters = [], $page = 1, $per_page = null) { - global $wpdb; - - // Sanitize all inputs - $tableId = sanitize_text_field($tableId); - $page = intval($page); - $per_page = $per_page ? intval($per_page) : get_option('igny8_records_per_page', 20); - - // Sanitize filters array - if (is_array($filters)) { - $filters = array_map('sanitize_text_field', $filters); - } else { - $filters = []; - } - - // Get table configuration to apply default filters - $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; - $GLOBALS['igny8_tables_config'] = $tables_config; - $table_config = igny8_get_dynamic_table_config($tableId); - - // Apply default filters - merge with existing filters - if (isset($table_config['default_filter'])) { - $default_filters = $table_config['default_filter']; - foreach ($default_filters as $key => $value) { - // Only apply default filter if not already set by user - if (!isset($filters[$key]) || empty($filters[$key])) { - $filters[$key] = $value; - } - } - } - - // Force completed status filter for writer_published table - if ($tableId === 'writer_published') { - $filters['status'] = ['completed']; - } - - // Get table name from table ID - $table_name = igny8_get_table_name($tableId); - - // Check if table exists - if (!igny8_table_exists($table_name)) { - // Return empty data if table doesn't exist - return [ - 'rows' => [], - 'pagination' => [ - 'page' => $page, - 'total' => 0, - 'per_page' => $per_page, - 'total_pages' => 0 - ] - ]; - } - - // Build WHERE clause for filters - $where_conditions = []; - $where_values = []; - - - if (!empty($filters)) { - foreach ($filters as $key => $value) { - if (!empty($value)) { - // Check if this is a range filter (min/max) - if (strpos($key, '-min') !== false) { - $field_name = str_replace('-min', '', $key); - $where_conditions[] = "`{$field_name}` >= %d"; - $where_values[] = intval($value); - } elseif (strpos($key, '-max') !== false) { - $field_name = str_replace('-max', '', $key); - $where_conditions[] = "`{$field_name}` <= %d"; - $where_values[] = intval($value); - } elseif (in_array($key, ['status', 'intent'])) { - // For dropdown filters (status, intent), handle both single values and arrays - if (is_array($value)) { - // For array values (like ['draft'] from default filters) - $placeholders = implode(',', array_fill(0, count($value), '%s')); - $where_conditions[] = "`{$key}` IN ({$placeholders})"; - $where_values = array_merge($where_values, $value); - } else { - // For single values - $where_conditions[] = "`{$key}` = %s"; - $where_values[] = $value; - } - } elseif ($key === 'difficulty') { - // For difficulty, convert text label to numeric range - $difficulty_range = igny8_get_difficulty_numeric_range($value); - if ($difficulty_range) { - $where_conditions[] = "`{$key}` >= %d AND `{$key}` <= %d"; - $where_values[] = $difficulty_range['min']; - $where_values[] = $difficulty_range['max']; - } - } else { - // For keyword search, use LIKE - $where_conditions[] = "`{$key}` LIKE %s"; - $where_values[] = '%' . $wpdb->esc_like($value) . '%'; - } - } - } - } - - $where_clause = ''; - if (!empty($where_conditions)) { - $where_clause = 'WHERE ' . implode(' AND ', $where_conditions); - } - - - // Build JOIN queries from column configurations - $join_queries = []; - foreach ($config_columns as $col_key => $col_config) { - if (isset($col_config['join_query'])) { - $join_query = str_replace(['{prefix}', '{table_name}'], [$wpdb->prefix, $table_name], $col_config['join_query']); - if (!in_array($join_query, $join_queries)) { - $join_queries[] = $join_query; - } - } - } - - // Get total count for pagination (with JOINs) - $join_clause = ''; - if (!empty($join_queries)) { - $join_clause = implode(' ', $join_queries); - } - - $count_query = "SELECT COUNT(*) FROM `{$table_name}` {$join_clause} {$where_clause}"; - if (!empty($where_values)) { - $total_count = $wpdb->get_var($wpdb->prepare($count_query, $where_values)); - } else { - $total_count = $wpdb->get_var($count_query); - } - - // Calculate pagination - $total_pages = ceil($total_count / $per_page); - $offset = ($page - 1) * $per_page; - - // Build main query with JOINs and proper field selection - $select_fields = []; - - // Add base table fields - $select_fields[] = "{$table_name}.*"; - - // Process column configurations to build select fields - foreach ($config_columns as $col_key => $col_config) { - if (isset($col_config['select_field'])) { - $select_fields[] = $col_config['select_field']; - } - } - - // Build final query with JOINs - $select_clause = implode(', ', $select_fields); - - $query = "SELECT {$select_clause} FROM `{$table_name}` {$join_clause} {$where_clause} ORDER BY {$table_name}.id DESC LIMIT %d OFFSET %d"; - $query_values = array_merge($where_values, [$per_page, $offset]); - - // Execute query - if (!empty($where_values)) { - $final_query = $wpdb->prepare($query, $query_values); - $results = $wpdb->get_results($final_query, ARRAY_A); - } else { - $final_query = $wpdb->prepare($query, $per_page, $offset); - $results = $wpdb->get_results($final_query, ARRAY_A); - } - - // Format results for frontend - return complete table body HTML using PHP templating - $table_body_html = ''; - - // Load table configuration once (needed for both results and empty state) - $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; - $GLOBALS['igny8_tables_config'] = $tables_config; - $table_config = igny8_get_dynamic_table_config($tableId); - $config_columns = $table_config['columns'] ?? []; - - // Get column keys from humanize_columns, including date columns - $column_keys = $table_config['humanize_columns'] ?? array_keys($config_columns); - - if ($results) { - - foreach ($results as $row) { - $id = $row['id'] ?? 0; - - // Use PHP templating instead of string concatenation - ob_start(); - ?> - - - prefix, $table_name], $calculation_query); - $query = str_replace('{table_name}.id', $row['id'], $query); - $value = $wpdb->get_var($query) ?? 0; - } else { - $value = 0; - } - } elseif ($col === 'cluster_id' || $col === 'keyword_cluster_id') { - // Check if this column has a display_field from JOIN query - $display_field = $column_config['display_field'] ?? null; - if ($display_field && isset($row[$display_field])) { - $value = $row[$display_field]; - } else { - // Fallback to fetching cluster name by ID - $cluster_id = isset($row[$col]) ? $row[$col] : ''; - $value = igny8_get_cluster_term_name($cluster_id); - } - } elseif ($col === 'source') { - // Handle source column - use actual field value - $value = isset($row[$col]) ? $row[$col] : ''; - } elseif ($col === 'sector_id') { - $sector_id = isset($row[$col]) ? $row[$col] : ''; - $value = igny8_get_sector_name($sector_id); - } elseif ($col === 'difficulty' || $col === 'avg_difficulty') { - $difficulty = isset($row[$col]) ? $row[$col] : ''; - $value = igny8_get_difficulty_range_name($difficulty); - } elseif (in_array($col, ['created_at', 'created_date', 'last_audit', 'discovered_date', 'start_date'])) { - // Format created/audit/discovered/start date columns for all tables - $date_value = isset($row[$col]) ? $row[$col] : ''; - $value = igny8_format_created_date($date_value); - } elseif (in_array($col, ['updated_at', 'updated_date', 'next_audit', 'end_date'])) { - // Format updated/next audit/end date columns for all tables - $date_value = isset($row[$col]) ? $row[$col] : ''; - $value = igny8_format_updated_date($date_value); - } else { - $value = isset($row[$col]) ? $row[$col] : ''; - // Apply proper case transformation to eligible columns - if (!empty($value) && igny8_should_apply_proper_case($col, $column_config)) { - $value = igny8_apply_proper_case($value, $col); - } - } - - // Special handling for idea_title column in planner_ideas table - if ($col === 'idea_title' && $tableId === 'planner_ideas') { - $description = isset($row['idea_description']) ? $row['idea_description'] : ''; - $image_prompts = isset($row['image_prompts']) ? $row['image_prompts'] : ''; - - // Format description for display (handle JSON vs plain text) - $display_description = ''; - if (!empty($description)) { - $decoded = json_decode($description, true); - if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { - // Format structured description for display - $display_description = igny8_format_structured_description_for_display($decoded); - } else { - // Use as plain text - $display_description = $description; - } - } - ?> - - - - - - - - - - - - - - - $page, - 'total_items' => intval($total_count), - 'per_page' => $per_page, - 'total_pages' => $total_pages - ]; - - - return [ - 'table_body_html' => $table_body_html, - 'pagination' => $pagination_data - ]; -} - -// Render table function -function igny8_render_table($table_id, $columns = []) { - // Load table configuration with AI-specific modifications - $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; - $GLOBALS['igny8_tables_config'] = $tables_config; - $table_config = igny8_get_dynamic_table_config($table_id); - - // Set variables for component - $module = explode('_', $table_id)[0]; - $tab = explode('_', $table_id)[1] ?? ''; - - // Use config columns if provided, otherwise use passed columns - $config_columns = $table_config['columns'] ?? []; - - // Convert associative array to indexed array with 'key' field, including date columns - if (!empty($config_columns)) { - $columns = []; - foreach ($config_columns as $key => $column_config) { - // Check if this column should be humanized - $humanize_columns = $table_config['humanize_columns'] ?? []; - $should_humanize = in_array($key, $humanize_columns); - - $columns[] = [ - 'key' => $key, - 'label' => $column_config['label'] ?? ($should_humanize ? igny8_humanize_label($key) : $key), - 'sortable' => $column_config['sortable'] ?? false, - 'type' => $column_config['type'] ?? 'text' - ]; - } - } - - // Start output buffering to capture HTML - ob_start(); - ?> - -
          - -

          '); - - // Add newline after block-level tags to preserve structure for regex matching - $content = preg_replace('/(<\/(p|ul|ol|table|blockquote|h[1-6])>)/i', "$1\n", $content); - - // Optional: trim inline whitespace only, preserve structural line breaks - $content = preg_replace('/>\s+<', $content); - - // Match all major blocks - preg_match_all('/(]*>.*?<\/h[1-6]>|]*>.*?<\/p>|]*>.*?<\/ul>|]*>.*?<\/ol>|]*>.*?<\/blockquote>|]*>.*?<\/table>|\[.*?\])/is', $content, $matches); - - $blocks = []; - $used_length = 0; - - foreach ($matches[0] as $block) { - $used_length += strlen($block); - - // Headings - FIXED: Always include level attribute - if (preg_match('/^]*>(.*?)<\/h\1>$/is', $block, $m)) { - $blocks[] = "\n

          {$m[2]}

          \n"; - } elseif (preg_match('/^]*>(.*?)<\/h\1>$/is', $block, $m)) { - $blocks[] = "\n{$m[2]}\n"; - } - - // Paragraph - elseif (preg_match('/^]*>(.*?)<\/p>$/is', $block, $m)) { - $blocks[] = "\n

          {$m[1]}

          \n"; - } - - // Unordered list - elseif (preg_match('/^]*>(.*?)<\/ul>$/is', $block, $m)) { - preg_match_all('/]*>(.*?)<\/li>/is', $m[1], $li_items); - $list_items = ''; - foreach ($li_items[1] as $li) { - $list_items .= "\n
        2. {$li}
        3. \n\n"; - } - $blocks[] = "\n
            \n{$list_items}
          \n"; - } - - // Ordered list - elseif (preg_match('/^]*>(.*?)<\/ol>$/is', $block, $m)) { - preg_match_all('/]*>(.*?)<\/li>/is', $m[1], $li_items); - $list_items = ''; - foreach ($li_items[1] as $li) { - $list_items .= "\n
        4. {$li}
        5. \n\n"; - } - $blocks[] = "\n
            \n{$list_items}
          \n"; - } - - // Blockquote - elseif (preg_match('/^]*>(.*?)<\/blockquote>$/is', $block, $m)) { - $inner = trim(strip_tags($m[1], '


          ')); - $blocks[] = "\n

          \n\n

          {$inner}

          \n\n
          \n"; - } - - // Table - elseif (preg_match('/^]*>(.*?)<\/table>$/is', $block, $m)) { - $blocks[] = "\n
          {$m[1]}
          \n"; - } - - // Shortcode - elseif (preg_match('/^\[(.*?)\]$/', $block, $m)) { - $blocks[] = "\n[{$m[1]}]\n"; - } - } - - // Handle trailing leftover text - $remaining = trim(substr($content, $used_length)); - if (!empty($remaining)) { - $remaining = strip_tags($remaining); - if ($remaining !== '') { - $blocks[] = "\n

          {$remaining}

          \n"; - } - } - - return implode("\n\n", $blocks); -} diff --git a/igny8-ai-seo-wp-plugin/core/admin/global-helpers.php b/igny8-ai-seo-wp-plugin/core/admin/global-helpers.php deleted file mode 100644 index a3bde6d5..00000000 --- a/igny8-ai-seo-wp-plugin/core/admin/global-helpers.php +++ /dev/null @@ -1,1084 +0,0 @@ -get_var($wpdb->prepare( - "SELECT cluster_name FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!empty($cluster_name)) { - return $cluster_name; - } - - // If no cluster found, return empty string - return ''; -} - - -/** - * Convert cluster name to cluster ID - * Used across multiple submodules and forms - * - * @param string $cluster_name The cluster name - * @return int The cluster ID or 0 if not found - */ -function igny8_get_cluster_id_by_name($cluster_name) { - if (empty($cluster_name) || !is_string($cluster_name)) { - return 0; - } - - global $wpdb; - - // Query the clusters table directly - $cluster_id = $wpdb->get_var($wpdb->prepare( - "SELECT id FROM {$wpdb->prefix}igny8_clusters WHERE cluster_name = %s", - $cluster_name - )); - - return $cluster_id ? (int)$cluster_id : 0; -} - -/** - * Get cluster options for dropdowns - * Used across all modules that need cluster selection - * - * @return array Array of cluster options in format [['value' => id, 'label' => name], ...] - */ -function igny8_get_cluster_options() { - global $wpdb; - - // Get all active clusters from database - $clusters = $wpdb->get_results( - "SELECT id, cluster_name FROM {$wpdb->prefix}igny8_clusters WHERE status = 'active' ORDER BY cluster_name ASC" - ); - - $options = [ - ['value' => '', 'label' => 'No Cluster'] - ]; - - if ($clusters) { - foreach ($clusters as $cluster) { - $options[] = [ - 'value' => $cluster->id, - 'label' => $cluster->cluster_name - ]; - } - } - - return $options; -} - -/** - * Get standardized cluster lookup configuration for tables - * This eliminates the need to repeat lookup configuration in every table - * - * @return array Standardized cluster lookup configuration - */ -function igny8_get_cluster_lookup_config() { - return [ - 'type' => 'lookup', - 'source_field' => 'cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ]; -} - -/** - * Get standardized cluster lookup configuration for ideas table - * Uses keyword_cluster_id instead of cluster_id - * - * @return array Standardized cluster lookup configuration for ideas - */ -function igny8_get_ideas_cluster_lookup_config() { - return [ - 'type' => 'lookup', - 'source_field' => 'keyword_cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.keyword_cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ]; -} - -/** - * Get dynamic table configuration based on AI mode - */ -function igny8_get_dynamic_table_config($table_id) { - $config = $GLOBALS['igny8_tables_config'][$table_id] ?? []; - - // Apply AI-specific modifications - if ($table_id === 'writer_queue') { - $writer_mode = igny8_get_ai_setting('writer_mode', 'manual'); - - // Hide due_date column for AI mode - if ($writer_mode === 'ai' && isset($config['columns']['due_date'])) { - unset($config['columns']['due_date']); - - // Also remove from filters - if (isset($config['filters']['due_date'])) { - unset($config['filters']['due_date']); - } - } - } - - // Apply status filter for drafts table - if ($table_id === 'writer_drafts') { - $config['default_filter'] = [ - 'status' => ['draft'] - ]; - } - - // Apply status filter for published table - if ($table_id === 'writer_published') { - $config['default_filter'] = [ - 'status' => ['published'] - ]; - } - - return $config; -} - -/** - * Get dynamic form configuration based on table columns - */ -function igny8_get_dynamic_form_config($form_id) { - $form_config = $GLOBALS['igny8_forms_config'][$form_id] ?? []; - - // For writer_queue form, conditionally include due_date field - if ($form_id === 'writer_queue') { - $writer_mode = igny8_get_ai_setting('writer_mode', 'manual'); - - // Remove due_date field for AI mode - if ($writer_mode === 'ai') { - $form_config['fields'] = array_filter($form_config['fields'], function($field) { - return $field['name'] !== 'due_date'; - }); - // Re-index array - $form_config['fields'] = array_values($form_config['fields']); - } - } - - return $form_config; -} - -/** - * Convert difficulty number to predefined difficulty range name - * - * @param float $difficulty The difficulty value (0-100) - * @return string The difficulty range name - */ -function igny8_get_difficulty_range_name($difficulty) { - if (empty($difficulty) || !is_numeric($difficulty)) { - return ''; - } - - $difficulty = floatval($difficulty); - - if ($difficulty <= 20) { - return 'Very Easy'; - } elseif ($difficulty <= 40) { - return 'Easy'; - } elseif ($difficulty <= 60) { - return 'Medium'; - } elseif ($difficulty <= 80) { - return 'Hard'; - } else { - return 'Very Hard'; - } -} - -/** - * Convert difficulty text label to numeric range for database queries - * - * @param string $difficulty_label The difficulty text label - * @return array|false Array with 'min' and 'max' keys, or false if invalid - */ -function igny8_get_difficulty_numeric_range($difficulty_label) { - if (empty($difficulty_label)) { - return false; - } - - switch ($difficulty_label) { - case 'Very Easy': - return ['min' => 0, 'max' => 20]; - case 'Easy': - return ['min' => 21, 'max' => 40]; - case 'Medium': - return ['min' => 41, 'max' => 60]; - case 'Hard': - return ['min' => 61, 'max' => 80]; - case 'Very Hard': - return ['min' => 81, 'max' => 100]; - default: - return false; - } -} - -/** - * Get page description - */ -function igny8_get_page_description() { - $current_page = $_GET['page'] ?? ''; - $sm = $_GET['sm'] ?? ''; - - switch ($current_page) { - case 'igny8-home': - return 'Welcome to Igny8 AI SEO OS - Your comprehensive SEO management platform.'; - case 'igny8-planner': - if ($sm === 'keywords') { - return 'Manage your keywords, track search volumes, and organize by intent and difficulty.'; - } elseif ($sm === 'clusters') { - return 'Group related keywords into content clusters for better topical authority.'; - } elseif ($sm === 'ideas') { - return 'Generate and organize content ideas based on your keyword research.'; - } elseif ($sm === 'mapping') { - return 'Map keywords and clusters to existing pages and content.'; - } else { - return 'Plan and organize your content strategy with keyword research and clustering.'; - } - case 'igny8-writer': - if ($sm === 'drafts') { - return 'Manage your content drafts and work in progress.'; - } elseif ($sm === 'templates') { - return 'Create and manage content templates for consistent writing.'; - } else { - return 'Content creation and writing tools for SEO-optimized content.'; - } - case 'igny8-optimizer': - if ($sm === 'audits') { - return 'Run comprehensive SEO audits on your content and pages.'; - } elseif ($sm === 'suggestions') { - return 'Get AI-powered optimization suggestions for better rankings.'; - } else { - return 'SEO optimization and performance tools for better rankings.'; - } - case 'igny8-linker': - if ($sm === 'backlinks') { - return 'Track and manage your backlink profile and authority.'; - } elseif ($sm === 'campaigns') { - return 'Plan and execute link building campaigns effectively.'; - } else { - return 'Link building and backlink management for improved authority.'; - } - case 'igny8-personalize': - if ($sm === 'settings') { - return 'Configure global settings, display options, and advanced personalization settings.'; - } elseif ($sm === 'content-generation') { - return 'Configure AI prompts, field detection, and content generation parameters.'; - } elseif ($sm === 'rewrites') { - return 'View and manage personalized content variations and rewrites.'; - } elseif ($sm === 'front-end') { - return 'Manage front-end display settings, shortcode usage, and implementation guides.'; - } else { - return 'Content personalization and targeting for better engagement.'; - } - case 'igny8-settings': - $sp = $_GET['sp'] ?? 'general'; - if ($sp === 'status') { - return 'Monitor system health, database status, and module performance.'; - } elseif ($sp === 'integration') { - return 'Configure API keys and integrate with external services.'; - } elseif ($sp === 'import-export') { - return 'Import and export data, manage backups, and transfer content.'; - } else { - return 'Configure plugin settings, automation, and table preferences.'; - } - case 'igny8-analytics': - return 'Performance analytics and reporting for data-driven decisions.'; - case 'igny8-schedules': - return 'Content scheduling and automation for consistent publishing.'; - case 'igny8-help': - return 'Documentation and support resources for getting started.'; - default: - return 'Igny8 AI SEO OS - Advanced SEO Management'; - } -} - -/** - * Get page title - */ -function igny8_get_page_title() { - $current_page = $_GET['page'] ?? ''; - $sm = $_GET['sm'] ?? ''; - - switch ($current_page) { - case 'igny8-home': - return 'Igny8 Home'; - case 'igny8-planner': - if ($sm === 'keywords') { - return 'Keywords'; - } elseif ($sm === 'clusters') { - return 'Clusters'; - } elseif ($sm === 'ideas') { - return 'Ideas'; - } elseif ($sm === 'mapping') { - return 'Mapping'; - } else { - return 'Planner'; - } - case 'igny8-writer': - if ($sm === 'tasks') { - return 'Content Queue / Tasks'; - } elseif ($sm === 'drafts') { - return 'Content Generated'; - } elseif ($sm === 'published') { - return 'Live Content'; - } else { - return 'Content Writer'; - } - case 'igny8-optimizer': - if ($sm === 'audits') { - return 'Audits'; - } elseif ($sm === 'suggestions') { - return 'Suggestions'; - } else { - return 'Optimizer'; - } - case 'igny8-linker': - if ($sm === 'backlinks') { - return 'Backlinks'; - } elseif ($sm === 'campaigns') { - return 'Campaigns'; - } else { - return 'Linker'; - } - case 'igny8-personalize': - if ($sm === 'settings') { - return 'Personalization Settings'; - } elseif ($sm === 'content-generation') { - return 'Content Generation'; - } elseif ($sm === 'rewrites') { - return 'Rewrites'; - } elseif ($sm === 'front-end') { - return 'Front-end'; - } else { - return 'Personalize'; - } - case 'igny8-thinker': - $sp = $_GET['sp'] ?? 'main'; - if ($sp === 'main') { - return 'AI Thinker Dashboard'; - } elseif ($sp === 'prompts') { - return 'AI Prompts'; - } elseif ($sp === 'profile') { - return 'AI Profile'; - } elseif ($sp === 'strategies') { - return 'Content Strategies'; - } elseif ($sp === 'image-testing') { - return 'AI Image Testing'; - } else { - return 'AI Thinker'; - } - case 'igny8-settings': - $sp = $_GET['sp'] ?? 'general'; - if ($sp === 'status') { - return 'System Status'; - } elseif ($sp === 'integration') { - return 'API Integration'; - } elseif ($sp === 'import-export') { - return 'Import/Export'; - } else { - return 'General Settings'; - } - case 'igny8-analytics': - return 'Analytics'; - case 'igny8-schedules': - return 'Schedules'; - case 'igny8-test': - $sp = $_GET['sp'] ?? 'system-testing'; - if ($sp === 'system-testing') { - return 'System Testing'; - } elseif ($sp === 'temp-function-testing') { - return 'Function Testing'; - } else { - return 'Test Page'; - } - case 'igny8-help': - return 'Help'; - default: - return 'Igny8 AI SEO OS'; - } -} - - - - - - - - -// REMOVED: Task variations functionality - tasks don't need variations - -/** - * Safe logging helper for Igny8 operations - * - * @param string $type Log type (e.g., 'queue_to_writer', 'task_created') - * @param mixed $data Log data (string, array, or object) - */ -function igny8_write_log($type, $data) { - global $wpdb; - - try { - $wpdb->insert($wpdb->prefix . 'igny8_logs', [ - 'level' => 'info', - 'message' => sanitize_text_field($type), - 'context' => is_array($data) ? wp_json_encode($data) : (is_string($data) ? $data : null), - 'source' => 'igny8_plugin', - 'user_id' => get_current_user_id(), - ], ['%s', '%s', '%s', '%s', '%d']); - } catch (Exception $e) { - // Log to error log if database logging fails - error_log('Igny8: Failed to write to logs table: ' . $e->getMessage()); - } -} - -/** - * Get clusters for select dropdown - * - * @return array Array of cluster options for select dropdown - */ -function igny8_get_clusters_for_select() { - global $wpdb; - - $clusters = $wpdb->get_results( - "SELECT id, cluster_name FROM {$wpdb->prefix}igny8_clusters ORDER BY cluster_name ASC" - ); - - $options = ['' => 'Select Cluster']; - foreach ($clusters as $cluster) { - $options[$cluster->id] = $cluster->cluster_name; - } - - return $options; -} - -/** - * Get sector options for dropdowns - * Used across all modules that need sector selection - * Only returns sectors from saved planner settings - * - * @return array Array of sector options in format [['value' => id, 'label' => name], ...] - */ -function igny8_get_sector_options() { - // Get saved sector selection from user meta - $user_id = get_current_user_id(); - $saved_selection = get_user_meta($user_id, 'igny8_planner_sector_selection', true); - - $options = []; - - // If no saved selection, return empty options - if (empty($saved_selection)) { - return $options; - } - - // Add only child sectors (not parent) - if (isset($saved_selection['children']) && is_array($saved_selection['children'])) { - foreach ($saved_selection['children'] as $child) { - if (isset($child['id']) && isset($child['name'])) { - $options[] = [ - 'value' => $child['id'], - 'label' => $child['name'] - ]; - } - } - } - - return $options; -} - -/** - * Get sectors for select dropdown - * Only returns sectors from saved planner settings - * - * @return array Array of sector options for select dropdown - */ -function igny8_get_sectors_for_select() { - // Get saved sector selection from user meta - $user_id = get_current_user_id(); - $saved_selection = get_user_meta($user_id, 'igny8_planner_sector_selection', true); - - $options = []; - - // If no saved selection, return empty options - if (empty($saved_selection)) { - return $options; - } - - // Add only child sectors (not parent) - if (isset($saved_selection['children']) && is_array($saved_selection['children'])) { - foreach ($saved_selection['children'] as $child) { - if (isset($child['id']) && isset($child['name'])) { - $options[$child['id']] = $child['name']; - } - } - } - - return $options; -} - -/** - * Get ideas for select dropdown - * - * @return array Array of idea options for select dropdown - */ -function igny8_get_ideas_for_select() { - global $wpdb; - - $ideas = $wpdb->get_results( - "SELECT id, idea_title FROM {$wpdb->prefix}igny8_content_ideas ORDER BY idea_title ASC" - ); - - $options = ['' => 'Select Idea']; - foreach ($ideas as $idea) { - $options[$idea->id] = $idea->idea_title; - } - - return $options; -} - -/** - * Transform database field names to human-readable labels - * - * @param string $field_name The database field name (snake_case) - * @return string The human-readable label (Title Case) - */ -function igny8_humanize_label($field) { - // Handle non-string input safely - if (!is_string($field)) { - return ''; - } - - // Remove any potentially dangerous characters - $field = sanitize_key($field); - - // Specific field mappings for better readability - $field_mappings = [ - // Clusters table - 'cluster_name' => 'Cluster Name', - 'sector_id' => 'Sectors', - 'keyword_count' => 'Keywords', - 'total_volume' => 'Total Volume', - 'avg_difficulty' => 'Avg KD', - 'mapped_pages_count' => 'Mapped Pages', - 'created_at' => 'Created', - 'updated_at' => 'Updated', - - // Keywords table - 'search_volume' => 'Volume', - 'difficulty' => 'Difficulty', - 'cpc' => 'CPC', - 'intent' => 'Intent', - 'status' => 'Status', - 'cluster_id' => 'Cluster', - - // Ideas table - 'idea_title' => 'Title', - 'idea_description' => 'Description', - 'content_structure' => 'Structure', - 'content_angle' => 'Content Angle', - 'keyword_cluster_id' => 'Cluster', - 'estimated_word_count' => 'Words', - 'ai_generated' => 'AI Generated', - 'mapped_post_id' => 'Mapped Post', - 'tasks_count' => 'Tasks Count', - - // Tasks table - 'content_type' => 'Content Type', - 'cluster_id' => 'Cluster', - 'keywords' => 'Keywords', - 'schedule_at' => 'Scheduled', - 'assigned_post_id' => 'Assigned Post', - 'created_at' => 'Created', - 'updated_at' => 'Updated', - - // Campaigns table - 'backlink_count' => 'Backlinks', - 'live_links_count' => 'Live Links', - - // Mapping table - 'page_id' => 'Page ID', - 'coverage_percentage' => 'Coverage %', - 'coverage_status' => 'Status', - 'last_updated' => 'Updated' - ]; - - // Check for specific mapping first - if (isset($field_mappings[$field])) { - return $field_mappings[$field]; - } - - // Fallback: convert snake_case to Title Case - return ucwords(str_replace('_', ' ', $field)); -} - -/** - * Transform database field names to human-readable labels (legacy function) - * @deprecated Use igny8_humanize_label() instead - */ -function igny8_transform_field_label($field_name) { - return igny8_humanize_label($field_name); -} - - - - -/** - * Map cluster to keywords - * - * @param int $cluster_id Cluster ID to map keywords to - * @param array $keyword_ids Array of keyword IDs to map - * @return array ['success' => bool, 'message' => string, 'mapped_count' => int] - */ -function igny8_map_cluster_to_keywords($cluster_id, $keyword_ids) { - global $wpdb; - - if (empty($cluster_id) || !is_numeric($cluster_id)) { - return ['success' => false, 'message' => 'Invalid cluster ID provided', 'mapped_count' => 0]; - } - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - return ['success' => false, 'message' => 'No keywords provided for mapping', 'mapped_count' => 0]; - } - - $cluster_id = intval($cluster_id); - - // Verify cluster exists - $cluster_exists = $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!$cluster_exists) { - return ['success' => false, 'message' => 'Cluster not found', 'mapped_count' => 0]; - } - - // Sanitize and validate keyword IDs - $keyword_ids = array_map('intval', $keyword_ids); - $keyword_ids = array_filter($keyword_ids, function($id) { return $id > 0; }); - - if (empty($keyword_ids)) { - return ['success' => false, 'message' => 'No valid keyword IDs provided', 'mapped_count' => 0]; - } - - // Get old cluster IDs for metrics update - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $old_cluster_ids = $wpdb->get_col($wpdb->prepare( - "SELECT DISTINCT cluster_id FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) AND cluster_id IS NOT NULL", - $keyword_ids - )); - - // Update keywords to new cluster - $mapped_count = $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->prefix}igny8_keywords SET cluster_id = %d, status = 'mapped', updated_at = CURRENT_TIMESTAMP WHERE id IN ({$placeholders})", - array_merge([$cluster_id], $keyword_ids) - )); - - if ($mapped_count === false) { - return ['success' => false, 'message' => 'Failed to map keywords to cluster', 'mapped_count' => 0]; - } - - // Update metrics for old clusters (they lost keywords) - if (!empty($old_cluster_ids)) { - foreach (array_unique($old_cluster_ids) as $old_cluster_id) { - if ($old_cluster_id && $old_cluster_id != $cluster_id) { - igny8_update_cluster_metrics($old_cluster_id); - } - } - } - - // Update metrics for new cluster (gained keywords) - igny8_update_cluster_metrics($cluster_id); - - return [ - 'success' => true, - 'message' => "Successfully mapped {$mapped_count} keyword(s) to cluster", - 'mapped_count' => $mapped_count - ]; -} - -/** - * ============================================= - * UNIFIED DATA VALIDATION LAYER - * ============================================= - */ - -/** - * Unified record validation function for all Planner module tables - * - * @param string $table_id Table ID (e.g., 'planner_keywords', 'planner_clusters') - * @param array $data Array of field data to validate - * @return array ['valid' => bool, 'error' => string|null] - */ - -/** - * Check if automation is enabled for the current user - * - * @return bool True if automation is enabled - */ -function igny8_is_automation_enabled() { - // Default ON for admin users, OFF for others - if (current_user_can('manage_options')) { - return true; - } - - // Check if user has specific automation capability - return current_user_can('edit_posts'); -} - -/** - * Helper function to get sector name - * Used for table display to show sector names instead of IDs - * - * @param mixed $sector_id The sector ID - * @return string The sector name or empty string if not found or 0/null - */ -function igny8_get_sector_name($sector_id) { - // Return empty string for 0, null, or empty values - if (empty($sector_id) || !is_numeric($sector_id) || intval($sector_id) == 0) { - return ''; - } - - // Check if sectors taxonomy exists - if (!taxonomy_exists('sectors')) { - return ''; - } - - // Try to get from WordPress taxonomy - $term = get_term(intval($sector_id), 'sectors'); - if ($term && !is_wp_error($term)) { - return $term->name; - } - - // If term not found, return empty string instead of fallback - return ''; -} - -/** - * Generate cluster name from keywords - * - * @param array $keywords Array of keyword objects - * @return string Generated cluster name - */ -function igny8_generate_cluster_name_from_keywords($keywords) { - if (empty($keywords)) { - return 'Auto-Generated Cluster'; - } - - // Get the most common words from keywords - $all_words = []; - foreach ($keywords as $keyword) { - $words = explode(' ', strtolower($keyword->keyword)); - $all_words = array_merge($all_words, $words); - } - - // Count word frequency - $word_count = array_count_values($all_words); - arsort($word_count); - - // Get top 2-3 most common words - $top_words = array_slice(array_keys($word_count), 0, 3); - - if (empty($top_words)) { - return 'Auto-Generated Cluster'; - } - - return ucwords(implode(' ', $top_words)) . ' Cluster'; -} - -/** - * Calculate relevance score for mapping suggestions - * - * @param string $term The search term - * @param string $title The post title - * @param string $slug The post slug - * @return int Relevance score (0-100) - */ -function igny8_calculate_relevance_score($term, $title, $slug) { - $score = 0; - $term_lower = strtolower($term); - $title_lower = strtolower($title); - $slug_lower = strtolower($slug); - - // Exact match in title - if (strpos($title_lower, $term_lower) !== false) { - $score += 50; - } - - // Exact match in slug - if (strpos($slug_lower, $term_lower) !== false) { - $score += 30; - } - - // Word match in title - $title_words = explode(' ', $title_lower); - $term_words = explode(' ', $term_lower); - foreach ($term_words as $word) { - if (in_array($word, $title_words)) { - $score += 10; - } - } - - return min($score, 100); -} - -/** - * Remove duplicate suggestions from mapping suggestions - * - * @param array $suggestions Array of suggestion objects - * @return array Deduplicated suggestions - */ -function igny8_remove_duplicate_suggestions($suggestions) { - $seen = []; - $unique = []; - - foreach ($suggestions as $suggestion) { - $key = $suggestion['page_id']; - if (!isset($seen[$key])) { - $seen[$key] = true; - $unique[] = $suggestion; - } - } - - return $unique; -} - -/** - * Get Import/Export Configuration for a table - * - * @param string $table_id The table ID (e.g., 'planner_keywords') - * @return array|false Configuration array or false if not found - */ -function igny8_get_import_export_config($table_id) { - static $config_cache = null; - - if ($config_cache === null) { - $config_path = plugin_dir_path(dirname(__FILE__)) . '../modules/config/import-export-config.php'; - if (file_exists($config_path)) { - $config_cache = include $config_path; - } else { - $config_cache = []; - } - } - - return isset($config_cache[$table_id]) ? $config_cache[$table_id] : false; -} - - -/** - * AJAX handler for testing API connection - */ -function igny8_test_connection_ajax() { - try { - // Check if user has permission - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - return; - } - - // Verify nonce - if (!check_ajax_referer('igny8_test_connection', 'nonce', false)) { - wp_send_json_error('Invalid nonce'); - return; - } - - // Test basic HTTP functionality - $test_url = 'https://httpbin.org/get'; - $response = wp_remote_get($test_url, ['timeout' => 10]); - - if (is_wp_error($response)) { - wp_send_json_error('HTTP request failed: ' . $response->get_error_message()); - return; - } - - $response_code = wp_remote_retrieve_response_code($response); - if ($response_code !== 200) { - wp_send_json_error('HTTP request returned status code: ' . $response_code); - return; - } - - wp_send_json_success('Connection test passed'); - - } catch (Exception $e) { - wp_send_json_error('Exception: ' . $e->getMessage()); - } -} -add_action('wp_ajax_igny8_test_connection', 'igny8_test_connection_ajax'); - -/** - * AJAX handler for testing API with response - */ -function igny8_test_api_ajax() { - try { - // Check if user has permission - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - return; - } - - // Verify nonce - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', false)) { - wp_send_json_error('Invalid nonce'); - return; - } - - // Get API key - $api_key = get_option('igny8_api_key', ''); - if (empty($api_key)) { - wp_send_json_error('API key not configured'); - return; - } - - // Get parameters - $with_response = isset($_POST['with_response']) ? (bool) $_POST['with_response'] : false; - - // Test API connection - $result = igny8_test_connection($api_key, $with_response); - - if (is_wp_error($result)) { - wp_send_json_error($result->get_error_message()); - return; - } - - if (is_array($result) && isset($result['success'])) { - if ($result['success']) { - wp_send_json_success($result); - } else { - wp_send_json_error($result['message'] ?? 'API test failed'); - } - } else { - wp_send_json_success(['message' => 'API connection successful', 'response' => $result]); - } - - } catch (Exception $e) { - wp_send_json_error('Exception: ' . $e->getMessage()); - } -} -add_action('wp_ajax_igny8_test_api', 'igny8_test_api_ajax'); - -/** - * Get saved sector selection from user meta - * - * @return array|false Saved sector selection data or false if not set - */ -function igny8_get_saved_sector_selection() { - $user_id = get_current_user_id(); - $saved_selection = get_user_meta($user_id, 'igny8_planner_sector_selection', true); - return !empty($saved_selection) ? $saved_selection : false; -} - -/** - * Get system-wide AI workflow data for main dashboard - * - * @return array Array of step data with status and counts for all 9 workflow steps - */ -function igny8_get_system_workflow_data() { - global $wpdb; - - // Get counts for each step - $keywords_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_keywords"); - $unmapped_keywords = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id IS NULL OR cluster_id = 0"); - $clusters_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters"); - $ideas_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_content_ideas"); - $queued_ideas = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_content_ideas WHERE status = 'new'"); - $queued_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status IN ('pending', 'queued', 'new')"); - $draft_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status IN ('draft', 'in_progress', 'review')"); - $published_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed'"); - - // Check sector selection - $sector_selected = !empty(igny8_get_saved_sector_selection()); - - // Check if modules are enabled - $planner_enabled = igny8_is_module_enabled('planner'); - $writer_enabled = igny8_is_module_enabled('writer'); - - return [ - 'keywords' => [ - 'count' => $keywords_count, - 'unmapped' => $unmapped_keywords, - 'status' => $keywords_count > 0 ? 'completed' : 'missing', - 'module_enabled' => $planner_enabled, - 'url' => $planner_enabled ? '?page=igny8-planner&sm=keywords' : null - ], - 'sector' => [ - 'selected' => $sector_selected, - 'status' => $sector_selected ? 'completed' : 'missing', - 'module_enabled' => $planner_enabled, - 'url' => $planner_enabled ? '?page=igny8-planner' : null - ], - 'clusters' => [ - 'count' => $clusters_count, - 'unmapped_keywords' => $unmapped_keywords, - 'status' => $unmapped_keywords == 0 && $clusters_count > 0 ? 'completed' : ($unmapped_keywords > 0 ? 'in_progress' : 'missing'), - 'module_enabled' => $planner_enabled, - 'url' => $planner_enabled ? '?page=igny8-planner&sm=clusters' : null - ], - 'ideas' => [ - 'count' => $ideas_count, - 'status' => $ideas_count > 0 ? 'completed' : 'missing', - 'module_enabled' => $planner_enabled, - 'url' => $planner_enabled ? '?page=igny8-planner&sm=ideas' : null - ], - 'queue' => [ - 'queued_ideas' => $queued_ideas, - 'status' => $queued_ideas == 0 && $ideas_count > 0 ? 'completed' : ($queued_ideas > 0 ? 'in_progress' : 'missing'), - 'module_enabled' => $planner_enabled, - 'url' => $planner_enabled ? '?page=igny8-planner&sm=ideas' : null - ], - 'drafts' => [ - 'queued_tasks' => $queued_tasks, - 'draft_tasks' => $draft_tasks, - 'status' => $queued_tasks > 0 ? 'in_progress' : ($draft_tasks > 0 ? 'completed' : 'missing'), - 'module_enabled' => $writer_enabled, - 'url' => $writer_enabled ? '?page=igny8-writer&sm=tasks' : null - ], - 'publish' => [ - 'published_tasks' => $published_tasks, - 'draft_tasks' => $draft_tasks, - 'status' => $published_tasks > 0 ? 'completed' : ($draft_tasks > 0 ? 'in_progress' : 'missing'), - 'module_enabled' => $writer_enabled, - 'url' => $writer_enabled ? '?page=igny8-writer&sm=drafts' : null - ] - ]; -} - -/** - * Display image prompts in a formatted way for table display - * - * @param string $image_prompts JSON string of image prompts - * @return string Formatted display string - */ - - diff --git a/igny8-ai-seo-wp-plugin/core/admin/init.php b/igny8-ai-seo-wp-plugin/core/admin/init.php deleted file mode 100644 index 882bed6a..00000000 --- a/igny8-ai-seo-wp-plugin/core/admin/init.php +++ /dev/null @@ -1,135 +0,0 @@ - $settings) { - foreach ($settings as $name => $config) { - register_setting($group, $name, $config); - } - } -} - -/** - * Settings Configuration (grouped) - */ -function igny8_get_settings_config() { - return [ - 'igny8_table_settings' => [ - 'igny8_records_per_page' => [ - 'type' => 'integer', - 'default' => 20, - 'sanitize_callback' => 'absint' - ] - ], - 'igny8_ai_integration_settings' => [ - 'igny8_ai_cluster_building' => ['type' => 'string', 'default' => 'enabled', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_ai_content_ideas' => ['type' => 'string', 'default' => 'enabled', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_ai_auto_mapping' => ['type' => 'string', 'default' => 'enabled', 'sanitize_callback' => 'sanitize_text_field'] - ], - 'igny8_api_settings' => [ - 'igny8_api_key' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_runware_api_key' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_model' => ['type' => 'string', 'default' => 'gpt-4.1', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_image_service' => ['type' => 'string', 'default' => 'openai', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_image_model' => ['type' => 'string', 'default' => 'dall-e-3', 'sanitize_callback' => 'sanitize_text_field'], - 'igny8_runware_model' => ['type' => 'string', 'default' => 'runware:97@1', 'sanitize_callback' => 'sanitize_text_field'] - ], - 'igny8_personalize_settings_group' => [ - 'igny8_content_engine_global_status' => ['sanitize_callback' => 'igny8_sanitize_checkbox_setting'], - 'igny8_content_engine_enabled_post_types' => ['sanitize_callback' => 'igny8_sanitize_array_setting'], - 'igny8_content_engine_insertion_position' => ['sanitize_callback' => 'sanitize_text_field'], - 'igny8_content_engine_display_mode' => ['sanitize_callback' => 'sanitize_text_field'], - 'igny8_content_engine_teaser_text' => ['sanitize_callback' => 'sanitize_textarea_field'], - 'igny8_content_engine_save_variations' => ['sanitize_callback' => 'intval'], - 'igny8_content_engine_field_mode' => ['sanitize_callback' => 'sanitize_text_field'], - 'igny8_content_engine_detection_prompt' => ['sanitize_callback' => 'sanitize_textarea_field'], - 'igny8_content_engine_context_source' => ['sanitize_callback' => 'sanitize_textarea_field'], - 'igny8_content_engine_include_page_context' => ['sanitize_callback' => 'intval'], - 'igny8_content_engine_content_length' => ['sanitize_callback' => 'sanitize_text_field'], - 'igny8_content_engine_rewrite_prompt' => ['sanitize_callback' => 'sanitize_textarea_field'], - 'igny8_content_engine_fixed_fields_config' => ['sanitize_callback' => 'igny8_sanitize_fields_config'] - ] - ]; -} - -/** - * ------------------------------------------------------------------------ - * SANITIZATION HELPERS - * ------------------------------------------------------------------------ - */ -function igny8_sanitize_checkbox_setting($raw) { - return isset($_POST['igny8_content_engine_global_status']) ? 'enabled' : 'disabled'; -} - -function igny8_sanitize_array_setting($raw) { - return is_array($raw) ? array_map('sanitize_text_field', $raw) : []; -} - -function igny8_sanitize_fields_config($raw) { - if (!is_array($raw)) return []; - $sanitized = []; - foreach ($raw as $index => $field) { - $sanitized[$index] = [ - 'label' => sanitize_text_field($field['label'] ?? ''), - 'type' => sanitize_text_field($field['type'] ?? 'text'), - 'options' => sanitize_text_field($field['options'] ?? '') - ]; - } - return $sanitized; -} - - -// MOVED TO: igny8.php - Admin assets enqueuing moved to main plugin file - -// --------------------------------------------------------------------- -// WORDPRESS FEATURE REGISTRATION -// --------------------------------------------------------------------- - -function igny8_init_wordpress_features() { - // Initialize module manager - add_action('init', 'igny8_module_manager'); - - // Register taxonomies - add_action('init', 'igny8_register_taxonomies'); - - // Register post meta once - add_action('init', function() { - if (!get_option('igny8_post_meta_registered')) { - igny8_register_post_meta(); - update_option('igny8_post_meta_registered', true); - } - }); -} - -//Initialize WordPress features -igny8_init_wordpress_features(); diff --git a/igny8-ai-seo-wp-plugin/core/admin/menu.php b/igny8-ai-seo-wp-plugin/core/admin/menu.php deleted file mode 100644 index ddf8018f..00000000 --- a/igny8-ai-seo-wp-plugin/core/admin/menu.php +++ /dev/null @@ -1,356 +0,0 @@ -'; - $breadcrumb .= 'Igny8 Home'; - - if ($current_page === 'igny8-planner') { - $breadcrumb .= ''; - $breadcrumb .= 'Planner'; - - if ($sm === 'keywords') { - $breadcrumb .= ''; - $breadcrumb .= 'Keywords'; - } elseif ($sm === 'clusters') { - $breadcrumb .= ''; - $breadcrumb .= 'Clusters'; - } elseif ($sm === 'ideas') { - $breadcrumb .= ''; - $breadcrumb .= 'Ideas'; - } elseif ($sm === 'mapping') { - $breadcrumb .= ''; - $breadcrumb .= 'Mapping'; - } - } elseif ($current_page === 'igny8-writer') { - $breadcrumb .= ''; - $breadcrumb .= 'Writer'; - - if ($sm === 'drafts') { - $breadcrumb .= ''; - $breadcrumb .= 'Drafts'; - } elseif ($sm === 'templates') { - $breadcrumb .= ''; - $breadcrumb .= 'Templates'; - } - } elseif ($current_page === 'igny8-optimizer') { - $breadcrumb .= ''; - $breadcrumb .= 'Optimizer'; - - if ($sm === 'audits') { - $breadcrumb .= ''; - $breadcrumb .= 'Audits'; - } elseif ($sm === 'suggestions') { - $breadcrumb .= ''; - $breadcrumb .= 'Suggestions'; - } - } elseif ($current_page === 'igny8-linker') { - $breadcrumb .= ''; - $breadcrumb .= 'Linker'; - - if ($sm === 'backlinks') { - $breadcrumb .= ''; - $breadcrumb .= 'Backlinks'; - } elseif ($sm === 'campaigns') { - $breadcrumb .= ''; - $breadcrumb .= 'Campaigns'; - } - } elseif ($current_page === 'igny8-personalize') { - $breadcrumb .= ''; - $breadcrumb .= 'Personalize'; - - if ($sm === 'settings') { - $breadcrumb .= ''; - $breadcrumb .= 'Settings'; - } elseif ($sm === 'content-generation') { - $breadcrumb .= ''; - $breadcrumb .= 'Content Generation'; - } elseif ($sm === 'rewrites') { - $breadcrumb .= ''; - $breadcrumb .= 'Rewrites'; - } elseif ($sm === 'front-end') { - $breadcrumb .= ''; - $breadcrumb .= 'Front-end'; - } - } elseif (strpos($current_page, 'igny8-analytics') !== false) { - $breadcrumb .= ''; - $breadcrumb .= 'Analytics'; - } elseif (strpos($current_page, 'igny8-schedules') !== false) { - $breadcrumb .= ''; - $breadcrumb .= 'Schedules'; - } elseif (strpos($current_page, 'igny8-settings') !== false) { - $breadcrumb .= ''; - $breadcrumb .= 'Settings'; - } elseif (strpos($current_page, 'igny8-help') !== false) { - $breadcrumb .= ''; - $breadcrumb .= 'Help'; - } - - $breadcrumb .= ''; - return $breadcrumb; -} - -/** - * Render submenu navigation - */ -function igny8_render_submenu() { - $current_page = $_GET['page'] ?? ''; - $sm = $_GET['sm'] ?? ''; - $submenu = ''; - - if ($current_page === 'igny8-planner') { - $submenu .= 'Dashboard'; - $submenu .= 'Keywords'; - $submenu .= 'Clusters'; - $submenu .= 'Ideas'; - } elseif ($current_page === 'igny8-writer') { - $submenu .= 'Dashboard'; - $submenu .= 'Tasks'; - $submenu .= 'Drafts'; - $submenu .= 'Published'; - } elseif ($current_page === 'igny8-thinker') { - $sp = $_GET['sp'] ?? 'main'; - $submenu .= 'Dashboard'; - $submenu .= 'Prompts'; - $submenu .= 'Profile'; - $submenu .= 'Strategies'; - $submenu .= 'Image Testing'; - } elseif ($current_page === 'igny8-optimizer') { - $submenu .= 'Dashboard'; - $submenu .= 'Audits'; - $submenu .= 'Suggestions'; - } elseif ($current_page === 'igny8-linker') { - $submenu .= 'Dashboard'; - $submenu .= 'Backlinks'; - $submenu .= 'Campaigns'; - } elseif ($current_page === 'igny8-personalize') { - $submenu .= 'Dashboard'; - $submenu .= 'Settings'; - $submenu .= 'Content Generation'; - $submenu .= 'Rewrites'; - $submenu .= 'Front-end'; - } elseif ($current_page === 'igny8-settings') { - $sp = $_GET['sp'] ?? 'general'; - $submenu .= 'Settings'; - $submenu .= 'Status'; - $submenu .= 'Integration'; - $submenu .= 'Import/Export'; - } elseif ($current_page === 'igny8-help') { - $sp = $_GET['sp'] ?? 'help'; - $submenu .= 'Help & Support'; - $submenu .= 'Documentation'; - $submenu .= 'System Testing'; - $submenu .= 'Function Testing'; - } - - return $submenu; -} - -/** - * Register admin menu pages - */ -function igny8_register_admin_menu() { - // Ensure module manager is available - if (!function_exists('igny8_is_module_enabled')) { - return; - } - // Main menu page - add_menu_page( - 'Igny8 AI SEO', // Page title - 'Igny8 AI SEO', // Menu title - 'manage_options', // Capability - 'igny8-home', // Menu slug - 'igny8_home_page', // Callback function - 'dashicons-chart-line', // Icon - 30 // Position - ); - - // Home page - add_submenu_page( - 'igny8-home', // Parent slug - 'Dashboard', // Page title - 'Dashboard', // Menu title - 'manage_options', // Capability - 'igny8-home', // Menu slug - 'igny8_home_page' // Callback function - ); - - // Module submenus (only if enabled) - if (igny8_is_module_enabled('planner')) { - add_submenu_page( - 'igny8-home', - 'Content Planner', - 'Planner', - 'manage_options', - 'igny8-planner', - 'igny8_planner_page' - ); - } - - if (igny8_is_module_enabled('writer')) { - add_submenu_page( - 'igny8-home', - 'Content Writer', - 'Writer', - 'manage_options', - 'igny8-writer', - 'igny8_writer_page' - ); - } - - if (igny8_is_module_enabled('thinker')) { - add_submenu_page( - 'igny8-home', - 'AI Thinker', - 'Thinker', - 'manage_options', - 'igny8-thinker', - 'igny8_thinker_page' - ); - - // Prompts subpage under Thinker - add_submenu_page( - 'igny8-thinker', - 'AI Prompts', - 'Prompts', - 'manage_options', - 'igny8-thinker&sp=prompts', - 'igny8_thinker_page' - ); - } - - if (igny8_is_module_enabled('schedules')) { - add_submenu_page( - 'igny8-home', - 'Smart Automation Schedules', - 'Schedules', - 'manage_options', - 'igny8-schedules', - 'igny8_schedules_page' - ); - } - - - // Analytics before Settings (only if enabled) - if (igny8_is_module_enabled('analytics')) { - add_submenu_page( - 'igny8-home', - 'Analytics', - 'Analytics', - 'manage_options', - 'igny8-analytics', - 'igny8_analytics_page' - ); - } - - // Cron Health page - - // Settings page - add_submenu_page( - 'igny8-home', - 'Settings', - 'Settings', - 'manage_options', - 'igny8-settings', - 'igny8_settings_page' - ); - - - // Help page - add_submenu_page( - 'igny8-home', - 'Help', - 'Help', - 'manage_options', - 'igny8-help', - 'igny8_help_page' - ); - - // Documentation subpage under Help - add_submenu_page( - 'igny8-help', - 'Documentation', - 'Documentation', - 'manage_options', - 'igny8-help&sp=docs', - 'igny8_help_page' - ); - - // System Testing subpage under Help - add_submenu_page( - 'igny8-help', - 'System Testing', - 'System Testing', - 'manage_options', - 'igny8-help&sp=system-testing', - 'igny8_help_page' - ); - - // Function Testing subpage under Help - add_submenu_page( - 'igny8-help', - 'Function Testing', - 'Function Testing', - 'manage_options', - 'igny8-help&sp=function-testing', - 'igny8_help_page' - ); - -} - -// Static page wrapper functions - each page handles its own layout -function igny8_home_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/home.php'; -} - -function igny8_planner_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/planner/planner.php'; -} - -function igny8_writer_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/writer/writer.php'; -} - -function igny8_thinker_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/thinker/thinker.php'; -} - -function igny8_settings_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/settings/general-settings.php'; -} - -function igny8_analytics_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/analytics/analytics.php'; -} - -function igny8_schedules_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/settings/schedules.php'; -} - -function igny8_help_page() { - include_once plugin_dir_path(__FILE__) . '../../modules/help/help.php'; -} - -// Hook into admin_menu -add_action('admin_menu', 'igny8_register_admin_menu'); diff --git a/igny8-ai-seo-wp-plugin/core/admin/meta-boxes.php b/igny8-ai-seo-wp-plugin/core/admin/meta-boxes.php deleted file mode 100644 index 85136547..00000000 --- a/igny8-ai-seo-wp-plugin/core/admin/meta-boxes.php +++ /dev/null @@ -1,387 +0,0 @@ -ID, '_igny8_meta_title', true); - $meta_desc = get_post_meta($post->ID, '_igny8_meta_description', true); - $primary_kw = get_post_meta($post->ID, '_igny8_primary_keywords', true); - $secondary_kw = get_post_meta($post->ID, '_igny8_secondary_keywords', true); - ?> -
          -
          -

          - -
          -

          - -
          -

          - -
          - -
          - ID, '_igny8_inarticle_images', true); - if (!is_array($images)) $images = []; - - // Add CSS for grid layout and remove button - ?> - - '; - echo '

          -

          '; - echo ''; - - - - - echo '
            '; - - // Sort images by device type and ID - $sorted_images = []; - foreach ($images as $label => $data) { - $sorted_images[$label] = $data; - } - - // Custom sort function to order by device type (desktop first) then by ID - uksort($sorted_images, function($a, $b) { - $a_parts = explode('-', $a); - $b_parts = explode('-', $b); - - $a_device = $a_parts[0]; - $b_device = $b_parts[0]; - - // Desktop comes before mobile - if ($a_device === 'desktop' && $b_device === 'mobile') return -1; - if ($a_device === 'mobile' && $b_device === 'desktop') return 1; - - // If same device, sort by ID number - if ($a_device === $b_device) { - $a_id = intval($a_parts[1]); - $b_id = intval($b_parts[1]); - return $a_id - $b_id; - } - - return 0; - }); - - foreach ($sorted_images as $label => $data) { - echo '
          • '; - echo '' . esc_html($label) . '
            '; - echo wp_get_attachment_image($data['attachment_id'], 'thumbnail', false, ['style' => 'width: 150px; height: 150px; object-fit: cover;']); - echo '
            '; - echo ''; - echo ''; - echo '
            '; - echo ''; - echo ''; - echo '
          • '; - } - echo '
          '; - - - // Inline JS - ?> - - $data) { - if (!empty($data['attachment_id'])) { - $filtered[$label] = [ - 'label' => sanitize_text_field($label), - 'attachment_id' => intval($data['attachment_id']), - 'url' => wp_get_attachment_url(intval($data['attachment_id'])), - 'device' => sanitize_text_field($data['device']) - ]; - } - } - update_post_meta($post_id, '_igny8_inarticle_images', $filtered); - - if (WP_DEBUG === true) { - error_log("[IGNY8 DEBUG] Saving In-Article Images for Post ID: $post_id"); - error_log(print_r($filtered, true)); - } -}); - diff --git a/igny8-ai-seo-wp-plugin/core/admin/module-manager-class.php b/igny8-ai-seo-wp-plugin/core/admin/module-manager-class.php deleted file mode 100644 index 2d940f2e..00000000 --- a/igny8-ai-seo-wp-plugin/core/admin/module-manager-class.php +++ /dev/null @@ -1,181 +0,0 @@ -init_modules(); - add_action('admin_init', [$this, 'register_module_settings']); - } - - /** - * Initialize module definitions - main modules only - */ - private function init_modules() { - $this->modules = [ - 'planner' => [ - 'name' => 'Planner', - 'description' => 'Keyword research and content planning with clusters, ideas, and mapping tools.', - 'default' => true, - 'icon' => 'dashicons-search', - 'category' => 'main', - 'cron_jobs' => [ - 'igny8_auto_cluster_cron', - 'igny8_auto_generate_ideas_cron', - 'igny8_auto_queue_cron' - ] - ], - 'writer' => [ - 'name' => 'Writer', - 'description' => 'AI-powered content generation with drafts and templates management.', - 'default' => false, - 'icon' => 'dashicons-edit', - 'category' => 'main', - 'cron_jobs' => [ - 'igny8_auto_generate_content_cron', - 'igny8_auto_generate_images_cron', - 'igny8_auto_publish_drafts_cron' - ] - ], - 'analytics' => [ - 'name' => 'Analytics', - 'description' => 'Performance tracking and data analysis with comprehensive reporting.', - 'default' => false, - 'icon' => 'dashicons-chart-bar', - 'category' => 'admin', - 'cron_jobs' => [ - 'igny8_process_ai_queue_cron', - 'igny8_auto_recalc_cron', - 'igny8_health_check_cron' - ] - ], - 'schedules' => [ - 'name' => 'Schedules', - 'description' => 'Content scheduling and automation with calendar management.', - 'default' => false, - 'icon' => 'dashicons-calendar-alt', - 'category' => 'admin' - ], - 'thinker' => [ - 'name' => 'AI Thinker', - 'description' => 'AI-powered content strategy, prompts, and intelligent content planning tools.', - 'default' => true, - 'icon' => 'dashicons-lightbulb', - 'category' => 'admin' - ] - ]; - } - - /** - * Check if a module is enabled - */ - public function is_module_enabled($module) { - $settings = get_option('igny8_module_settings', []); - return isset($settings[$module]) ? (bool) $settings[$module] : (isset($this->modules[$module]) ? $this->modules[$module]['default'] : false); - } - - /** - * Get all enabled modules - */ - public function get_enabled_modules() { - $enabled = []; - foreach ($this->modules as $key => $module) { - if ($this->is_module_enabled($key)) { - $enabled[$key] = $module; - } - } - return $enabled; - } - - /** - * Get all modules - */ - public function get_modules() { - return $this->modules; - } - - /** - * Register module settings - */ - public function register_module_settings() { - register_setting('igny8_module_settings', 'igny8_module_settings'); - } - - /** - * Save module settings - */ - public function save_module_settings() { - if (!isset($_POST['igny8_module_nonce']) || !wp_verify_nonce($_POST['igny8_module_nonce'], 'igny8_module_settings')) { - wp_die('Security check failed'); - } - - $settings = $_POST['igny8_module_settings'] ?? []; - - // Initialize all modules as disabled first - $all_modules = $this->get_modules(); - $final_settings = []; - foreach ($all_modules as $module_key => $module) { - $final_settings[$module_key] = false; // Default to disabled - } - - // Set enabled modules to true - foreach ($settings as $key => $value) { - if (isset($final_settings[$key])) { - $final_settings[$key] = (bool) $value; - } - } - - update_option('igny8_module_settings', $final_settings); - - // Force page reload using JavaScript - echo ''; - exit; - } -} - -// Initialize the module manager -function igny8_module_manager() { - return Igny8_Module_Manager::get_instance(); -} - -// Helper functions for easy access -function igny8_is_module_enabled($module) { - return igny8_module_manager()->is_module_enabled($module); -} - -function igny8_get_enabled_modules() { - return igny8_module_manager()->get_enabled_modules(); -} - -function igny8_get_modules() { - return igny8_module_manager()->get_modules(); -} diff --git a/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-handlers.php b/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-handlers.php deleted file mode 100644 index 90aef380..00000000 --- a/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-handlers.php +++ /dev/null @@ -1,1610 +0,0 @@ - 0) { - igny8_log_ai_event('AI Queue Processed', 'ai', 'queue_processing', 'success', 'AI queue tasks processed', "Tasks processed: $processed"); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $processed; - $GLOBALS['igny8_cron_result_details'] = "Processed {$processed} AI queue tasks"; - echo "Igny8 AI QUEUE HANDLER: Set global variables - processed_count: $processed, result_details: Processed {$processed} AI queue tasks
          "; - } else { - igny8_log_ai_event('AI Queue Empty', 'ai', 'queue_processing', 'info', 'No tasks in queue', 'All tasks are already processed'); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "No AI queue tasks to process"; - } - - } catch (Exception $e) { - igny8_log_ai_event('AI Queue Error', 'ai', 'queue_processing', 'error', 'AI queue processing failed', $e->getMessage()); - - // Set global variables for detailed logging (failure case) - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: " . $e->getMessage(); - } finally { - // Always release the lock - delete_transient($lock_key); - } -} - -/** - * Auto Cluster Cron Handler - * - * Automatically clusters unmapped keywords with taxonomy safety. - */ -function igny8_auto_cluster_cron_handler() { - // Suppress PHP warnings for model rates in cron context - $old_error_reporting = error_reporting(); - error_reporting($old_error_reporting & ~E_WARNING); - - echo "
          "; - echo "Igny8 CRON HANDLER: auto_cluster started
          "; - error_log("Igny8 CRON HANDLER: auto_cluster started"); - - // Check if automation is enabled via cron settings - echo "Igny8 CRON HANDLER: Checking if automation is enabled
          "; - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings['igny8_auto_cluster_cron'] ?? []; - $auto_cluster_enabled = $job_settings['enabled'] ?? false; - echo "Igny8 CRON HANDLER: auto_cluster_enabled = " . ($auto_cluster_enabled ? 'enabled' : 'disabled') . "
          "; - - if (!$auto_cluster_enabled) { - echo "Igny8 CRON HANDLER: Automation disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: auto_cluster automation disabled"); - echo "
          "; - return; - } - echo "Igny8 CRON HANDLER: Automation enabled, continuing
          "; - - // Check if AI mode is enabled - echo "Igny8 CRON HANDLER: Checking AI mode
          "; - $planner_mode = igny8_get_ai_setting('planner_mode', 'manual'); - echo "Igny8 CRON HANDLER: planner_mode = " . $planner_mode . "
          "; - - if ($planner_mode !== 'ai') { - echo "Igny8 CRON HANDLER: AI mode disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: AI mode disabled"); - echo ""; - return; - } - echo "Igny8 CRON HANDLER: AI mode enabled, continuing
          "; - - // Check if sector is selected - echo "Igny8 CRON HANDLER: Checking sector options
          "; - - // Check if function exists first - if (!function_exists('igny8_get_sector_options')) { - echo "Igny8 CRON HANDLER: ERROR - igny8_get_sector_options function not found
          "; - igny8_log_ai_event('Auto Cluster Failed', 'planner', 'auto_cluster', 'error', 'Sector options function not available', 'Function igny8_get_sector_options not found'); - echo "Igny8 CRON HANDLER: Exiting due to missing function
          "; - echo ""; - return; - } - - try { - echo "Igny8 CRON HANDLER: Calling igny8_get_sector_options()
          "; - $sector_options = igny8_get_sector_options(); - echo "Igny8 CRON HANDLER: sector_options count = " . count($sector_options) . "
          "; - echo "Igny8 CRON HANDLER: sector_options content: " . print_r($sector_options, true) . "
          "; - } catch (Exception $e) { - echo "Igny8 CRON HANDLER: ERROR - Exception in igny8_get_sector_options: " . $e->getMessage() . "
          "; - igny8_log_ai_event('Auto Cluster Failed', 'planner', 'auto_cluster', 'error', 'Exception in sector options function', $e->getMessage()); - echo "Igny8 CRON HANDLER: Exiting due to sector options exception
          "; - echo ""; - return; - } - - if (empty($sector_options)) { - echo "Igny8 CRON HANDLER: No sector selected, checking alternatives
          "; - echo "Igny8 CRON HANDLER: Checking if igny8_get_sector_options function exists: " . (function_exists('igny8_get_sector_options') ? 'YES' : 'NO') . "
          "; - - // Try to get sectors directly from database - global $wpdb; - $sectors = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}igny8_sectors WHERE status = 'active'"); - echo "Igny8 CRON HANDLER: Direct DB query found " . count($sectors) . " active sectors
          "; - - if (empty($sectors)) { - echo "Igny8 CRON HANDLER: ERROR - No active sectors found, cannot proceed
          "; - igny8_log_ai_event('Auto Cluster Failed', 'planner', 'auto_cluster', 'error', 'No active sectors configured', 'Sector configuration required for clustering'); - echo "Igny8 CRON HANDLER: Exiting due to missing sectors
          "; - echo ""; - return; - } else { - echo "Igny8 CRON HANDLER: Found sectors in DB, using them
          "; - $sector_options = $sectors; - } - } - echo "Igny8 CRON HANDLER: Sector selected, continuing
          "; - - global $wpdb; - echo "Igny8 CRON HANDLER: Database connection established
          "; - - // Get unmapped keywords (use dynamic limit from settings) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - // Try to get limit from Smart Automation Jobs table directly - $cron_limits = get_option('igny8_cron_limits', []); - $limit = $cron_limits['igny8_auto_cluster_cron'] ?? null; - - if ($limit === null) { - error_log('Igny8 Auto Cluster Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - - echo "Igny8 CRON HANDLER: Using limit from Smart Automation Jobs table: $limit
          "; - } - echo "Igny8 CRON HANDLER: Querying unmapped keywords (limit: $limit)
          "; - $query = "SELECT id, keyword FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id IS NULL OR cluster_id = 0 LIMIT $limit"; - echo "Igny8 CRON HANDLER: SQL Query: " . $query . "
          "; - - $unmapped_keywords = $wpdb->get_results($query); - - echo "Igny8 CRON HANDLER: Found " . count($unmapped_keywords) . " unmapped keywords
          "; - - if (!empty($unmapped_keywords)) { - echo "Igny8 CRON HANDLER: Sample keywords:
          "; - foreach (array_slice($unmapped_keywords, 0, 3) as $keyword) { - echo "- ID: " . $keyword->id . ", Keyword: " . $keyword->keyword . "
          "; - } - } - - if (empty($unmapped_keywords)) { - echo "Igny8 CRON HANDLER: ERROR - No unmapped keywords found
          "; - igny8_log_ai_event('Auto Cluster Failed', 'planner', 'auto_cluster', 'error', 'No unmapped keywords available for clustering', 'All keywords are already clustered or no keywords exist'); - echo "Igny8 CRON HANDLER: Exiting due to no keywords to process
          "; - echo ""; - return; - } - - $keyword_ids = array_column($unmapped_keywords, 'id'); - echo "Igny8 CRON HANDLER: Processing " . count($keyword_ids) . " keywords
          "; - - // Log automation start - echo "Igny8 CRON HANDLER: Logging automation start
          "; - igny8_log_ai_event('Auto Cluster Started', 'planner', 'auto_cluster', 'info', 'Starting automated clustering', 'Keywords: ' . count($keyword_ids)); - - // Direct clustering without AJAX simulation - echo "Igny8 CRON HANDLER: Starting direct clustering process
          "; - - // Set up user context for permissions - if (!current_user_can('manage_options')) { - // Get admin user for cron context - $admin_users = get_users(['role' => 'administrator', 'number' => 1]); - if (!empty($admin_users)) { - wp_set_current_user($admin_users[0]->ID); - echo "Igny8 CRON HANDLER: Set admin user context for permissions
          "; - } - } - - // Get keywords data - echo "Igny8 CRON HANDLER: Getting keywords data
          "; - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $keywords = $wpdb->get_results($wpdb->prepare(" - SELECT * FROM {$wpdb->prefix}igny8_keywords - WHERE id IN ({$placeholders}) - ", $keyword_ids)); - - echo "Igny8 CRON HANDLER: Found " . count($keywords) . " keywords in database
          "; - - if (empty($keywords)) { - echo "Igny8 CRON HANDLER: ERROR - No valid keywords found
          "; - igny8_log_ai_event('Auto Cluster Failed', 'planner', 'auto_cluster', 'error', 'No valid keywords found in database', 'Keyword IDs: ' . implode(',', $keyword_ids)); - echo ""; - return; - } - - // Check if keywords already have clusters - $keywords_with_clusters = array_filter($keywords, function($keyword) { - return !empty($keyword->cluster_id) && $keyword->cluster_id > 0; - }); - - if (!empty($keywords_with_clusters)) { - echo "Igny8 CRON HANDLER: WARNING - Some keywords already have clusters
          "; - $keyword_names = array_column($keywords_with_clusters, 'keyword'); - echo "Igny8 CRON HANDLER: Already clustered: " . implode(', ', array_slice($keyword_names, 0, 3)) . "
          "; - } - - // Get clustering prompt - echo "Igny8 CRON HANDLER: Getting clustering prompt
          "; - $prompt_template = wp_unslash(igny8_get_ai_setting('clustering_prompt', igny8_get_default_clustering_prompt())); - echo "Igny8 CRON HANDLER: Prompt length: " . strlen($prompt_template) . "
          "; - - // Generate session ID for progress tracking - $session_id = 'cron_clustering_' . time() . '_' . wp_generate_password(8, false); - echo "Igny8 CRON HANDLER: Session ID: " . $session_id . "
          "; - - // Log AI request initiation - igny8_log_ai_event('Cron AI Request Initiated', 'planner', 'clustering', 'info', 'Starting cron AI clustering process', 'Keywords: ' . count($keyword_ids) . ', Session: ' . $session_id); - - // Process with AI - echo "Igny8 CRON HANDLER: Calling AI processing
          "; - try { - $ai_result = igny8_process_ai_request('clustering', $keywords, $prompt_template); - echo "Igny8 CRON HANDLER: AI processing completed
          "; - - if ($ai_result === false) { - echo "Igny8 CRON HANDLER: ERROR - AI processing returned false
          "; - igny8_log_ai_event('Cron AI Processing Failed', 'planner', 'clustering', 'error', 'AI processing returned false', 'Check OpenAI API configuration'); - echo ""; - return; - } - - if (!is_array($ai_result) || !isset($ai_result['clusters'])) { - echo "Igny8 CRON HANDLER: ERROR - AI returned invalid result
          "; - igny8_log_ai_event('Cron AI Processing Failed', 'planner', 'clustering', 'error', 'AI returned invalid result', 'Result type: ' . gettype($ai_result)); - echo ""; - return; - } - - echo "Igny8 CRON HANDLER: AI returned " . count($ai_result['clusters']) . " clusters
          "; - - // Debug: Show AI response structure - echo "Igny8 CRON HANDLER: AI Response Debug:
          "; - echo "Igny8 CRON HANDLER: Full AI result structure: " . json_encode($ai_result, JSON_PRETTY_PRINT) . "
          "; - - igny8_log_ai_event('Cron AI Processing Complete', 'planner', 'clustering', 'success', 'AI returned ' . count($ai_result['clusters']) . ' clusters', 'Clusters: ' . json_encode(array_column($ai_result['clusters'], 'name'))); - - // Log database operations start - echo "Igny8 CRON HANDLER: Starting database operations
          "; - igny8_log_ai_event('Cron Database Operations Started', 'planner', 'clustering', 'info', 'Starting to create clusters in database', 'Clusters to create: ' . count($ai_result['clusters'])); - - // Get sector options for assignment logic (same as AJAX handler) - echo "Igny8 CRON HANDLER: Getting sector options for assignment
          "; - $sector_options = igny8_get_sector_options(); - $sector_count = count($sector_options); - echo "Igny8 CRON HANDLER: Found " . $sector_count . " sectors
          "; - - // Create clusters in database (following AJAX handler process) - $created_clusters = []; - $clusters_created = 0; - $keywords_processed = 0; - - foreach ($ai_result['clusters'] as $cluster_data) { - echo "Igny8 CRON HANDLER: Processing cluster: " . $cluster_data['name'] . "
          "; - - try { - - // Determine sector_id based on sector count (same logic as AJAX handler) - $sector_id = 1; // Default fallback - echo "Igny8 CRON HANDLER: Determining sector assignment
          "; - - if ($sector_count == 1) { - // Only 1 sector: assign all clusters to that sector - $sector_id = $sector_options[0]['value']; - echo "Igny8 CRON HANDLER: Single sector found, assigning to sector ID: " . $sector_id . "
          "; - } elseif ($sector_count > 1) { - // Multiple sectors: use AI response sector assignment - if (isset($cluster_data['sector']) && !empty($cluster_data['sector'])) { - echo "Igny8 CRON HANDLER: AI provided sector: " . $cluster_data['sector'] . "
          "; - // Find sector ID by matching sector name from AI response - foreach ($sector_options as $sector) { - if (strtolower(trim($sector['label'])) === strtolower(trim($cluster_data['sector']))) { - $sector_id = $sector['value']; - echo "Igny8 CRON HANDLER: Matched sector ID: " . $sector_id . "
          "; - break; - } - } - } - // If no match found or no sector in AI response, use first sector as fallback - if ($sector_id == 1 && !isset($cluster_data['sector'])) { - $sector_id = $sector_options[0]['value']; - echo "Igny8 CRON HANDLER: No sector match, using first sector ID: " . $sector_id . "
          "; - } - } - - // Create cluster record in database (same as AJAX handler) - echo "Igny8 CRON HANDLER: Creating cluster record in database
          "; - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_clusters', - [ - 'cluster_name' => sanitize_text_field($cluster_data['name']), - 'sector_id' => $sector_id, - 'status' => 'active', - 'keyword_count' => count($cluster_data['keywords']), - 'total_volume' => 0, - 'avg_difficulty' => 0, - 'mapped_pages_count' => 0, - 'created_at' => current_time('mysql') - ], - ['%s', '%d', '%s', '%d', '%d', '%f', '%d', '%s'] - ); - - if ($result) { - $cluster_id = $wpdb->insert_id; - $created_clusters[] = $cluster_id; - $clusters_created++; - echo "Igny8 CRON HANDLER: SUCCESS - Created cluster record with ID: " . $cluster_id . "
          "; - - // Trigger taxonomy term creation for AI-generated cluster (same as AJAX handler) - echo "Igny8 CRON HANDLER: Triggering taxonomy term creation
          "; - try { - do_action('igny8_cluster_added', $cluster_id); - echo "Igny8 CRON HANDLER: SUCCESS - Taxonomy term creation triggered
          "; - igny8_log_ai_event('Cron Cluster Taxonomy Triggered', 'planner', 'clustering', 'info', 'Triggered igny8_cluster_added action', "Cluster: {$cluster_data['name']} (ID: {$cluster_id})"); - } catch (Exception $e) { - echo "Igny8 CRON HANDLER: WARNING - Taxonomy term creation failed: " . $e->getMessage() . "
          "; - igny8_log_ai_event('Cron Cluster Taxonomy Failed', 'planner', 'clustering', 'warning', 'Taxonomy term creation failed', "Cluster: {$cluster_data['name']} (ID: {$cluster_id}), Error: " . $e->getMessage()); - } catch (Throwable $e) { - echo "Igny8 CRON HANDLER: ERROR - Fatal error in taxonomy term creation: " . $e->getMessage() . "
          "; - igny8_log_ai_event('Cron Cluster Taxonomy Fatal Error', 'planner', 'clustering', 'error', 'Fatal error in taxonomy term creation', "Cluster: {$cluster_data['name']} (ID: {$cluster_id}), Error: " . $e->getMessage()); - } - - // Log cluster creation - igny8_log_ai_event('Cron Cluster Created', 'planner', 'clustering', 'success', 'Cluster created successfully', "Cluster: {$cluster_data['name']} (ID: {$cluster_id})"); - - // Update keywords with cluster_id (same as AJAX handler) - if (isset($cluster_data['keywords']) && is_array($cluster_data['keywords'])) { - echo "Igny8 CRON HANDLER: Processing " . count($cluster_data['keywords']) . " keywords for this cluster
          "; - - foreach ($cluster_data['keywords'] as $keyword_name) { - echo "Igny8 CRON HANDLER: Looking for keyword: " . $keyword_name . "
          "; - - $update_result = $wpdb->update( - $wpdb->prefix . 'igny8_keywords', - ['cluster_id' => $cluster_id], - ['keyword' => $keyword_name], - ['%d'], - ['%s'] - ); - - if ($update_result !== false) { - $keywords_processed++; - echo "Igny8 CRON HANDLER: SUCCESS - Assigned keyword '" . $keyword_name . "' to cluster ID " . $cluster_id . "
          "; - } else { - echo "Igny8 CRON HANDLER: ERROR - Failed to update keyword '" . $keyword_name . "': " . $wpdb->last_error . "
          "; - } - } - - // Log keyword updates - igny8_log_ai_event('Cron Keywords Updated', 'planner', 'clustering', 'success', 'Keywords assigned to cluster', "Cluster: {$cluster_data['name']}, Keywords: " . count($cluster_data['keywords'])); - } else { - echo "Igny8 CRON HANDLER: WARNING - No keywords found in cluster data
          "; - } - - // Update cluster metrics (same as AJAX handler) - echo "Igny8 CRON HANDLER: Updating cluster metrics
          "; - try { - $metrics_result = igny8_update_cluster_metrics($cluster_id); - if ($metrics_result) { - echo "Igny8 CRON HANDLER: SUCCESS - Cluster metrics updated
          "; - igny8_log_ai_event('Cron Metrics Updated', 'planner', 'clustering', 'success', 'Cluster metrics calculated', "Cluster: {$cluster_data['name']}"); - } else { - echo "Igny8 CRON HANDLER: WARNING - Failed to update cluster metrics
          "; - igny8_log_ai_event('Cron Metrics Update Failed', 'planner', 'clustering', 'warning', 'Failed to update cluster metrics', "Cluster: {$cluster_data['name']}"); - } - } catch (Exception $e) { - echo "Igny8 CRON HANDLER: ERROR - Exception during metrics update: " . $e->getMessage() . "
          "; - igny8_log_ai_event('Cron Metrics Update Error', 'planner', 'clustering', 'error', 'Exception during metrics update', "Cluster: {$cluster_data['name']}, Error: " . $e->getMessage()); - } - } else { - // Log cluster creation failure - echo "Igny8 CRON HANDLER: ERROR - Failed to create cluster record: " . $wpdb->last_error . "
          "; - igny8_log_ai_event('Cron Cluster Creation Failed', 'planner', 'clustering', 'error', 'Failed to create cluster in database', "Cluster: {$cluster_data['name']}, Error: " . $wpdb->last_error); - } - - } catch (Exception $e) { - echo "Igny8 CRON HANDLER: ERROR - Exception processing cluster '{$cluster_data['name']}': " . $e->getMessage() . "
          "; - igny8_log_ai_event('Cron Cluster Processing Error', 'planner', 'clustering', 'error', 'Exception processing cluster', "Cluster: {$cluster_data['name']}, Error: " . $e->getMessage()); - echo "Igny8 CRON HANDLER: Continuing with next cluster
          "; - continue; - } catch (Throwable $e) { - echo "Igny8 CRON HANDLER: FATAL ERROR - Fatal error processing cluster '{$cluster_data['name']}': " . $e->getMessage() . "
          "; - igny8_log_ai_event('Cron Cluster Processing Fatal Error', 'planner', 'clustering', 'error', 'Fatal error processing cluster', "Cluster: {$cluster_data['name']}, Error: " . $e->getMessage()); - echo "Igny8 CRON HANDLER: Continuing with next cluster
          "; - continue; - } - } - - echo "Igny8 CRON HANDLER: Clustering completed successfully
          "; - echo "Igny8 CRON HANDLER: Database clusters created: " . $clusters_created . "
          "; - echo "Igny8 CRON HANDLER: Keywords processed: " . $keywords_processed . "
          "; - - // Verify the results by checking database - echo "Igny8 CRON HANDLER: Verifying results in database
          "; - $verify_clusters = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters WHERE status = 'active'"); - $verify_keywords = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id IS NOT NULL AND cluster_id > 0"); - $verify_taxonomy_terms = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->terms} t INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'clusters'"); - - echo "Igny8 CRON HANDLER: Total active clusters in database: " . $verify_clusters . "
          "; - echo "Igny8 CRON HANDLER: Total clustered keywords in database: " . $verify_keywords . "
          "; - echo "Igny8 CRON HANDLER: Total taxonomy terms for clusters: " . $verify_taxonomy_terms . "
          "; - - // Log completion with verification - igny8_log_ai_event('Cron Auto Cluster Complete', 'planner', 'auto_cluster', 'success', 'Cron automated clustering completed', 'Database clusters: ' . $clusters_created . ', Keywords processed: ' . $keywords_processed . ', Verified clusters: ' . $verify_clusters . ', Verified keywords: ' . $verify_keywords . ', Verified taxonomy terms: ' . $verify_taxonomy_terms); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $keywords_processed; - $GLOBALS['igny8_cron_result_details'] = "Processed {$keywords_processed} keywords, created {$clusters_created} clusters"; - - } catch (Exception $e) { - echo "Igny8 CRON HANDLER: ERROR - Exception during AI processing: " . $e->getMessage() . "
          "; - igny8_log_ai_event('Cron Auto Cluster Failed', 'planner', 'auto_cluster', 'error', 'Exception during AI processing', $e->getMessage()); - - // Set global variables for detailed logging (failure case) - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: " . $e->getMessage(); - - echo ""; - return; - } - echo "Igny8 CRON HANDLER: auto_cluster completed
          "; - echo ""; // Close the handler div - - // Restore error reporting - error_reporting($old_error_reporting); -} - -/** - * Auto Generate Ideas Cron Handler - * - * Automatically generates ideas from clusters without ideas. - */ -function igny8_auto_generate_ideas_cron_handler() { - // Suppress PHP warnings for model rates in cron context - $old_error_reporting = error_reporting(); - error_reporting($old_error_reporting & ~E_WARNING); - - echo "
          "; - echo "Igny8 CRON HANDLER: auto_generate_ideas started
          "; - error_log("Igny8 CRON HANDLER: auto_generate_ideas started"); - - // Check if automation is enabled via cron settings - echo "Igny8 CRON HANDLER: Checking if automation is enabled
          "; - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings['igny8_auto_generate_ideas_cron'] ?? []; - $auto_ideas_enabled = $job_settings['enabled'] ?? false; - echo "Igny8 CRON HANDLER: auto_ideas_enabled = " . ($auto_ideas_enabled ? 'enabled' : 'disabled') . "
          "; - - if (!$auto_ideas_enabled) { - echo "Igny8 CRON HANDLER: Automation disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: auto_generate_ideas automation disabled"); - echo "
          "; - return; - } - echo "Igny8 CRON HANDLER: Automation enabled, continuing
          "; - - // Check if AI mode is enabled - echo "Igny8 CRON HANDLER: Checking AI mode
          "; - $planner_mode = igny8_get_ai_setting('planner_mode', 'manual'); - echo "Igny8 CRON HANDLER: planner_mode = " . $planner_mode . "
          "; - - if ($planner_mode !== 'ai') { - echo "Igny8 CRON HANDLER: AI mode disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: AI mode disabled"); - echo ""; - return; - } - echo "Igny8 CRON HANDLER: AI mode enabled, continuing
          "; - - global $wpdb; - echo "Igny8 CRON HANDLER: Database connection established
          "; - - // Get clusters without ideas (use dynamic limit from settings) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - error_log('Igny8 Auto Cluster Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - echo "Igny8 CRON HANDLER: Querying clusters without ideas (limit: $limit)
          "; - $clusters_without_ideas = $wpdb->get_results(" - SELECT c.id FROM {$wpdb->prefix}igny8_clusters c - LEFT JOIN {$wpdb->prefix}igny8_content_ideas i ON c.id = i.keyword_cluster_id - WHERE i.id IS NULL - LIMIT $limit - "); - - echo "Igny8 CRON HANDLER: Found " . count($clusters_without_ideas) . " clusters without ideas
          "; - - if (empty($clusters_without_ideas)) { - echo "Igny8 CRON HANDLER: No clusters without ideas found, exiting
          "; - igny8_log_ai_event('Auto Generate Ideas Skipped', 'planner', 'auto_generate_ideas', 'info', 'No clusters without ideas found', 'All clusters already have ideas'); - echo ""; - return; - } - - $cluster_ids = array_column($clusters_without_ideas, 'id'); - echo "Igny8 CRON HANDLER: Processing " . count($cluster_ids) . " clusters
          "; - - // Log automation start - echo "Igny8 CRON HANDLER: Logging automation start
          "; - igny8_log_ai_event('Auto Generate Ideas Started', 'planner', 'auto_generate_ideas', 'info', 'Starting automated idea generation', 'Clusters: ' . count($cluster_ids)); - - // Set up user context for permissions - echo "Igny8 CRON HANDLER: Setting up user context
          "; - if (!current_user_can('manage_options')) { - // Get admin user for cron context - $admin_users = get_users(['role' => 'administrator', 'number' => 1]); - if (!empty($admin_users)) { - wp_set_current_user($admin_users[0]->ID); - echo "Igny8 CRON HANDLER: Set admin user context for permissions
          "; - } - } - - // Simulate AJAX request to existing AI function - echo "Igny8 CRON HANDLER: Starting AJAX simulation
          "; - $_POST['cluster_ids'] = json_encode($cluster_ids); - $_POST['nonce'] = wp_create_nonce('igny8_planner_settings'); - - // Capture output to prevent JSON response in cron - ob_start(); - igny8_ajax_ai_generate_ideas(); - $output = ob_get_clean(); - - echo "Igny8 CRON HANDLER: AJAX execution completed
          "; - - // Parse JSON response - $response = json_decode($output, true); - echo "Igny8 CRON HANDLER: Response parsed, success = " . ($response['success'] ?? 'false') . "
          "; - - if ($response && $response['success']) { - $ideas_created = $response['data']['ideas_created'] ?? count($cluster_ids); - echo "Igny8 CRON HANDLER: SUCCESS - Ideas generated: " . $ideas_created . "
          "; - igny8_log_ai_event('Auto Generate Ideas Complete', 'planner', 'auto_generate_ideas', 'success', 'Automated idea generation completed', 'Ideas created: ' . $ideas_created); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $ideas_created; - $GLOBALS['igny8_cron_result_details'] = "Processed {$ideas_created} clusters, created {$ideas_created} ideas"; - } else { - $error_msg = $response['data']['message'] ?? 'Unknown error'; - echo "Igny8 CRON HANDLER: ERROR - " . $error_msg . "
          "; - igny8_log_ai_event('Auto Generate Ideas Failed', 'planner', 'auto_generate_ideas', 'error', 'Automated idea generation failed', $error_msg); - - // Set global variables for detailed logging (failure case) - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: " . $error_msg; - } - - echo "Igny8 CRON HANDLER: auto_generate_ideas completed
          "; - echo ""; // Close the handler div - - // Restore error reporting - error_reporting($old_error_reporting); -} - -/** - * Auto Queue Cron Handler - * - * Automatically queues new ideas for content generation. - */ -function igny8_auto_queue_cron_handler() { - // Suppress PHP warnings for model rates in cron context - $old_error_reporting = error_reporting(); - error_reporting($old_error_reporting & ~E_WARNING); - - echo "
          "; - echo "Igny8 CRON HANDLER: auto_queue started
          "; - error_log("Igny8 CRON HANDLER: auto_queue started"); - - // Check if automation is enabled via cron settings - echo "Igny8 CRON HANDLER: Checking if automation is enabled
          "; - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings['igny8_auto_queue_cron'] ?? []; - $auto_queue_enabled = $job_settings['enabled'] ?? false; - echo "Igny8 CRON HANDLER: auto_queue_enabled = " . ($auto_queue_enabled ? 'enabled' : 'disabled') . "
          "; - - if (!$auto_queue_enabled) { - echo "Igny8 CRON HANDLER: Automation disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: auto_queue automation disabled"); - echo "
          "; - return; - } - echo "Igny8 CRON HANDLER: Automation enabled, continuing
          "; - - global $wpdb; - echo "Igny8 CRON HANDLER: Database connection established
          "; - - // Get new ideas (use dynamic limit from settings) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - // Try to get limit from Smart Automation Jobs table directly - $cron_limits = get_option('igny8_cron_limits', []); - $limit = $cron_limits['igny8_auto_queue_cron'] ?? null; - - if ($limit === null) { - error_log('Igny8 Auto Queue Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - - echo "Igny8 CRON HANDLER: Using limit from Smart Automation Jobs table: $limit
          "; - } - echo "Igny8 CRON HANDLER: Querying new ideas (limit: $limit)
          "; - $new_ideas = $wpdb->get_results(" - SELECT id FROM {$wpdb->prefix}igny8_content_ideas - WHERE status = 'new' - LIMIT $limit - "); - - echo "Igny8 CRON HANDLER: Found " . count($new_ideas) . " new ideas
          "; - - if (empty($new_ideas)) { - echo "Igny8 CRON HANDLER: No new ideas found, exiting
          "; - igny8_log_ai_event('Auto Queue Skipped', 'planner', 'auto_queue', 'info', 'No new ideas found', 'All ideas are already queued'); - echo ""; - return; - } - - $idea_ids = array_column($new_ideas, 'id'); - echo "Igny8 CRON HANDLER: Processing " . count($idea_ids) . " ideas
          "; - - // Log automation start - echo "Igny8 CRON HANDLER: Logging automation start
          "; - igny8_log_ai_event('Auto Queue Started', 'planner', 'auto_queue', 'info', 'Starting automated queueing', 'Ideas: ' . count($idea_ids)); - - // Set up user context for permissions - echo "Igny8 CRON HANDLER: Setting up user context
          "; - if (!current_user_can('edit_posts')) { - // Get admin user for cron context - $admin_users = get_users(['role' => 'administrator', 'number' => 1]); - if (!empty($admin_users)) { - wp_set_current_user($admin_users[0]->ID); - echo "Igny8 CRON HANDLER: Set admin user context for permissions
          "; - } - } - - // Skip AJAX simulation and go directly to function calls - // (AJAX has issues in cron context with nonce verification) - echo "Igny8 CRON HANDLER: Skipping AJAX simulation (cron context issues)
          "; - echo "Igny8 CRON HANDLER: Using direct function calls
          "; - - $created = []; - $skipped = []; - $failed = []; - - foreach ($idea_ids as $idea_id) { - echo "Igny8 CRON HANDLER: Processing idea ID: " . $idea_id . "
          "; - $result = igny8_create_task_from_idea($idea_id); - - if ($result['success']) { - if (!empty($result['task_id'])) { - $created[] = $result['task_id']; - echo "Igny8 CRON HANDLER: SUCCESS - Created task ID: " . $result['task_id'] . "
          "; - } else { - $skipped[] = $idea_id; - echo "Igny8 CRON HANDLER: SKIPPED - " . $result['message'] . "
          "; - } - } else { - $failed[] = $idea_id; - echo "Igny8 CRON HANDLER: FAILED - " . $result['message'] . "
          "; - } - } - - // Update metrics for processed ideas - foreach ($idea_ids as $idea_id) { - igny8_update_idea_metrics($idea_id); - } - - $created_count = count($created); - $skipped_count = count($skipped); - $failed_count = count($failed); - - echo "Igny8 CRON HANDLER: DIRECT SUCCESS - Tasks created: " . $created_count . ", Skipped: " . $skipped_count . ", Failed: " . $failed_count . "
          "; - igny8_log_ai_event('Auto Queue Complete', 'planner', 'auto_queue', 'success', 'Automated queueing completed (direct)', 'Tasks created: ' . $created_count . ', Skipped: ' . $skipped_count . ', Failed: ' . $failed_count); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $created_count; - $GLOBALS['igny8_cron_result_details'] = "Processed {$created_count} ideas, created {$created_count} tasks"; - - echo "Igny8 CRON HANDLER: auto_queue completed
          "; - echo ""; // Close the handler div - - // Restore error reporting - error_reporting($old_error_reporting); -} - -/** - * Auto Generate Content Cron Handler - * - * Automatically generates content from queued tasks. - */ -function igny8_auto_generate_content_cron_handler() { - // Suppress PHP warnings for model rates in cron context - $old_error_reporting = error_reporting(); - error_reporting($old_error_reporting & ~E_WARNING); - - echo "
          "; - echo "Igny8 CRON HANDLER: auto_generate_content started
          "; - error_log("Igny8 CRON HANDLER: auto_generate_content started"); - - // Check if automation is enabled via cron settings - echo "Igny8 CRON HANDLER: Checking if automation is enabled
          "; - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings['igny8_auto_generate_content_cron'] ?? []; - $auto_content_enabled = $job_settings['enabled'] ?? false; - echo "Igny8 CRON HANDLER: auto_content_enabled = " . ($auto_content_enabled ? 'enabled' : 'disabled') . "
          "; - - if (!$auto_content_enabled) { - echo "Igny8 CRON HANDLER: Automation disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: auto_generate_content automation disabled"); - echo "
          "; - return; - } - echo "Igny8 CRON HANDLER: Automation enabled, continuing
          "; - - // Check if AI mode is enabled - echo "Igny8 CRON HANDLER: Checking AI mode
          "; - $writer_mode = igny8_get_ai_setting('writer_mode', 'manual'); - echo "Igny8 CRON HANDLER: writer_mode = " . $writer_mode . "
          "; - - if ($writer_mode !== 'ai') { - echo "Igny8 CRON HANDLER: AI mode disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: AI mode disabled"); - echo ""; - return; - } - echo "Igny8 CRON HANDLER: AI mode enabled, continuing
          "; - - global $wpdb; - echo "Igny8 CRON HANDLER: Database connection established
          "; - - // Get queued tasks (use dynamic limit from settings) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - // Try to get limit from Smart Automation Jobs table directly - $cron_limits = get_option('igny8_cron_limits', []); - $limit = $cron_limits['igny8_auto_generate_content_cron'] ?? null; - - if ($limit === null) { - error_log('Igny8 Auto Generate Content Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - - echo "Igny8 CRON HANDLER: Using limit from Smart Automation Jobs table: $limit
          "; - } - echo "Igny8 CRON HANDLER: Querying queued tasks (limit: $limit)
          "; - $queued_tasks = $wpdb->get_results(" - SELECT id FROM {$wpdb->prefix}igny8_tasks - WHERE status = 'queued' - LIMIT $limit - "); - - echo "Igny8 CRON HANDLER: Found " . count($queued_tasks) . " queued tasks
          "; - - if (empty($queued_tasks)) { - echo "Igny8 CRON HANDLER: No queued tasks found, exiting
          "; - igny8_log_ai_event('Auto Generate Content Skipped', 'writer', 'auto_generate_content', 'info', 'No queued tasks found', 'All tasks are already processed'); - echo ""; - return; - } - - $task_ids = array_column($queued_tasks, 'id'); - echo "Igny8 CRON HANDLER: Processing " . count($task_ids) . " tasks
          "; - - // Log automation start - echo "Igny8 CRON HANDLER: Logging automation start
          "; - igny8_log_ai_event('Auto Generate Content Started', 'writer', 'auto_generate_content', 'info', 'Starting automated content generation', 'Tasks: ' . count($task_ids)); - - // Set up user context for permissions - echo "Igny8 CRON HANDLER: Setting up user context
          "; - if (!current_user_can('manage_options')) { - // Get admin user for cron context - $admin_users = get_users(['role' => 'administrator', 'number' => 1]); - if (!empty($admin_users)) { - wp_set_current_user($admin_users[0]->ID); - echo "Igny8 CRON HANDLER: Set admin user context for permissions
          "; - } - } - - $completed = 0; - $failed = 0; - - foreach ($task_ids as $task_id) { - echo "

          📋 SECTION 1: PROCESSING TASK " . $task_id . "

          "; - echo "Igny8 CRON HANDLER: Processing task ID: " . $task_id . "
          "; - echo "Igny8 CRON HANDLER: Sending to AI for content generation...
          "; - - // Log AI request initiation - igny8_log_ai_event('AI Request Initiated', 'writer', 'auto_content_generation', 'info', 'Starting AI content generation', 'Task ID: ' . $task_id); - - // Get AI configuration for logging - $api_key = get_option('igny8_api_key'); - $model = get_option('igny8_model', 'gpt-4.1'); - - // Log AI request details - echo "Igny8 CRON HANDLER: AI Request Details - Model: " . $model . "
          "; - echo "Igny8 CRON HANDLER: API Key Status: " . (empty($api_key) ? 'Missing' : 'Configured') . "
          "; - igny8_log_ai_event('AI Request Details', 'writer', 'auto_content_generation', 'info', 'AI request configuration', 'Model: ' . $model . ', API Key: ' . (empty($api_key) ? 'Missing' : 'Configured')); - - // Log the actual AI request being made - echo "Igny8 CRON HANDLER: Making AI API call to OpenAI...
          "; - igny8_log_ai_event('AI API Call', 'writer', 'auto_content_generation', 'info', 'Calling OpenAI API', 'Model: ' . $model . ', Task: ' . $task_id); - - // Simulate AJAX request to content generation function - $_POST['task_id'] = $task_id; - $_POST['nonce'] = wp_create_nonce('igny8_writer_settings'); - - // Capture the response from AI call - ob_start(); - igny8_ajax_ai_generate_content(); - $ai_response = ob_get_clean(); - - // Log AI response - echo "Igny8 CRON HANDLER: AI Response received
          "; - echo "Igny8 CRON HANDLER: Response Length: " . strlen($ai_response) . " characters
          "; - igny8_log_ai_event('AI Response Received', 'writer', 'auto_content_generation', 'info', 'AI processing completed', 'Response captured for task: ' . $task_id); - - // Parse the AI response to extract success/error status - $json_response = null; - $response_status = 'unknown'; - $error_message = ''; - - // Try multiple methods to extract JSON from response - if (preg_match('/\{.*\}/s', $ai_response, $matches)) { - $json_response = json_decode($matches[0], true); - } - - // If no JSON found, try to detect response type from content - if (!$json_response) { - if (strpos($ai_response, 'success') !== false && strpos($ai_response, 'true') !== false) { - $response_status = 'success'; - echo "Igny8 CRON HANDLER: ✅ AI Response - SUCCESS (detected from content)
          "; - igny8_log_ai_event('AI Response Success', 'writer', 'auto_content_generation', 'success', 'AI content generation successful', 'Task ID: ' . $task_id); - } elseif (strpos($ai_response, 'error') !== false || strpos($ai_response, 'failed') !== false) { - $response_status = 'error'; - $error_message = 'Error detected in response content'; - echo "Igny8 CRON HANDLER: ❌ AI Response - FAILED (detected from content)
          "; - echo "Igny8 CRON HANDLER: Error: " . $error_message . "
          "; - igny8_log_ai_event('AI Response Failed', 'writer', 'auto_content_generation', 'error', 'AI content generation failed', 'Task ID: ' . $task_id . ', Error: ' . $error_message); - } else { - $response_status = 'unknown'; - echo "Igny8 CRON HANDLER: ⚠️ AI Response - UNKNOWN FORMAT
          "; - igny8_log_ai_event('AI Response Unknown', 'writer', 'auto_content_generation', 'warning', 'AI response format not recognized', 'Task ID: ' . $task_id . ', Raw response: ' . substr($ai_response, 0, 200)); - } - } else { - // JSON response found, parse it - if (isset($json_response['success']) && $json_response['success']) { - $response_status = 'success'; - echo "Igny8 CRON HANDLER: ✅ AI Response - SUCCESS (JSON parsed)
          "; - igny8_log_ai_event('AI Response Success', 'writer', 'auto_content_generation', 'success', 'AI content generation successful', 'Task ID: ' . $task_id); - } else { - $response_status = 'error'; - $error_message = $json_response['data']['message'] ?? $json_response['message'] ?? 'Unknown error'; - echo "Igny8 CRON HANDLER: ❌ AI Response - FAILED (JSON parsed)
          "; - echo "Igny8 CRON HANDLER: Error: " . $error_message . "
          "; - igny8_log_ai_event('AI Response Failed', 'writer', 'auto_content_generation', 'error', 'AI content generation failed', 'Task ID: ' . $task_id . ', Error: ' . $error_message); - } - } - - // Log response details for debugging - echo "Igny8 CRON HANDLER: Response Status: " . $response_status . "
          "; - echo "Igny8 CRON HANDLER: Response Length: " . strlen($ai_response) . " characters
          "; - igny8_log_ai_event('AI Response Analysis', 'writer', 'auto_content_generation', 'info', 'Response analysis completed', 'Status: ' . $response_status . ', Length: ' . strlen($ai_response) . ', Task: ' . $task_id); - - // Get the actual post ID from the most recently created post by this user - $recent_posts = get_posts([ - 'post_type' => 'post', - 'post_status' => 'draft', - 'author' => get_current_user_id(), - 'numberposts' => 1, - 'orderby' => 'date', - 'order' => 'DESC' - ]); - - $actual_post_id = !empty($recent_posts) ? $recent_posts[0]->ID : null; - - $response = ['success' => true, 'data' => ['post_id' => $actual_post_id]]; - - if ($response && $response['success']) { - echo "Igny8 CRON HANDLER: ✅ AI Response received successfully
          "; - - echo "

          🔧 SECTION 2: PROCESSING AI RESPONSE & SAVING DATA

          "; - echo "Igny8 CRON HANDLER: Processing AI response data...
          "; - - // The detailed field processing logs will come from igny8_create_post_from_ai_response function - // which already has all the debugging output we added - - echo "

          ✅ SECTION 3: FINAL VERIFICATION & SUCCESS SUMMARY

          "; - - if (!empty($response['data']['post_id'])) { - $post_id = $response['data']['post_id']; - echo "Igny8 CRON HANDLER: ✅ Post Created Successfully - ID: " . $post_id . "
          "; - echo "Igny8 CRON HANDLER: Checking fields for post ID: " . $post_id . "
          "; - - // Verify all saved fields - $meta_title = get_post_meta($post_id, '_igny8_meta_title', true); - $meta_description = get_post_meta($post_id, '_igny8_meta_description', true); - $primary_keywords = get_post_meta($post_id, '_igny8_primary_keywords', true); - $secondary_keywords = get_post_meta($post_id, '_igny8_secondary_keywords', true); - $word_count = get_post_meta($post_id, '_igny8_word_count', true); - - echo "Igny8 CRON HANDLER: ✅ Meta Title: " . (!empty($meta_title) ? 'Saved' : 'Missing') . "
          "; - echo "Igny8 CRON HANDLER: ✅ Meta Description: " . (!empty($meta_description) ? 'Saved' : 'Missing') . "
          "; - echo "Igny8 CRON HANDLER: ✅ Primary Keywords: " . (!empty($primary_keywords) ? 'Saved' : 'Missing') . "
          "; - echo "Igny8 CRON HANDLER: ✅ Secondary Keywords: " . (!empty($secondary_keywords) ? 'Saved' : 'Missing') . "
          "; - echo "Igny8 CRON HANDLER: ✅ Word Count: " . (!empty($word_count) ? 'Saved' : 'Missing') . "
          "; - - // Verify taxonomies - $cluster_terms = wp_get_object_terms($post_id, 'clusters'); - $sector_terms = wp_get_object_terms($post_id, 'sectors'); - - echo "Igny8 CRON HANDLER: ✅ Cluster Taxonomy: " . (!empty($cluster_terms) && !is_wp_error($cluster_terms) ? 'Associated' : 'Not Associated') . "
          "; - echo "Igny8 CRON HANDLER: ✅ Sector Taxonomy: " . (!empty($sector_terms) && !is_wp_error($sector_terms) ? 'Associated' : 'Not Associated') . "
          "; - - // Verify Yoast meta - $yoast_meta = get_post_meta($post_id, '_yoast_wpseo_metadesc', true); - echo "Igny8 CRON HANDLER: ✅ Yoast Meta Description: " . (!empty($yoast_meta) ? 'Saved' : 'Missing') . "
          "; - - $completed++; - echo "🎉 TASK " . $task_id . " COMPLETED SUCCESSFULLY!
          "; - } else { - echo "❌ Post creation failed - No post ID returned
          "; - $failed++; - } - } else { - $failed++; - $error_msg = $response['data']['message'] ?? 'Unknown error'; - echo "❌ TASK " . $task_id . " FAILED: " . $error_msg . "
          "; - } - - echo "
          "; - } - - echo "Igny8 CRON HANDLER: Content generation completed - Success: " . $completed . ", Failed: " . $failed . "
          "; - - if ($completed > 0) { - igny8_log_ai_event('Auto Generate Content Complete', 'writer', 'auto_generate_content', 'success', 'Automated content generation completed', 'Content generated: ' . $completed . ', Failed: ' . $failed); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $completed; - $GLOBALS['igny8_cron_result_details'] = "Processed {$completed} tasks, generated {$completed} content pieces"; - echo "Igny8 AUTO GENERATE CONTENT HANDLER: Set global variables - processed_count: $completed, result_details: Processed {$completed} tasks, generated {$completed} content pieces
          "; - } else { - igny8_log_ai_event('Auto Generate Content Failed', 'writer', 'auto_generate_content', 'error', 'Automated content generation failed', 'No content was generated'); - - // Set global variables for detailed logging (failure case) - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No content was generated"; - } - - echo "Igny8 CRON HANDLER: auto_generate_content completed
          "; - echo ""; // Close the handler div - - // Restore error reporting - error_reporting($old_error_reporting); -} - -/** - * Auto Publish Drafts Cron Handler - * - * Automatically publishes completed draft posts. - */ -function igny8_auto_publish_drafts_cron_handler() { - echo "
          "; - echo "Igny8 CRON HANDLER: auto_publish_drafts started
          "; - error_log("Igny8 CRON HANDLER: auto_publish_drafts started"); - - // Check if automation is enabled via cron settings - echo "Igny8 CRON HANDLER: Checking if automation is enabled
          "; - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings['igny8_auto_publish_drafts_cron'] ?? []; - $auto_publish_enabled = $job_settings['enabled'] ?? false; - echo "Igny8 CRON HANDLER: auto_publish_enabled = " . ($auto_publish_enabled ? 'enabled' : 'disabled') . "
          "; - - if (!$auto_publish_enabled) { - echo "Igny8 CRON HANDLER: Automation disabled, exiting
          "; - error_log("Igny8 CRON HANDLER: auto_publish_drafts automation disabled"); - echo "
          "; - return; - } - echo "Igny8 CRON HANDLER: Automation enabled, continuing
          "; - - global $wpdb; - - // Get completed tasks with published content (use admin limit) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - // Try to get limit from Smart Automation Jobs table directly - $cron_limits = get_option('igny8_cron_limits', []); - $limit = $cron_limits['igny8_auto_publish_drafts_cron'] ?? null; - - if ($limit === null) { - error_log('Igny8 Auto Publish Drafts Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - - echo "Igny8 CRON HANDLER: Using limit from Smart Automation Jobs table: $limit
          "; - } - echo "Igny8 CRON HANDLER: Querying completed tasks with draft posts (limit: $limit)
          "; - $completed_tasks = $wpdb->get_results(" - SELECT t.id, t.assigned_post_id FROM {$wpdb->prefix}igny8_tasks t - INNER JOIN {$wpdb->posts} p ON t.assigned_post_id = p.ID - WHERE t.status = 'completed' - AND p.post_status = 'draft' - LIMIT $limit - "); - - echo "Igny8 CRON HANDLER: Found " . count($completed_tasks) . " completed tasks with draft posts
          "; - - if (empty($completed_tasks)) { - echo "Igny8 CRON HANDLER: No draft posts found, exiting
          "; - igny8_log_ai_event('Auto Publish Drafts Skipped', 'writer', 'auto_publish_drafts', 'info', 'No draft posts found', 'All content is already published'); - echo ""; - return; - } - - $post_ids = array_column($completed_tasks, 'assigned_post_id'); - - // Log automation start - igny8_log_ai_event('Auto Publish Drafts Started', 'writer', 'auto_publish_drafts', 'info', 'Starting automated publishing', 'Posts: ' . count($post_ids)); - - $published = 0; - echo "Igny8 CRON HANDLER: Processing " . count($post_ids) . " draft posts
          "; - - foreach ($post_ids as $post_id) { - echo "Igny8 CRON HANDLER: Publishing post ID: " . $post_id . "
          "; - $result = wp_update_post([ - 'ID' => $post_id, - 'post_status' => 'publish' - ]); - - if (!is_wp_error($result) && $result) { - $published++; - echo "Igny8 CRON HANDLER: ✅ SUCCESS - Published post ID: " . $post_id . "
          "; - } else { - echo "Igny8 CRON HANDLER: ❌ FAILED - Could not publish post ID: " . $post_id . "
          "; - } - } - - echo "Igny8 CRON HANDLER: Publishing completed - Published: " . $published . " posts
          "; - - if ($published > 0) { - igny8_log_ai_event('Auto Publish Drafts Complete', 'writer', 'auto_publish_drafts', 'success', 'Automated publishing completed', 'Posts published: ' . $published); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $published; - $GLOBALS['igny8_cron_result_details'] = "Processed {$published} drafts, published {$published} posts"; - } else { - igny8_log_ai_event('Auto Publish Drafts Failed', 'writer', 'auto_publish_drafts', 'error', 'Automated publishing failed', 'No posts were published'); - - // Set global variables for detailed logging (failure case) - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No posts were published"; - } - - echo "Igny8 CRON HANDLER: auto_publish_drafts completed
          "; - echo ""; // Close the handler div -} - -/** - * Auto Optimizer Cron Handler (NEW) - * - * Automatically optimizes content and keywords. - */ -function igny8_auto_optimizer_cron_handler() { - // Check if automation is enabled - if (igny8_get_ai_setting('auto_optimizer_enabled', 'disabled') !== 'enabled') { - return; - } - - global $wpdb; - - // Get posts that need optimization (use admin limit) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - // Try to get limit from Smart Automation Jobs table directly - $cron_limits = get_option('igny8_cron_limits', []); - $limit = $cron_limits['igny8_auto_optimizer_cron'] ?? null; - - if ($limit === null) { - error_log('Igny8 Auto Optimizer Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - - echo "Igny8 CRON HANDLER: Using limit from Smart Automation Jobs table: $limit
          "; - } - $posts_to_optimize = $wpdb->get_results(" - SELECT p.ID, p.post_title - FROM {$wpdb->posts} p - LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'igny8_optimized' - WHERE p.post_status = 'publish' - AND p.post_type IN ('post', 'page') - AND pm.meta_value IS NULL - LIMIT $limit - "); - - if (empty($posts_to_optimize)) { - igny8_log_ai_event('Auto Optimizer Skipped', 'optimizer', 'auto_optimizer', 'info', 'No posts need optimization', 'All posts are already optimized'); - return; - } - - $post_ids = array_column($posts_to_optimize, 'ID'); - - // Log automation start - igny8_log_ai_event('Auto Optimizer Started', 'optimizer', 'auto_optimizer', 'info', 'Starting automated optimization', 'Posts: ' . count($post_ids)); - - $optimized = 0; - foreach ($post_ids as $post_id) { - // Run optimization process - $result = igny8_optimize_post_content($post_id); - if ($result['success']) { - $optimized++; - // Mark as optimized - update_post_meta($post_id, 'igny8_optimized', current_time('mysql')); - } - } - - if ($optimized > 0) { - igny8_log_ai_event('Auto Optimizer Complete', 'optimizer', 'auto_optimizer', 'success', 'Automated optimization completed', 'Posts optimized: ' . $optimized); - } else { - igny8_log_ai_event('Auto Optimizer Failed', 'optimizer', 'auto_optimizer', 'error', 'Automated optimization failed', 'No posts were optimized'); - } -} - -/** - * Auto Recalc Cron Handler (NEW) - * - * Automatically recalculates metrics for all entities. - */ -function igny8_auto_recalc_cron_handler() { - global $wpdb; - - // Get entities that need recalculation - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - error_log('Igny8 Auto Cluster Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - $clusters_to_recalc = $wpdb->get_results(" - SELECT id FROM {$wpdb->prefix}igny8_clusters - WHERE status = 'active' - LIMIT $limit - "); - - $tasks_to_recalc = $wpdb->get_results(" - SELECT id FROM {$wpdb->prefix}igny8_tasks - WHERE status IN ('completed', 'in_progress') - LIMIT $limit - "); - - $total_recalc = count($clusters_to_recalc) + count($tasks_to_recalc); - - if ($total_recalc === 0) { - igny8_log_ai_event('Auto Recalc Skipped', 'analytics', 'auto_recalc', 'info', 'No entities need recalculation', 'All metrics are up to date'); - return; - } - - // Log automation start - igny8_log_ai_event('Auto Recalc Started', 'analytics', 'auto_recalc', 'info', 'Starting automated recalculation', 'Entities: ' . $total_recalc); - - $recalculated = 0; - - // Recalculate cluster metrics - foreach ($clusters_to_recalc as $cluster) { - igny8_update_cluster_metrics($cluster->id); - $recalculated++; - } - - // Recalculate task metrics - foreach ($tasks_to_recalc as $task) { - igny8_update_task_metrics($task->id); - $recalculated++; - } - - if ($recalculated > 0) { - igny8_log_ai_event('Auto Recalc Complete', 'analytics', 'auto_recalc', 'success', 'Automated recalculation completed', 'Entities recalculated: ' . $recalculated); - } else { - igny8_log_ai_event('Auto Recalc Failed', 'analytics', 'auto_recalc', 'error', 'Automated recalculation failed', 'No entities were recalculated'); - } -} - -/** - * Health Check Cron Handler (NEW) - * - * Performs system health checks, cleanup, and dependency validation. - */ -function igny8_health_check_cron_handler() { - global $wpdb; - - $health_issues = []; - $cleanup_performed = []; - $dependency_issues = []; - - // Check for orphaned records - $orphaned_keywords = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->prefix}igny8_keywords k - LEFT JOIN {$wpdb->prefix}igny8_clusters c ON k.cluster_id = c.id - WHERE k.cluster_id IS NOT NULL AND k.cluster_id > 0 AND c.id IS NULL - "); - - if ($orphaned_keywords > 0) { - $health_issues[] = "Found $orphaned_keywords orphaned keywords"; - - // Clean up orphaned keywords - $wpdb->query(" - UPDATE {$wpdb->prefix}igny8_keywords - SET cluster_id = NULL - WHERE cluster_id IS NOT NULL - AND cluster_id > 0 - AND cluster_id NOT IN (SELECT id FROM {$wpdb->prefix}igny8_clusters) - "); - $cleanup_performed[] = "Cleaned up $orphaned_keywords orphaned keywords"; - } - - // Check for old failed AI tasks - $old_failed_tasks = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->prefix}igny8_ai_queue - WHERE status = 'failed' - AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY) - "); - - if ($old_failed_tasks > 0) { - $wpdb->query(" - DELETE FROM {$wpdb->prefix}igny8_ai_queue - WHERE status = 'failed' - AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY) - "); - $cleanup_performed[] = "Cleaned up $old_failed_tasks old failed AI tasks"; - } - - // Dependency validation - check execution order - $dependency_issues = igny8_validate_automation_dependencies(); - - // Check database table sizes - $table_sizes = []; - $tables = ['igny8_keywords', 'igny8_clusters', 'igny8_content_ideas', 'igny8_tasks', 'igny8_ai_queue']; - - foreach ($tables as $table) { - $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}$table"); - $table_sizes[$table] = $count; - } - - // Log health check results - if (empty($health_issues) && empty($dependency_issues)) { - igny8_log_ai_event('Health Check Passed', 'system', 'health_check', 'success', 'System health check completed', 'No issues found'); - } else { - $all_issues = array_merge($health_issues, $dependency_issues); - igny8_log_ai_event('Health Check Issues', 'system', 'health_check', 'warning', 'Health check found issues', implode('; ', $all_issues)); - } - - if (!empty($cleanup_performed)) { - igny8_log_ai_event('Health Check Cleanup', 'system', 'health_check', 'info', 'Cleanup performed', implode('; ', $cleanup_performed)); - } - - // Store health metrics - update_option('igny8_health_metrics', [ - 'last_check' => current_time('mysql'), - 'table_sizes' => $table_sizes, - 'issues_found' => count($health_issues), - 'dependency_issues' => count($dependency_issues), - 'cleanup_performed' => count($cleanup_performed) - ]); -} - -/** - * Validate automation dependencies and execution order - */ -function igny8_validate_automation_dependencies() { - $issues = []; - global $wpdb; - - // Check if clusters exist but no ideas generated - $clusters_without_ideas = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters c - LEFT JOIN {$wpdb->prefix}igny8_content_ideas i ON c.id = i.keyword_cluster_id - WHERE c.status = 'active' AND i.id IS NULL - "); - - if ($clusters_without_ideas > 0) { - $issues[] = "Found $clusters_without_ideas clusters without content ideas (auto_generate_ideas may be disabled)"; - } - - // Check if ideas exist but not queued - $ideas_not_queued = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->prefix}igny8_content_ideas i - LEFT JOIN {$wpdb->prefix}igny8_tasks t ON i.id = t.idea_id - WHERE i.status = 'new' AND t.id IS NULL - "); - - if ($ideas_not_queued > 0) { - $issues[] = "Found $ideas_not_queued ideas not queued for content generation (auto_queue may be disabled)"; - } - - // Check if tasks exist but no content generated - $tasks_without_content = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks t - LEFT JOIN {$wpdb->posts} p ON t.assigned_post_id = p.ID - WHERE t.status = 'queued' AND (p.ID IS NULL OR p.post_content = '') - "); - - if ($tasks_without_content > 0) { - $issues[] = "Found $tasks_without_content tasks without content (auto_generate_content may be disabled)"; - } - - // Check if content exists but no images - $content_without_images = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->posts} p - LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'igny8_has_ai_image' - WHERE p.post_status = 'publish' - AND p.post_type IN ('post', 'page') - AND p.post_content LIKE '%[image%' - AND pm.meta_value IS NULL - "); - - if ($content_without_images > 0) { - $issues[] = "Found $content_without_images posts with image placeholders but no AI images (auto_generate_images may be disabled)"; - } - - // Check if content exists but not published - $drafts_not_published = $wpdb->get_var(" - SELECT COUNT(*) FROM {$wpdb->posts} p - INNER JOIN {$wpdb->prefix}igny8_tasks t ON p.ID = t.assigned_post_id - WHERE p.post_status = 'draft' - AND t.status = 'completed' - "); - - if ($drafts_not_published > 0) { - $issues[] = "Found $drafts_not_published completed tasks with unpublished content (auto_publish_drafts may be disabled)"; - } - - return $issues; -} - -/** - * Auto Generate Images Cron Handler (NEW) - * - * Automatically generates images for content that needs them. - */ -function igny8_auto_generate_images_cron_handler() { - // Check if automation is enabled - if (igny8_get_ai_setting('auto_generate_images_enabled', 'disabled') !== 'enabled') { - return; - } - - // Check if AI mode is enabled - if (igny8_get_ai_setting('writer_mode', 'manual') !== 'ai') { - return; - } - - global $wpdb; - - // Get posts that need images (use admin limit) - $limit = $GLOBALS['igny8_cron_limit'] ?? null; - if ($limit === null) { - // Try to get limit from Smart Automation Jobs table directly - $cron_limits = get_option('igny8_cron_limits', []); - $limit = $cron_limits['igny8_auto_generate_images_cron'] ?? null; - - if ($limit === null) { - error_log('Igny8 Auto Generate Images Cron: No limit set in Smart Automation Jobs table'); - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No limit set in Smart Automation Jobs table"; - return; - } - - echo "Igny8 CRON HANDLER: Using limit from Smart Automation Jobs table: $limit
          "; - } - $posts_needing_images = $wpdb->get_results(" - SELECT p.ID, p.post_title, p.post_content - FROM {$wpdb->posts} p - LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'igny8_has_ai_image' - WHERE p.post_status = 'publish' - AND p.post_type IN ('post', 'page') - AND pm.meta_value IS NULL - AND p.post_content LIKE '%[image%' - LIMIT $limit - "); - - if (empty($posts_needing_images)) { - igny8_log_ai_event('Auto Generate Images Skipped', 'writer', 'auto_generate_images', 'info', 'No posts need images', 'All posts already have images or no image placeholders found'); - return; - } - - $post_ids = array_column($posts_needing_images, 'ID'); - - // Log automation start - igny8_log_ai_event('Auto Generate Images Started', 'writer', 'auto_generate_images', 'info', 'Starting automated image generation', 'Posts: ' . count($post_ids)); - - $generated = 0; - foreach ($post_ids as $post_id) { - // Run image generation process - $result = igny8_generate_post_images($post_id); - if ($result['success']) { - $generated++; - // Mark as having AI image - update_post_meta($post_id, 'igny8_has_ai_image', current_time('mysql')); - } - } - - if ($generated > 0) { - igny8_log_ai_event('Auto Generate Images Complete', 'writer', 'auto_generate_images', 'success', 'Automated image generation completed', 'Images generated: ' . $generated); - - // Set global variables for detailed logging - $GLOBALS['igny8_cron_processed_count'] = $generated; - $GLOBALS['igny8_cron_result_details'] = "Processed {$generated} posts, generated {$generated} images"; - } else { - igny8_log_ai_event('Auto Generate Images Failed', 'writer', 'auto_generate_images', 'error', 'Automated image generation failed', 'No images were generated'); - - // Set global variables for detailed logging (failure case) - $GLOBALS['igny8_cron_processed_count'] = 0; - $GLOBALS['igny8_cron_result_details'] = "FAILED: No images were generated"; - } -} - -/** - * Test Cron Endpoint Handler (NEW) - * - * Simple test handler for cron endpoint validation. - */ -function igny8_test_cron_endpoint() { - igny8_log_ai_event('Cron Test', 'system', 'test_endpoint', 'info', 'Cron endpoint test executed', 'Test successful at ' . current_time('mysql')); - - // Return success response for external cron - if (defined('DOING_CRON') && DOING_CRON) { - return [ - 'success' => true, - 'message' => 'Igny8 CRON endpoint is working', - 'timestamp' => current_time('mysql'), - 'server' => $_SERVER['SERVER_NAME'] ?? 'unknown' - ]; - } -} - -/** - * Safe taxonomy term creation with duplicate prevention - * - * @param string $term_name Term name - * @param string $taxonomy Taxonomy name - * @param array $args Additional arguments - * @return int|WP_Error Term ID or error - */ -function igny8_safe_create_term($term_name, $taxonomy, $args = []) { - // Check if term already exists - $existing_term = get_term_by('name', $term_name, $taxonomy); - if ($existing_term) { - return $existing_term->term_id; - } - - // Create term with duplicate slug prevention - $slug = sanitize_title($term_name); - $counter = 1; - $original_slug = $slug; - - while (term_exists($slug, $taxonomy)) { - $slug = $original_slug . '-' . $counter; - $counter++; - } - - $args['slug'] = $slug; - - $result = wp_insert_term($term_name, $taxonomy, $args); - - if (is_wp_error($result)) { - // If still error, try to get existing term by slug - $existing = get_term_by('slug', $slug, $taxonomy); - if ($existing) { - return $existing->term_id; - } - return $result; - } - - return $result['term_id']; -} - -/** - * Enhanced cluster term creation with safety checks - * - * @param int $cluster_id Cluster ID - * @return int|false Term ID or false on failure - */ -function igny8_safe_create_cluster_term($cluster_id) { - global $wpdb; - - // Get cluster data - $cluster = $wpdb->get_row($wpdb->prepare( - "SELECT cluster_name, cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!$cluster) { - return false; - } - - // Skip if already mapped - if (!empty($cluster->cluster_term_id)) { - return $cluster->cluster_term_id; - } - - // Ensure taxonomy exists - if (!taxonomy_exists('clusters') && function_exists('igny8_register_taxonomies')) { - igny8_register_taxonomies(); - } - - // Create term safely - $term_id = igny8_safe_create_term($cluster->cluster_name, 'clusters'); - - if ($term_id && !is_wp_error($term_id)) { - // Update cluster with term ID - $wpdb->update( - "{$wpdb->prefix}igny8_clusters", - ['cluster_term_id' => $term_id], - ['id' => $cluster_id], - ['%d'], - ['%d'] - ); - - return $term_id; - } - - return false; -} - -// =================================================================== -// REGISTER CRON HOOKS -// =================================================================== - -// Register all cron job hooks -add_action('igny8_auto_cluster_cron', 'igny8_auto_cluster_cron_handler'); -add_action('igny8_auto_generate_ideas_cron', 'igny8_auto_generate_ideas_cron_handler'); -add_action('igny8_auto_queue_cron', 'igny8_auto_queue_cron_handler'); -add_action('igny8_auto_generate_content_cron', 'igny8_auto_generate_content_cron_handler'); -add_action('igny8_auto_generate_images_cron', 'igny8_auto_generate_images_cron_handler'); -add_action('igny8_auto_publish_drafts_cron', 'igny8_auto_publish_drafts_cron_handler'); -add_action('igny8_process_ai_queue_cron', 'igny8_process_ai_queue_cron_handler'); -add_action('igny8_auto_recalc_cron', 'igny8_auto_recalc_cron_handler'); -add_action('igny8_auto_optimizer_cron', 'igny8_auto_optimizer_cron_handler'); -add_action('igny8_health_check_cron', 'igny8_health_check_cron_handler'); diff --git a/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-master-dispatcher.php b/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-master-dispatcher.php deleted file mode 100644 index a8294f44..00000000 --- a/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-master-dispatcher.php +++ /dev/null @@ -1,384 +0,0 @@ -"; - echo "Igny8 MASTER DISPATCHER: Starting smart automation check
          "; - error_log("Igny8 MASTER DISPATCHER: Starting smart automation check"); - - // Get all defined cron jobs - $cron_jobs = igny8_get_defined_cron_jobs(); - $current_time = current_time('timestamp'); - $executed_jobs = []; - $skipped_jobs = []; - - echo "Igny8 MASTER DISPATCHER: Found " . count($cron_jobs) . " defined jobs
          "; - - // Get settings and limits - $cron_settings = get_option('igny8_cron_settings', []); - $cron_limits = get_option('igny8_cron_limits', []); - - // Initialize default settings if missing - if (empty($cron_settings)) { - $cron_settings = igny8_get_default_cron_settings(); - update_option('igny8_cron_settings', $cron_settings); - echo "Igny8 MASTER DISPATCHER: Initialized default settings
          "; - } - - if (empty($cron_limits)) { - $cron_limits = igny8_get_default_cron_limits(); - update_option('igny8_cron_limits', $cron_limits); - echo "Igny8 MASTER DISPATCHER: Initialized default limits
          "; - } - - // Process each job in priority order - foreach ($cron_jobs as $job_name => $job_config) { - echo "Igny8 MASTER DISPATCHER: Checking job: " . $job_name . "
          "; - - // Check if job is enabled - $job_settings = $cron_settings[$job_name] ?? []; - if (!($job_settings['enabled'] ?? false)) { - echo "Igny8 MASTER DISPATCHER: Job disabled, skipping
          "; - $skipped_jobs[] = $job_name; - continue; - } - - // Check if job is due (simplified - just check if enabled and not recently run) - $last_run = $job_settings['last_run'] ?? 0; - $time_since_last_run = $current_time - $last_run; - - // Run job if it hasn't been run in the last 5 minutes (to prevent duplicate runs) - if ($time_since_last_run < 300) { - echo "Igny8 MASTER DISPATCHER: Job run recently, skipping
          "; - $skipped_jobs[] = $job_name; - continue; - } - - // Check if job is already running (duplicate prevention) - $lock_key = 'igny8_cron_running_' . $job_name; - if (get_transient($lock_key)) { - echo "Igny8 MASTER DISPATCHER: Job already running, skipping
          "; - $skipped_jobs[] = $job_name; - continue; - } - - // Set lock for this job - $max_execution_time = $job_config['max_execution_time'] ?? 300; - set_transient($lock_key, true, $max_execution_time); - - echo "Igny8 MASTER DISPATCHER: Executing job: " . $job_name . "
          "; - - try { - // Get job limit - $job_limit = $cron_limits[$job_name] ?? 1; - - // Set limit as global variable for handlers to use - $GLOBALS['igny8_cron_limit'] = $job_limit; - - // Execute the job - $start_time = microtime(true); - do_action($job_name); - $execution_time = microtime(true) - $start_time; - - // Update last run time - $cron_settings[$job_name]['last_run'] = $current_time; - update_option('igny8_cron_settings', $cron_settings); - - // Track individual job execution with detailed logging - $processed_count = $GLOBALS['igny8_cron_processed_count'] ?? 0; - $result_details = $GLOBALS['igny8_cron_result_details'] ?? ''; - - echo "Igny8 MASTER DISPATCHER: Global variables - processed_count: $processed_count, result_details: $result_details
          "; - - $job_health = [ - 'last_run' => $current_time, - 'success' => true, - 'last_success' => true, - 'execution_time' => round($execution_time, 2), - 'error_message' => '', - 'processed_count' => $processed_count, - 'result_details' => $result_details, - 'execution_method' => (isset($_GET['import_key']) && !empty($_GET['import_key'])) ? 'external_url' : 'server_cron' - ]; - update_option('igny8_cron_health_' . $job_name, $job_health); - - $executed_jobs[] = [ - 'job' => $job_name, - 'execution_time' => round($execution_time, 2), - 'success' => true - ]; - - echo "Igny8 MASTER DISPATCHER: Job completed successfully in " . round($execution_time, 2) . "s
          "; - - } catch (Exception $e) { - echo "Igny8 MASTER DISPATCHER: Job failed: " . $e->getMessage() . "
          "; - error_log("Igny8 MASTER DISPATCHER: Job $job_name failed - " . $e->getMessage()); - - // Track individual job failure - $job_health = [ - 'last_run' => $current_time, - 'success' => false, - 'last_success' => false, - 'execution_time' => 0, - 'error_message' => $e->getMessage(), - 'processed_count' => $GLOBALS['igny8_cron_processed_count'] ?? 0, - 'result_details' => 'FAILED: ' . $e->getMessage(), - 'execution_method' => (isset($_GET['import_key']) && !empty($_GET['import_key'])) ? 'external_url' : 'server_cron' - ]; - update_option('igny8_cron_health_' . $job_name, $job_health); - - $executed_jobs[] = [ - 'job' => $job_name, - 'execution_time' => 0, - 'success' => false, - 'last_success' => false, - 'error' => $e->getMessage() - ]; - } catch (Throwable $e) { - echo "Igny8 MASTER DISPATCHER: Job fatal error: " . $e->getMessage() . "
          "; - error_log("Igny8 MASTER DISPATCHER: Job $job_name fatal error - " . $e->getMessage()); - - // Track individual job failure - $job_health = [ - 'last_run' => $current_time, - 'success' => false, - 'last_success' => false, - 'execution_time' => 0, - 'error_message' => $e->getMessage(), - 'processed_count' => $GLOBALS['igny8_cron_processed_count'] ?? 0, - 'result_details' => 'FAILED: ' . $e->getMessage(), - 'execution_method' => (isset($_GET['import_key']) && !empty($_GET['import_key'])) ? 'external_url' : 'server_cron' - ]; - update_option('igny8_cron_health_' . $job_name, $job_health); - - $executed_jobs[] = [ - 'job' => $job_name, - 'execution_time' => 0, - 'success' => false, - 'last_success' => false, - 'error' => $e->getMessage() - ]; - } finally { - // Always release the lock - delete_transient($lock_key); - } - } - - // Log summary - echo "Igny8 MASTER DISPATCHER: Execution summary
          "; - echo "Igny8 MASTER DISPATCHER: Jobs executed: " . count($executed_jobs) . "
          "; - echo "Igny8 MASTER DISPATCHER: Jobs skipped: " . count($skipped_jobs) . "
          "; - - // Store execution log - update_option('igny8_cron_last_execution', [ - 'timestamp' => $current_time, - 'executed' => $executed_jobs, - 'skipped' => $skipped_jobs - ]); - - echo "Igny8 MASTER DISPATCHER: Smart automation check completed
          "; - echo ""; - - // Return success response for external cron - return [ - 'success' => true, - 'message' => 'Master dispatcher executed successfully', - 'executed' => count($executed_jobs), - 'skipped' => count($skipped_jobs), - 'timestamp' => current_time('mysql') - ]; -} - -/** - * Get all defined cron jobs with their configurations - */ -function igny8_get_defined_cron_jobs() { - return [ - 'igny8_auto_cluster_cron' => [ - 'handler' => 'igny8_auto_cluster_cron_handler', - 'priority' => 1, - 'max_execution_time' => 600, // 10 minutes - 'description' => 'Auto cluster unmapped keywords', - 'module' => 'planner' - ], - 'igny8_auto_generate_ideas_cron' => [ - 'handler' => 'igny8_auto_generate_ideas_cron_handler', - 'priority' => 2, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'Auto generate ideas from clusters', - 'module' => 'planner' - ], - 'igny8_auto_queue_cron' => [ - 'handler' => 'igny8_auto_queue_cron_handler', - 'priority' => 3, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'Auto queue new ideas', - 'module' => 'planner' - ], - 'igny8_auto_generate_content_cron' => [ - 'handler' => 'igny8_auto_generate_content_cron_handler', - 'priority' => 4, - 'max_execution_time' => 600, // 10 minutes - 'description' => 'Auto generate content from queued tasks', - 'module' => 'writer' - ], - 'igny8_auto_generate_images_cron' => [ - 'handler' => 'igny8_auto_generate_images_cron_handler', - 'priority' => 5, - 'max_execution_time' => 900, // 15 minutes - 'description' => 'Auto generate images for content', - 'module' => 'writer' - ], - 'igny8_auto_publish_drafts_cron' => [ - 'handler' => 'igny8_auto_publish_drafts_cron_handler', - 'priority' => 6, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'Auto publish completed drafts', - 'module' => 'writer' - ], - 'igny8_process_ai_queue_cron' => [ - 'handler' => 'igny8_process_ai_queue_cron_handler', - 'priority' => 7, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'Process AI queue tasks', - 'module' => 'ai' - ], - 'igny8_auto_recalc_cron' => [ - 'handler' => 'igny8_auto_recalc_cron_handler', - 'priority' => 8, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'Auto recalculate metrics', - 'module' => 'analytics' - ], - 'igny8_auto_optimizer_cron' => [ - 'handler' => 'igny8_auto_optimizer_cron_handler', - 'priority' => 9, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'Auto optimize content and keywords', - 'module' => 'optimizer' - ], - 'igny8_health_check_cron' => [ - 'handler' => 'igny8_health_check_cron_handler', - 'priority' => 10, - 'max_execution_time' => 300, // 5 minutes - 'description' => 'System health check and cleanup', - 'module' => 'system' - ] - ]; -} - - -/** - * Get default cron settings - */ -function igny8_get_default_cron_settings() { - $jobs = igny8_get_defined_cron_jobs(); - $settings = []; - - foreach ($jobs as $job_name => $config) { - $settings[$job_name] = [ - 'enabled' => false, // Default to disabled - 'last_run' => 0 - ]; - } - - return $settings; -} - -/** - * Get default cron limits - */ -function igny8_get_default_cron_limits() { - return [ - 'igny8_auto_cluster_cron' => 1, - 'igny8_auto_generate_ideas_cron' => 1, - 'igny8_auto_queue_cron' => 1, - 'igny8_auto_generate_content_cron' => 1, - 'igny8_auto_generate_images_cron' => 1, - 'igny8_auto_publish_drafts_cron' => 1, - 'igny8_process_ai_queue_cron' => 1, - 'igny8_auto_recalc_cron' => 1, - 'igny8_auto_optimizer_cron' => 1, - 'igny8_health_check_cron' => 1 - ]; -} - -/** - * Update cron settings for a specific job - */ -function igny8_update_cron_job_settings($job_name, $settings) { - $cron_settings = get_option('igny8_cron_settings', []); - $cron_settings[$job_name] = array_merge($cron_settings[$job_name] ?? [], $settings); - update_option('igny8_cron_settings', $cron_settings); -} - -/** - * Update cron limits for a specific job - */ -function igny8_update_cron_job_limits($job_name, $limit) { - $cron_limits = get_option('igny8_cron_limits', []); - $cron_limits[$job_name] = $limit; - update_option('igny8_cron_limits', $cron_limits); -} - -/** - * Get health status for a specific job - */ -function igny8_get_job_health_status($job_name) { - $health = get_option('igny8_cron_health_' . $job_name, []); - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings[$job_name] ?? []; - - return [ - 'enabled' => $job_settings['enabled'] ?? false, - 'last_run' => isset($health['last_run']) ? date('Y-m-d H:i:s', $health['last_run']) : 'Never', - 'last_success' => $health['success'] ?? null, - 'execution_time' => isset($health['execution_time']) ? round($health['execution_time'], 2) : 0, - 'error_message' => $health['error_message'] ?? '', - 'processed_count' => $health['processed_count'] ?? 0, - 'result_details' => $health['result_details'] ?? '', - 'execution_method' => $health['execution_method'] ?? 'unknown' - ]; -} - -/** - * Get cron job status and next run time - */ -function igny8_get_cron_job_status($job_name) { - $cron_settings = get_option('igny8_cron_settings', []); - $job_settings = $cron_settings[$job_name] ?? []; - - if (empty($job_settings)) { - return [ - 'enabled' => false, - 'last_run' => 'Never' - ]; - } - - return [ - 'enabled' => $job_settings['enabled'] ?? false, - 'last_run' => isset($job_settings['last_run']) && $job_settings['last_run'] ? date('Y-m-d H:i:s', $job_settings['last_run']) : 'Never' - ]; -} diff --git a/igny8-ai-seo-wp-plugin/core/db/db-migration.php b/igny8-ai-seo-wp-plugin/core/db/db-migration.php deleted file mode 100644 index d939ea47..00000000 --- a/igny8-ai-seo-wp-plugin/core/db/db-migration.php +++ /dev/null @@ -1,253 +0,0 @@ -prefix . 'igny8_example_table'; - // $column_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'new_column'"); - // if (!$column_exists) { - // $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `new_column` VARCHAR(255) DEFAULT NULL"); - // } - - // Example: Create new table - // $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}igny8_new_table ( - // id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - // name VARCHAR(255) NOT NULL, - // created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - // PRIMARY KEY (id) - // ) {$wpdb->get_charset_collate()};"; - // dbDelta($sql); - - // Example: Migrate data - // $wpdb->query("UPDATE {$wpdb->prefix}igny8_table SET old_field = new_field WHERE condition"); - - error_log("Igny8 Migration: Successfully migrated from $from_version to $to_version"); - return true; - - } catch (Exception $e) { - error_log("Igny8 Migration Error: " . $e->getMessage()); - return false; - } -} - -/** - * ======================================================================== - * MIGRATION UTILITIES - * ======================================================================== - */ - -/** - * Backup table before migration - */ -function igny8_backup_table($table_name, $suffix = null) { - global $wpdb; - - if (!$suffix) { - $suffix = '_backup_' . date('Y_m_d_H_i_s'); - } - - $backup_table = $table_name . $suffix; - - try { - $wpdb->query("CREATE TABLE `$backup_table` LIKE `$table_name`"); - $wpdb->query("INSERT INTO `$backup_table` SELECT * FROM `$table_name`"); - return $backup_table; - } catch (Exception $e) { - error_log("Igny8 Migration: Failed to backup table $table_name - " . $e->getMessage()); - return false; - } -} - -/** - * Restore table from backup - */ -function igny8_restore_table($table_name, $backup_table) { - global $wpdb; - - try { - $wpdb->query("DROP TABLE IF EXISTS `$table_name`"); - $wpdb->query("CREATE TABLE `$table_name` LIKE `$backup_table`"); - $wpdb->query("INSERT INTO `$table_name` SELECT * FROM `$backup_table`"); - return true; - } catch (Exception $e) { - error_log("Igny8 Migration: Failed to restore table $table_name - " . $e->getMessage()); - return false; - } -} - -/** - * Check if table exists - */ -function igny8_table_exists($table_name) { - global $wpdb; - return $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; -} - -/** - * Check if column exists in table - */ -function igny8_column_exists($table_name, $column_name) { - global $wpdb; - $result = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE '$column_name'"); - return !empty($result); -} - -/** - * Get table structure - */ -function igny8_get_table_structure($table_name) { - global $wpdb; - return $wpdb->get_results("DESCRIBE `$table_name`", ARRAY_A); -} - -/** - * ======================================================================== - * AUTO-MIGRATION ON PLUGIN UPDATE - * ======================================================================== - */ - -/** - * Auto-run migrations on plugin update - */ -function igny8_auto_run_migrations() { - if (current_user_can('manage_options') && igny8_is_migration_needed()) { - igny8_run_migrations(); - } -} - -// Hook to auto-run migrations on admin_init -add_action('admin_init', 'igny8_auto_run_migrations'); - -/** - * ======================================================================== - * MIGRATION STATUS & LOGGING - * ======================================================================== - */ - -/** - * Log migration event - */ -function igny8_log_migration($from_version, $to_version, $status = 'success', $message = '') { - $log_entry = [ - 'timestamp' => current_time('mysql'), - 'from_version' => $from_version, - 'to_version' => $to_version, - 'status' => $status, - 'message' => $message, - 'user_id' => get_current_user_id() - ]; - - // Store in options (you could also use the logs table) - $migration_logs = get_option('igny8_migration_logs', []); - $migration_logs[] = $log_entry; - - // Keep only last 50 migration logs - if (count($migration_logs) > 50) { - $migration_logs = array_slice($migration_logs, -50); - } - - update_option('igny8_migration_logs', $migration_logs); -} - -/** - * Get migration logs - */ -function igny8_get_migration_logs($limit = 10) { - $logs = get_option('igny8_migration_logs', []); - return array_slice(array_reverse($logs), 0, $limit); -} - -/** - * Clear migration logs - */ -function igny8_clear_migration_logs() { - delete_option('igny8_migration_logs'); -} diff --git a/igny8-ai-seo-wp-plugin/core/db/db.php b/igny8-ai-seo-wp-plugin/core/db/db.php deleted file mode 100644 index 059f9122..00000000 --- a/igny8-ai-seo-wp-plugin/core/db/db.php +++ /dev/null @@ -1,970 +0,0 @@ -prefix . 'igny8_content_ideas'; - - // Only run cleanup if table exists - if (!$wpdb->get_var("SHOW TABLES LIKE '$table_name'")) { - return true; - } - - try { - // Remove legacy priority column if it exists (from very old versions) - $priority_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'priority'"); - if ($priority_exists) { - // Remove index first if it exists - $index_exists = $wpdb->get_var("SHOW INDEX FROM `$table_name` WHERE Key_name = 'idx_priority'"); - if ($index_exists) { - $wpdb->query("ALTER TABLE `$table_name` DROP INDEX `idx_priority`"); - } - - // Drop the column - $wpdb->query("ALTER TABLE `$table_name` DROP COLUMN `priority`"); - error_log('Igny8 Cleanup: Removed legacy priority column'); - } - - // Remove legacy ai_generated column if it exists (should be source now) - $ai_generated_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'ai_generated'"); - if ($ai_generated_exists) { - // Check if source column exists - $source_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'source'"); - - if (!$source_exists) { - // Migrate data from ai_generated to source before dropping - $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `source` ENUM('AI','Manual') DEFAULT 'Manual'"); - $wpdb->query("UPDATE `$table_name` SET source = CASE WHEN ai_generated = 1 THEN 'AI' ELSE 'Manual' END"); - error_log('Igny8 Cleanup: Migrated ai_generated to source field'); - } - - // Drop the old ai_generated column - $wpdb->query("ALTER TABLE `$table_name` DROP COLUMN `ai_generated`"); - error_log('Igny8 Cleanup: Removed legacy ai_generated column'); - } - - // Update any old status values to new format - $wpdb->query("UPDATE `$table_name` SET status = 'new' WHERE status NOT IN ('new','scheduled','published')"); - - return true; - - } catch (Exception $e) { - error_log('Igny8 Cleanup Error: ' . $e->getMessage()); - return false; - } -} - -/** - * Check if legacy cleanup is needed - */ -function igny8_is_legacy_cleanup_needed() { - global $wpdb; - - $table_name = $wpdb->prefix . 'igny8_content_ideas'; - - // Check if table exists - if (!$wpdb->get_var("SHOW TABLES LIKE '$table_name'")) { - return false; - } - - // Check for legacy columns - $priority_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'priority'"); - $ai_generated_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'ai_generated'"); - - return $priority_exists || $ai_generated_exists; -} - -/** - * Auto-run legacy cleanup on admin_init if needed - */ -function igny8_auto_run_legacy_cleanup() { - if (current_user_can('manage_options') && igny8_is_legacy_cleanup_needed()) { - igny8_cleanup_legacy_structures(); - } -} - -// Hook to auto-run legacy cleanup (only for existing installations) -add_action('admin_init', 'igny8_auto_run_legacy_cleanup'); - -/** - * Auto-run logs table migration on admin_init if needed - */ -function igny8_auto_run_logs_migration() { - if (current_user_can('manage_options')) { - igny8_migrate_logs_table(); - } -} - -// Hook to auto-run logs migration (only for existing installations) -add_action('admin_init', 'igny8_auto_run_logs_migration'); - -/** - * Remove old migration option on plugin activation - */ -function igny8_cleanup_migration_options() { - delete_option('igny8_migration_ideas_schema_updated'); -} - -/** - * ======================================================================== - * COMPLETE DATABASE SCHEMA CREATION - * ======================================================================== - */ - -/** - * Create all Igny8 database tables (15 tables total) - */ -function igny8_create_all_tables() { - global $wpdb; - $charset_collate = $wpdb->get_charset_collate(); - - // Keywords table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_keywords ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - keyword VARCHAR(255) NOT NULL, - search_volume INT UNSIGNED DEFAULT 0, - difficulty INT UNSIGNED DEFAULT 0, - cpc FLOAT DEFAULT 0.00, - intent VARCHAR(50) DEFAULT 'informational', - cluster_id BIGINT UNSIGNED DEFAULT NULL, - sector_id BIGINT UNSIGNED DEFAULT NULL, - mapped_post_id BIGINT UNSIGNED DEFAULT NULL, - status ENUM('unmapped','mapped','queued','published') DEFAULT 'unmapped', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY unique_keyword (keyword), - KEY idx_cluster_id (cluster_id), - KEY idx_sector_id (sector_id), - KEY idx_mapped_post_id (mapped_post_id), - KEY idx_status (status), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Tasks table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_tasks ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - title VARCHAR(255) NOT NULL, - description TEXT DEFAULT NULL, - status ENUM('pending','in_progress','completed','cancelled','draft','queued','review','published') DEFAULT 'pending', - priority ENUM('low','medium','high','urgent') DEFAULT 'medium', - due_date DATETIME DEFAULT NULL, - content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub', - content_type ENUM('post','product','page','CPT') DEFAULT 'post', - cluster_id BIGINT UNSIGNED DEFAULT NULL, - keywords TEXT DEFAULT NULL, - meta_title VARCHAR(255) DEFAULT NULL, - meta_description TEXT DEFAULT NULL, - word_count INT UNSIGNED DEFAULT 0, - raw_ai_response LONGTEXT DEFAULT NULL, - schedule_at DATETIME DEFAULT NULL, - assigned_post_id BIGINT UNSIGNED DEFAULT NULL, - idea_id BIGINT UNSIGNED DEFAULT NULL, - ai_writer ENUM('ai','human') DEFAULT 'ai', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_content_structure (content_structure), - KEY idx_content_type (content_type), - KEY idx_cluster_id (cluster_id), - KEY idx_status (status), - KEY idx_priority (priority), - KEY idx_assigned_post_id (assigned_post_id), - KEY idx_schedule_at (schedule_at), - KEY idx_idea_id (idea_id), - KEY idx_ai_writer (ai_writer), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Data table for personalization - $sql = "CREATE TABLE {$wpdb->prefix}igny8_data ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - post_id BIGINT UNSIGNED NOT NULL, - data_type VARCHAR(50) NOT NULL, - data JSON NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_post_id (post_id), - KEY idx_data_type (data_type), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Personalization variations table - stores AI-generated personalized content - $sql = "CREATE TABLE {$wpdb->prefix}igny8_variations ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - post_id BIGINT UNSIGNED NOT NULL, - fields_hash CHAR(64) NOT NULL, - fields_json LONGTEXT NOT NULL, - content LONGTEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_post_id (post_id), - KEY idx_fields_hash (fields_hash), - KEY idx_created_at (created_at), - UNIQUE KEY unique_variation (post_id, fields_hash) - ) $charset_collate;"; - dbDelta($sql); - - // Rankings table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_rankings ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - post_id BIGINT UNSIGNED NOT NULL, - keyword VARCHAR(255) NOT NULL, - impressions INT UNSIGNED DEFAULT 0, - clicks INT UNSIGNED DEFAULT 0, - ctr FLOAT DEFAULT 0.00, - avg_position FLOAT DEFAULT NULL, - source ENUM('gsc','ahrefs','manual') DEFAULT 'manual', - fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_post_id (post_id), - KEY idx_keyword (keyword), - KEY idx_source (source), - KEY idx_fetched_at (fetched_at) - ) $charset_collate;"; - dbDelta($sql); - - // Suggestions table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_suggestions ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - post_id BIGINT UNSIGNED NOT NULL, - cluster_id BIGINT UNSIGNED DEFAULT NULL, - suggestion_type ENUM('internal_link','keyword_injection','rewrite') NOT NULL, - payload JSON DEFAULT NULL, - status ENUM('pending','applied','rejected') DEFAULT 'pending', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - applied_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - KEY idx_post_id (post_id), - KEY idx_cluster_id (cluster_id), - KEY idx_suggestion_type (suggestion_type), - KEY idx_status (status), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Campaigns table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_campaigns ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - cluster_id BIGINT UNSIGNED DEFAULT NULL, - target_post_id BIGINT UNSIGNED DEFAULT NULL, - name VARCHAR(255) NOT NULL, - status ENUM('active','completed','paused') DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_cluster_id (cluster_id), - KEY idx_target_post_id (target_post_id), - KEY idx_status (status), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Content Ideas table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_content_ideas ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - idea_title VARCHAR(255) NOT NULL, - idea_description LONGTEXT DEFAULT NULL, - content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub', - content_type ENUM('post','product','page','CPT') DEFAULT 'post', - keyword_cluster_id BIGINT UNSIGNED DEFAULT NULL, - status ENUM('new','scheduled','published') DEFAULT 'new', - estimated_word_count INT UNSIGNED DEFAULT 0, - target_keywords TEXT DEFAULT NULL, - image_prompts TEXT DEFAULT NULL, - source ENUM('AI','Manual') DEFAULT 'Manual', - mapped_post_id BIGINT UNSIGNED DEFAULT NULL, - tasks_count INT UNSIGNED DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_idea_title (idea_title), - KEY idx_content_structure (content_structure), - KEY idx_content_type (content_type), - KEY idx_status (status), - KEY idx_keyword_cluster_id (keyword_cluster_id), - KEY idx_mapped_post_id (mapped_post_id), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Clusters table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_clusters ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - cluster_name VARCHAR(255) NOT NULL, - sector_id BIGINT UNSIGNED DEFAULT NULL, - cluster_term_id BIGINT UNSIGNED DEFAULT NULL, - status ENUM('active','inactive','archived') DEFAULT 'active', - keyword_count INT UNSIGNED DEFAULT 0, - total_volume INT UNSIGNED DEFAULT 0, - avg_difficulty DECIMAL(5,2) DEFAULT 0.00, - mapped_pages_count INT UNSIGNED DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_cluster_name (cluster_name), - KEY idx_sector_id (sector_id), - KEY idx_cluster_term_id (cluster_term_id), - KEY idx_status (status), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Sites table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_sites ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - site_url VARCHAR(500) NOT NULL, - site_name VARCHAR(255) DEFAULT NULL, - domain_authority INT UNSIGNED DEFAULT 0, - referring_domains INT UNSIGNED DEFAULT 0, - organic_traffic INT UNSIGNED DEFAULT 0, - status ENUM('active','inactive','blocked') DEFAULT 'active', - last_crawled DATETIME DEFAULT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY unique_site_url (site_url), - KEY idx_domain_authority (domain_authority), - KEY idx_status (status), - KEY idx_last_crawled (last_crawled), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Backlinks table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_backlinks ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - source_url VARCHAR(500) NOT NULL, - target_url VARCHAR(500) NOT NULL, - anchor_text VARCHAR(255) DEFAULT NULL, - link_type ENUM('dofollow','nofollow','sponsored','ugc') DEFAULT 'dofollow', - domain_authority INT UNSIGNED DEFAULT 0, - page_authority INT UNSIGNED DEFAULT 0, - status ENUM('pending','live','lost','disavowed') DEFAULT 'pending', - campaign_id BIGINT UNSIGNED DEFAULT NULL, - discovered_date DATE DEFAULT NULL, - lost_date DATE DEFAULT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_source_url (source_url(191)), - KEY idx_target_url (target_url(191)), - KEY idx_link_type (link_type), - KEY idx_status (status), - KEY idx_campaign_id (campaign_id), - KEY idx_domain_authority (domain_authority), - KEY idx_discovered_date (discovered_date), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Mapping table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_mapping ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - source_type ENUM('keyword','cluster','idea','task') NOT NULL, - source_id BIGINT UNSIGNED NOT NULL, - target_type ENUM('post','page','product') NOT NULL, - target_id BIGINT UNSIGNED NOT NULL, - mapping_type ENUM('primary','secondary','related') DEFAULT 'primary', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_source_type_id (source_type, source_id), - KEY idx_target_type_id (target_type, target_id), - KEY idx_mapping_type (mapping_type), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Prompts table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_prompts ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - prompt_name VARCHAR(255) NOT NULL, - prompt_type ENUM('content','optimization','generation','custom') DEFAULT 'content', - prompt_text LONGTEXT NOT NULL, - variables JSON DEFAULT NULL, - is_active TINYINT(1) DEFAULT 1, - usage_count INT UNSIGNED DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY unique_prompt_name (prompt_name), - KEY idx_prompt_type (prompt_type), - KEY idx_is_active (is_active), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Logs table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_logs ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - event_type VARCHAR(191) NOT NULL, - message TEXT NOT NULL, - context LONGTEXT NULL, - api_id VARCHAR(255) NULL, - status VARCHAR(50) NULL, - level VARCHAR(50) NULL, - source VARCHAR(100) NULL, - user_id BIGINT UNSIGNED NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_event_type (event_type), - KEY idx_created_at (created_at), - KEY idx_source (source), - KEY idx_status (status), - KEY idx_user_id (user_id) - ) $charset_collate;"; - dbDelta($sql); - - // AI Queue table - $sql = "CREATE TABLE {$wpdb->prefix}igny8_ai_queue ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - action VARCHAR(50) NOT NULL, - data LONGTEXT NOT NULL, - user_id BIGINT UNSIGNED NOT NULL, - status ENUM('pending','processing','completed','failed') DEFAULT 'pending', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - processed_at TIMESTAMP NULL, - result LONGTEXT NULL, - error_message TEXT NULL, - PRIMARY KEY (id), - KEY idx_user_id (user_id), - KEY idx_status (status), - KEY idx_action (action), - KEY idx_created_at (created_at) - ) $charset_collate;"; - dbDelta($sql); - - // Update database version - update_option('igny8_db_version', '0.1'); -} - - - - -/** - * Register Igny8 taxonomies with WordPress - */ -function igny8_register_taxonomies() { - // Register sectors taxonomy (hierarchical) - only if not exists - if (!taxonomy_exists('sectors')) { - register_taxonomy('sectors', ['post', 'page', 'product'], [ - 'hierarchical' => true, - 'labels' => [ - 'name' => 'Sectors', - 'singular_name' => 'Sector', - 'menu_name' => 'Sectors', - 'all_items' => 'All Sectors', - 'edit_item' => 'Edit Sector', - 'view_item' => 'View Sector', - 'update_item' => 'Update Sector', - 'add_new_item' => 'Add New Sector', - 'new_item_name' => 'New Sector Name', - 'parent_item' => 'Parent Sector', - 'parent_item_colon' => 'Parent Sector:', - 'search_items' => 'Search Sectors', - 'popular_items' => 'Popular Sectors', - 'separate_items_with_commas' => 'Separate sectors with commas', - 'add_or_remove_items' => 'Add or remove sectors', - 'choose_from_most_used' => 'Choose from most used sectors', - 'not_found' => 'No sectors found', - ], - 'public' => true, - 'show_ui' => true, - 'show_admin_column' => true, - 'show_in_nav_menus' => true, - 'show_tagcloud' => true, - 'show_in_rest' => true, - 'rewrite' => [ - 'slug' => 'sectors', - 'with_front' => false, - ], - 'capabilities' => [ - 'manage_terms' => 'manage_categories', - 'edit_terms' => 'manage_categories', - 'delete_terms' => 'manage_categories', - 'assign_terms' => 'edit_posts', - ], - ]); - } - - // Register clusters taxonomy (hierarchical) - only if not exists - if (!taxonomy_exists('clusters')) { - register_taxonomy('clusters', ['post', 'page', 'product'], [ - 'hierarchical' => true, - 'labels' => [ - 'name' => 'Content Clusters', - 'singular_name' => 'Cluster', - 'menu_name' => 'Clusters', - 'all_items' => 'All Clusters', - 'edit_item' => 'Edit Cluster', - 'view_item' => 'View Cluster', - 'update_item' => 'Update Cluster', - 'add_new_item' => 'Add New Cluster', - 'new_item_name' => 'New Cluster Name', - 'parent_item' => 'Parent Cluster', - 'parent_item_colon' => 'Parent Cluster:', - 'search_items' => 'Search Clusters', - 'popular_items' => 'Popular Clusters', - 'separate_items_with_commas' => 'Separate clusters with commas', - 'add_or_remove_items' => 'Add or remove clusters', - 'choose_from_most_used' => 'Choose from most used clusters', - 'not_found' => 'No clusters found', - ], - 'public' => true, - 'show_ui' => true, - 'show_admin_column' => true, - 'show_in_nav_menus' => true, - 'show_tagcloud' => true, - 'show_in_rest' => true, - 'rewrite' => [ - 'slug' => 'clusters', - 'with_front' => false, - ], - 'capabilities' => [ - 'manage_terms' => 'manage_categories', - 'edit_terms' => 'manage_categories', - 'delete_terms' => 'manage_categories', - 'assign_terms' => 'edit_posts', - ], - ]); - } -} - -// ========================================================== -// SEO: Prevent indexing of Cluster and Sector taxonomy pages -// ========================================================== -add_action('wp_head', function() { - if (is_tax(['clusters', 'sectors'])) { - echo '' . "\n"; - } -}, 1); - -/** - * Register Igny8 post meta fields with WordPress - */ -function igny8_register_post_meta() { - $post_types = ['post', 'page', 'product']; - - // Define all meta fields with proper schema for REST API - $meta_fields = [ - '_igny8_cluster_id' => [ - 'type' => 'integer', - 'description' => 'Assigns content to a cluster', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_keyword_ids' => [ - 'type' => 'array', - 'description' => 'Maps multiple keywords to content', - 'single' => true, - 'show_in_rest'=> [ - 'schema' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'integer' - ] - ] - ] - ], - '_igny8_task_id' => [ - 'type' => 'integer', - 'description' => 'Links WP content back to Writer task', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_campaign_ids' => [ - 'type' => 'array', - 'description' => 'Associates content with backlink campaigns', - 'single' => true, - 'show_in_rest'=> [ - 'schema' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'integer' - ] - ] - ] - ], - '_igny8_backlink_count' => [ - 'type' => 'integer', - 'description' => 'Quick summary count of backlinks to content', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_last_optimized' => [ - 'type' => 'string', - 'description' => 'Tracks last optimization timestamp', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_meta_title' => [ - 'type' => 'string', - 'description' => 'SEO meta title for the content', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_meta_description' => [ - 'type' => 'string', - 'description' => 'SEO meta description for the content', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_primary_keywords' => [ - 'type' => 'string', - 'description' => 'Primary keywords for the content', - 'single' => true, - 'show_in_rest'=> true, - ], - '_igny8_secondary_keywords' => [ - 'type' => 'string', - 'description' => 'Secondary keywords for the content', - 'single' => true, - 'show_in_rest'=> true, - ], - ]; - - // Register each meta field for all relevant post types - foreach ($meta_fields as $meta_key => $config) { - foreach ($post_types as $post_type) { - register_post_meta($post_type, $meta_key, $config); - } - } -} - -/** - * Set default plugin options - */ -function igny8_set_default_options() { - // Set default options if they don't exist - if (!get_option('igny8_api_key')) { - add_option('igny8_api_key', ''); - } - if (!get_option('igny8_ai_enabled')) { - add_option('igny8_ai_enabled', 1); - } - if (!get_option('igny8_debug_enabled')) { - add_option('igny8_debug_enabled', 0); - } - if (!get_option('igny8_monitoring_enabled')) { - add_option('igny8_monitoring_enabled', 1); - } - if (!get_option('igny8_version')) { - add_option('igny8_version', '0.1'); - } -} - -/** - * Migrate logs table to add missing columns for OpenAI API logging - */ -function igny8_migrate_logs_table() { - global $wpdb; - - $table_name = $wpdb->prefix . 'igny8_logs'; - - // Check if table exists - if (!$wpdb->get_var("SHOW TABLES LIKE '$table_name'")) { - return true; - } - - // Check if migration is needed - $columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name"); - $column_names = array_column($columns, 'Field'); - - $needed_columns = ['api_id', 'status', 'level', 'source', 'user_id']; - $missing_columns = array_diff($needed_columns, $column_names); - - if (empty($missing_columns)) { - return true; // Migration not needed - } - - try { - // Add missing columns - if (in_array('api_id', $missing_columns)) { - $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `api_id` VARCHAR(255) NULL"); - } - if (in_array('status', $missing_columns)) { - $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `status` VARCHAR(50) NULL"); - } - if (in_array('level', $missing_columns)) { - $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `level` VARCHAR(50) NULL"); - } - if (in_array('source', $missing_columns)) { - $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `source` VARCHAR(100) NULL"); - } - if (in_array('user_id', $missing_columns)) { - $wpdb->query("ALTER TABLE `$table_name` ADD COLUMN `user_id` BIGINT UNSIGNED NULL"); - } - - // Add indexes for new columns - $indexes_to_add = [ - 'source' => "ALTER TABLE `$table_name` ADD INDEX `idx_source` (`source`)", - 'status' => "ALTER TABLE `$table_name` ADD INDEX `idx_status` (`status`)", - 'user_id' => "ALTER TABLE `$table_name` ADD INDEX `idx_user_id` (`user_id`)" - ]; - - foreach ($indexes_to_add as $column => $sql) { - if (in_array($column, $missing_columns)) { - $wpdb->query($sql); - } - } - - error_log('Igny8: Logs table migration completed successfully'); - return true; - - } catch (Exception $e) { - error_log('Igny8 Logs Migration Error: ' . $e->getMessage()); - return false; - } -} - -/** - * Complete plugin installation function - */ -function igny8_install_database() { - // Create all database tables - igny8_create_all_tables(); - - // Migrate logs table if needed - igny8_migrate_logs_table(); - - // Register taxonomies - igny8_register_taxonomies(); - - // Register post meta fields - igny8_register_post_meta(); - - // Set default options - igny8_set_default_options(); - - // Update version - update_option('igny8_version', '0.1'); - update_option('igny8_db_version', '0.1'); - - // Add word_count field to tasks table if it doesn't exist - igny8_add_word_count_to_tasks(); - - // Add raw_ai_response field to tasks table if it doesn't exist - igny8_add_raw_ai_response_to_tasks(); - - // Add tasks_count field to content_ideas table if it doesn't exist - igny8_add_tasks_count_to_content_ideas(); - - // Add image_prompts field to content_ideas table if it doesn't exist - igny8_add_image_prompts_to_content_ideas(); - - // Update idea_description field to LONGTEXT for structured JSON descriptions - igny8_update_idea_description_to_longtext(); - - // Migrate ideas and tasks table structure - igny8_migrate_ideas_tasks_structure(); - - // Run legacy cleanup if needed - igny8_cleanup_legacy_structures(); -} - -/** - * Add word_count field to tasks table if it doesn't exist - */ -function igny8_add_word_count_to_tasks() { - global $wpdb; - - // Check if word_count column exists - $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_tasks LIKE 'word_count'"); - - if (empty($column_exists)) { - // Add word_count column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks ADD COLUMN word_count INT UNSIGNED DEFAULT 0 AFTER keywords"); - error_log('Igny8: Added word_count column to tasks table'); - } -} - -/** - * Add raw_ai_response field to tasks table if it doesn't exist - */ -function igny8_add_raw_ai_response_to_tasks() { - global $wpdb; - - $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_tasks LIKE 'raw_ai_response'"); - - if (empty($column_exists)) { - // Add raw_ai_response column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks ADD COLUMN raw_ai_response LONGTEXT DEFAULT NULL AFTER word_count"); - error_log('Igny8: Added raw_ai_response column to tasks table'); - } -} - -/** - * Add tasks_count field to content_ideas table if it doesn't exist - */ -function igny8_add_tasks_count_to_content_ideas() { - global $wpdb; - - // Check if tasks_count column exists - $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_content_ideas LIKE 'tasks_count'"); - - if (empty($column_exists)) { - // Add tasks_count column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas ADD COLUMN tasks_count INT UNSIGNED DEFAULT 0 AFTER mapped_post_id"); - error_log('Igny8: Added tasks_count column to content_ideas table'); - } -} - -/** - * Add image_prompts field to content_ideas table if it doesn't exist - */ -function igny8_add_image_prompts_to_content_ideas() { - global $wpdb; - - // Check if image_prompts column exists - $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_content_ideas LIKE 'image_prompts'"); - - if (empty($column_exists)) { - // Add image_prompts column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas ADD COLUMN image_prompts TEXT DEFAULT NULL AFTER target_keywords"); - error_log('Igny8: Added image_prompts column to content_ideas table'); - } -} - -/** - * Update idea_description field to LONGTEXT to support structured JSON descriptions - */ -function igny8_update_idea_description_to_longtext() { - global $wpdb; - - // Check current column type - $column_info = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_content_ideas LIKE 'idea_description'"); - - if (!empty($column_info)) { - $column_type = $column_info[0]->Type; - - // Only update if it's not already LONGTEXT - if (strpos(strtoupper($column_type), 'LONGTEXT') === false) { - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas MODIFY COLUMN idea_description LONGTEXT DEFAULT NULL"); - error_log('Igny8: Updated idea_description column to LONGTEXT'); - } - } -} - -/** - * Migrate ideas and tasks table structure - */ -function igny8_migrate_ideas_tasks_structure() { - global $wpdb; - - // Migrate ideas table - igny8_migrate_ideas_table(); - - // Migrate tasks table - igny8_migrate_tasks_table(); -} - -/** - * Migrate ideas table structure - */ -function igny8_migrate_ideas_table() { - global $wpdb; - - // Check if idea_type column exists (old column) and remove it - $old_column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_content_ideas LIKE 'idea_type'"); - if (!empty($old_column_exists)) { - // Drop the old idea_type column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas DROP COLUMN idea_type"); - error_log('Igny8: Removed idea_type column from ideas table'); - } - - // Check if content_structure column exists - $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_content_ideas LIKE 'content_structure'"); - - if (empty($column_exists)) { - // Add content_structure column with new options - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas ADD COLUMN content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub' AFTER idea_description"); - error_log('Igny8: Added content_structure column to ideas table'); - } else { - // Update existing content_structure column with new options - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas MODIFY COLUMN content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub'"); - error_log('Igny8: Updated content_structure column options in ideas table'); - } - - // Check if content_type column exists - $content_type_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_content_ideas LIKE 'content_type'"); - - if (empty($content_type_exists)) { - // Add content_type column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas ADD COLUMN content_type ENUM('post','product','page','CPT') DEFAULT 'post' AFTER content_structure"); - error_log('Igny8: Added content_type column to ideas table'); - } - - // Update indexes - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas DROP INDEX IF EXISTS idx_idea_type"); - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas ADD INDEX idx_content_structure (content_structure)"); - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_content_ideas ADD INDEX idx_content_type (content_type)"); -} - -/** - * Migrate tasks table structure - */ -function igny8_migrate_tasks_table() { - global $wpdb; - - // Check if content_structure column exists - $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_tasks LIKE 'content_structure'"); - - if (empty($column_exists)) { - // Check if content_type column exists (old column) - $old_column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_tasks LIKE 'content_type'"); - - if (!empty($old_column_exists)) { - // Rename content_type to content_structure with new options - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks CHANGE COLUMN content_type content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub'"); - error_log('Igny8: Renamed content_type to content_structure in tasks table'); - } else { - // Add content_structure column with new options - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks ADD COLUMN content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub' AFTER due_date"); - error_log('Igny8: Added content_structure column to tasks table'); - } - } else { - // Update existing content_structure column with new options - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks MODIFY COLUMN content_structure ENUM('cluster_hub','landing_page','guide_tutorial','how_to','comparison','review','top_listicle','question','product_description','service_page','home_page') DEFAULT 'cluster_hub'"); - error_log('Igny8: Updated content_structure column options in tasks table'); - } - - // Check if content_type column exists (new column) - $content_type_exists = $wpdb->get_results("SHOW COLUMNS FROM {$wpdb->prefix}igny8_tasks LIKE 'content_type'"); - - if (empty($content_type_exists)) { - // Add content_type column - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks ADD COLUMN content_type ENUM('post','product','page','CPT') DEFAULT 'post' AFTER content_structure"); - error_log('Igny8: Added content_type column to tasks table'); - } - - // Update indexes - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks DROP INDEX IF EXISTS idx_content_type"); - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks ADD INDEX idx_content_structure (content_structure)"); - $wpdb->query("ALTER TABLE {$wpdb->prefix}igny8_tasks ADD INDEX idx_content_type (content_type)"); -} \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/core/global-layout.php b/igny8-ai-seo-wp-plugin/core/global-layout.php deleted file mode 100644 index 1ebd709c..00000000 --- a/igny8-ai-seo-wp-plugin/core/global-layout.php +++ /dev/null @@ -1,463 +0,0 @@ - -
          - - - - - -
          - - -
          - -
          -
          -
          - -
          -
          -
          - - -
          -
          -

          'Dashboard', - 'igny8-planner' => 'Planner', - 'igny8-writer' => 'Writer', - 'igny8-thinker' => 'Thinker', - 'igny8-analytics' => 'Analytics', - 'igny8-settings' => 'Settings', - 'igny8-schedules' => 'Schedules', - 'igny8-help' => 'Help' - ]; - echo $page_titles[$current_page] ?? 'IGNY8 AI SEO'; - ?>

          -
          -
          - - -
          - -
          - 0, 'label' => 'Active', 'color' => 'green'], - ['value' => 0, 'label' => 'Pending', 'color' => 'amber'], - ['value' => 0, 'label' => 'Completed', 'color' => 'purple'], - ['value' => 0, 'label' => 'Recent', 'color' => 'blue'] - - ]; - $fallback_metrics = array_slice($all_fallback_metrics, 0, $max_fallback); - - foreach ($fallback_metrics as $metric): - ?> -
          - - -
          - $metric_value): - $kpi_config = $GLOBALS['igny8_kpi_config'] ?? []; - $color = ''; - if (isset($kpi_config[$table_id][$metric_key]['color'])) { - $color = $kpi_config[$table_id][$metric_key]['color']; - } else { - $color = $color_map[$color_index] ?? ''; - } - - $label = $metric_key; - if (isset($kpi_config[$table_id][$metric_key]['label'])) { - $label = $kpi_config[$table_id][$metric_key]['label']; - } - ?> -
          - - -
          - -
          - - -
          - -
          -
          -
          - - -
          - No content provided.

          '; ?> - - - -
          - - -
          - -
          -
          -
          \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/debug/_README.php b/igny8-ai-seo-wp-plugin/debug/_README.php deleted file mode 100644 index af1bc34e..00000000 --- a/igny8-ai-seo-wp-plugin/debug/_README.php +++ /dev/null @@ -1,14 +0,0 @@ - -
          -
          -

          Debug Functionality Moved

          -

          - All debug monitoring functionality has been moved to the Settings > Status page. -

          - - Go to Status Page - -
          -
          - $current_module, - 'submodule' => $current_submodule, - 'table_id' => $table_id, - 'page' => $_GET['page'] ?? '' - ]; -} - - - - - - -/** - * Track AI Logs State - */ -function igny8_track_ai_logs_state($module_info) { - // Check if AI functions are available - if (!function_exists('igny8_get_ai_setting')) { - return [ - 'status' => 'error', - 'message' => 'AI system not available', - 'details' => 'igny8_get_ai_setting function missing' - ]; - } - - // Check AI mode - $ai_mode = igny8_get_ai_setting('planner_mode', 'manual'); - $ai_enabled = $ai_mode === 'ai'; - - // Get recent AI logs (last 10 events) - $ai_logs = get_option('igny8_ai_logs', []); - $recent_logs = array_slice($ai_logs, 0, 10); - - $log_count = count($recent_logs); - $error_count = 0; - $success_count = 0; - - foreach ($recent_logs as $log) { - if ($log['status'] === 'error') { - $error_count++; - } elseif ($log['status'] === 'success') { - $success_count++; - } - } - - $details = [ - 'AI Mode: ' . ($ai_enabled ? '✅ Enabled' : '❌ Disabled'), - 'Recent Events: ' . $log_count, - 'Success: ' . $success_count . ' | Errors: ' . $error_count, - 'Module: ' . $module_info['module'], - 'Submodule: ' . ($module_info['submodule'] ?: 'NONE') - ]; - - if ($ai_enabled && $log_count > 0) { - $status = $error_count === 0 ? 'success' : ($error_count < $success_count ? 'warning' : 'error'); - $message = "AI Logs: {$log_count} events ({$success_count} success, {$error_count} errors)"; - } elseif ($ai_enabled) { - $status = 'success'; - $message = 'AI Logs: Ready (no events yet)'; - } else { - $status = 'warning'; - $message = 'AI Logs: AI mode disabled'; - } - - return [ - 'status' => $status, - 'message' => $message, - 'details' => implode('
          ', $details) - ]; -} - -/** - * Track Database Validation State - */ -function igny8_track_database_validation_state() { - $module_info = igny8_get_current_module_info(); - $table_id = $module_info['table_id']; - - // Get actual database table name and fields - global $wpdb; - $table_name = igny8_get_table_name($table_id); - - if (!$table_name) { - return [ - 'status' => 'error', - 'message' => 'Table not found', - 'details' => 'Table ID: ' . $table_id . ' - No table name mapping found' - ]; - } - - // Get actual database fields - $db_fields = []; - $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") !== null; - - if ($table_exists) { - $columns = $wpdb->get_results("DESCRIBE {$table_name}"); - foreach ($columns as $column) { - $db_fields[] = $column->Field; - } - } else { - return [ - 'status' => 'error', - 'message' => 'Database table missing', - 'details' => 'Table: ' . $table_name . ' does not exist in database' - ]; - } - - // Validate table config fields - $table_errors = igny8_validate_table_config_fields($table_id, $db_fields); - $filter_errors = igny8_validate_filter_config_fields($table_id, $db_fields); - $form_errors = igny8_validate_form_config_fields($table_id, $db_fields); - - $total_errors = count($table_errors) + count($filter_errors) + count($form_errors); - - if ($total_errors === 0) { - return [ - 'status' => 'success', - 'message' => 'Database validation passed', - 'details' => 'Table: ✅
          Filters: ✅
          Forms: ✅
          Automation: ✅' - ]; - } else { - $error_details = []; - - // Format table errors - if (!empty($table_errors)) { - // Filter out config errors and show only field names - $field_errors = array_filter($table_errors, function($error) { - return !strpos($error, 'config') && !strpos($error, 'missing'); - }); - if (!empty($field_errors)) { - $error_details[] = 'Table:
          ' . implode(', ', $field_errors) . ' mismatch with db, doesn\'t exist'; - } - } - - // Format filter errors - if (!empty($filter_errors)) { - // Filter out config errors and show only field names - $field_errors = array_filter($filter_errors, function($error) { - return !strpos($error, 'config') && !strpos($error, 'missing'); - }); - if (!empty($field_errors)) { - $error_details[] = 'Filters:
          ' . implode(', ', $field_errors) . ' mismatch with db, doesn\'t exist'; - } - } - - // Format form errors - if (!empty($form_errors)) { - // Filter out config errors and show only field names - $field_errors = array_filter($form_errors, function($error) { - return !strpos($error, 'config') && !strpos($error, 'missing'); - }); - if (!empty($field_errors)) { - $error_details[] = 'Forms:
          ' . implode(', ', $field_errors) . ' mismatch with db, doesn\'t exist'; - } - } - - // If no field errors, show config errors - if (empty($error_details)) { - if (!empty($table_errors)) { - $error_details[] = 'Table:
          ' . implode(', ', $table_errors); - } - if (!empty($filter_errors)) { - $error_details[] = 'Filters:
          ' . implode(', ', $filter_errors); - } - if (!empty($form_errors)) { - $error_details[] = 'Forms:
          ' . implode(', ', $form_errors); - } - } - - return [ - 'status' => 'error', - 'message' => 'Database validation failed', - 'details' => implode('

          ', $error_details) - ]; - } -} - -/** - * Validate table config fields against database schema - */ -function igny8_validate_table_config_fields($table_id, $db_fields) { - $errors = []; - - try { - // Load config file directly - $config_path = plugin_dir_path(__FILE__) . '../modules/config/tables-config.php'; - if (!file_exists($config_path)) { - return ['tables_config_file_missing']; - } - - // Define constant to bypass access control - if (!defined('IGNY8_INCLUDE_CONFIG')) { - define('IGNY8_INCLUDE_CONFIG', true); - } - - $tables_config = require $config_path; - if (!isset($tables_config[$table_id])) { - return ['table_config_not_found']; - } - - $table_config = $tables_config[$table_id]; - - // Check columns - if (isset($table_config['columns'])) { - foreach ($table_config['columns'] as $column_name => $column_config) { - // For display columns, check the source_field instead of the column name - $field_to_check = $column_name; - if (isset($column_config['source_field'])) { - $field_to_check = $column_config['source_field']; - } - - if (!in_array($field_to_check, $db_fields)) { - $errors[] = $column_name; - } - } - } - - // Check humanize_columns - if (isset($table_config['humanize_columns'])) { - foreach ($table_config['humanize_columns'] as $column_name) { - if (!in_array($column_name, $db_fields)) { - $errors[] = $column_name; - } - } - } - - } catch (Exception $e) { - $errors[] = 'config_load_error: ' . $e->getMessage(); - } - - return $errors; -} - -/** - * Validate filter config fields against database schema - */ -function igny8_validate_filter_config_fields($table_id, $db_fields) { - $errors = []; - - try { - // Load config file directly - $config_path = plugin_dir_path(__FILE__) . '../modules/config/filters-config.php'; - if (!file_exists($config_path)) { - return ['filters_config_file_missing']; - } - - // Define constant to bypass access control - if (!defined('IGNY8_INCLUDE_CONFIG')) { - define('IGNY8_INCLUDE_CONFIG', true); - } - - $filters_config = require $config_path; - if (!isset($filters_config[$table_id])) { - return []; // No filters config is OK - } - - $filter_config = $filters_config[$table_id]; - - foreach ($filter_config as $filter_name => $filter_data) { - if (isset($filter_data['field']) && !in_array($filter_data['field'], $db_fields)) { - $errors[] = $filter_data['field']; - } - } - - } catch (Exception $e) { - $errors[] = 'config_load_error: ' . $e->getMessage(); - } - - return $errors; -} - -/** - * Validate form config fields against database schema - */ -function igny8_validate_form_config_fields($table_id, $db_fields) { - $errors = []; - - try { - // Load config file directly - $config_path = plugin_dir_path(__FILE__) . '../modules/config/forms-config.php'; - if (!file_exists($config_path)) { - return ['forms_config_file_missing']; - } - - // Define constant to bypass access control - if (!defined('IGNY8_INCLUDE_CONFIG')) { - define('IGNY8_INCLUDE_CONFIG', true); - } - - $forms_config = require $config_path; - if (!isset($forms_config[$table_id])) { - return []; // No form config is OK - } - - $form_config = $forms_config[$table_id]; - if (!$form_config || !isset($form_config['fields'])) { - return []; // No form config is OK - } - - foreach ($form_config['fields'] as $field) { - if (isset($field['name']) && !in_array($field['name'], $db_fields)) { - $errors[] = $field['name']; - } - } - - } catch (Exception $e) { - $errors[] = 'config_load_error: ' . $e->getMessage(); - } - - return $errors; -} - -function igny8_test_column_type_validation($config_data, $table_id) { - if (!isset($config_data[$table_id]['columns'])) { - return ['passed' => false, 'error' => 'no columns']; - } - - $columns = $config_data[$table_id]['columns']; - foreach ($columns as $key => $column) { - if (!isset($column['type']) || !in_array($column['type'], ['text', 'number', 'date', 'select', 'textarea', 'enum'])) { - return ['passed' => false, 'error' => 'invalid type: ' . $key]; - } - } - return ['passed' => true, 'error' => '']; -} - -function igny8_test_filter_field_validation($config_data, $table_id) { - if (!isset($config_data[$table_id])) { - return ['passed' => false, 'error' => 'no filters']; - } - - $filters = $config_data[$table_id]; - foreach ($filters as $key => $filter) { - if (!isset($filter['field']) || empty($filter['field'])) { - return ['passed' => false, 'error' => 'missing field: ' . $key]; - } - } - return ['passed' => true, 'error' => '']; -} - -function igny8_test_field_validation_check($config_data, $table_id) { - if (!function_exists('igny8_get_form_config')) { - return ['passed' => false, 'error' => 'function missing']; - } - - $form_config = igny8_get_form_config($table_id); - if (!$form_config || !isset($form_config['fields'])) { - return ['passed' => false, 'error' => 'no form fields']; - } - - foreach ($form_config['fields'] as $field) { - if (!isset($field['name']) || !isset($field['type'])) { - return ['passed' => false, 'error' => 'invalid field structure']; - } - - // Test foreign key relationships - if (isset($field['type']) && $field['type'] === 'select' && (isset($field['options']) || isset($field['source']))) { - $fk_result = igny8_test_foreign_key_relationship($field, $table_id); - if (!$fk_result['passed']) { - return ['passed' => false, 'error' => 'FK issue: ' . $field['name'] . ' - ' . $fk_result['error']]; - } - } - } - return ['passed' => true, 'error' => '']; -} - -function igny8_test_foreign_key_relationship($field, $table_id) { - // Check if this is a foreign key field (like cluster_id, category_id, etc.) - $field_name = $field['name']; - - // Common foreign key patterns - $fk_patterns = ['cluster_id', 'category_id', 'parent_id', 'user_id', 'group_id']; - $is_foreign_key = false; - - foreach ($fk_patterns as $pattern) { - if (strpos($field_name, $pattern) !== false) { - $is_foreign_key = true; - break; - } - } - - if (!$is_foreign_key) { - return ['passed' => true, 'error' => '']; // Not a foreign key, skip test - } - - // CRITICAL ISSUE: Check if field uses 'source' with custom select rendering - if (isset($field['source']) && $field['type'] === 'select') { - // This is the cluster_id issue - custom select button vs form submission mismatch - return ['passed' => false, 'error' => 'custom select rendering - form submission mismatch']; - } - - return ['passed' => true, 'error' => '']; -} - -function igny8_test_query_syntax_validation($config_data, $table_id) { - if (!isset($config_data[$table_id])) { - return ['passed' => false, 'error' => 'no kpi config']; - } - - $kpis = $config_data[$table_id]; - foreach ($kpis as $key => $kpi) { - if (!isset($kpi['query']) || empty($kpi['query'])) { - return ['passed' => false, 'error' => 'missing query: ' . $key]; - } - - // Check for basic SQL syntax - if (!preg_match('/SELECT.*FROM.*\{table_name\}/i', $kpi['query'])) { - return ['passed' => false, 'error' => 'invalid query syntax: ' . $key]; - } - } - return ['passed' => true, 'error' => '']; -} - -/** - * Track Routing State - */ -function igny8_track_routing_state() { - $module_info = igny8_get_current_module_info(); - $current_page = $_GET['page'] ?? ''; - $current_submodule = $_GET['sm'] ?? ''; - - $routing_ok = !empty($current_page) && strpos($current_page, 'igny8-') === 0; - $submodule_ok = !empty($current_submodule); - - return [ - 'status' => $routing_ok ? 'success' : 'error', - 'message' => "Routing: {$current_page}" . ($submodule_ok ? " → {$current_submodule}" : ""), - 'details' => "Table ID: {$module_info['table_id']}" - ]; -} - -/** - * Track Initial Render State - */ -function igny8_track_initial_render_state() { - $module_info = igny8_get_current_module_info(); - $table_id = $module_info['table_id']; - - $render_functions = [ - 'igny8_render_filters' => 'Filters', - 'igny8_render_table_actions' => 'Actions', - 'igny8_render_pagination' => 'Pagination' - ]; - - $working_functions = 0; - $total_functions = count($render_functions); - $details = []; - - foreach ($render_functions as $function => $name) { - if (function_exists($function)) { - $working_functions++; - $details[] = "{$name}: ✅"; - } else { - $details[] = "{$name}: ❌"; - } - } - - // Test form rendering system - $form_tests = igny8_test_form_rendering_system($table_id); - $details[] = "Form Rendering: " . ($form_tests['passed'] ? '✅' : '❌ ' . $form_tests['error']); - if ($form_tests['passed']) { - $working_functions++; - } - $total_functions++; - - return [ - 'status' => $working_functions === $total_functions ? 'success' : 'warning', - 'message' => "Render functions: {$working_functions}/{$total_functions}", - 'details' => implode('
          ', $details) - ]; -} - -/** - * Test form rendering system - */ -function igny8_test_form_rendering_system($table_id) { - // Test 1: Form config function exists - if (!function_exists('igny8_get_form_config')) { - return ['passed' => false, 'error' => 'igny8_get_form_config missing']; - } - - // Test 2: Form rendering function exists - if (!function_exists('igny8_render_inline_form_row')) { - return ['passed' => false, 'error' => 'igny8_render_inline_form_row missing']; - } - - // Test 3: Form field rendering functions exist - $field_functions = [ - 'igny8_render_form_field', - 'igny8_render_text_field', - 'igny8_render_number_field', - 'igny8_render_select_field', - 'igny8_render_textarea_field' - ]; - - foreach ($field_functions as $func) { - if (!function_exists($func)) { - return ['passed' => false, 'error' => "{$func} missing"]; - } - } - - // Test 4: Form config loads for current table - try { - $form_config = igny8_get_form_config($table_id); - if (!$form_config) { - return ['passed' => false, 'error' => 'No form config for table']; - } - - if (empty($form_config['fields'])) { - return ['passed' => false, 'error' => 'Empty form fields']; - } - - // Test 5: Form can render without errors - $test_output = igny8_render_inline_form_row($table_id, 'add', []); - if (empty($test_output) || strpos($test_output, 'Form not configured') !== false) { - return ['passed' => false, 'error' => 'Form render failed']; - } - - } catch (Exception $e) { - return ['passed' => false, 'error' => 'Form config error: ' . $e->getMessage()]; - } - - return ['passed' => true, 'error' => '']; -} - -/** - * Track Table Rendering State - */ -function igny8_track_table_rendering_state() { - $module_info = igny8_get_current_module_info(); - $table_id = $module_info['table_id']; - - // Test 1: Table render function exists - if (!function_exists('igny8_render_table')) { - return [ - 'status' => 'error', - 'message' => 'Table render function missing', - 'details' => 'igny8_render_table function not found' - ]; - } - - // Test 2: AJAX table loading functions exist - $ajax_functions = [ - 'igny8_get_table_data' => 'Main table data loader', - 'igny8_ajax_load_table_data' => 'Submodule table loader' - ]; - - $working_ajax = 0; - $total_ajax = count($ajax_functions); - $ajax_details = []; - - foreach ($ajax_functions as $func => $name) { - if (function_exists($func)) { - $working_ajax++; - $ajax_details[] = "{$name}: ✅"; - } else { - $ajax_details[] = "{$name}: ❌"; - } - } - - // Test 3: Check if table has actually loaded data - $table_data_status = igny8_check_table_data_loaded($table_id); - - // Test 4: AJAX states tracking (only meaningful after AJAX has run) - $ajax_states = igny8_track_ajax_table_states(); - - $total_tests = 4; - $passed_tests = ($working_ajax === $total_ajax ? 1 : 0) + - ($table_data_status['passed'] ? 1 : 0) + - ($ajax_states['passed'] ? 1 : 0) + 1; // +1 for function existence - - $details = [ - 'Function exists: ✅', - 'AJAX functions: ' . $working_ajax . '/' . $total_ajax, - implode('
          ', $ajax_details), - 'Table data loaded: ' . ($table_data_status['passed'] ? '✅' : '⏳ ' . $table_data_status['message']), - 'AJAX states: ' . ($ajax_states['passed'] ? '✅' : '⏳ ' . $ajax_states['error']), - $ajax_states['details'] - ]; - - return [ - 'status' => $passed_tests === $total_tests ? 'success' : ($passed_tests > 2 ? 'warning' : 'error'), - 'message' => "Table rendering: {$passed_tests}/{$total_tests} tests passed", - 'details' => implode('
          ', $details) - ]; -} - -/** - * Check if table has actually loaded data - */ -function igny8_check_table_data_loaded($table_id) { - // Check if there's recent AJAX response data - if (isset($GLOBALS['igny8_last_ajax_response'])) { - $response = $GLOBALS['igny8_last_ajax_response']; - $time_diff = time() - $response['timestamp']; - - // If response is recent (within 30 seconds) and has data - if ($time_diff < 30 && !empty($response['data']['items'])) { - return [ - 'passed' => true, - 'message' => 'Data loaded (' . count($response['data']['items']) . ' rows)' - ]; - } - } - - // Check if AJAX states indicate successful loading - if (isset($GLOBALS['igny8_debug_states']['TABLE_AJAX_RESPONSE_OK'])) { - $state = $GLOBALS['igny8_debug_states']['TABLE_AJAX_RESPONSE_OK']; - if ($state['status'] === true) { - return [ - 'passed' => true, - 'message' => 'AJAX response successful' - ]; - } - } - - // If no data loaded yet - return [ - 'passed' => false, - 'message' => 'Waiting for AJAX data load' - ]; -} - -/** - * Debug state helper function - stores debug states in globals - */ -function igny8_debug_state($stage, $ok, $msg) { - if (!isset($GLOBALS['igny8_debug_states'])) { - $GLOBALS['igny8_debug_states'] = []; - } - - $GLOBALS['igny8_debug_states'][$stage] = [ - 'status' => $ok, - 'message' => $msg, - 'timestamp' => time() - ]; -} - -/** - * Track AJAX table states - */ -function igny8_track_ajax_table_states() { - // The 4 AJAX states that indicate successful table loading - $ajax_states = [ - 'AJAX_NONCE_VALIDATED' => false, - 'USER_CAPABILITY_OK' => false, - 'TABLE_AJAX_RESPONSE_OK' => false, - 'TABLE_AJAX_REQUEST_SENT' => false - ]; - - $details = []; - $passed_states = 0; - - // Check each AJAX state - foreach ($ajax_states as $state_name => $state_value) { - // Check if this state has been set by AJAX functions - if (isset($GLOBALS['igny8_debug_states'][$state_name])) { - $state_data = $GLOBALS['igny8_debug_states'][$state_name]; - if ($state_data['status'] === true) { - $passed_states++; - $details[] = "{$state_name}: ✅"; - } else { - $details[] = "{$state_name}: ❌ " . $state_data['message']; - } - } else { - $details[] = "{$state_name}: ⏳ Not triggered yet"; - } - } - - // Check if AJAX tracking system is available - if (!function_exists('igny8_debug_state')) { - return [ - 'passed' => false, - 'error' => 'AJAX tracking system not available', - 'details' => implode('
          ', $details) - ]; - } - - // If no states have been triggered yet (page just loaded) - if ($passed_states === 0 && !isset($GLOBALS['igny8_debug_states'])) { - return [ - 'passed' => true, - 'error' => 'AJAX tracking ready (no requests yet)', - 'details' => implode('
          ', $details) - ]; - } - - return [ - 'passed' => $passed_states === 4, - 'error' => $passed_states === 4 ? '' : "Only {$passed_states}/4 states passed", - 'details' => implode('
          ', $details) - ]; -} - -/** - * Track Filters State - */ -function igny8_track_filters_state() { - $module_info = igny8_get_current_module_info(); - $table_id = $module_info['table_id']; - - // Test 1: Filter render function exists - if (!function_exists('igny8_render_filters')) { - return [ - 'status' => 'error', - 'message' => 'Filter render function missing', - 'details' => 'igny8_render_filters function not found' - ]; - } - - // Test 2: Filter config loads for current table (direct method) - try { - // Load filters config directly like igny8_render_filters() does - $filters_config_path = plugin_dir_path(__FILE__) . '../modules/config/filters-config.php'; - if (!file_exists($filters_config_path)) { - return [ - 'status' => 'error', - 'message' => 'Filter config file missing', - 'details' => 'filters-config.php not found at: ' . $filters_config_path - ]; - } - - $filters_config = require $filters_config_path; - $filter_config = $filters_config[$table_id] ?? []; - - if (empty($filter_config)) { - return [ - 'status' => 'warning', - 'message' => 'No filter config for table', - 'details' => 'Filter config not found for: ' . $table_id - ]; - } - - $filter_count = count($filter_config); - - } catch (Exception $e) { - return [ - 'status' => 'error', - 'message' => 'Filter config error', - 'details' => 'Error loading filter config: ' . $e->getMessage() - ]; - } - - // Test 4: Filter can render without errors - try { - $test_output = igny8_render_filters($table_id); - if (empty($test_output) || strpos($test_output, 'Filter not configured') !== false) { - $render_test = '❌ Filter render failed'; - } else { - $render_test = '✅ Filter renders OK'; - } - } catch (Exception $e) { - $render_test = '❌ Render error: ' . $e->getMessage(); - } - - $details = [ - 'Render function: ✅', - 'Config file: ✅', - 'Filter config: ✅ (' . $filter_count . ' filters)', - 'Render test: ' . $render_test - ]; - - return [ - 'status' => 'success', - 'message' => "Filters: {$filter_count} filters configured", - 'details' => implode('
          ', $details) - ]; -} - -/** - * Track Forms State - */ -function igny8_track_forms_state() { - $module_info = igny8_get_current_module_info(); - $table_id = $module_info['table_id']; - - // Test 1: Form render function exists - if (!function_exists('igny8_render_inline_form_row')) { - return [ - 'status' => 'error', - 'message' => 'Form render function missing', - 'details' => 'igny8_render_inline_form_row function not found' - ]; - } - - // Test 2: Form config function exists - if (!function_exists('igny8_get_form_config')) { - return [ - 'status' => 'error', - 'message' => 'Form config function missing', - 'details' => 'igny8_get_form_config function not found' - ]; - } - - // Test 3: Form config loads for current table - try { - $form_config = igny8_get_form_config($table_id); - if (!$form_config) { - return [ - 'status' => 'warning', - 'message' => 'No form config for table', - 'details' => 'Form config not found for: ' . $table_id - ]; - } - - if (empty($form_config['fields'])) { - return [ - 'status' => 'warning', - 'message' => 'Empty form fields', - 'details' => 'Form config exists but has no fields' - ]; - } - - $field_count = count($form_config['fields']); - - } catch (Exception $e) { - return [ - 'status' => 'error', - 'message' => 'Form config error', - 'details' => 'Error loading form config: ' . $e->getMessage() - ]; - } - - // Test 4: Form can render without errors - try { - $test_output = igny8_render_inline_form_row($table_id, 'add', []); - if (empty($test_output) || strpos($test_output, 'Form not configured') !== false) { - $render_test = '❌ Form render failed'; - } else { - $render_test = '✅ Form renders OK'; - } - } catch (Exception $e) { - $render_test = '❌ Render error: ' . $e->getMessage(); - } - - $details = [ - 'Function exists: ✅', - 'Config function: ✅', - 'Form config: ✅ (' . $field_count . ' fields)', - 'Render test: ' . $render_test - ]; - - return [ - 'status' => 'success', - 'message' => "Forms: {$field_count} fields configured", - 'details' => implode('
          ', $details) - ]; -} - -/** - * Track Automation Validation State - Submodule Specific - */ -function igny8_track_automation_validation_state() { - $module_info = igny8_get_current_module_info(); - $current_submodule = $module_info['submodule']; - - $automation_errors = []; - $automation_functions = []; - $working_functions = 0; - - try { - // Define submodule-specific automation functions - switch ($current_submodule) { - case 'keywords': - $automation_functions = [ - 'igny8_update_cluster_metrics' => 'Cluster metrics update', - 'igny8_handle_keyword_cluster_update' => 'Keyword cluster updates', - 'igny8_bulk_delete_keywords' => 'Bulk keyword deletion', - 'igny8_bulk_map_keywords' => 'Bulk keyword mapping', - 'igny8_bulk_unmap_keywords' => 'Bulk keyword unmapping', - 'igny8_ajax_ai_cluster_keywords' => 'AI cluster creation', - 'igny8_workflow_triggers' => 'Workflow triggers', - 'igny8_ajax_import_keywords' => 'Keyword import automation' - ]; - break; - - case 'clusters': - $automation_functions = [ - 'igny8_update_cluster_metrics' => 'Cluster metrics update', - 'igny8_auto_create_cluster_term' => 'Cluster taxonomy creation', - 'igny8_auto_update_cluster_term' => 'Cluster taxonomy updates', - 'igny8_handle_content_cluster_association' => 'Content cluster associations', - 'igny8_ajax_ai_generate_ideas' => 'AI idea generation', - 'igny8_workflow_triggers' => 'Workflow triggers' - ]; - break; - - case 'ideas': - $automation_functions = [ - 'igny8_update_idea_metrics' => 'Idea metrics update', - 'igny8_create_task_from_idea' => 'Task creation from ideas', - 'igny8_workflow_triggers' => 'Workflow triggers', - 'igny8_write_log' => 'Automation logging', - 'igny8_ajax_ai_generate_ideas' => 'AI idea generation', - 'igny8_ajax_ai_generate_content' => 'AI content generation' - ]; - break; - - default: - // Fallback to basic automation functions - $automation_functions = [ - 'igny8_update_cluster_metrics' => 'Cluster metrics update', - 'igny8_handle_keyword_cluster_update' => 'Keyword cluster updates', - 'igny8_workflow_triggers' => 'Workflow triggers' - ]; - } - - // Check if each automation function exists - $function_details = []; - foreach ($automation_functions as $function => $description) { - if (function_exists($function)) { - $working_functions++; - $function_details[] = "{$description}: ✅"; - } else { - // Check if this is a known missing function - if (strpos($description, 'MISSING') !== false) { - $automation_errors[] = "{$description}: ❌ (Function never implemented - referenced in docs but missing from code)"; - $function_details[] = "{$description}: ❌ (NOT IMPLEMENTED)"; - } else { - $automation_errors[] = "{$description}: ❌ ({$function} missing)"; - $function_details[] = "{$description}: ❌"; - } - } - } - - $total_functions = count($automation_functions); - - } catch (Exception $e) { - $automation_errors[] = 'Automation check error: ' . $e->getMessage(); - } - - if (empty($automation_errors)) { - return [ - 'status' => 'success', - 'message' => "Automation: {$working_functions}/{$total_functions} functions active", - 'details' => implode('
          ', $function_details) - ]; - } else { - return [ - 'status' => $working_functions > ($total_functions / 2) ? 'warning' : 'error', - 'message' => "Automation: {$working_functions}/{$total_functions} functions active", - 'details' => implode('
          ', $function_details) . '

          Issues:
          ' . implode('
          ', $automation_errors) - ]; - } -} - -/** - * Track Database Pre-Fetch State - */ -function igny8_track_db_prefetch_state() { - global $wpdb; - $module_info = igny8_get_current_module_info(); - - // Get table name - if (function_exists('igny8_get_table_name')) { - $table_name = igny8_get_table_name($module_info['table_id']); - } else { - $table_name = $wpdb->prefix . 'igny8_' . str_replace('_', '', $module_info['table_id']); - } - - // Check if table exists - $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") !== null; - - if ($table_exists) { - $row_count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}"); - return [ - 'status' => 'success', - 'message' => "Table: {$table_name} ({$row_count} rows)", - 'details' => "Database connection: ✅
          Table exists: ✅" - ]; - } else { - return [ - 'status' => 'error', - 'message' => "Table not found: {$table_name}", - 'details' => "Database connection: " . (!empty($wpdb->dbh) ? '✅' : '❌') . "
          Table exists: ❌" - ]; - } -} - -/** - * Track Frontend Initialization State - */ -function igny8_track_frontend_init_state() { - // Check if core.js is enqueued - $core_js_enqueued = wp_script_is('igny8-admin-js', 'enqueued'); - - // Check if required DOM elements exist (this will be checked by JavaScript) - return [ - 'status' => $core_js_enqueued ? 'success' : 'warning', - 'message' => "Frontend JS: " . ($core_js_enqueued ? 'Enqueued' : 'Not enqueued'), - 'details' => "Core.js loaded: " . ($core_js_enqueued ? '✅' : '❌') . "
          DOM elements: Checked by JS" - ]; -} - - - -// Get module debug content using consolidated evidence system -function igny8_get_module_debug_content() { - // Get current module information - $module_info = igny8_get_current_module_info(); - $module_name = ucfirst($module_info['module']); - $current_submodule = $module_info['submodule']; - - // Track debug states - split into component states and automation states - $component_states = [ - 'database' => igny8_track_database_validation_state(), - 'table' => igny8_track_initial_render_state(), - 'filters' => igny8_track_filters_state(), - 'forms' => igny8_track_forms_state() - ]; - - $automation_states = [ - 'automation' => igny8_track_automation_validation_state(), - 'ai_logs' => igny8_track_ai_logs_state($module_info) - ]; - - ob_start(); - ?> - -
          -
          -
          - 🔍 - Module Debug: -
          - -
          - -
          - -
          -
          -

          Module Information

          - - -
          - Module:
          - Submodule:
          - Table ID:
          - Page: -
          -
          -
          - - -
          -

          Submodule Components Render

          -
          - $state_data): ?> -
          -
          - - - - -
          -
          - -
          -
          - -
          -
          - -
          -
          - - -
          -

          Automation & AI Systems

          -
          - $state_data): ?> - -
          -
          - - - - -
          -
          - -
          -
          - -
          -
          - -
          -
          - -
          -
          - - -
          -
          -
          - 🤖 - AI Logs & Events -
          -
          - - -
          -
          -
          -
          -
          - Status: Loading... -
          -
          - Initializing... -
          -
          - - -
          -
          - -

          Loading AI events... -
          -
          -
          -
          - - - -
          -
          -
          - 🎨 - Image Generation Logs -
          -
          - - -
          -
          -
          -
          -
          - Status: Ready for image generation -
          -
          - Waiting for image generation process -
          -
          - - -
          -
          - -

          No image generation events yet -
          -
          -
          -
          - - - - - Generate Images - -``` - -### JavaScript Event Handler -- **File**: `assets/js/image-queue-processor.js` -- **Function**: `processAIImageGenerationDrafts(postIds)` -- **Lines**: 7-84 -- **Validation**: Max 10 posts, requires selection - -## 2. JavaScript Processing Flow - -### Main Processing Function -- **File**: `assets/js/image-queue-processor.js` -- **Function**: `processAIImageGenerationDrafts(postIds)` -- **Lines**: 7-84 - -### Settings Retrieved -```javascript -const desktopEnabled = window.IGNY8_PAGE?.imageSettings?.desktop_enabled || false; -const mobileEnabled = window.IGNY8_PAGE?.imageSettings?.mobile_enabled || false; -const maxInArticleImages = window.IGNY8_PAGE?.imageSettings?.max_in_article_images || 1; -``` - -### Image Calculation -- **Featured Images**: Always 1 per post -- **Desktop Images**: `maxInArticleImages` per post (if enabled) -- **Mobile Images**: `maxInArticleImages` per post (if enabled) -- **Total**: `postIds.length * imagesPerPost` - -### Queue Processing -- **Sequential Processing**: One image at a time to avoid API limits -- **Progress Tracking**: Real-time progress updates with individual progress bars -- **Error Handling**: Continue processing on individual failures -- **Modal Display**: Shows progress for each image being generated - -## 3. AJAX Handlers - -### Primary Handler -- **File**: `core/admin/ajax.php` -- **Function**: `igny8_ajax_ai_generate_images_drafts()` -- **Lines**: 2913-3150 -- **Hook**: `wp_ajax_igny8_ai_generate_images_drafts` - -### Single Image Handler -- **File**: `core/admin/ajax.php` -- **Function**: `igny8_ajax_ai_generate_single_image()` -- **Lines**: 3283-3350 -- **Hook**: `wp_ajax_igny8_ai_generate_single_image` -- **Process**: - 1. Validates task ID and retrieves WordPress post ID - 2. Calls appropriate generation function based on type - 3. Saves image metadata to post meta - 4. Returns success/error response - -### Supporting Handlers -- **Image Counts**: `igny8_ajax_get_image_counts()` (Lines 2645-2809) -- **Single Image Queue**: `igny8_ajax_generate_single_image_queue()` (Lines 2814-2908) -- **Settings Save**: `igny8_ajax_save_image_settings()` (Lines 4408-4457) -- **Template Save**: `igny8_ajax_save_image_prompt_template()` (Lines 4461-4505) - -## 4. Settings and Configuration - -### WordPress Options -| **Option** | **Default** | **Purpose** | -|------------|-------------|-------------| -| `igny8_desktop_enabled` | `'1'` | Enable desktop image generation | -| `igny8_mobile_enabled` | `'0'` | Enable mobile image generation | -| `igny8_max_in_article_images` | `1` | Maximum in-article images per post | -| `igny8_image_type` | `'realistic'` | Image style type | -| `igny8_image_provider` | `'runware'` | AI provider for image generation | -| `igny8_image_format` | `'jpg'` | Output image format | -| `igny8_negative_prompt` | `'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title'` | Negative prompt for image generation | -| `igny8_image_prompt_template` | `'Create a high-quality {image_type} image to use as a featured photo for a blog post titled "{post_title}". The image should visually represent the theme, mood, and subject implied by the image prompt: {image_prompt}. Focus on a realistic, well-composed scene that naturally communicates the topic without text or logos. Use balanced lighting, pleasing composition, and photographic detail suitable for lifestyle or editorial web content. Avoid adding any visible or readable text, brand names, or illustrative effects. **And make sure image is not blurry.**'` | Image prompt template | - -### Settings Page -- **File**: `modules/settings/general-settings.php` -- **Lines**: 309-462 -- **Form Fields**: Desktop/Mobile toggles, Max images dropdown, Image type, Provider, Format, Negative prompt, Template - -## 5. Image Generation Functions - -### Core Generation Functions -- **File**: `ai/writer/images/image-generation.php` - -#### Featured Image Generation -- **Function**: `igny8_generate_featured_image_for_post($post_id, $image_size_type = 'featured')` -- **Lines**: 24-200 -- **Process**: - 1. Get featured image prompt from `_igny8_featured_image_prompt` - 2. Get image generation settings from WordPress options - 3. Build final prompt using template with placeholders - 4. Call AI provider (Runware/OpenAI) with appropriate dimensions - 5. Download image from AI provider URL - 6. Upload via `media_handle_sideload()` - 7. Set as featured image with `set_post_thumbnail()` - 8. Generate attachment metadata - -#### In-Article Image Generation -- **Function**: `igny8_generate_single_article_image($post_id, $device_type = 'desktop', $index = 1)` -- **Lines**: 202-400 -- **Process**: - 1. Get article image prompts from `_igny8_article_images_data` - 2. Extract prompt for specific index (`prompt-img-1`, `prompt-img-2`, etc.) - 3. Get image generation settings from WordPress options - 4. Build final prompt using template with placeholders - 5. Call AI provider with device-specific dimensions - 6. Download image from AI provider URL - 7. Upload via `media_handle_sideload()` - 8. Return attachment ID and metadata - -### Supporting Functions -- **Image Dimensions**: `igny8_get_image_dimensions($size_preset, $provider)` (Lines 1526-1558 in ai/modules-ai.php) -- **Safe Quantity**: `igny8_calculate_safe_image_quantity($idea_data, $max_in_article_images)` (Lines 918-933 in ai/modules-ai.php) -- **Image Meta**: `igny8_add_inarticle_image_meta($post_id, $attachment_id, $label, $device, $section)` (Lines 933-963 in ai/modules-ai.php) - -## 6. AI Provider Integration - -### Runware API Integration -- **File**: `ai/runware-api.php` -- **Function**: `igny8_runway_generate_image($prompt, $negative_prompt, $width, $height, $steps, $cfg_scale, $number_results, $output_format)` -- **API Endpoint**: `https://api.runware.ai/v1` -- **Authentication**: API key via `igny8_runware_api_key` option -- **Model**: `runware:97@1` (HiDream-I1 Full) -- **Request Format**: - ```json - [ - { - "taskType": "authentication", - "apiKey": "api_key_here" - }, - { - "taskType": "imageInference", - "taskUUID": "uuid_here", - "positivePrompt": "prompt_text", - "negativePrompt": "negative_prompt", - "model": "runware:97@1", - "width": 1024, - "height": 1024, - "steps": 30, - "CFGScale": 7.5, - "numberResults": 1, - "outputFormat": "jpg" - } - ] - ``` -- **Response Handling**: Extract `data[0].imageURL` from response -- **Error Handling**: Log API errors, return error messages - -### OpenAI DALL-E Integration -- **File**: `ai/openai-api.php` -- **Function**: `igny8_call_openai_images($prompt, $api_key, $model, $size, $quality, $style)` -- **API Endpoint**: `https://api.openai.com/v1/images/generations` -- **Authentication**: API key via `igny8_api_key` option -- **Model**: `dall-e-3` -- **Request Format**: - ```json - { - "model": "dall-e-3", - "prompt": "prompt_text", - "n": 1, - "size": "1024x1024", - "quality": "standard", - "style": "natural" - } - ``` -- **Response Handling**: Extract `data[0].url` from response -- **Error Handling**: Log API errors, return error messages - -### Provider Detection and Selection -- **Settings**: `igny8_image_provider` option (`runware` or `openai`) -- **API Key Validation**: Check `igny8_runware_api_key` or `igny8_api_key` -- **Dynamic Selection**: Based on user settings in prompts page -- **Fallback Handling**: Return error if required API key not configured - -### Image Dimensions by Provider -- **Runware Dimensions**: - - Featured: 1280x832 - - Desktop: 1024x1024 - - Mobile: 960x1280 -- **OpenAI Dimensions**: - - Featured: 1024x1024 - - Desktop: 1024x1024 - - Mobile: 1024x1024 - -## 7. Data Storage and Meta Keys - -### Post Meta Keys -| **Meta Key** | **Purpose** | **Data Type** | -|--------------|-------------|---------------| -| `_igny8_featured_image_prompt` | Featured image AI prompt | Text | -| `_igny8_article_images_data` | In-article image prompts | JSON Array | -| `_igny8_inarticle_images` | Generated image metadata | JSON Object | - -### Image Prompts Data Structure -```json -[ - { - "prompt-img-1": "A close-up of a neatly made bed showcasing a well-fitted duvet cover that enhances the aesthetic of the room." - }, - { - "prompt-img-2": "An image of tools laid out for measuring a duvet insert, including a measuring tape, notepad, and a flat surface." - }, - { - "prompt-img-3": "A detailed size chart displaying different duvet cover sizes alongside their corresponding duvet insert dimensions." - }, - { - "prompt-img-4": "An infographic illustrating common mistakes when choosing duvet covers, highlighting shrinkage risks and misreading labels." - } -] -``` - -### Image Metadata Structure -```json -{ - "desktop-1": { - "label": "desktop-1", - "attachment_id": 1825, - "url": "https://example.com/image.jpg", - "device": "desktop", - "section": 1 - }, - "mobile-1": { - "label": "mobile-1", - "attachment_id": 1826, - "url": "https://example.com/image2.jpg", - "device": "mobile", - "section": 1 - } -} -``` - -## 8. Image Processing Workflow - -### Step-by-Step Process -1. **Button Click**: User selects posts and clicks "Generate Images" -2. **Validation**: Check selection count (max 10), get settings -3. **Queue Building**: Build image queue with featured and in-article images -4. **Progress Modal**: Show progress with individual progress bars -5. **Sequential Processing**: Process each image individually -6. **Image Generation**: For each image: - - Get prompt from appropriate meta field - - Call AI provider with settings - - Download image from provider URL - - Upload to WordPress Media Library - - Save metadata to post meta -7. **Progress Updates**: Update UI with success/failure status -8. **Completion**: Show final results - -### Error Handling -- **Download Failures**: Log errors, continue with next image -- **Upload Failures**: Log errors, return failure status -- **API Failures**: Log errors, return failure status -- **Progress Tracking**: Update modal with success/failure counts -- **JSON Parsing**: Handle HTML content in prompts with `wp_strip_all_tags()` - -## 9. Content Generation Integration - -### Image Prompts in Content Generation -- **File**: `ai/modules-ai.php` -- **Function**: `igny8_create_post_from_ai_response($ai_response)` -- **Lines**: 1119-1462 -- **Process**: - 1. AI generates content with image prompts - 2. Featured image prompt saved to `_igny8_featured_image_prompt` - 3. In-article image prompts saved to `_igny8_article_images_data` - 4. HTML tags stripped from prompts using `wp_strip_all_tags()` - 5. JSON structure validated and saved - -### Prompt Template Processing -- **Template**: Uses `{image_type}`, `{post_title}`, `{image_prompt}` placeholders -- **Replacement**: Dynamic replacement with actual values -- **Settings Integration**: Uses all settings from prompts page - -## 10. Image Generation Queue Mechanism - -### Queue Processing -- **File**: `assets/js/image-queue-processor.js` -- **Function**: `processAIImageGenerationDrafts(postIds)` -- **Lines**: 7-84 -- **Process**: Sequential image generation with progress tracking - -### Queue Features -- **Sequential Processing**: One image at a time to avoid API limits -- **Progress Tracking**: Real-time progress updates with individual bars -- **Error Handling**: Continue processing on individual failures -- **Batch Management**: Handle multiple posts with image counts -- **Modal Display**: Shows detailed progress for each image - -### Queue Handler -- **File**: `core/admin/ajax.php` -- **Function**: `igny8_ajax_ai_generate_single_image()` -- **Lines**: 3283-3350 -- **Hook**: `wp_ajax_igny8_ai_generate_single_image` - -## 11. Debug and Logging - -### Debug Functions -- **File**: `debug/module-debug.php` -- **Lines**: 1185-1221 (HTML), 1447-1613 (JavaScript) -- **Features**: - - Image generation logs interface - - Refresh/clear buttons (`refresh-image-gen`, `clear-image-gen`) - - Real-time event display (`image-gen-events`) - - Status messages (`image-gen-message`, `image-gen-details`) - - Global debug function (`window.addImageGenDebugLog`) - -### Comprehensive Logging System -- **Event-Based Logging**: `IMAGE_GEN_EVENT_1` through `IMAGE_GEN_EVENT_9` -- **Debug Events Array**: `$debug_events[]` for detailed tracking -- **Error Logging**: `error_log()` with specific prefixes -- **AI Event Logging**: `igny8_log_ai_event()` for AI interactions - -### Logging Points -- **AJAX Entry**: `error_log('Igny8: AJAX HANDLER CALLED - igny8_ajax_ai_generate_images_drafts')` -- **Task Validation**: `error_log('Igny8: IMAGE_GEN_EVENT_3 - Task IDs validated')` -- **Post Retrieval**: `error_log('Igny8: IMAGE_GEN_EVENT_4 - WordPress post IDs retrieved')` -- **Image Prompts**: `error_log('Igny8: IMAGE_GEN_EVENT_5 - Image prompts loaded')` -- **Featured Generation**: `error_log('Igny8: IMAGE_GEN_EVENT_6 - Featured image generation initiated')` -- **API Requests**: `error_log('Igny8: IMAGE_GEN_EVENT_7 - API request sent')` -- **Image URLs**: `error_log('Igny8: IMAGE_GEN_EVENT_8 - Image URL received')` -- **WordPress Save**: `error_log('Igny8: IMAGE_GEN_EVENT_9 - Saving image to WordPress')` -- **Success/Failure**: `error_log('Igny8: IMAGE_GEN_EVENT_9_SUCCESS/ERROR')` - -## 12. File Dependencies - -### Core Files -- `ai/writer/images/image-generation.php` - Main image generation functions -- `core/admin/ajax.php` - AJAX handlers -- `assets/js/image-queue-processor.js` - Queue processing JavaScript -- `ai/modules-ai.php` - Content generation response handler -- `ai/openai-api.php` - OpenAI DALL-E API integration -- `ai/runware-api.php` - Runware API integration - -### Settings Files -- `modules/settings/general-settings.php` - Main image generation settings page -- `modules/thinker/prompts.php` - Image prompt templates only -- `modules/thinker/image-testing.php` - Image testing interface -- `modules/modules-pages/writer.php` - Settings localization - -### Debug Files -- `debug/module-debug.php` - Debug interface - -## 13. Hooks and Actions - -### WordPress Hooks -- `wp_ajax_igny8_ai_generate_images_drafts` - Main generation handler -- `wp_ajax_igny8_ai_generate_single_image` - Single image handler -- `wp_ajax_igny8_generate_single_image_queue` - Queue handler -- `wp_ajax_igny8_get_image_counts` - Image count preview -- `wp_ajax_igny8_save_image_settings` - Settings save -- `wp_ajax_igny8_save_image_prompt_template` - Template save -- `wp_ajax_igny8_reset_image_prompt_template` - Template reset - -### Internal Hooks -- `transition_post_status` - Post status changes -- `save_post` - Post save events - -## 14. Security Considerations - -### Nonce Verification -- All AJAX handlers verify nonces -- Settings forms use proper nonce fields -- User capability checks for admin functions - -### Data Sanitization -- All input data sanitized with `sanitize_text_field()` -- File uploads handled via WordPress functions -- Image URLs validated before processing -- HTML tags stripped from prompts using `wp_strip_all_tags()` - -## 15. Performance Considerations - -### Sequential Processing -- Images generated one at a time to avoid API limits -- Progress tracking for user feedback -- Error handling to continue processing - -### Media Library Integration -- Proper WordPress Media Library registration -- Automatic thumbnail generation -- Metadata attachment for SEO - -## 16. Complete Function Reference - -### AJAX Handlers -- `igny8_ajax_ai_generate_images_drafts()` - Main generation -- `igny8_ajax_ai_generate_single_image()` - Single image -- `igny8_ajax_generate_single_image_queue()` - Queue processing -- `igny8_ajax_get_image_counts()` - Count preview -- `igny8_ajax_save_image_settings()` - Settings save -- `igny8_ajax_save_image_prompt_template()` - Template save -- `igny8_ajax_reset_image_prompt_template()` - Template reset - -### Generation Functions -- `igny8_generate_featured_image_for_post()` - Featured image -- `igny8_generate_single_article_image()` - In-article image -- `igny8_get_image_dimensions()` - Size calculation -- `igny8_calculate_safe_image_quantity()` - Quantity safety - -### Content Functions -- `igny8_add_inarticle_image_meta()` - Image metadata saving -- `igny8_format_image_prompts_for_ai()` - Prompt formatting -- `igny8_create_post_from_ai_response()` - Content generation response handler - -### Queue Processing Functions -- `processAIImageGenerationDrafts()` - Main queue processor -- `generateAllImagesForPost()` - Single post processing -- `updateProgressModal()` - Progress updates - -### API Functions -- `igny8_runway_generate_image()` - Runware API integration -- `igny8_call_openai_images()` - OpenAI DALL-E API integration - -### Settings Functions -- `igny8_ajax_save_image_settings()` - Settings save -- `igny8_ajax_save_image_prompt_template()` - Template save -- `igny8_ajax_reset_image_prompt_template()` - Template reset - -## 17. Recent Changes and Improvements - -### Code Reorganization -- **Image generation functions moved** from `ai/modules-ai.php` to `ai/writer/images/image-generation.php` -- **Dedicated module** for image generation functionality -- **Improved separation of concerns** between content generation and image generation - -### Enhanced Error Handling -- **JSON parsing improvements** with HTML tag stripping -- **Better error messages** for debugging -- **Graceful fallbacks** for API failures - -### Improved Queue Processing -- **Individual progress bars** for each image -- **Better error tracking** and reporting -- **Enhanced user feedback** during processing - -### Settings Integration -- **Dynamic settings** from prompts page -- **Template-based prompts** with placeholder replacement -- **Provider selection** with appropriate API key validation - -This audit covers every aspect of the current image generation process from initial button click to final image storage and metadata saving, including the complete queue mechanism, settings integration, and content generation workflow. \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/docs/COMPLETE_WORKFLOWS_DOCUMENTATION.md b/igny8-ai-seo-wp-plugin/docs/COMPLETE_WORKFLOWS_DOCUMENTATION.md deleted file mode 100644 index df162345..00000000 --- a/igny8-ai-seo-wp-plugin/docs/COMPLETE_WORKFLOWS_DOCUMENTATION.md +++ /dev/null @@ -1,723 +0,0 @@ -# Igny8 AI SEO Plugin - Complete Workflows Documentation - -## Summary Table - -| Workflow | Process Steps | Functions Involved | Dependencies | Files Involved | -|----------|---------------|-------------------|--------------|----------------| -| **Content Planning** | Keyword Research → Clustering → Ideas → Queue | `igny8_research_keywords()`, `igny8_ai_cluster_keywords()`, `igny8_ai_generate_ideas()`, `igny8_queue_ideas_to_writer()` | OpenAI API, Database, WordPress | `modules/modules-pages/planner.php`, `ai/modules-ai.php`, `flows/sync-functions.php` | -| **Content Creation** | Task Creation → AI Generation → Draft Review → Publishing | `igny8_create_task()`, `igny8_generate_content()`, `igny8_review_draft()`, `igny8_publish_content()` | AI APIs, WordPress, Database | `modules/modules-pages/writer.php`, `ai/modules-ai.php`, `flows/sync-functions.php` | -| **SEO Optimization** | Content Analysis → Suggestions → Implementation → Monitoring | `igny8_analyze_content()`, `igny8_generate_suggestions()`, `igny8_implement_optimizations()`, `igny8_monitor_performance()` | SEO APIs, Database | `modules/modules-pages/optimizer.php`, `ai/modules-ai.php` | -| **Link Building** | Campaign Planning → Outreach → Tracking → Analysis | `igny8_plan_campaign()`, `igny8_manage_outreach()`, `igny8_track_backlinks()`, `igny8_analyze_results()` | External APIs, Database | `modules/modules-pages/linker.php`, `flows/sync-functions.php` | -| **Content Personalization** | Field Detection → Content Rewriting → Frontend Display → Analytics | `igny8_detect_fields()`, `igny8_rewrite_content()`, `igny8_display_personalized()`, `igny8_track_personalization()` | OpenAI API, Frontend | `modules/modules-pages/personalize/`, `ai/integration.php` | -| **Automation Workflows** | Schedule Setup → Task Execution → Monitoring → Optimization | `igny8_schedule_tasks()`, `igny8_execute_automation()`, `igny8_monitor_automation()`, `igny8_optimize_automation()` | WordPress CRON, Database | `core/cron/`, `core/pages/settings/schedules.php` | -| **Analytics & Reporting** | Data Collection → Analysis → Visualization → Reporting | `igny8_collect_metrics()`, `igny8_analyze_data()`, `igny8_visualize_results()`, `igny8_generate_reports()` | Database, WordPress | `core/admin/`, `modules/config/kpi-config.php` | - ---- - -## 1. CONTENT PLANNING WORKFLOW - -### 1.1 Keyword Research Process - -#### Step 1: Keyword Import -- **Function**: `igny8_import_keywords()` -- **Process**: CSV file upload and validation -- **Dependencies**: File upload system, data validation -- **Files**: `modules/components/import-modal-tpl.php`, `flows/sync-functions.php` -- **Validation**: Duplicate detection, data format validation -- **Output**: Validated keyword data in database - -#### Step 2: Keyword Analysis -- **Function**: `igny8_analyze_keywords()` -- **Process**: Keyword metrics calculation and categorization -- **Dependencies**: External SEO APIs, database queries -- **Files**: `ai/modules-ai.php`, `core/db/db.php` -- **Analysis**: Volume, difficulty, competition scoring -- **Output**: Analyzed keyword data with metrics - -#### Step 3: Keyword Categorization -- **Function**: `igny8_categorize_keywords()` -- **Process**: Primary/secondary keyword classification -- **Dependencies**: AI analysis, keyword relationships -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Classification**: Primary, secondary, long-tail categorization -- **Output**: Categorized keyword data - -### 1.2 AI Clustering Process - -#### Step 1: Cluster Analysis -- **Function**: `igny8_ai_cluster_keywords()` -- **Process**: AI-powered semantic clustering -- **Dependencies**: OpenAI API, keyword data -- **Files**: `ai/modules-ai.php`, `ai/openai-api.php` -- **Analysis**: Semantic similarity analysis -- **Output**: Keyword cluster assignments - -#### Step 2: Cluster Optimization -- **Function**: `igny8_optimize_clusters()` -- **Process**: Cluster refinement and optimization -- **Dependencies**: Cluster data, AI analysis -- **Files**: `flows/sync-functions.php`, `ai/modules-ai.php` -- **Optimization**: Cluster size, keyword distribution -- **Output**: Optimized cluster structure - -#### Step 3: Cluster Metrics -- **Function**: `igny8_calculate_cluster_metrics()` -- **Process**: Cluster performance calculation -- **Dependencies**: Cluster data, keyword metrics -- **Files**: `core/admin/global-helpers.php`, `flows/sync-functions.php` -- **Metrics**: Volume aggregation, keyword count -- **Output**: Cluster performance metrics - -### 1.3 Content Idea Generation - -#### Step 1: Idea Generation -- **Function**: `igny8_ai_generate_ideas()` -- **Process**: AI-powered content idea creation -- **Dependencies**: OpenAI API, cluster data -- **Files**: `ai/modules-ai.php`, `ai/openai-api.php` -- **Generation**: Content ideas based on clusters -- **Output**: Generated content ideas - -#### Step 2: Idea Categorization -- **Function**: `igny8_categorize_ideas()` -- **Process**: Content type and priority classification -- **Dependencies**: AI analysis, content templates -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Categorization**: Content type, priority scoring -- **Output**: Categorized content ideas - -#### Step 3: Idea Queue Management -- **Function**: `igny8_queue_ideas_to_writer()` -- **Process**: Idea prioritization and queue management -- **Dependencies**: Idea data, writer module -- **Files**: `flows/sync-functions.php`, `modules/modules-pages/writer.php` -- **Queue**: Priority-based idea queuing -- **Output**: Queued content ideas for writer - ---- - -## 2. CONTENT CREATION WORKFLOW - -### 2.1 Task Creation Process - -#### Step 1: Task Generation -- **Function**: `igny8_create_writing_task()` -- **Process**: Content task creation from ideas -- **Dependencies**: Content ideas, writer settings -- **Files**: `modules/modules-pages/writer.php`, `flows/sync-functions.php` -- **Creation**: Task assignment and configuration -- **Output**: Writing tasks in database - -#### Step 2: Task Prioritization -- **Function**: `igny8_prioritize_tasks()` -- **Process**: Task priority calculation and ordering -- **Dependencies**: Task data, priority algorithms -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Prioritization**: Priority scoring and ordering -- **Output**: Prioritized task queue - -#### Step 3: Task Assignment -- **Function**: `igny8_assign_tasks()` -- **Process**: Task assignment to writers or AI -- **Dependencies**: Task data, user preferences -- **Files**: `modules/modules-pages/writer.php`, `flows/sync-functions.php` -- **Assignment**: Writer or AI assignment -- **Output**: Assigned writing tasks - -### 2.2 AI Content Generation - -#### Step 1: Content Generation -- **Function**: `igny8_generate_content()` -- **Process**: AI-powered content creation -- **Dependencies**: OpenAI API, task data -- **Files**: `ai/modules-ai.php`, `ai/openai-api.php` -- **Generation**: AI content creation -- **Output**: Generated content drafts - -#### Step 2: Content Optimization -- **Function**: `igny8_optimize_content()` -- **Process**: SEO and readability optimization -- **Dependencies**: AI analysis, SEO rules -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Optimization**: SEO, readability improvements -- **Output**: Optimized content - -#### Step 3: Content Validation -- **Function**: `igny8_validate_content()` -- **Process**: Content quality and compliance checking -- **Dependencies**: Content data, quality rules -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Validation**: Quality, compliance checks -- **Output**: Validated content - -### 2.3 Draft Management - -#### Step 1: Draft Creation -- **Function**: `igny8_create_draft()` -- **Process**: WordPress draft post creation -- **Dependencies**: WordPress, content data -- **Files**: `flows/sync-functions.php`, `core/db/db.php` -- **Creation**: WordPress draft creation -- **Output**: Draft posts in WordPress - -#### Step 2: Draft Review -- **Function**: `igny8_review_draft()` -- **Process**: Content review and editing interface -- **Dependencies**: WordPress admin, draft data -- **Files**: `modules/modules-pages/writer.php`, `core/admin/meta-boxes.php` -- **Review**: Content review interface -- **Output**: Reviewed draft content - -#### Step 3: Draft Optimization -- **Function**: `igny8_optimize_draft()` -- **Process**: Draft content optimization -- **Dependencies**: AI analysis, optimization rules -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Optimization**: Content improvement -- **Output**: Optimized draft content - -### 2.4 Publishing Process - -#### Step 1: Publishing Preparation -- **Function**: `igny8_prepare_publishing()` -- **Process**: Pre-publication checks and preparation -- **Dependencies**: Draft data, publishing rules -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Preparation**: Pre-publication validation -- **Output**: Publishing-ready content - -#### Step 2: Content Publishing -- **Function**: `igny8_publish_content()` -- **Process**: WordPress post publication -- **Dependencies**: WordPress, draft data -- **Files**: `flows/sync-functions.php`, `core/db/db.php` -- **Publishing**: WordPress post publication -- **Output**: Published content - -#### Step 3: Post-Publishing -- **Function**: `igny8_post_publish_actions()` -- **Process**: Post-publication tasks and monitoring -- **Dependencies**: Published content, monitoring systems -- **Files**: `flows/sync-functions.php`, `flows/image-injection-responsive.php` -- **Actions**: Image injection, monitoring setup -- **Output**: Fully published and monitored content - ---- - -## 3. SEO OPTIMIZATION WORKFLOW - -### 3.1 Content Analysis Process - -#### Step 1: SEO Audit -- **Function**: `igny8_audit_content()` -- **Process**: Comprehensive SEO analysis -- **Dependencies**: Content data, SEO rules -- **Files**: `modules/modules-pages/optimizer.php`, `ai/modules-ai.php` -- **Analysis**: SEO factor analysis -- **Output**: SEO audit results - -#### Step 2: Keyword Analysis -- **Function**: `igny8_analyze_keywords()` -- **Process**: Keyword usage and optimization analysis -- **Dependencies**: Content data, keyword data -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Analysis**: Keyword density, placement analysis -- **Output**: Keyword optimization recommendations - -#### Step 3: Technical SEO -- **Function**: `igny8_analyze_technical_seo()` -- **Process**: Technical SEO factor analysis -- **Dependencies**: Content data, technical rules -- **Files**: `modules/modules-pages/optimizer.php`, `core/admin/global-helpers.php` -- **Analysis**: Technical SEO factors -- **Output**: Technical SEO recommendations - -### 3.2 Optimization Suggestions - -#### Step 1: Suggestion Generation -- **Function**: `igny8_generate_suggestions()` -- **Process**: AI-powered optimization suggestions -- **Dependencies**: AI analysis, content data -- **Files**: `ai/modules-ai.php`, `ai/openai-api.php` -- **Generation**: AI optimization suggestions -- **Output**: Optimization recommendations - -#### Step 2: Suggestion Prioritization -- **Function**: `igny8_prioritize_suggestions()` -- **Process**: Suggestion priority and impact analysis -- **Dependencies**: Suggestion data, impact algorithms -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Prioritization**: Impact-based prioritization -- **Output**: Prioritized suggestions - -#### Step 3: Implementation Planning -- **Function**: `igny8_plan_implementation()` -- **Process**: Implementation strategy development -- **Dependencies**: Suggestions, content data -- **Files**: `modules/modules-pages/optimizer.php`, `flows/sync-functions.php` -- **Planning**: Implementation strategy -- **Output**: Implementation plan - -### 3.3 Performance Monitoring - -#### Step 1: Metrics Collection -- **Function**: `igny8_collect_metrics()` -- **Process**: Performance data collection -- **Dependencies**: Analytics APIs, content data -- **Files**: `core/admin/global-helpers.php`, `modules/config/kpi-config.php` -- **Collection**: Performance data gathering -- **Output**: Performance metrics - -#### Step 2: Performance Analysis -- **Function**: `igny8_analyze_performance()` -- **Process**: Performance trend and pattern analysis -- **Dependencies**: Metrics data, analysis algorithms -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Analysis**: Performance trend analysis -- **Output**: Performance insights - -#### Step 3: Optimization Adjustment -- **Function**: `igny8_adjust_optimization()` -- **Process**: Performance-based optimization adjustments -- **Dependencies**: Performance data, optimization rules -- **Files**: `flows/sync-functions.php`, `ai/modules-ai.php` -- **Adjustment**: Optimization fine-tuning -- **Output**: Adjusted optimization strategy - ---- - -## 4. LINK BUILDING WORKFLOW - -### 4.1 Campaign Planning - -#### Step 1: Campaign Strategy -- **Function**: `igny8_plan_campaign()` -- **Process**: Link building campaign strategy development -- **Dependencies**: Content data, target analysis -- **Files**: `modules/modules-pages/linker.php`, `ai/modules-ai.php` -- **Planning**: Campaign strategy development -- **Output**: Campaign strategy - -#### Step 2: Target Identification -- **Function**: `igny8_identify_targets()` -- **Process**: High-value link target identification -- **Dependencies**: Content data, authority analysis -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Identification**: Target site identification -- **Output**: Link building targets - -#### Step 3: Outreach Planning -- **Function**: `igny8_plan_outreach()` -- **Process**: Outreach strategy and message development -- **Dependencies**: Target data, content data -- **Files**: `modules/modules-pages/linker.php`, `flows/sync-functions.php` -- **Planning**: Outreach strategy -- **Output**: Outreach plan - -### 4.2 Outreach Management - -#### Step 1: Outreach Execution -- **Function**: `igny8_execute_outreach()` -- **Process**: Automated outreach campaign execution -- **Dependencies**: Outreach data, communication systems -- **Files**: `flows/sync-functions.php`, `modules/modules-pages/linker.php` -- **Execution**: Outreach campaign execution -- **Output**: Outreach activities - -#### Step 2: Follow-up Management -- **Function**: `igny8_manage_followups()` -- **Process**: Follow-up communication management -- **Dependencies**: Outreach data, follow-up rules -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Management**: Follow-up communication -- **Output**: Follow-up activities - -#### Step 3: Relationship Building -- **Function**: `igny8_build_relationships()` -- **Process**: Long-term relationship development -- **Dependencies**: Contact data, relationship rules -- **Files**: `modules/modules-pages/linker.php`, `flows/sync-functions.php` -- **Building**: Relationship development -- **Output**: Established relationships - -### 4.3 Backlink Tracking - -#### Step 1: Backlink Discovery -- **Function**: `igny8_discover_backlinks()` -- **Process**: Automated backlink detection and tracking -- **Dependencies**: External APIs, monitoring systems -- **Files**: `flows/sync-functions.php`, `core/db/db.php` -- **Discovery**: Backlink detection -- **Output**: Discovered backlinks - -#### Step 2: Backlink Analysis -- **Function**: `igny8_analyze_backlinks()` -- **Process**: Backlink quality and authority analysis -- **Dependencies**: Backlink data, authority metrics -- **Files**: `ai/modules-ai.php`, `core/admin/global-helpers.php` -- **Analysis**: Backlink quality analysis -- **Output**: Backlink analysis results - -#### Step 3: Performance Tracking -- **Function**: `igny8_track_backlink_performance()` -- **Process**: Backlink impact and performance monitoring -- **Dependencies**: Backlink data, performance metrics -- **Files**: `flows/sync-functions.php`, `modules/config/kpi-config.php` -- **Tracking**: Performance monitoring -- **Output**: Performance tracking data - ---- - -## 5. CONTENT PERSONALIZATION WORKFLOW - -### 5.1 Field Detection Process - -#### Step 1: Content Analysis -- **Function**: `igny8_analyze_content_for_fields()` -- **Process**: AI-powered field detection from content -- **Dependencies**: OpenAI API, content data -- **Files**: `modules/modules-pages/personalize/content-generation.php`, `ai/integration.php` -- **Analysis**: Content field analysis -- **Output**: Detected personalization fields - -#### Step 2: Field Configuration -- **Function**: `igny8_configure_fields()` -- **Process**: Field configuration and customization -- **Dependencies**: Field data, user preferences -- **Files**: `modules/modules-pages/personalize/content-generation.php`, `flows/sync-functions.php` -- **Configuration**: Field setup and customization -- **Output**: Configured personalization fields - -#### Step 3: Field Validation -- **Function**: `igny8_validate_fields()` -- **Process**: Field validation and testing -- **Dependencies**: Field data, validation rules -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Validation**: Field validation and testing -- **Output**: Validated personalization fields - -### 5.2 Content Rewriting Process - -#### Step 1: Content Rewriting -- **Function**: `igny8_rewrite_content()` -- **Process**: AI-powered content personalization -- **Dependencies**: OpenAI API, user data -- **Files**: `ai/integration.php`, `ai/openai-api.php` -- **Rewriting**: Content personalization -- **Output**: Personalized content - -#### Step 2: Content Optimization -- **Function**: `igny8_optimize_personalized_content()` -- **Process**: Personalized content optimization -- **Dependencies**: Personalized content, optimization rules -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Optimization**: Content optimization -- **Output**: Optimized personalized content - -#### Step 3: Content Validation -- **Function**: `igny8_validate_personalized_content()` -- **Process**: Personalized content quality validation -- **Dependencies**: Personalized content, quality rules -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Validation**: Content quality validation -- **Output**: Validated personalized content - -### 5.3 Frontend Integration - -#### Step 1: Frontend Setup -- **Function**: `igny8_setup_frontend()` -- **Process**: Frontend personalization setup -- **Dependencies**: Frontend configuration, personalization data -- **Files**: `modules/modules-pages/personalize/front-end.php`, `flows/sync-functions.php` -- **Setup**: Frontend configuration -- **Output**: Configured frontend personalization - -#### Step 2: User Interface -- **Function**: `igny8_display_personalization_interface()` -- **Process**: Personalization interface display -- **Dependencies**: Frontend templates, user data -- **Files**: `assets/js/core.js`, `modules/components/forms-tpl.php` -- **Display**: Personalization interface -- **Output**: User personalization interface - -#### Step 3: Content Delivery -- **Function**: `igny8_deliver_personalized_content()` -- **Process**: Personalized content delivery -- **Dependencies**: Personalized content, delivery systems -- **Files**: `flows/sync-functions.php`, `core/db/db.php` -- **Delivery**: Content delivery -- **Output**: Delivered personalized content - ---- - -## 6. AUTOMATION WORKFLOWS - -### 6.1 Schedule Management - -#### Step 1: Schedule Configuration -- **Function**: `igny8_configure_schedules()` -- **Process**: Automation schedule setup and configuration -- **Dependencies**: Schedule data, automation rules -- **Files**: `core/pages/settings/schedules.php`, `flows/sync-functions.php` -- **Configuration**: Schedule setup -- **Output**: Configured automation schedules - -#### Step 2: Task Scheduling -- **Function**: `igny8_schedule_tasks()` -- **Process**: Automated task scheduling -- **Dependencies**: WordPress CRON, task data -- **Files**: `core/cron/igny8-cron-master-dispatcher.php`, `core/cron/igny8-cron-handlers.php` -- **Scheduling**: Task scheduling -- **Output**: Scheduled automation tasks - -#### Step 3: Schedule Monitoring -- **Function**: `igny8_monitor_schedules()` -- **Process**: Schedule performance monitoring -- **Dependencies**: Schedule data, monitoring systems -- **Files**: `core/cron/igny8-cron-master-dispatcher.php`, `core/admin/global-helpers.php` -- **Monitoring**: Schedule monitoring -- **Output**: Schedule monitoring data - -### 6.2 Automation Execution - -#### Step 1: Task Execution -- **Function**: `igny8_execute_automation()` -- **Process**: Automated task execution -- **Dependencies**: Task data, execution systems -- **Files**: `core/cron/igny8-cron-handlers.php`, `flows/sync-functions.php` -- **Execution**: Task execution -- **Output**: Executed automation tasks - -#### Step 2: Process Monitoring -- **Function**: `igny8_monitor_automation()` -- **Process**: Automation process monitoring -- **Dependencies**: Automation data, monitoring systems -- **Files**: `core/cron/igny8-cron-master-dispatcher.php`, `core/admin/global-helpers.php` -- **Monitoring**: Process monitoring -- **Output**: Automation monitoring data - -#### Step 3: Error Handling -- **Function**: `igny8_handle_automation_errors()` -- **Process**: Automation error handling and recovery -- **Dependencies**: Error data, recovery systems -- **Files**: `core/cron/igny8-cron-handlers.php`, `flows/sync-functions.php` -- **Handling**: Error handling and recovery -- **Output**: Error handling results - -### 6.3 Performance Optimization - -#### Step 1: Performance Analysis -- **Function**: `igny8_analyze_automation_performance()` -- **Process**: Automation performance analysis -- **Dependencies**: Performance data, analysis algorithms -- **Files**: `core/admin/global-helpers.php`, `modules/config/kpi-config.php` -- **Analysis**: Performance analysis -- **Output**: Performance insights - -#### Step 2: Optimization Adjustment -- **Function**: `igny8_optimize_automation()` -- **Process**: Automation optimization -- **Dependencies**: Performance data, optimization rules -- **Files**: `flows/sync-functions.php`, `ai/modules-ai.php` -- **Optimization**: Automation optimization -- **Output**: Optimized automation - -#### Step 3: Continuous Improvement -- **Function**: `igny8_improve_automation()` -- **Process**: Continuous automation improvement -- **Dependencies**: Performance data, improvement algorithms -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Improvement**: Continuous improvement -- **Output**: Improved automation - ---- - -## 7. ANALYTICS & REPORTING WORKFLOW - -### 7.1 Data Collection - -#### Step 1: Metrics Collection -- **Function**: `igny8_collect_metrics()` -- **Process**: Performance metrics collection -- **Dependencies**: Analytics APIs, content data -- **Files**: `core/admin/global-helpers.php`, `modules/config/kpi-config.php` -- **Collection**: Metrics gathering -- **Output**: Collected performance metrics - -#### Step 2: Data Processing -- **Function**: `igny8_process_analytics_data()` -- **Process**: Analytics data processing and preparation -- **Dependencies**: Raw data, processing algorithms -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Processing**: Data processing -- **Output**: Processed analytics data - -#### Step 3: Data Storage -- **Function**: `igny8_store_analytics_data()` -- **Process**: Analytics data storage and organization -- **Dependencies**: Processed data, database systems -- **Files**: `core/db/db.php`, `flows/sync-functions.php` -- **Storage**: Data storage -- **Output**: Stored analytics data - -### 7.2 Analysis & Insights - -#### Step 1: Data Analysis -- **Function**: `igny8_analyze_analytics_data()` -- **Process**: Analytics data analysis and insights -- **Dependencies**: Stored data, analysis algorithms -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Analysis**: Data analysis -- **Output**: Analytics insights - -#### Step 2: Trend Analysis -- **Function**: `igny8_analyze_trends()` -- **Process**: Performance trend analysis -- **Dependencies**: Historical data, trend algorithms -- **Files**: `ai/modules-ai.php`, `core/admin/global-helpers.php` -- **Analysis**: Trend analysis -- **Output**: Trend insights - -#### Step 3: Predictive Analytics -- **Function**: `igny8_predict_performance()` -- **Process**: Performance prediction and forecasting -- **Dependencies**: Historical data, prediction algorithms -- **Files**: `ai/modules-ai.php`, `flows/sync-functions.php` -- **Prediction**: Performance prediction -- **Output**: Predictive insights - -### 7.3 Reporting & Visualization - -#### Step 1: Report Generation -- **Function**: `igny8_generate_reports()` -- **Process**: Automated report generation -- **Dependencies**: Analytics data, report templates -- **Files**: `modules/components/kpi-tpl.php`, `flows/sync-functions.php` -- **Generation**: Report generation -- **Output**: Generated reports - -#### Step 2: Data Visualization -- **Function**: `igny8_visualize_data()` -- **Process**: Analytics data visualization -- **Dependencies**: Analytics data, visualization tools -- **Files**: `modules/components/kpi-tpl.php`, `assets/js/core.js` -- **Visualization**: Data visualization -- **Output**: Visualized analytics data - -#### Step 3: Report Distribution -- **Function**: `igny8_distribute_reports()` -- **Process**: Report distribution and delivery -- **Dependencies**: Generated reports, distribution systems -- **Files**: `flows/sync-functions.php`, `core/admin/global-helpers.php` -- **Distribution**: Report distribution -- **Output**: Distributed reports - ---- - -## 8. INTEGRATION WORKFLOWS - -### 8.1 WordPress Integration - -#### Step 1: WordPress Setup -- **Function**: `igny8_setup_wordpress_integration()` -- **Process**: WordPress integration setup -- **Dependencies**: WordPress hooks, plugin system -- **Files**: `igny8.php`, `core/admin/init.php` -- **Setup**: WordPress integration -- **Output**: Integrated WordPress functionality - -#### Step 2: Post Meta Management -- **Function**: `igny8_manage_post_meta()` -- **Process**: WordPress post metadata management -- **Dependencies**: WordPress post system, metadata -- **Files**: `core/db/db.php`, `flows/sync-functions.php` -- **Management**: Post metadata management -- **Output**: Managed post metadata - -#### Step 3: Taxonomy Integration -- **Function**: `igny8_integrate_taxonomies()` -- **Process**: Custom taxonomy integration -- **Dependencies**: WordPress taxonomy system -- **Files**: `core/db/db.php`, `core/admin/init.php` -- **Integration**: Taxonomy integration -- **Output**: Integrated taxonomies - -### 8.2 AI Service Integration - -#### Step 1: AI Service Setup -- **Function**: `igny8_setup_ai_services()` -- **Process**: AI service integration setup -- **Dependencies**: AI APIs, authentication -- **Files**: `ai/integration.php`, `ai/openai-api.php` -- **Setup**: AI service setup -- **Output**: Configured AI services - -#### Step 2: API Management -- **Function**: `igny8_manage_ai_apis()` -- **Process**: AI API management and optimization -- **Dependencies**: API credentials, rate limiting -- **Files**: `ai/openai-api.php`, `ai/runware-api.php` -- **Management**: API management -- **Output**: Managed AI APIs - -#### Step 3: Performance Monitoring -- **Function**: `igny8_monitor_ai_performance()` -- **Process**: AI service performance monitoring -- **Dependencies**: AI services, monitoring systems -- **Files**: `ai/integration.php`, `core/admin/global-helpers.php` -- **Monitoring**: AI performance monitoring -- **Output**: AI performance data - -### 8.3 Database Integration - -#### Step 1: Database Setup -- **Function**: `igny8_setup_database()` -- **Process**: Database table creation and setup -- **Dependencies**: Database system, table schemas -- **Files**: `core/db/db.php`, `install.php` -- **Setup**: Database setup -- **Output**: Configured database - -#### Step 2: Data Migration -- **Function**: `igny8_migrate_data()` -- **Process**: Data migration and version management -- **Dependencies**: Database system, migration scripts -- **Files**: `core/db/db-migration.php`, `flows/sync-functions.php` -- **Migration**: Data migration -- **Output**: Migrated data - -#### Step 3: Performance Optimization -- **Function**: `igny8_optimize_database()` -- **Process**: Database performance optimization -- **Dependencies**: Database system, optimization rules -- **Files**: `core/db/db.php`, `flows/sync-functions.php` -- **Optimization**: Database optimization -- **Output**: Optimized database - ---- - -## Technical Implementation Details - -### Workflow Dependencies -- **WordPress Core**: Hooks, actions, filters -- **Database Layer**: Custom tables, queries, migrations -- **AI Services**: OpenAI, Runware APIs -- **Frontend**: JavaScript, CSS, responsive design -- **Automation**: WordPress CRON, scheduled tasks - -### File Structure -- **Core Files**: Plugin initialization and setup -- **Module Files**: Feature-specific implementations -- **AI Integration**: AI service integrations -- **Workflows**: Process automation and management -- **Assets**: Frontend resources and templates - -### Performance Considerations -- **Caching**: Data caching and optimization -- **Database**: Query optimization and indexing -- **AI APIs**: Rate limiting and cost optimization -- **Automation**: Efficient task scheduling and execution -- **Monitoring**: Performance tracking and optimization - -This comprehensive workflows documentation covers all aspects of the Igny8 AI SEO Plugin's workflow processes, providing detailed step-by-step guidance for each workflow, including functions, dependencies, and file references. - diff --git a/igny8-ai-seo-wp-plugin/docs/FILE_TREE.txt b/igny8-ai-seo-wp-plugin/docs/FILE_TREE.txt deleted file mode 100644 index 30ad91eb..00000000 --- a/igny8-ai-seo-wp-plugin/docs/FILE_TREE.txt +++ /dev/null @@ -1,122 +0,0 @@ -igny8-ai-seo/ -├── ai/ -│ ├── _README.php -│ ├── integration.php -│ ├── model-rates-config.php -│ ├── modules-ai.php -│ ├── openai-api.php -│ ├── prompts-library.php -│ ├── runware-api.php -│ └── writer/ -│ ├── content/ -│ └── images/ -│ └── image-generation.php -├── assets/ -│ ├── ai-images/ -│ ├── css/ -│ │ ├── core-backup.css -│ │ ├── core.css -│ │ └── image-injection.css -│ ├── img/ -│ ├── js/ -│ │ ├── core.js -│ │ └── image-queue-processor.js -│ ├── shortcodes/ -│ │ ├── _README.php -│ │ └── image-gallery.php -│ └── templates/ -│ ├── igny8_clusters_template.csv -│ ├── igny8_ideas_template.csv -│ └── igny8_keywords_template.csv -├── CHANGELOG_live.md -├── core/ -│ ├── _README.php -│ ├── admin/ -│ │ ├── ajax.php -│ │ ├── global-helpers.php -│ │ ├── init.php -│ │ ├── menu.php -│ │ ├── meta-boxes.php -│ │ └── module-manager-class.php -│ ├── cron/ -│ │ ├── igny8-cron-handlers.php -│ │ └── igny8-cron-master-dispatcher.php -│ ├── db/ -│ │ ├── db-migration.php -│ │ └── db.php -│ └── global-layout.php -├── debug/ -│ ├── _README.php -│ ├── debug.php -│ ├── module-debug.php -│ └── monitor-helpers.php -├── docs/ -│ ├── _README.php -│ ├── COMPLETE_FEATURES_LIST.md -│ ├── COMPLETE_FUNCTION_REFERENCE.md -│ ├── COMPLETE_IMAGE_GENERATION_AUDIT.md -│ ├── COMPLETE_WORKFLOWS_DOCUMENTATION.md -│ ├── FILE_TREE.txt -│ ├── IGNY8_PAGES_TABLE.md -│ ├── IGNY8_SNAPSHOT_V5.2.0.md -│ └── TROUBLESHOOTING_Converting_to_blocks_and_image_shortcode_injection.md -├── flows/ -│ ├── sync-ajax.php -│ ├── sync-functions.php -│ └── sync-hooks.php -├── igny8-wp-load-handler.php -├── igny8.php -├── install.php -├── modules/ -│ ├── _README.php -│ ├── analytics/ -│ │ └── analytics.php -│ ├── components/ -│ │ ├── _README.php -│ │ ├── actions-tpl.php -│ │ ├── export-modal-tpl.php -│ │ ├── filters-tpl.php -│ │ ├── forms-tpl.php -│ │ ├── import-modal-tpl.php -│ │ ├── kpi-tpl.php -│ │ ├── pagination-tpl.php -│ │ └── table-tpl.php -│ ├── config/ -│ │ ├── _README.php -│ │ ├── filters-config.php -│ │ ├── forms-config.php -│ │ ├── import-export-config.php -│ │ ├── kpi-config.php -│ │ └── tables-config.php -│ ├── help/ -│ │ ├── docs.php -│ │ ├── function-testing.php -│ │ ├── help.php -│ │ └── system-testing.php -│ ├── home.php -│ ├── planner/ -│ │ ├── clusters.php -│ │ ├── ideas.php -│ │ ├── keywords.php -│ │ └── planner.php -│ ├── settings/ -│ │ ├── general-settings.php -│ │ ├── import-export.php -│ │ ├── integration.php -│ │ ├── schedules.php -│ │ └── status.php -│ ├── thinker/ -│ │ ├── image-testing.php -│ │ ├── profile.php -│ │ ├── prompts.php -│ │ ├── strategies.php -│ │ └── thinker.php -│ └── writer/ -│ ├── drafts.php -│ ├── published.php -│ ├── tasks.php -│ └── writer.php -├── shortcodes/ -│ ├── ai-shortcodes.php -│ └── writer-shortcodes.php -└── uninstall.php \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/docs/IGNY8_PAGES_TABLE.md b/igny8-ai-seo-wp-plugin/docs/IGNY8_PAGES_TABLE.md deleted file mode 100644 index 0fe85ba9..00000000 --- a/igny8-ai-seo-wp-plugin/docs/IGNY8_PAGES_TABLE.md +++ /dev/null @@ -1,162 +0,0 @@ -# Igny8 AI SEO Plugin - Complete Pages Table - -## Overview -This table provides a comprehensive list of all pages in the Igny8 AI SEO Plugin, including their URLs, purposes, and functionality. - ---- - -## Main Navigation Pages - -| Page Name | URL | Purpose | Module | Subpages | -|-----------|-----|---------|--------|----------| -| **Dashboard** | `admin.php?page=igny8-home` | Main dashboard with complete AI workflow guide | Core | None | -| **Planner** | `admin.php?page=igny8-planner` | Content planning and keyword research | Planner | 4 subpages | -| **Writer** | `admin.php?page=igny8-writer` | Content creation and writing tools | Writer | 3 subpages | -| **Optimizer** | `admin.php?page=igny8-optimizer` | SEO optimization and performance tools | Optimizer | 2 subpages | -| **Linker** | `admin.php?page=igny8-linker` | Link building and backlink management | Linker | 2 subpages | -| **Personalize** | `admin.php?page=igny8-personalize` | Content personalization and targeting | Personalize | 4 subpages | -| **Thinker** | `admin.php?page=igny8-thinker` | AI thinker and strategy tools | Thinker | 4 subpages | -| **Analytics** | `admin.php?page=igny8-analytics` | Performance analytics and reporting | Analytics | None | -| **Schedules** | `admin.php?page=igny8-schedules` | Smart automation schedules | Schedules | None | -| **Settings** | `admin.php?page=igny8-settings` | Plugin configuration and settings | Core | 3 subpages | -| **Help** | `admin.php?page=igny8-help` | Documentation and support resources | Core | 3 subpages | - ---- - -## Planner Module Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Planner Dashboard** | `admin.php?page=igny8-planner` | Main planner overview | Overview of keywords, clusters, and ideas | -| **Keywords** | `admin.php?page=igny8-planner&sm=keywords` | Keyword management | Manage keywords, track search volumes, organize by intent and difficulty | -| **Clusters** | `admin.php?page=igny8-planner&sm=clusters` | Keyword clustering | Group related keywords into content clusters for better topical authority | -| **Ideas** | `admin.php?page=igny8-planner&sm=ideas` | Content ideas generation | Generate and organize content ideas based on keyword research | -| **Mapping** | `admin.php?page=igny8-planner&sm=mapping` | Content mapping | Map keywords and clusters to existing pages and content | - ---- - -## Writer Module Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Writer Dashboard** | `admin.php?page=igny8-writer` | Main writer overview | Overview of content tasks and workflow | -| **Tasks** | `admin.php?page=igny8-writer&sm=tasks` | Content queue management | Manage content tasks and work queue | -| **Drafts** | `admin.php?page=igny8-writer&sm=drafts` | Draft content management | Manage content drafts and work in progress | -| **Published** | `admin.php?page=igny8-writer&sm=published` | Published content | View and manage published content | - ---- - -## Optimizer Module Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Optimizer Dashboard** | `admin.php?page=igny8-optimizer` | Main optimizer overview | Overview of optimization tools and performance | -| **Audits** | `admin.php?page=igny8-optimizer&sm=audits` | SEO audits | Run comprehensive SEO audits on content and pages | -| **Suggestions** | `admin.php?page=igny8-optimizer&sm=suggestions` | Optimization suggestions | Get AI-powered optimization suggestions for better rankings | - ---- - -## Linker Module Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Linker Dashboard** | `admin.php?page=igny8-linker` | Main linker overview | Overview of link building tools and campaigns | -| **Backlinks** | `admin.php?page=igny8-linker&sm=backlinks` | Backlink management | Track and manage backlink profile and authority | -| **Campaigns** | `admin.php?page=igny8-linker&sm=campaigns` | Link building campaigns | Plan and execute link building campaigns effectively | - ---- - -## Personalize Module Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Personalize Dashboard** | `admin.php?page=igny8-personalize` | Main personalization overview | Overview of personalization tools and settings | -| **Settings** | `admin.php?page=igny8-personalize&sm=settings` | Personalization settings | Configure global settings, display options, and advanced personalization settings | -| **Content Generation** | `admin.php?page=igny8-personalize&sm=content-generation` | AI content generation | Configure AI prompts, field detection, and content generation parameters | -| **Rewrites** | `admin.php?page=igny8-personalize&sm=rewrites` | Content variations | View and manage personalized content variations and rewrites | -| **Front-end** | `admin.php?page=igny8-personalize&sm=front-end` | Frontend implementation | Manage front-end display settings, shortcode usage, and implementation guides | - ---- - -## Thinker Module Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Thinker Dashboard** | `admin.php?page=igny8-thinker&sp=main` | Main AI thinker overview | Overview of AI tools and strategies | -| **Prompts** | `admin.php?page=igny8-thinker&sp=prompts` | AI prompts management | Manage and configure AI prompts for content generation | -| **Profile** | `admin.php?page=igny8-thinker&sp=profile` | AI profile settings | Configure AI personality and writing style | -| **Strategies** | `admin.php?page=igny8-thinker&sp=strategies` | Content strategies | Plan and manage content strategies and approaches | -| **Image Testing** | `admin.php?page=igny8-thinker&sp=image-testing` | AI image testing | Test and configure AI image generation capabilities | - ---- - -## Settings Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **General Settings** | `admin.php?page=igny8-settings&sp=general` | Plugin configuration | Configure plugin settings, automation, and table preferences | -| **System Status** | `admin.php?page=igny8-settings&sp=status` | System monitoring | Monitor system health, database status, and module performance | -| **API Integration** | `admin.php?page=igny8-settings&sp=integration` | External integrations | Configure API keys and integrate with external services | -| **Import/Export** | `admin.php?page=igny8-settings&sp=import-export` | Data management | Import and export data, manage backups, and transfer content | - ---- - -## Help Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Help & Support** | `admin.php?page=igny8-help&sp=help` | Main help page | Documentation and support resources for getting started | -| **Documentation** | `admin.php?page=igny8-help&sp=docs` | Complete documentation | Comprehensive documentation and guides | -| **System Testing** | `admin.php?page=igny8-help&sp=system-testing` | System diagnostics | Test system functionality and diagnose issues | -| **Function Testing** | `admin.php?page=igny8-help&sp=function-testing` | Function testing | Test individual functions and components | - ---- - -## Special Pages - -| Page Name | URL | Purpose | Description | -|-----------|-----|---------|-------------| -| **Analytics** | `admin.php?page=igny8-analytics` | Performance analytics | Performance analytics and reporting for data-driven decisions | -| **Schedules** | `admin.php?page=igny8-schedules` | Automation schedules | Content scheduling and automation for consistent publishing | - ---- - -## Page Access Requirements - -| Requirement | Description | -|-------------|-------------| -| **Capability** | All pages require `manage_options` capability | -| **Module Status** | Module pages only accessible if corresponding module is enabled | -| **User Context** | All pages require authenticated WordPress user | - ---- - -## Page Structure Notes - -### URL Parameters -- **`page`**: Main page identifier (e.g., `igny8-planner`) -- **`sm`**: Submodule parameter for module subpages (e.g., `keywords`, `clusters`) -- **`sp`**: Subpage parameter for settings/help pages (e.g., `general`, `docs`) - -### Page Rendering -- All pages use `core/global-layout.php` as the master layout template -- Module pages use `modules/modules-pages/{module}.php` for content -- Settings/Help pages use `core/pages/{category}/{page}.php` for content -- All pages include breadcrumb navigation and submenu systems - -### Dynamic Content -- Pages show different content based on module enablement status -- Subpages are conditionally rendered based on URL parameters -- All pages include workflow guides and progress tracking - ---- - -## Summary - -**Total Pages**: 25+ individual pages across 8 modules -**Main Modules**: Planner, Writer, Optimizer, Linker, Personalize, Thinker, Analytics, Schedules -**Core Pages**: Dashboard, Settings, Help -**Subpages**: 20+ subpages with specialized functionality -**Access Control**: All pages require admin privileges and module enablement - -This comprehensive page structure provides a complete SEO management platform with specialized tools for each aspect of content creation, optimization, and performance tracking. diff --git a/igny8-ai-seo-wp-plugin/docs/IGNY8_SNAPSHOT_V5.2.0.md b/igny8-ai-seo-wp-plugin/docs/IGNY8_SNAPSHOT_V5.2.0.md deleted file mode 100644 index c0fc9414..00000000 --- a/igny8-ai-seo-wp-plugin/docs/IGNY8_SNAPSHOT_V5.2.0.md +++ /dev/null @@ -1,523 +0,0 @@ -# Igny8 AI SEO Plugin - Complete System Snapshot v0.1 - -## Summary Table - -| System Component | Current State | Modules | Functions | Dependencies | Files Involved | -|------------------|---------------|---------|-----------|--------------|----------------| -| **Core System** | Fully Operational | 8 Active Modules | 200+ Functions | WordPress, Database | `igny8.php`, `core/`, `install.php`, `uninstall.php` | -| **AI Integration** | Advanced AI Processing | OpenAI, Runware | 25+ AI Functions | OpenAI API, Runware API | `ai/integration.php`, `ai/openai-api.php`, `ai/runware-api.php` | -| **Database Layer** | 15 Custom Tables | Data Management | 30+ DB Functions | MySQL, WordPress | `core/db/db.php`, `core/db/db-migration.php` | -| **Workflow Automation** | ⚠️ CRITICAL ISSUES IDENTIFIED | 7 Workflow Types | 40+ Automation Functions | WordPress CRON, Database | `flows/`, `core/cron/` | -| **Admin Interface** | Complete UI System | 8 Module Interfaces | 35+ UI Functions | WordPress Admin, JavaScript | `core/admin/`, `modules/` | -| **Frontend Integration** | Responsive Design | Personalization, Shortcodes | 15+ Frontend Functions | JavaScript, CSS | `assets/`, `modules/modules-pages/personalize/` | -| **Analytics & Reporting** | Advanced Analytics | KPI Tracking, Reporting | 25+ Analytics Functions | Database, WordPress | `core/admin/global-helpers.php`, `modules/config/` | - ---- - -## ⚠️ CRITICAL SYSTEM ISSUES IDENTIFIED - -### Cron vs Manual Function Discrepancies -- **HIGH RISK**: Cron functions have significant architectural differences from manual counterparts -- **Function Dependencies**: Cron handlers include extensive fallback logic suggesting unreliable function loading -- **User Context**: Cron handlers manually set admin user context while manual handlers rely on authenticated users -- **Error Handling**: Cron handlers suppress PHP warnings that manual handlers don't, potentially masking critical issues -- **Database Access**: Inconsistent database connection handling between cron and manual functions - -### Affected Automation Functions -1. **Auto Cluster**: `igny8_auto_cluster_cron_handler()` vs `igny8_ajax_ai_cluster_keywords()` -2. **Auto Ideas**: `igny8_auto_generate_ideas_cron_handler()` vs `igny8_ajax_ai_generate_ideas()` -3. **Auto Queue**: `igny8_auto_queue_cron_handler()` vs `igny8_ajax_queue_ideas_to_writer()` -4. **Auto Content**: `igny8_auto_generate_content_cron_handler()` vs `igny8_ajax_ai_generate_content()` -5. **Auto Image**: `igny8_auto_generate_images_cron_handler()` vs `igny8_ajax_ai_generate_images_drafts()` -6. **Auto Publish**: `igny8_auto_publish_drafts_cron_handler()` vs `igny8_ajax_bulk_publish_drafts()` - -### Impact Assessment -- **Manual Functions**: ✅ Healthy and functioning correctly -- **Cron Functions**: ❌ High risk of failure due to architectural differences -- **Recommendation**: 🔴 IMMEDIATE review and alignment required -- **Priority**: CRITICAL - Automation system reliability compromised - ---- - -## 1. SYSTEM ARCHITECTURE OVERVIEW - -### 1.1 Core System Components -- **Plugin Initialization**: Complete WordPress integration with hooks, actions, and filters -- **Database Management**: 15 custom tables with full migration and version control -- **Module System**: 8 active modules with dynamic loading and configuration -- **AI Integration**: Advanced OpenAI and Runware API integration -- **Automation System**: Comprehensive CRON-based workflow automation -- **Admin Interface**: Complete WordPress admin integration with responsive design -- **Frontend Integration**: Personalization and shortcode system -- **Analytics System**: Advanced KPI tracking and reporting - -### 1.2 Current Version Status -- **Version**: 0.1 -- **WordPress Compatibility**: 5.0+ -- **PHP Requirements**: 7.4+ -- **Database**: MySQL 5.7+ -- **Status**: ⚠️ Production Ready with Critical Automation Issues -- **Last Updated**: January 15, 2025 -- **Stability**: Stable (Manual Functions) / Unstable (Cron Functions) -- **Performance**: Optimized -- **Critical Issues**: Cron vs Manual function discrepancies identified - ---- - -## 2. MODULE SYSTEM STATUS - -### 2.1 Active Modules - -#### **Planner Module** - Content Planning & Strategy -- **Status**: Fully Operational -- **Features**: Keyword research, AI clustering, content idea generation -- **Functions**: 25+ planning functions -- **Dependencies**: OpenAI API, Database, WordPress -- **Files**: `modules/modules-pages/planner.php`, `ai/modules-ai.php` -- **Workflow**: Keyword Research → Clustering → Ideas → Queue - -#### **Writer Module** - Content Creation & Management -- **Status**: Fully Operational -- **Features**: AI content generation, draft management, publishing workflow -- **Functions**: 30+ writing functions -- **Dependencies**: AI APIs, WordPress, Database -- **Files**: `modules/modules-pages/writer.php`, `ai/modules-ai.php` -- **Workflow**: Task Creation → AI Generation → Draft Review → Publishing - -#### **Optimizer Module** - SEO Analysis & Optimization -- **Status**: Fully Operational -- **Features**: Content audits, optimization suggestions, performance monitoring -- **Functions**: 20+ optimization functions -- **Dependencies**: SEO APIs, Database -- **Files**: `modules/modules-pages/optimizer.php`, `ai/modules-ai.php` -- **Workflow**: Content Analysis → Suggestions → Implementation → Monitoring - -#### **Linker Module** - Backlink Management & Campaigns -- **Status**: Fully Operational -- **Features**: Backlink tracking, campaign management, authority building -- **Functions**: 25+ linking functions -- **Dependencies**: External APIs, Database -- **Files**: `modules/modules-pages/linker.php`, `flows/sync-functions.php` -- **Workflow**: Campaign Planning → Outreach → Tracking → Analysis - -#### **Personalize Module** - AI Content Personalization -- **Status**: Fully Operational -- **Features**: AI personalization, user targeting, frontend integration -- **Functions**: 20+ personalization functions -- **Dependencies**: OpenAI API, Frontend -- **Files**: `modules/modules-pages/personalize/`, `ai/integration.php` -- **Workflow**: Field Detection → Content Rewriting → Frontend Display → Analytics - -#### **Thinker Module** - AI Strategy & Prompt Management -- **Status**: Fully Operational -- **Features**: AI strategy, prompt management, content planning -- **Functions**: 15+ thinking functions -- **Dependencies**: AI APIs, Database -- **Files**: `core/pages/thinker/`, `ai/prompts-library.php` -- **Workflow**: Strategy Development → Prompt Management → Content Planning - -#### **Analytics Module** - Performance Tracking & Reporting -- **Status**: Fully Operational -- **Features**: KPI tracking, performance monitoring, report generation -- **Functions**: 25+ analytics functions -- **Dependencies**: Database, WordPress -- **Files**: `core/admin/`, `modules/config/kpi-config.php` -- **Workflow**: Data Collection → Analysis → Visualization → Reporting - -#### **Schedules Module** - Automation Management -- **Status**: Fully Operational -- **Features**: CRON management, workflow automation, task scheduling -- **Functions**: 15+ automation functions -- **Dependencies**: WordPress CRON, Database -- **Files**: `core/cron/`, `core/pages/settings/schedules.php` -- **Workflow**: Schedule Setup → Task Execution → Monitoring → Optimization - -### 2.2 Module Dependencies -- **Core Dependencies**: WordPress, Database, PHP -- **AI Dependencies**: OpenAI API, Runware API -- **External Dependencies**: cURL, JSON, CSV -- **Internal Dependencies**: Module Manager, CRON System, Admin Interface - ---- - -## 3. DATABASE ARCHITECTURE - -### 3.1 Custom Tables (15 Tables) - -#### **Core Data Tables** -- `igny8_keywords` - Keyword research and analysis data -- `igny8_tasks` - Content creation and writing tasks -- `igny8_data` - General plugin data and configurations -- `igny8_variations` - Content variations and personalization data -- `igny8_rankings` - SEO ranking and performance data -- `igny8_suggestions` - Optimization suggestions and recommendations -- `igny8_campaigns` - Link building and marketing campaigns -- `igny8_content_ideas` - AI-generated content ideas and concepts -- `igny8_clusters` - Keyword clusters and semantic groupings -- `igny8_sites` - Target sites and domain information -- `igny8_backlinks` - Backlink tracking and analysis data -- `igny8_mapping` - Data relationships and mappings -- `igny8_prompts` - AI prompts and templates -- `igny8_logs` - System logs and debugging information -- `igny8_ai_queue` - AI processing queue and task management - -### 3.2 Database Features -- **Migration System**: Complete version control and data migration -- **Data Validation**: Comprehensive data integrity and validation -- **Performance Optimization**: Indexed queries and optimized operations -- **Backup & Recovery**: Automated backup and disaster recovery -- **Data Relationships**: Foreign key constraints and data integrity - -### 3.3 WordPress Integration -- **Post Meta**: Advanced post metadata management -- **Taxonomies**: Custom taxonomies (sectors, clusters) -- **User Management**: Role-based access control -- **Options API**: Plugin settings and configuration -- **Transients**: Caching and performance optimization - ---- - -## 4. AI INTEGRATION STATUS - -### 4.1 OpenAI Integration -- **API Client**: Complete OpenAI API integration -- **Models Supported**: GPT-4, GPT-3.5-turbo, GPT-4-turbo -- **Features**: Content generation, keyword clustering, idea generation -- **Functions**: 15+ OpenAI functions -- **Cost Tracking**: API usage monitoring and cost optimization -- **Error Handling**: Robust error handling and recovery - -### 4.2 Runware Integration -- **API Client**: Complete Runware API integration -- **Image Generation**: AI-powered image creation -- **Features**: Multi-size image generation, responsive images -- **Functions**: 10+ Runware functions -- **Image Processing**: Automated image processing and optimization -- **Integration**: WordPress media library integration - -### 4.3 AI Workflow Integration -- **Content Generation**: Automated AI content creation -- **Image Generation**: Automated AI image creation -- **Personalization**: AI-powered content personalization -- **Optimization**: AI-driven content optimization -- **Analytics**: AI-powered performance analysis - ---- - -## 5. WORKFLOW AUTOMATION STATUS ⚠️ CRITICAL ISSUES - -### 5.1 CRON System -- **Master Dispatcher**: Centralized CRON job management -- **Job Handlers**: 10+ specialized CRON handlers -- **Scheduling**: Flexible task scheduling and management -- **Monitoring**: Health monitoring and performance tracking -- **Error Handling**: ⚠️ INCONSISTENT error handling between cron and manual functions -- **Status**: 🔴 HIGH RISK - Cron functions may fail due to architectural differences - -### 5.2 Automation Workflows -- **Content Planning**: Automated keyword research and clustering -- **Content Creation**: Automated content generation and publishing -- **SEO Optimization**: Automated content optimization and monitoring -- **Link Building**: Automated outreach and backlink tracking -- **Personalization**: Automated content personalization -- **Analytics**: Automated reporting and performance tracking - -### 5.3 Process Automation -- **Task Management**: Automated task creation and assignment -- **Content Processing**: Automated content generation and optimization -- **Publishing**: Automated content publishing and distribution -- **Monitoring**: Automated performance monitoring and alerting -- **Maintenance**: Automated system maintenance and optimization - ---- - -## 6. ADMIN INTERFACE STATUS - -### 6.1 User Interface Components -- **Dashboard**: Comprehensive dashboard with KPI tracking -- **Data Tables**: Advanced data tables with sorting and filtering -- **Forms**: Dynamic forms with validation and AJAX -- **Modals**: Import/export modals with progress tracking -- **Navigation**: Intuitive navigation with breadcrumbs -- **Responsive Design**: Mobile-optimized responsive design - -### 6.2 Module Interfaces -- **Planner Interface**: Keyword research and clustering interface -- **Writer Interface**: Content creation and management interface -- **Optimizer Interface**: SEO analysis and optimization interface -- **Linker Interface**: Backlink management and campaign interface -- **Personalize Interface**: Content personalization interface -- **Thinker Interface**: AI strategy and prompt management interface -- **Analytics Interface**: Performance tracking and reporting interface -- **Schedules Interface**: Automation management interface - -### 6.3 Component System -- **Reusable Components**: Table, form, filter, pagination components -- **Configuration System**: Dynamic configuration management -- **Template System**: Flexible template rendering system -- **Asset Management**: Optimized asset loading and caching - ---- - -## 7. FRONTEND INTEGRATION STATUS - -### 7.1 Personalization System -- **Shortcode Integration**: `[igny8]` shortcode for content personalization -- **Display Modes**: Button, inline, and automatic personalization modes -- **User Interface**: Customizable personalization forms and interfaces -- **Responsive Design**: Mobile-optimized personalization experience -- **Performance**: Fast-loading personalization features - -### 7.2 Frontend Assets -- **JavaScript**: Core functionality and AJAX handling -- **CSS**: Responsive design and styling -- **Templates**: Frontend template system -- **Media**: Image and media handling -- **Performance**: Optimized asset delivery - -### 7.3 User Experience -- **Personalization**: AI-powered content personalization -- **Responsive Design**: Mobile-first responsive design -- **Performance**: Optimized loading and performance -- **Accessibility**: WCAG compliant accessibility features -- **Integration**: Seamless WordPress theme integration - ---- - -## 8. ANALYTICS & REPORTING STATUS - -### 8.1 KPI Tracking -- **Performance Metrics**: Comprehensive performance tracking -- **Dashboard Analytics**: Real-time dashboard analytics -- **Trend Analysis**: Performance trend identification -- **Comparative Analysis**: Period-over-period comparison -- **Predictive Analytics**: AI-powered performance prediction - -### 8.2 Reporting System -- **Automated Reports**: Scheduled performance reports -- **Custom Reports**: User-defined report creation -- **Export Functionality**: Multiple export formats -- **Report Scheduling**: Automated report delivery -- **Report Analytics**: Report usage and effectiveness tracking - -### 8.3 Data Visualization -- **Interactive Charts**: Dynamic performance charts -- **Dashboard Customization**: Personalized dashboard configuration -- **Real-time Updates**: Live performance data updates -- **Visual Analytics**: Advanced data visualization -- **Performance Insights**: AI-powered performance insights - ---- - -## 9. SECURITY & PERFORMANCE STATUS - -### 9.1 Security Features -- **Data Sanitization**: Comprehensive input sanitization -- **Nonce Verification**: WordPress nonce security -- **User Permissions**: Role-based access control -- **API Security**: Secure API communication -- **Data Encryption**: Sensitive data encryption - -### 9.2 Performance Optimization -- **Database Optimization**: Optimized queries and indexing -- **Caching System**: Advanced caching and performance optimization -- **Asset Optimization**: Minified and optimized assets -- **API Optimization**: Efficient API usage and rate limiting -- **Memory Management**: Optimized memory usage and garbage collection - -### 9.3 Error Handling -- **Robust Error Handling**: Comprehensive error handling and recovery -- **Logging System**: Advanced logging and debugging -- **Monitoring**: System health monitoring and alerting -- **Recovery**: Automated error recovery and system restoration -- **Debugging**: Advanced debugging and troubleshooting tools - ---- - -## 10. INTEGRATION STATUS - -### 10.1 WordPress Integration -- **Core Integration**: Complete WordPress core integration -- **Hook System**: WordPress hooks, actions, and filters -- **Post System**: Advanced post metadata management -- **User System**: User role and permission management -- **Theme Integration**: Seamless theme integration - -### 10.2 External Integrations -- **OpenAI API**: Advanced AI content generation -- **Runware API**: AI-powered image generation -- **SEO APIs**: External SEO service integration -- **Analytics APIs**: Performance tracking integration -- **Social APIs**: Social media integration - -### 10.3 Data Integration -- **CSV Import/Export**: Comprehensive data portability -- **API Integration**: RESTful API integration -- **Webhook Support**: Real-time data synchronization -- **Data Synchronization**: Multi-platform data consistency -- **Custom Integrations**: Flexible integration development - ---- - -## 11. CURRENT CAPABILITIES - -### 11.1 Content Management -- **AI Content Generation**: Automated content creation using OpenAI -- **Image Generation**: AI-powered image creation using Runware -- **Content Optimization**: SEO-optimized content generation -- **Content Personalization**: AI-powered content personalization -- **Content Publishing**: Automated content publishing and distribution - -### 11.2 SEO Optimization -- **Keyword Research**: Advanced keyword research and analysis -- **Content Audits**: Comprehensive SEO content audits -- **Optimization Suggestions**: AI-powered optimization recommendations -- **Performance Monitoring**: Real-time SEO performance tracking -- **Ranking Tracking**: Keyword ranking monitoring and analysis - -### 11.3 Link Building -- **Backlink Tracking**: Automated backlink detection and analysis -- **Campaign Management**: Strategic link building campaign management -- **Outreach Automation**: Automated outreach and follow-up systems -- **Authority Building**: Long-term domain authority building -- **Relationship Management**: Influencer and industry relationship management - -### 11.4 Analytics & Reporting -- **Performance Tracking**: Comprehensive performance metrics tracking -- **KPI Monitoring**: Key performance indicator monitoring -- **Trend Analysis**: Performance trend identification and analysis -- **Predictive Analytics**: AI-powered performance prediction -- **Custom Reporting**: User-defined report creation and scheduling - ---- - -## 12. TECHNICAL SPECIFICATIONS - -### 12.1 System Requirements -- **WordPress**: 5.0+ (Core platform) -- **PHP**: 7.4+ (Server-side language) -- **MySQL**: 5.7+ (Database system) -- **JavaScript**: ES6+ (Client-side functionality) -- **cURL**: HTTP client for API communication -- **JSON**: Data format for AI communication - -### 12.2 Performance Specifications -- **Database**: 15 custom tables with optimized queries -- **Memory Usage**: Optimized memory usage and garbage collection -- **API Limits**: Efficient API usage and rate limiting -- **Caching**: Advanced caching and performance optimization -- **Asset Delivery**: Optimized asset loading and delivery - -### 12.3 Security Specifications -- **Data Sanitization**: Comprehensive input sanitization -- **Nonce Verification**: WordPress nonce security -- **User Permissions**: Role-based access control -- **API Security**: Secure API communication -- **Data Encryption**: Sensitive data encryption - ---- - -## 13. FUTURE ROADMAP - -### 13.1 Planned Features -- **Advanced AI Models**: Integration with additional AI models -- **Enhanced Analytics**: Advanced analytics and reporting features -- **Mobile App**: Mobile application for content management -- **API Expansion**: Extended API capabilities -- **Third-party Integrations**: Additional third-party service integrations - -### 13.2 Performance Improvements -- **Database Optimization**: Further database performance optimization -- **Caching Enhancement**: Advanced caching and performance optimization -- **API Optimization**: Further API usage optimization -- **Asset Optimization**: Enhanced asset optimization -- **Memory Optimization**: Advanced memory usage optimization - -### 13.3 Security Enhancements -- **Advanced Security**: Enhanced security features -- **Data Protection**: Advanced data protection and privacy -- **Compliance**: Industry standard compliance and security -- **Audit Logging**: Enhanced audit logging and monitoring -- **Access Control**: Advanced access control and permissions - ---- - -## 14. SUPPORT & MAINTENANCE - -### 14.1 Support System -- **Documentation**: Comprehensive documentation and guides -- **Help System**: Integrated help system and tutorials -- **Community**: Community support and forums -- **Professional Support**: Enterprise-level support and maintenance -- **Training**: User training and onboarding - -### 14.2 Maintenance -- **Regular Updates**: Regular plugin updates and improvements -- **Security Updates**: Security updates and patches -- **Performance Optimization**: Continuous performance optimization -- **Bug Fixes**: Bug fixes and issue resolution -- **Feature Enhancements**: New feature development and enhancement - -### 14.3 Monitoring -- **System Health**: Continuous system health monitoring -- **Performance Tracking**: Performance monitoring and optimization -- **Error Tracking**: Error tracking and resolution -- **Usage Analytics**: Usage analytics and optimization -- **User Feedback**: User feedback collection and implementation - ---- - -## 15. CONCLUSION - -The Igny8 AI SEO Plugin v0.1 represents a comprehensive, AI-powered content management and SEO optimization platform with **COMPLETE REFACTOR IMPLEMENTED**. With 8 active modules, 200+ functions, and advanced AI integration, the system provides: - -- **Complete Content Management**: From planning to publishing ✅ -- **Advanced AI Integration**: OpenAI and Runware API integration ✅ -- **Comprehensive SEO Tools**: Keyword research to performance monitoring ✅ -- **Automated Workflows**: ⚠️ END-TO-END PROCESS AUTOMATION AT RISK -- **Advanced Analytics**: Performance tracking and reporting ✅ -- **Scalable Architecture**: Modular, extensible design ✅ -- **Production Ready**: ⚠️ MANUAL FUNCTIONS STABLE, CRON FUNCTIONS UNSTABLE - -### Critical Status Summary -- **Manual Functions**: ✅ Fully operational and healthy -- **Cron Functions**: ❌ High risk of failure due to architectural discrepancies -- **Recommendation**: 🔴 IMMEDIATE action required to align cron functions with manual counterparts -- **Priority**: CRITICAL - Automation system reliability compromised - -The system requires immediate attention to resolve cron vs manual function discrepancies before automation can be considered reliable for production use. - ---- - -## File Structure Summary - -### Core Files -- `igny8.php` - Main plugin file -- `install.php` - Installation script -- `uninstall.php` - Uninstallation script -- `igny8-wp-load-handler.php` - WordPress load handler - -### Module Files -- `modules/modules-pages/` - Module interfaces -- `modules/components/` - Reusable components -- `modules/config/` - Configuration files - -### AI Integration -- `ai/integration.php` - AI service integration -- `ai/openai-api.php` - OpenAI API client -- `ai/runware-api.php` - Runware API client -- `ai/modules-ai.php` - AI module functions - -### Core System -- `core/admin/` - Admin interface -- `core/db/` - Database management -- `core/cron/` - Automation system -- `core/pages/` - Page templates - -### Workflows -- `flows/` - Workflow automation -- `assets/` - Frontend assets -- `docs/` - Documentation - -This snapshot provides a complete overview of the Igny8 AI SEO Plugin's current state, capabilities, and technical specifications, including critical automation issues that require immediate attention. - diff --git a/igny8-ai-seo-wp-plugin/docs/Igny8 WP Plugin to Igny8 App Migration Plan.docx b/igny8-ai-seo-wp-plugin/docs/Igny8 WP Plugin to Igny8 App Migration Plan.docx deleted file mode 100644 index 0bf54a18..00000000 Binary files a/igny8-ai-seo-wp-plugin/docs/Igny8 WP Plugin to Igny8 App Migration Plan.docx and /dev/null differ diff --git a/igny8-ai-seo-wp-plugin/docs/MIGRATION_PLAN_EXTRACTED.txt b/igny8-ai-seo-wp-plugin/docs/MIGRATION_PLAN_EXTRACTED.txt deleted file mode 100644 index a4d27d44..00000000 --- a/igny8-ai-seo-wp-plugin/docs/MIGRATION_PLAN_EXTRACTED.txt +++ /dev/null @@ -1,524 +0,0 @@ -IGNY8 WP PLUGIN TO IGNY8 APP MIGRATION PLAN -============================================ - -SECTION 1 - CODEBASE & DIRECTORY STRUCTURE (IGNY8 APP) -Goal: Design the top-level and module-level folder hierarchy for the new Django + React (Vite + Tailwind) full-stack IGNY8 App, preserving 1:1 parity with the current WordPress plugin modules while introducing clear API, automation, and AI boundaries. - -1.1 Top-Level Repository Layout -igny8-app/ - backend/ # Django backend (core API, models, automation) - igny8_core/ # Main Django app (replaces plugin core) - __init__.py - settings.py # Env-aware settings (local / staging / prod) - urls.py - wsgi.py - asgi.py - modules/ # 1:1 migration of plugin modules - planner/ - models.py - views.py - serializers.py - tasks.py # CRON/async/queue handlers - urls.py - writer/ - thinker/ - ... (future: linker, optimizer, etc.) - api/ # Unified REST entrypoints - routes/ - ai/ - content.py - images.py - publish.py - reparse.py - system/ - middleware/ - auth/ # Multi-tenant + RBAC + Stripe linkage - models.py - roles.py - views.py - signals.py - serializers.py - utils/ # Shared helpers (AI, logs, etc.) - cron/ # Timed jobs (mapped from plugin CRON) - migrations/ - manage.py - frontend/ # React + Vite + Tailwind UI - src/ - app/ - globals.css - layout.tsx - pages/ # Page-per-module parity with WP admin - PlannerPage.tsx - WriterPage.tsx - ThinkerPage.tsx - DashboardPage.tsx - components/ - ui/ # Local UI library (Card, Button, etc.) - layout/ - charts/ - hooks/ - services/ # API clients (axios/fetch for each module) - store/ # Zustand/Redux state - vite.config.js - package.json - tsconfig.json - infra/ # Docker, Portainer, Caddy, Postgres, Redis - docker-compose.yml - caddy/ - scripts/ - dev-config.yml - README.md - docs/ - MIGRATION_PLAN.md - API_REFERENCE.md - DATABASE_MAP.md - .env / .env.dev / .env.prod # environment-specific configs - -1.2 Key Design Rules -- Modules: Each former WordPress module = isolated Django app inside /backend/modules/ -- API Layer: Use /backend/api/routes/{domain}/ for all external triggers -- Frontend: Page = Module parity (PlannerPage.tsx, etc.) -- UI Library: /frontend/src/components/ui/ replaces @/components/ui/ from TailAdmin -- DevOps: Single infra/docker-compose.yml defines the full stack -- Roles/Billing: /backend/auth/roles.py + Stripe integration -- Docs: /docs folder in-repo - -1.3 Role-Based Access (RBAC) Foundation -Roles: -- Owner: Full access to all tenants, billing, and automation -- Admin: Manage content modules, view billing but not edit -- Editor: Generate AI content, manage clusters/tasks -- Viewer: Read-only dashboards -- SystemBot: Automation + CRON actions (No UI, API-only) - -1.4 AI-Assisted Dev Environment (Cursor / Local PC) -- Expose selective sub-repos /backend/modules/, /frontend/src/, /docs/ to GitHub/GitLab -- Keep /infra & .env excluded (.gitignore) for security -- AI Context Indexing: Each module includes README.md describing schema, routes, and functions -- Dockerized Dev Setup: Cursor runs Node + Python containers via docker-compose -- Source Sync Hooks: Git hooks ensure migrations and schema docs stay synchronized - - -SECTION 2 - DATABASE & SCHEMA MIGRATION PLAN -Goal: Migrate all IGNY8 custom database tables and essential data from WordPress to a Django + PostgreSQL schema. - -2.1 IGNY8 Core Tables to Migrate 1:1 -WP Table → Django Model: -- igny8_keywords → Keywords -- igny8_clusters → Clusters -- igny8_content_ideas → ContentIdeas -- igny8_tasks → Tasks -- igny8_variations → Variations -- igny8_prompts → Prompts -- igny8_logs → Logs -- igny8_images → Images -- igny8_schedules → Schedules -- igny8_settings → Settings (Split into SystemSettings and UserSettings) -- igny8_accounts → Accounts -- igny8_links → Links (Future) -- igny8_metrics → Metrics - -2.2 WordPress Default Tables (Partial Extraction) -- wp_posts → WPPosts (ID, post_title, post_content, post_status, post_type, post_date) -- wp_postmeta → WPPostMeta (post_id, meta_key, meta_value - only IGNY8 keys) -- wp_users → WPUserMap (ID, user_email, display_name) -- wp_options → WPOptions (option_name, option_value - plugin config only) - -2.3 Cross-System Integration Map -- Keywords/Clusters: App → WP via WP REST API -- Tasks/Content: App → WP via WP /wp-json/igny8/v1/import-content -- Images: App → WP via WP Media REST endpoint -- Settings: Bidirectional sync -- Logs/Metrics: App-only - -2.4 Schema Example: Tasks Model (Django ORM) -Includes: id, cluster, idea, task_type, ai_model, prompt, raw_ai_response, status, result, timestamps, user, tenant - -2.5 Database Migration & Sync Process -Steps: Export → Create Django models → Import data → Verify → Migrate WP data → Run integrity tests → Enable periodic sync - -2.6 Tenant-Aware Model Design -Every model inherits from TenantBaseModel with tenant, created_at, updated_at fields. - - -SECTION 3 - COMPONENT & UI MAPPING PLAN -Goal: Rebuild the WordPress plugin's admin interface as React/Vite pages. - -3.1 One-to-One Module → Page Mapping -- Planner Dashboard → PlannerPage.tsx (/planner) -- Writer Dashboard → WriterPage.tsx (/writer) -- Thinker → ThinkerPage.tsx (/thinker) -- Optimizer (Phase 2) → OptimizerPage.tsx (/optimizer) -- Linker (Phase 2) → LinkerPage.tsx (/linker) -- Settings → SettingsPage.tsx (/settings) -- Dashboard → DashboardPage.tsx (/dashboard) - -3.2 Shared Frontend Component Map -- DataTable, FilterPanel, FormModal, ActionButtons, StatsCards, ToastNotifications, AIStatusBar, ConfirmDialog, TenantSwitcher - -3.3 UI Data Flow Diagram (Per Module) -User → React Page → Zustand Store → API Service → Django API → AI Processing → Database → React Refresh - -3.4 State Management Rules (Zustand) -Each module defines its store following the same interface pattern. - - -SECTION 4 - API ROUTES & AUTOMATION TRIGGERS -Goal: Define all Django REST API endpoints and automation triggers. - -4.1 Unified API Structure -- /api/planner/ - Keyword → Cluster → Idea generation -- /api/writer/ - Content generation, reparsing, image injection -- /api/thinker/ - Prompt testing, image model handling -- /api/settings/ - Model rates, limits, keys, Stripe data -- /api/system/ - Metrics, logs, and CRON control -- /api/auth/ - Tenant registration, login, roles -- /api/publish/ - Push content/images to WP site - -4.2 Example Endpoint Tree -Detailed endpoint listing for each module with GET/POST/DELETE methods - -4.3 API → AI Execution Chain -Common AI service layer (/backend/utils/ai.py) handles all AI requests - -4.4 Automation Triggers (Replacing WordPress CRON) -- auto_cluster_keywords: every 6h -- auto_generate_ideas: daily -- auto_generate_content: hourly -- auto_generate_images: hourly -- auto_publish_posts: daily -- cleanup_logs: weekly - -4.5 Queue Control Logic -Shared queue core (/backend/utils/queue_manager.py) for automation and manual triggers - -4.6 Stripe Integration (API) -Endpoints for billing, subscription, webhooks - -4.7 AI Key & Model Management -Configured through /api/settings/ai-models - -4.8 Example: /api/writer/generate/ Workflow -8-step process from React frontend to WordPress publish - -4.9 Authentication & Role Guard Middleware -All endpoints protected by RoleRequired decorator - -4.10 CRON & API Coherence -CRON functions reuse the same endpoints as manual buttons - - -SECTION 5 - WORDPRESS INTEGRATION & SYNC LAYER -Goal: Design a clean, secure bridge between Django backend and WordPress sites. - -5.1 Integration Overview -- AI Content Publish: App → WP -- Image Upload: App → WP -- Settings Sync: Bidirectional -- Post Status Check: WP → App -- Keyword/Cluster Sync: App → WP -- Auth Link: Manual JWT-based connection - -5.2 Connection Setup (One-Time Auth Handshake) -WPIntegration model stores site_url, api_key, tenant, status, last_sync - -5.3 WordPress REST Endpoints (Added to Plugin) -- /wp-json/igny8/v1/import-content -- /wp-json/igny8/v1/upload-image -- /wp-json/igny8/v1/sync-settings -- /wp-json/igny8/v1/status -- /wp-json/igny8/v1/pull-updates - -5.4 Django → WordPress Communication Flow -Example publishing workflow with request/response structure - -5.5 WordPress Plugin: Receiving Side Implementation -Minimalistic handler example code - -5.6 Bidirectional Settings Sync -Field mapping between App and WP - -5.7 Multi-Tenant Integration Mapping -Each tenant can connect multiple WP sites - -5.8 Sync Safety & Rate Control Layer -Queue throttling, error handling, fallback mode, audit logs - -5.9 Security Model -JWT with 12h expiry, HMAC signatures, CORS whitelist - -5.10 Example: Full Publish & Feedback Cycle -Complete workflow from user action to periodic sync - -5.11 Future Extension (Phase-2+) -Planned integrations for metrics, links, user data, schema - - -SECTION 6 - DEPLOYMENT, ENVIRONMENT & LOCAL DEV PLAN -Goal: Define complete development, deployment, and synchronization environment. - -6.1 Full Stack Architecture Summary -- Reverse Proxy: Caddy (80/443) -- Frontend: React (Vite) + Node 20 (8020→8021) -- Backend: Django + Gunicorn (8010→8011) -- Database: PostgreSQL 15 (5432) -- Cache/Queue: Redis 7 (6379) -- Admin DB UI: pgAdmin4 (5050) -- File Manager: Filebrowser (8080) -- Docker Manager: Portainer CE (9443/8000) - -6.2 Environment File (.env) -Variables for DB, Redis, AI keys, Stripe, domains - -6.3 Docker Compose Structure -Service definitions with volumes, networks, environment variables - -6.4 Local Development Setup (Cursor AI) -Steps for local development with live mounts - -6.5 Portainer Stack Deployment -Production deployment via Portainer stacks - -6.6 Environment-Specific Configs -Separate configs for local, staging, production - -6.7 Backup & Recovery Procedures -Automated backups for database, media, configs - - -SECTION 7 - DATA MIGRATION & SYNC STRATEGY -Goal: Extract, transform, and import all IGNY8 plugin data from WordPress MySQL to PostgreSQL. - -7.1 Migration Overview -Extract from WP → Transform schema → Import to Django → Validate → Sync - -7.2 Table Mapping Details -Complete mapping of all WP tables to Django models - -7.3 Migration Phases -1. Extraction (Dump plugin tables) -2. Transformation (Convert to Postgres schema) -3. Import (Bulk insert via Django) -4. Validation (Compare counts, hashes) -5. Sync (Enable real-time sync to WP) - -7.4 Extraction Script Example -Python script using mysql.connector - -7.5 Transformation Example -Data transformation script - -7.6 Import to Django -Django management command for bulk import - -7.7 Verification Step -Comparison script for validation - -7.8 Syncable Tables (Remain Linked to WP) -Tables that maintain bidirectional sync - -7.9 Migration Validation Dashboard -UI section showing migration status - -7.10 Rollback Strategy -Procedure for handling migration failures - -7.11 Final Verification Checklist -Checkpoints for successful migration - -7.12 Post-Migration Tasks -Deactivate old plugin CRON, update plugin config to Remote Mode - - -SECTION 8 - TENANCY, BILLING & USER ACCESS MODEL -Goal: Define multi-tenant access, workspace isolation, role permissions, and subscription billing. - -8.1 Core Concepts -- Tenant: Logical workspace -- User: Authenticated account inside tenant -- Subscription: Stripe-linked billing plan -- Workspace: UI grouping under tenant -- Site Integration: Connected WordPress instances - -8.2 Data Model Overview -Django Models: Tenant, User, Plan, Subscription - -8.3 Stripe Integration Workflow -6-step process from plan selection to webhook handling - -8.4 Credit System Logic -Credit costs per action: -- Keyword clustering: 1 credit / 30 keywords -- Content idea generation: 1 credit / idea -- Full blog content: 3 credits -- AI image generation: 1 credit / image -- Reparse content: 1 credit -- Auto publish: Free if already generated - -8.5 Roles & Access Permissions -Owner, Admin, Editor, Viewer roles with specific permissions - -8.6 Tenant Isolation Enforcement -Database-level, file-level, API-level, worker-level isolation - -8.7 Tenant Dashboard Layout (Frontend) -React components for tenant management - -8.8 Billing Plans (Finalized Baseline) -- Free/Trial: $1, 25 credits, 1 user, 1 site -- Starter: $39, 200 credits, 3 users, 3 sites -- Pro: $89, 600 credits, 5 users, 5 sites -- Agency: $199, 2000 credits, 15 users, 10 sites -- Custom/Enterprise: Variable - -8.9 Webhooks for Sync -Stripe and WordPress webhook handlers - -8.10 Suspended Tenant Behavior -Handling payment failures - -8.11 Multi-Tenant CRON Scheduler -Per-tenant queue system - -8.12 Cross-Module Tenant Integration -How each module enforces tenant boundaries - -8.13 Stripe Integration Files -Backend implementation structure - -8.14 Security Enforcement -Authentication, authorization, webhook validation, file access, admin audit - -8.15 Example Flow - New Tenant Signup -Complete signup to activation workflow - - -SECTION 9 - AI AUTOMATION PIPELINE & TASK ENGINE -Goal: Establish unified, tenant-aware automation system for all AI-based tasks. - -9.1 Core Design Principles -- Single-Item Execution -- Tenant Isolation -- Unified Scheduler -- Configurable Limits -- Recoverable Jobs - -9.2 AI Task Types & Flow -- Planner: Keyword Clustering → Idea Generation -- Writer: AI Draft Generation → Reparse → Publish -- Thinker: Prompt Creation / Persona / Strategy -- Image Generator: DALL-E / Runware image tasks -- Linker (Phase-2): Backlink planner - -9.3 Queue Architecture (Redis-Backed) -Redis 7.x with Celery or django-q, tenant-based queue naming - -9.4 AI Execution Workflow -Scheduler → Redis Queue → Django Worker → AI Engine API → Parser Layer → DB + Logs - -9.5 Execution Limits & Global Settings -Parameters for AI_MAX_ITEMS_PER_REQUEST, batch sizes, timeouts, retries, cost caps - -9.6 Task Lifecycle (Writer Example) -Queued → Dispatch → AI Request → Response Handling → Validation → Storage → Optional Actions - -9.7 CRON Automation Flows -- cron_auto_cluster(): 6h -- cron_auto_ideas(): 6h -- cron_auto_writer(): 3h -- cron_auto_image(): 3h -- cron_auto_publish(): 12h -- cron_cleanup_logs(): 24h - -9.8 AI Model Routing Logic -Model selector with auto-balancing and fallback - -9.9 AI Pipeline Directory Map -Backend module structure for AI pipeline - -9.10 Credit Deduction Example -Credit deduction logic - -9.11 Error Recovery Flow -Handling timeouts, invalid JSON, parser failures, sync failures, credit errors - -9.12 Frontend Task Control (React Components) -TaskQueueTable, TaskControlPanel, AutomationConfigForm with WebSocket feed - -9.13 Monitoring & Telemetry -Structured logging with tenant, task_type, status, duration, model, credits_spent, output_size - -9.14 Local AI Dev Mode (Cursor-Ready) -Development configuration for local testing - -9.15 Verification Checklist -Checkpoints for queue, worker, task enqueue, credit deduction, AI response, CRON logs - -9.16 Future Enhancements -Parallel pipelines, semantic caching, rate shaping, cost anomaly alerts, tracing integration - - -SECTION 10 - SECURITY, LOGGING & MONITORING FRAMEWORK -Goal: Define full-stack security, audit, and observability framework. - -10.1 Security Architecture Overview -Layers: Frontend, Backend, Database, AI Layer, Infrastructure, Integrations - -10.2 Authentication & Token System -JWT authentication with tenant context injection, role verification, session expiry - -10.3 Authorization & Role-Based Rules -Role access scope definitions - -10.4 Secure API Architecture -Endpoint pattern, JWT verification, tenant match, role access, input validation - -10.5 Stripe & Webhook Security -HMAC validation for webhooks - -10.6 Data Encryption & Storage Policy -Encryption for credentials/keys, storage policies for AI responses, files, backups, logs - -10.7 API Rate Limiting -Per tenant (30 req/min), per IP (100 req/min), per worker (1 AI request/iteration), per model (60 req/min) - -10.8 Logging System Overview -Centralized logging: App → Django Logger → Postgres → Loki Stream → Grafana - -10.9 Monitoring & Alerts Stack -Tools: Portainer, Loki, Grafana, Alertmanager, Redis Inspector -Custom alerts for AI timeout, failed tasks, Stripe failures, CRON lag - -10.10 Data Backup & Recovery -Backup frequencies and retention: -- Postgres DB: Every 6 hours, 7 days retention -- FileBrowser/Media: Daily, 14 days -- Caddy/Config: Daily, 7 days -- Logs (Loki): Rolling window, 30 days - -10.11 Error & Exception Handling -Handling for AI API errors, DB write errors, worker crashes, CRON failures, user API errors - -10.12 Developer Audit Trail -Critical system events logged in igny8_audit with 1 year retention - -10.13 Local Dev & Security Mirrors -Development configuration with AI_DEV_MODE, CORS settings, mock data - -10.14 Security Verification Checklist -Checkpoints for HTTPS, JWT validation, tenant isolation, webhook verification, file access, backups, Redis security, network isolation, rate limiting - -10.15 Future Enhancements -OAuth 2.0 login, tenant-level dashboards, ElasticSearch sink, offsite sync, smart anomaly detection - -10.16 Final Summary -Security model fully container-aware and tenant-isolated -Logging & metrics unified under Loki + Grafana -Stripe billing, AI cost tracking, and access control audited -Developer-friendly dev mode supported -Production deployment validated under current Docker infrastructure - - -END OF DOCUMENT - - diff --git a/igny8-ai-seo-wp-plugin/docs/TROUBLESHOOTING_Converting_to_blocks_and_image_shortcode_injection.md b/igny8-ai-seo-wp-plugin/docs/TROUBLESHOOTING_Converting_to_blocks_and_image_shortcode_injection.md deleted file mode 100644 index 1a12e54b..00000000 --- a/igny8-ai-seo-wp-plugin/docs/TROUBLESHOOTING_Converting_to_blocks_and_image_shortcode_injection.md +++ /dev/null @@ -1,308 +0,0 @@ -# TROUBLESHOOTING: Converting to Blocks and Image Shortcode Injection - -**Date:** October 26, 2025 -**Session Duration:** Extended debugging session -**Issue:** AI-generated content failing to save due to malformed heading blocks and shortcode injection failures -**Status:** ✅ **RESOLVED** - ---- - -## 🚨 **PROBLEM SUMMARY** - -The AI content generation pipeline was failing at the post creation stage due to: - -1. **Malformed Heading Blocks**: `igny8_convert_to_wp_blocks()` was creating heading blocks without required `level` attributes -2. **Shortcode Injection Failure**: `insert_igny8_shortcode_blocks_into_blocks()` was skipping all heading blocks due to missing level attributes -3. **Complete Pipeline Failure**: Post creation was aborting when shortcode injection failed -4. **Incorrect Task Status**: Tasks were being marked as 'failed' instead of staying in draft status - -### **Debug Log Evidence:** -``` -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Skipping heading block #4 — missing 'level' attribute -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Skipping heading block #18 — missing 'level' attribute -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Skipping heading block #36 — missing 'level' attribute -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Skipping heading block #54 — missing 'level' attribute -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: ❌ Shortcode injection failed — no blocks found after serialization -[26-Oct-2025 11:23:00 UTC] IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in parsed blocks -[26-Oct-2025 11:23:00 UTC] IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN AJAX.PHP - igny8_create_post_from_ai_response() returned: false -``` - ---- - -## 🔍 **ROOT CAUSE ANALYSIS** - -### **Primary Issue: Missing Level Attributes** -The `igny8_convert_to_wp_blocks()` function was generating heading blocks like this: -```php -// WRONG - Missing level attribute -"\n

          Title

          \n" -``` - -Instead of: -```php -// CORRECT - With level attribute -"\n

          Title

          \n" -``` - -### **Secondary Issue: No Fallback Logic** -When shortcode injection failed, the entire post creation process was aborted with `return false`, preventing any content from being saved. - -### **Tertiary Issue: Incorrect Task Status Management** -Tasks were being marked as 'failed' when post creation failed, instead of remaining in draft status for retry. - ---- - -## 🛠️ **SOLUTIONS IMPLEMENTED** - -### **1. Fixed Block Conversion Function** - -**File:** `core/admin/ajax.php` (lines 4722-4727) - -**Problem:** H2 headings missing level attributes -```php -// BEFORE (WRONG) -if (preg_match('/^]*>(.*?)<\/h\1>$/is', $block, $m)) { - $blocks[] = "\n

          {$m[2]}

          \n"; -} -``` - -**Solution:** Added level attribute to all heading blocks -```php -// AFTER (FIXED) -if (preg_match('/^]*>(.*?)<\/h\1>$/is', $block, $m)) { - $blocks[] = "\n

          {$m[2]}

          \n"; -} -``` - -### **2. Added Block Structure Validation** - -**File:** `ai/modules-ai.php` (lines 1795-1827) - -**New Function:** `igny8_validate_and_fix_blocks()` -```php -function igny8_validate_and_fix_blocks($block_content) { - if (empty($block_content)) { - return $block_content; - } - - $blocks = parse_blocks($block_content); - $fixed_blocks = []; - - foreach ($blocks as $index => $block) { - // Fix heading blocks missing level attribute - if (($block['blockName'] ?? null) === 'core/heading') { - $level = $block['attrs']['level'] ?? null; - - if ($level === null) { - // Try to extract level from innerHTML - $inner_html = $block['innerHTML'] ?? ''; - if (preg_match('/]*>/i', $inner_html, $matches)) { - $detected_level = intval($matches[1]); - $block['attrs']['level'] = $detected_level; - error_log("IGNY8 BLOCKS: Fixed heading block #$index - detected level $detected_level from innerHTML"); - } else { - // Default to H2 if we can't detect - $block['attrs']['level'] = 2; - error_log("IGNY8 BLOCKS: Fixed heading block #$index - defaulted to level 2"); - } - } - } - - $fixed_blocks[] = $block; - } - - return serialize_blocks($fixed_blocks); -} -``` - -### **3. Enhanced Shortcode Injection Debug Logging** - -**File:** `ai/modules-ai.php` (lines 1862-1904) - -**Added:** Comprehensive debug logging -```php -error_log("IGNY8 BLOCKS: Parsed " . count($blocks) . " total blocks"); - -foreach ($blocks as $index => $block) { - if (($block['blockName'] ?? null) === 'core/heading') { - $heading_blocks_found++; - $level = $block['attrs']['level'] ?? null; - - error_log("IGNY8 BLOCKS: Heading block #$index - level: " . ($level ?? 'NULL') . ", innerHTML: " . substr($block['innerHTML'] ?? '', 0, 50) . "..."); - - if ($level !== 2) { - if ($level === null) { - error_log("IGNY8 BLOCKS: Skipping heading block #$index — missing 'level' attribute"); - } else { - error_log("IGNY8 BLOCKS: Skipping heading block #$index — level $level (not H2)"); - } - continue; - } - - $valid_h2_blocks++; - // ... injection logic - } -} - -error_log("IGNY8 BLOCKS: Summary - Total headings: $heading_blocks_found, Valid H2s: $valid_h2_blocks, Shortcodes injected: $injected"); -``` - -### **4. Implemented Fallback Logic for Post Creation** - -**File:** `ai/modules-ai.php` (lines 1203-1210, 1221-1225) - -**Problem:** Post creation failed completely when shortcode injection failed -```php -// BEFORE (WRONG) -if (!$has_shortcode) { - error_log("IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in parsed blocks"); - igny8_log_ai_event('Shortcode Injection Failed', 'writer', 'content_generation', 'error', 'No shortcodes found after injection', 'Editor type: ' . $editor_type); - return false; // ← This aborted the entire process -} -``` - -**Solution:** Added fallback logic to continue without shortcodes -```php -// AFTER (FIXED) -if (!$has_shortcode) { - error_log("IGNY8 DEBUG - Shortcode injection failed: No shortcodes found in parsed blocks"); - igny8_log_ai_event('Shortcode Injection Failed', 'writer', 'content_generation', 'warning', 'No shortcodes found after injection - proceeding without shortcodes', 'Editor type: ' . $editor_type); - // FALLBACK: Continue with post creation without shortcodes - $content = $final_block_content; -} else { - $content = $final_block_content; -} -``` - -### **5. Fixed Task Status Management** - -**File:** `core/admin/ajax.php` (lines 1968-1972) - -**Problem:** Tasks marked as 'failed' when post creation failed -```php -// BEFORE (WRONG) -} else { - // Update task status to failed if post creation failed - $wpdb->update( - $wpdb->prefix . 'igny8_tasks', - [ - 'status' => 'failed', // ← Wrong! - 'updated_at' => current_time('mysql') - ], - ['id' => $task_id], - ['%s', '%s'], - ['%d'] - ); -} -``` - -**Solution:** Removed status change on failure -```php -// AFTER (FIXED) -} else { - // Log failure but DO NOT change task status - keep it as draft - igny8_log_ai_event('WordPress Post Creation Failed', 'writer', 'content_generation', 'error', 'Failed to create WordPress post from AI content', 'Task ID: ' . $task_id); - igny8_log_ai_event('AI Content Generation Failed', 'writer', 'content_generation', 'error', 'Content generation failed - post creation unsuccessful', 'Task ID: ' . $task_id); -} -``` - ---- - -## 📊 **BEFORE vs AFTER COMPARISON** - -### **Before (Broken Pipeline):** -1. ❌ HTML → Blocks (missing level attributes) -2. ❌ Block Validation (skipped) -3. ❌ Shortcode Injection (failed - no valid H2s) -4. ❌ Post Creation (aborted with `return false`) -5. ❌ Task Status (set to 'failed') - -### **After (Fixed Pipeline):** -1. ✅ HTML → Blocks (with proper level attributes) -2. ✅ Block Validation (auto-fixes malformed blocks) -3. ✅ Shortcode Injection (works with valid H2s) -4. ✅ Post Creation (succeeds with or without shortcodes) -5. ✅ Task Status (only changes on success) - ---- - -## 🔧 **TECHNICAL DETAILS** - -### **Files Modified:** -- `core/admin/ajax.php` - Fixed block conversion function -- `ai/modules-ai.php` - Added validation, enhanced logging, implemented fallbacks - -### **New Functions Added:** -- `igny8_validate_and_fix_blocks()` - Block structure validation and repair - -### **Functions Enhanced:** -- `igny8_convert_to_wp_blocks()` - Added level attributes to headings -- `insert_igny8_shortcode_blocks_into_blocks()` - Enhanced debug logging -- `igny8_create_post_from_ai_response()` - Added fallback logic - -### **Debug Logging Added:** -- Block conversion progress -- Heading block analysis (level, content preview) -- Shortcode injection statistics -- Fallback warnings instead of errors - ---- - -## 🎯 **RESOLUTION VERIFICATION** - -### **Expected Debug Logs (After Fix):** -``` -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Parsed 25 total blocks -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Heading block #4 - level: 2, innerHTML:

          Section Title

          ... -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Heading block #18 - level: 2, innerHTML:

          Another Section

          ... -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Injecting shortcode after H2 #2: [igny8-image id="desktop-2"] [igny8-image id="mobile-2"] -[26-Oct-2025 11:23:00 UTC] IGNY8 BLOCKS: Summary - Total headings: 4, Valid H2s: 4, Shortcodes injected: 3 -[26-Oct-2025 11:23:00 UTC] IGNY8 DEBUG: I AM ACTIVE AND RUNNING IN AJAX.PHP - igny8_create_post_from_ai_response() returned: 123 -``` - -### **Success Criteria Met:** -- ✅ All heading blocks have proper level attributes -- ✅ Shortcode injection works correctly -- ✅ Posts are created successfully -- ✅ Task status only changes on success -- ✅ Comprehensive debug logging available -- ✅ Fallback behavior when shortcodes fail - ---- - -## 🚀 **LESSONS LEARNED** - -### **Key Insights:** -1. **Block Structure Validation is Critical**: Gutenberg blocks require specific attributes to function properly -2. **Fallback Logic is Essential**: Pipeline should continue even when optional features fail -3. **Debug Logging is Invaluable**: Detailed logging helps identify issues quickly -4. **Task Status Management**: Only change status on actual success, not on partial failures - -### **Best Practices Established:** -1. Always validate block structure before processing -2. Implement fallback logic for non-critical features -3. Use comprehensive debug logging for complex pipelines -4. Separate critical failures from optional feature failures -5. Maintain task status integrity for retry scenarios - ---- - -## 📋 **MAINTENANCE NOTES** - -### **Future Considerations:** -- Monitor debug logs for any new block structure issues -- Consider adding more robust HTML parsing for edge cases -- May need to enhance shortcode injection for different content types -- Keep fallback logic in mind for future pipeline additions - -### **Testing Recommendations:** -- Test with various HTML content structures -- Verify shortcode injection with different heading patterns -- Confirm fallback behavior works correctly -- Monitor task status changes in production - ---- - -**Session Completed:** October 26, 2025 -**Status:** ✅ **FULLY RESOLVED** -**Next Steps:** Monitor production logs and verify stability diff --git a/igny8-ai-seo-wp-plugin/docs/_README.php b/igny8-ai-seo-wp-plugin/docs/_README.php deleted file mode 100644 index a17d71c2..00000000 --- a/igny8-ai-seo-wp-plugin/docs/_README.php +++ /dev/null @@ -1,14 +0,0 @@ -]+>', "`n" - $xml = $xml -replace '\s+', " " - $xml = $xml -replace '\n\s*\n', "`n" - - Write-Output $xml.Trim() -} - -$zip.Dispose() - - diff --git a/igny8-ai-seo-wp-plugin/flows/sync-ajax.php b/igny8-ai-seo-wp-plugin/flows/sync-ajax.php deleted file mode 100644 index dbeca0c4..00000000 --- a/igny8-ai-seo-wp-plugin/flows/sync-ajax.php +++ /dev/null @@ -1,485 +0,0 @@ -getMessage()); - } - } -} - - -/** - * AJAX handler for keyword imports with workflow automation - */ -// Hook moved to sync-hooks.php -function igny8_ajax_import_keywords() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - $keywords_data = $_POST['keywords'] ?? []; - - if (empty($keywords_data) || !is_array($keywords_data)) { - wp_send_json_error('No keywords data provided'); - } - - global $wpdb; - $imported_ids = []; - - try { - // Import keywords - foreach ($keywords_data as $keyword_data) { - $sanitized_data = [ - 'keyword' => sanitize_text_field($keyword_data['keyword'] ?? ''), - 'search_volume' => intval($keyword_data['search_volume'] ?? 0), - 'difficulty' => sanitize_text_field($keyword_data['difficulty'] ?? ''), - 'cpc' => floatval($keyword_data['cpc'] ?? 0), - 'intent' => sanitize_text_field($keyword_data['intent'] ?? ''), - 'status' => 'unmapped', - 'created_at' => current_time('mysql'), - 'updated_at' => current_time('mysql') - ]; - - // Skip empty keywords - if (empty($sanitized_data['keyword'])) { - continue; - } - - $result = $wpdb->insert( - $wpdb->prefix . 'igny8_keywords', - $sanitized_data, - ['%s', '%d', '%s', '%f', '%s', '%s', '%s', '%s'] - ); - - if ($result !== false) { - $imported_ids[] = $wpdb->insert_id; - } - } - - if (empty($imported_ids)) { - wp_send_json_error('No keywords were imported'); - } - - // Trigger workflow automation for imported keywords - $workflow_result = igny8_workflow_triggers('keywords_imported', ['keyword_ids' => $imported_ids]); - - // Prepare response - $response = [ - 'message' => 'Keywords imported successfully', - 'imported_count' => count($imported_ids), - 'keyword_ids' => $imported_ids - ]; - - if ($workflow_result && $workflow_result['success']) { - $response['workflow_message'] = $workflow_result['message']; - if (isset($workflow_result['clusters_created'])) { - $response['workflow_data'] = [ - 'clusters_created' => $workflow_result['clusters_created'], - 'cluster_ids' => $workflow_result['cluster_ids'] ?? [] - ]; - } - } - - wp_send_json_success($response); - - } catch (Exception $e) { - wp_send_json_error('Import error: ' . $e->getMessage()); - } -} - -/** - * AJAX handler for creating a single task from an idea - */ -// Hook moved to sync-hooks.php -function igny8_create_task_from_idea_ajax() { - try { - check_ajax_referer('igny8_ajax_nonce', 'nonce'); - if (!current_user_can('edit_posts')) { - wp_send_json_error(['message' => 'Unauthorized']); - } - - $idea_id = isset($_POST['idea_id']) ? absint($_POST['idea_id']) : 0; - if (!$idea_id) { - wp_send_json_error(['message' => 'Invalid idea id']); - } - - $result = igny8_create_task_from_idea($idea_id); - wp_send_json($result); - } catch (Exception $e) { - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} - -/** - * AJAX handler for bulk creating tasks from ideas - */ -// Hook moved to sync-hooks.php -function igny8_bulk_create_tasks_from_ideas_ajax() { - try { - check_ajax_referer('igny8_ajax_nonce', 'nonce'); - if (!current_user_can('edit_posts')) { - wp_send_json_error(['message' => 'Unauthorized']); - } - - $ids = isset($_POST['idea_ids']) ? array_map('absint', (array)$_POST['idea_ids']) : []; - $ids = array_values(array_filter($ids)); - if (empty($ids)) { - wp_send_json_error(['message' => 'No idea ids provided']); - } - - // Check if ideas have status other than 'new' - global $wpdb; - $placeholders = implode(',', array_fill(0, count($ids), '%d')); - $ideas_status = $wpdb->get_results($wpdb->prepare(" - SELECT id, idea_title, status FROM {$wpdb->prefix}igny8_content_ideas - WHERE id IN ({$placeholders}) AND status != 'new' - ", $ids)); - - if (!empty($ideas_status)) { - $idea_titles = array_column($ideas_status, 'idea_title'); - wp_send_json_error(['message' => 'Ideas are not in "new" status: ' . implode(', ', array_slice($idea_titles, 0, 3)) . (count($idea_titles) > 3 ? '...' : '')]); - } - - $created = []; - $skipped = []; - $failed = []; - - foreach ($ids as $id) { - $res = igny8_create_task_from_idea($id); - if (!empty($res['success'])) { - if (!empty($res['task_id'])) { - $created[] = $res['task_id']; - } else { - $skipped[] = $id; - } - } else { - $failed[] = $id; - } - } - - // Update metrics once per unique idea id to be safe - foreach ($ids as $id) { - igny8_update_idea_metrics($id); - } - - wp_send_json_success([ - 'created' => $created, - 'skipped' => $skipped, - 'failed' => $failed, - 'message' => sprintf('Created %d, skipped %d, failed %d', count($created), count($skipped), count($failed)) - ]); - } catch (Exception $e) { - wp_send_json_error(['message' => 'Error: ' . $e->getMessage()]); - } -} - -/** - * Helper function to create a task from an idea - * - * @param int $idea_id The idea ID to create task from - * @return array Result array with success status and message - */ -function igny8_create_task_from_idea($idea_id) { - global $wpdb; - - $idea = $wpdb->get_row($wpdb->prepare( - "SELECT id, idea_title, idea_description, content_structure, content_type, keyword_cluster_id, estimated_word_count, target_keywords - FROM {$wpdb->prefix}igny8_content_ideas WHERE id=%d", - $idea_id - )); - - if (!$idea) { - return ['success' => false, 'message' => 'Idea not found']; - } - - // Optional dedupe policy: One open task per idea - $existing = $wpdb->get_var($wpdb->prepare( - "SELECT id FROM {$wpdb->prefix}igny8_tasks WHERE idea_id=%d AND status IN ('draft','queued','in_progress','review') LIMIT 1", - $idea_id - )); - - if ($existing) { - return ['success' => true, 'message' => 'Task already exists for this idea', 'task_id' => (int)$existing]; - } - - $ins = $wpdb->insert($wpdb->prefix . 'igny8_tasks', [ - 'title' => $idea->idea_title, - 'description' => $idea->idea_description, - 'content_structure' => $idea->content_structure ?: 'cluster_hub', - 'content_type' => $idea->content_type ?: 'post', - 'cluster_id' => $idea->keyword_cluster_id ?: null, - 'priority' => 'medium', - 'status' => 'queued', - 'idea_id' => (int)$idea_id, - 'keywords' => $idea->target_keywords ?: '', - 'word_count' => $idea->estimated_word_count ?: 0, - 'schedule_at' => null, - 'assigned_post_id' => null, - 'created_at' => current_time('mysql'), - 'updated_at' => current_time('mysql') - ], ['%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d', '%s', '%d', '%s', '%d', '%s', '%s']); - - if ($ins === false) { - return ['success' => false, 'message' => 'Failed to create task']; - } - - $task_id = (int)$wpdb->insert_id; - - // Update idea status to 'scheduled' when successfully queued to writer - $wpdb->update( - $wpdb->prefix . 'igny8_content_ideas', - ['status' => 'scheduled'], - ['id' => $idea_id], - ['%s'], - ['%d'] - ); - - // Update keyword status to 'queued' when task is created from idea - if ($idea->keyword_cluster_id) { - // Get all keywords in this cluster and update their status to 'queued' - $keyword_ids = $wpdb->get_col($wpdb->prepare(" - SELECT id FROM {$wpdb->prefix}igny8_keywords - WHERE cluster_id = %d - ", $idea->keyword_cluster_id)); - - if (!empty($keyword_ids)) { - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $wpdb->query($wpdb->prepare(" - UPDATE {$wpdb->prefix}igny8_keywords - SET status = 'queued' - WHERE id IN ({$placeholders}) - ", $keyword_ids)); - } - } - - igny8_update_idea_metrics($idea_id); - igny8_write_log('queue_to_writer', ['idea_id' => $idea_id, 'task_id' => $task_id]); - - return ['success' => true, 'message' => "Task queued for Writer", "task_id" => $task_id]; -} - -/** - * AJAX handler for bulk deleting keywords - */ -// Hook moved to sync-hooks.php -function igny8_ajax_bulk_delete_keywords() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - // Get parameters - $keyword_ids = $_POST['keyword_ids'] ?? []; - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - wp_send_json_error(['message' => 'No keywords selected for deletion.']); - } - - // Call bulk delete function - $result = igny8_bulk_delete_keywords($keyword_ids); - - if ($result['success']) { - wp_send_json_success([ - 'message' => $result['message'], - 'deleted_count' => $result['deleted_count'] - ]); - } else { - wp_send_json_error(['message' => $result['message']]); - } -} - -/** - * AJAX handler for bulk mapping keywords to cluster - */ -// Hook moved to sync-hooks.php -function igny8_ajax_bulk_map_keywords() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - // Get parameters - $keyword_ids = $_POST['keyword_ids'] ?? []; - $cluster_id = intval($_POST['cluster_id'] ?? 0); - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - wp_send_json_error(['message' => 'No keywords selected for mapping.']); - } - - if (empty($cluster_id)) { - wp_send_json_error(['message' => 'No cluster selected for mapping.']); - } - - // Call bulk map function - $result = igny8_bulk_map_keywords($keyword_ids, $cluster_id); - - if ($result['success']) { - wp_send_json_success([ - 'message' => $result['message'], - 'mapped_count' => $result['mapped_count'] - ]); - } else { - wp_send_json_error(['message' => $result['message']]); - } -} - -/** - * AJAX handler for bulk unmapping keywords from clusters - */ -// Hook moved to sync-hooks.php -function igny8_ajax_bulk_unmap_keywords() { - // Verify nonce for security - if (!check_ajax_referer('igny8_ajax_nonce', 'nonce', true)) { - wp_send_json_error(['message' => 'Security check failed.']); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'You do not have permission to perform this action.']); - } - - // Get parameters - $keyword_ids = $_POST['keyword_ids'] ?? []; - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - wp_send_json_error(['message' => 'No keywords selected for unmapping.']); - } - - // Call bulk unmap function - $result = igny8_bulk_unmap_keywords($keyword_ids); - - if ($result['success']) { - wp_send_json_success([ - 'message' => $result['message'], - 'unmapped_count' => $result['unmapped_count'] - ]); - } else { - wp_send_json_error(['message' => $result['message']]); - } -} - -/** - * AJAX handler for bulk deleting records - */ -// Hook moved to sync-hooks.php -function igny8_delete_bulk_records() { - // Verify nonce - if (!wp_verify_nonce($_POST['nonce'], 'igny8_ajax_nonce')) { - wp_send_json_error('Security check failed'); - } - - // Check user capabilities - if (!current_user_can('manage_options')) { - wp_send_json_error('Insufficient permissions'); - } - - // Get parameters - $table_id = sanitize_text_field($_POST['table_id'] ?? ''); - $record_ids = $_POST['record_ids'] ?? []; - - if (empty($table_id) || empty($record_ids) || !is_array($record_ids)) { - wp_send_json_error('Table ID and Record IDs required'); - } - - // Get table name - $table_name = igny8_get_table_name($table_id); - if (!$table_name) { - wp_send_json_error('Invalid table ID'); - } - - // Sanitize IDs - $record_ids = array_map('intval', $record_ids); - $record_ids = array_filter($record_ids, function($id) { return $id > 0; }); - - if (empty($record_ids)) { - wp_send_json_error('No valid record IDs provided'); - } - - global $wpdb; - - // Handle cluster deletion - clean up keyword relationships - if ($table_id === 'planner_clusters') { - // Before deleting clusters, unmap all keywords from these clusters - $placeholders = implode(',', array_fill(0, count($record_ids), '%d')); - $unmapped_count = $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->prefix}igny8_keywords - SET cluster_id = NULL, status = 'unmapped', updated_at = CURRENT_TIMESTAMP - WHERE cluster_id IN ({$placeholders})", - $record_ids - )); - - if ($unmapped_count !== false) { - // Log the unmapping - error_log("Igny8: Unmapped {$unmapped_count} keywords from deleted clusters: " . implode(',', $record_ids)); - } - } - - // Build placeholders for IN clause - $placeholders = implode(',', array_fill(0, count($record_ids), '%d')); - - try { - $result = $wpdb->query($wpdb->prepare( - "DELETE FROM `{$table_name}` WHERE id IN ({$placeholders})", - $record_ids - )); - - if ($result === false) { - wp_send_json_error('Failed to delete records'); - } - - wp_send_json_success([ - 'message' => "Successfully deleted {$result} record(s)", - 'deleted_count' => $result - ]); - - } catch (Exception $e) { - wp_send_json_error('Database error: ' . $e->getMessage()); - } -} diff --git a/igny8-ai-seo-wp-plugin/flows/sync-functions.php b/igny8-ai-seo-wp-plugin/flows/sync-functions.php deleted file mode 100644 index 7d46db03..00000000 --- a/igny8-ai-seo-wp-plugin/flows/sync-functions.php +++ /dev/null @@ -1,673 +0,0 @@ - false, 'message' => 'Operation failed']; - } - - try { - - // Get keyword count and aggregated data - $keyword_data = $wpdb->get_row($wpdb->prepare(" - SELECT - COUNT(*) as keyword_count, - COALESCE(SUM(search_volume), 0) as total_volume, - COALESCE(AVG(difficulty), 0) as avg_difficulty - FROM {$wpdb->prefix}igny8_keywords - WHERE cluster_id = %d - ", $cluster_id)); - - // Get mapped pages count from taxonomy relationships - $cluster_term_id = $wpdb->get_var($wpdb->prepare(" - SELECT cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d - ", $cluster_id)); - - $mapped_pages_count = 0; - if ($cluster_term_id) { - // Count content (posts, pages, products) associated with this cluster term - $mapped_pages_count = $wpdb->get_var($wpdb->prepare(" - SELECT COUNT(DISTINCT object_id) - FROM {$wpdb->prefix}term_relationships tr - INNER JOIN {$wpdb->prefix}term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id - WHERE tt.term_id = %d AND tt.taxonomy = 'clusters' - ", $cluster_term_id)); - } - - // Update cluster record - $wpdb->update( - $wpdb->prefix . 'igny8_clusters', - [ - 'keyword_count' => intval($keyword_data->keyword_count), - 'total_volume' => intval($keyword_data->total_volume), - 'avg_difficulty' => floatval($keyword_data->avg_difficulty), - 'mapped_pages_count' => intval($mapped_pages_count) - ], - ['id' => $cluster_id], - ['%d', '%d', '%f', '%d'], - ['%d'] - ); - - return true; - - } catch (Exception $e) { - error_log("ERROR in igny8_update_cluster_metrics: " . $e->getMessage()); - return ['success' => false, 'message' => 'Failed to update cluster metrics: ' . $e->getMessage()]; - } -} - -/** - * Update campaign metrics - */ -function igny8_update_campaign_metrics($campaign_id) { - global $wpdb; - - if (!$campaign_id) { - return ['success' => false, 'message' => 'Operation failed']; - } - - // Get campaign performance data - $performance_data = $wpdb->get_row($wpdb->prepare(" - SELECT - COUNT(*) as total_tasks, - COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_tasks, - COALESCE(AVG(seo_score), 0) as avg_seo_score - FROM {$wpdb->prefix}igny8_tasks - WHERE campaign_id = %d - ", $campaign_id)); - - // Update campaign record - $wpdb->update( - $wpdb->prefix . 'igny8_campaigns', - [ - 'total_tasks' => intval($performance_data->total_tasks), - 'completed_tasks' => intval($performance_data->completed_tasks), - 'avg_seo_score' => floatval($performance_data->avg_seo_score) - ], - ['id' => $campaign_id], - ['%d', '%d', '%f'], - ['%d'] - ); - - return true; -} - -/** - * Update idea metrics based on related tasks - */ -function igny8_update_idea_metrics($idea_id) { - global $wpdb; - - if (!$idea_id) { - return ['success' => false, 'message' => 'Operation failed']; - } - - // Get tasks count for this idea - $tasks_count = $wpdb->get_var($wpdb->prepare(" - SELECT COUNT(*) - FROM {$wpdb->prefix}igny8_tasks - WHERE idea_id = %d - ", $idea_id)); - - // Update idea record - $wpdb->update( - $wpdb->prefix . 'igny8_content_ideas', - [ - 'tasks_count' => intval($tasks_count) - ], - ['id' => $idea_id], - ['%d'], - ['%d'] - ); - - return true; -} - -/** - * Update task metrics based on related variations - */ -// REMOVED: Task variations functionality - tasks don't need variations - -/** - * Update keyword status when WordPress post is published - * - * @param int $post_id The WordPress post ID that was published - */ -function igny8_update_keywords_on_post_publish($post_id) { - global $wpdb; - - if (!$post_id) { - return; - } - - // Find tasks that are assigned to this post - $tasks = $wpdb->get_results($wpdb->prepare(" - SELECT id, cluster_id - FROM {$wpdb->prefix}igny8_tasks - WHERE assigned_post_id = %d AND cluster_id IS NOT NULL - ", $post_id)); - - if (empty($tasks)) { - return; - } - - // Update keyword status to 'mapped' for all keywords in the clusters of published tasks - foreach ($tasks as $task) { - if ($task->cluster_id) { - // Get all keywords in this cluster and update their status to 'mapped' - $keyword_ids = $wpdb->get_col($wpdb->prepare(" - SELECT id FROM {$wpdb->prefix}igny8_keywords - WHERE cluster_id = %d - ", $task->cluster_id)); - - if (!empty($keyword_ids)) { - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $wpdb->query($wpdb->prepare(" - UPDATE {$wpdb->prefix}igny8_keywords - SET status = 'mapped' - WHERE id IN ({$placeholders}) - ", $keyword_ids)); - } - - // Update task status to 'completed' - $wpdb->update( - $wpdb->prefix . 'igny8_tasks', - ['status' => 'completed'], - ['id' => $task->id], - ['%s'], - ['%d'] - ); - } - } - - // Log the event - if (function_exists('igny8_write_log')) { - igny8_write_log('post_published', [ - 'post_id' => $post_id, - 'tasks_updated' => count($tasks), - 'message' => 'Keywords updated to mapped status' - ]); - } -} - -/** - * Automatically create clusters from keywords using AI clustering - * - * @param array $keyword_ids Optional array of keyword IDs to cluster. If empty, uses all unmapped keywords. - * @return array Result array with success status and message - */ -function igny8_auto_create_clusters_from_keywords($keyword_ids = []) { - global $wpdb; - - // If no keyword IDs provided, get all unmapped keywords - if (empty($keyword_ids)) { - $keyword_ids = $wpdb->get_col(" - SELECT id FROM {$wpdb->prefix}igny8_keywords - WHERE cluster_id IS NULL - "); - } - - if (empty($keyword_ids)) { - return ['success' => true, 'message' => 'No unmapped keywords found for clustering']; - } - - // Limit to 20 keywords for AI processing - if (count($keyword_ids) > 20) { - $keyword_ids = array_slice($keyword_ids, 0, 20); - } - - // Get keywords data - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $keywords = $wpdb->get_results($wpdb->prepare(" - SELECT * FROM {$wpdb->prefix}igny8_keywords - WHERE id IN ({$placeholders}) - ", $keyword_ids)); - - if (empty($keywords)) { - return ['success' => false, 'message' => 'No valid keywords found for clustering']; - } - - // Create clusters using AI (this would call the AI clustering function) - // For now, create a simple cluster with all keywords - $cluster_name = igny8_generate_cluster_name_from_keywords(array_column($keywords, 'keyword')); - - // Create cluster - $cluster_result = $wpdb->insert( - $wpdb->prefix . 'igny8_clusters', - [ - 'cluster_name' => $cluster_name, - 'status' => 'active', - 'created_at' => current_time('mysql') - ], - ['%s', '%s', '%s'] - ); - - if ($cluster_result === false) { - return ['success' => false, 'message' => 'Failed to create cluster']; - } - - $cluster_id = $wpdb->insert_id; - - // Trigger cluster_added action to create taxonomy term - do_action('igny8_cluster_added', $cluster_id); - - // Map all keywords to this cluster - $mapped_count = $wpdb->query($wpdb->prepare(" - UPDATE {$wpdb->prefix}igny8_keywords - SET cluster_id = %d, status = 'mapped' - WHERE id IN ({$placeholders}) - ", array_merge([$cluster_id], $keyword_ids))); - - // Update cluster metrics - igny8_update_cluster_metrics($cluster_id); - - return [ - 'success' => true, - 'message' => "Created cluster '{$cluster_name}' with {$mapped_count} keywords", - 'cluster_id' => $cluster_id, - 'mapped_count' => $mapped_count - ]; -} - -/** - * Workflow automation trigger system - */ -function igny8_workflow_triggers($event, $payload = []) { - switch ($event) { - case 'keywords_imported': - if (isset($payload['keyword_ids'])) { - return igny8_auto_create_clusters_from_keywords($payload['keyword_ids']); - } - break; - - case 'cluster_created': - if (isset($payload['cluster_id'])) { - // Create ideas from cluster - $ideas_result = igny8_auto_create_ideas_from_clusters($payload['cluster_id']); - - return [ - 'success' => true, - 'message' => 'Cluster workflow completed', - 'ideas' => $ideas_result - ]; - } - break; - - } - - return ['success' => true, 'message' => 'No workflow automation for this event']; -} - - - -/** - * Bulk delete keywords with cluster metrics update - */ -function igny8_bulk_delete_keywords($keyword_ids) { - global $wpdb; - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - return ['success' => false, 'message' => 'Operation failed']; - } - - // Get cluster IDs before deletion for metrics update - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $cluster_ids = $wpdb->get_col($wpdb->prepare(" - SELECT DISTINCT cluster_id - FROM {$wpdb->prefix}igny8_keywords - WHERE id IN ({$placeholders}) - AND cluster_id IS NOT NULL - ", $keyword_ids)); - - // Delete keywords - $result = $wpdb->query($wpdb->prepare(" - DELETE FROM {$wpdb->prefix}igny8_keywords - WHERE id IN ({$placeholders}) - ", $keyword_ids)); - - if ($result !== false) { - // Update cluster metrics for affected clusters - foreach ($cluster_ids as $cluster_id) { - if ($cluster_id) { - igny8_update_cluster_metrics($cluster_id); - } - } - - return $result; - } - - return ['success' => false, 'message' => 'Operation failed']; -} - -/** - * Bulk map keywords to cluster - * - * @param array $keyword_ids Array of keyword IDs to map - * @param int $cluster_id Cluster ID to map keywords to - * @return array ['success' => bool, 'message' => string, 'mapped_count' => int] - */ -function igny8_bulk_map_keywords($keyword_ids, $cluster_id) { - global $wpdb; - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - return ['success' => false, 'message' => 'No keywords selected for mapping', 'mapped_count' => 0]; - } - - if (empty($cluster_id) || !is_numeric($cluster_id)) { - return ['success' => false, 'message' => 'Invalid cluster ID provided', 'mapped_count' => 0]; - } - - $cluster_id = intval($cluster_id); - - // Verify cluster exists - $cluster_exists = $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!$cluster_exists) { - return ['success' => false, 'message' => 'Cluster not found', 'mapped_count' => 0]; - } - - // Sanitize and validate IDs - $keyword_ids = array_map('intval', $keyword_ids); - $keyword_ids = array_filter($keyword_ids, function($id) { return $id > 0; }); - - if (empty($keyword_ids)) { - return ['success' => false, 'message' => 'No valid keyword IDs provided', 'mapped_count' => 0]; - } - - // Get old cluster IDs for metrics update - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $old_cluster_ids = $wpdb->get_col($wpdb->prepare( - "SELECT DISTINCT cluster_id FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) AND cluster_id IS NOT NULL", - $keyword_ids - )); - - // Update keywords to new cluster - $mapped_count = $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->prefix}igny8_keywords SET cluster_id = %d, status = 'mapped', updated_at = CURRENT_TIMESTAMP WHERE id IN ({$placeholders})", - array_merge([$cluster_id], $keyword_ids) - )); - - if ($mapped_count === false) { - return ['success' => false, 'message' => 'Failed to map keywords to cluster', 'mapped_count' => 0]; - } - - // Update metrics for old clusters (they lost keywords) - if (!empty($old_cluster_ids)) { - foreach (array_unique($old_cluster_ids) as $old_cluster_id) { - if ($old_cluster_id && $old_cluster_id != $cluster_id) { - igny8_update_cluster_metrics($old_cluster_id); - } - } - } - - // Update metrics for new cluster (gained keywords) - igny8_update_cluster_metrics($cluster_id); - - return [ - 'success' => true, - 'message' => "Successfully mapped {$mapped_count} keyword(s) to cluster", - 'mapped_count' => $mapped_count - ]; -} - -/** - * Bulk unmap keywords from their clusters - * - * @param array $keyword_ids Array of keyword IDs to unmap - * @return array ['success' => bool, 'message' => string, 'unmapped_count' => int] - */ -function igny8_bulk_unmap_keywords($keyword_ids) { - global $wpdb; - - if (empty($keyword_ids) || !is_array($keyword_ids)) { - return ['success' => false, 'message' => 'No keywords selected for unmapping', 'unmapped_count' => 0]; - } - - // Sanitize and validate IDs - $keyword_ids = array_map('intval', $keyword_ids); - $keyword_ids = array_filter($keyword_ids, function($id) { return $id > 0; }); - - if (empty($keyword_ids)) { - return ['success' => false, 'message' => 'No valid keyword IDs provided', 'unmapped_count' => 0]; - } - - // Get cluster IDs before unmapping for metrics update - $placeholders = implode(',', array_fill(0, count($keyword_ids), '%d')); - $cluster_ids = $wpdb->get_col($wpdb->prepare( - "SELECT DISTINCT cluster_id FROM {$wpdb->prefix}igny8_keywords WHERE id IN ({$placeholders}) AND cluster_id IS NOT NULL", - $keyword_ids - )); - - // Update keywords to unmap them - $unmapped_count = $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->prefix}igny8_keywords SET cluster_id = NULL, status = 'unmapped', updated_at = CURRENT_TIMESTAMP WHERE id IN ({$placeholders})", - $keyword_ids - )); - - if ($unmapped_count === false) { - return ['success' => false, 'message' => 'Failed to unmap keywords from clusters', 'unmapped_count' => 0]; - } - - // Update metrics for affected clusters (they lost keywords) - if (!empty($cluster_ids)) { - foreach (array_unique($cluster_ids) as $cluster_id) { - if ($cluster_id) { - igny8_update_cluster_metrics($cluster_id); - } - } - } - - return [ - 'success' => true, - 'message' => "Successfully unmapped {$unmapped_count} keyword(s) from clusters", - 'unmapped_count' => $unmapped_count - ]; -} - -/** - * Automatically create or link a taxonomy term when a new cluster is created. - * - * @param int $cluster_id The cluster ID from wp_igny8_clusters. - */ -function igny8_auto_create_cluster_term($cluster_id) { - global $wpdb; - - // 1. Validate cluster - $cluster = $wpdb->get_row($wpdb->prepare( - "SELECT cluster_name, cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!$cluster) { - error_log("Igny8: Cluster not found for ID $cluster_id."); - return; - } - - // 2. Skip if already mapped - if (!empty($cluster->cluster_term_id)) { - error_log("Igny8: Cluster $cluster_id already linked to term {$cluster->cluster_term_id}."); - return; - } - - // 3. Ensure taxonomy exists - if (!taxonomy_exists('clusters') && function_exists('igny8_register_taxonomies')) { - igny8_register_taxonomies(); - } - - // 4. Create or find existing term - $term_result = wp_insert_term( - sanitize_text_field($cluster->cluster_name), - 'clusters', - ['slug' => sanitize_title($cluster->cluster_name)] - ); - - if (is_wp_error($term_result)) { - // If term exists, fetch it - $existing = get_term_by('name', $cluster->cluster_name, 'clusters'); - $term_id = $existing ? $existing->term_id : 0; - } else { - $term_id = $term_result['term_id']; - } - - // 5. Save term ID back to cluster table - if ($term_id > 0) { - $wpdb->update( - "{$wpdb->prefix}igny8_clusters", - ['cluster_term_id' => $term_id], - ['id' => $cluster_id], - ['%d'], - ['%d'] - ); - error_log("Igny8: Created and linked taxonomy term $term_id for cluster $cluster_id."); - } else { - error_log("Igny8: Failed to link taxonomy term for cluster $cluster_id."); - } -} - -/** - * Automatically update taxonomy term when a cluster name is changed/updated. - * - * @param int $cluster_id The cluster ID from wp_igny8_clusters. - */ -function igny8_auto_update_cluster_term($cluster_id) { - global $wpdb; - - // 1. Validate cluster - $cluster = $wpdb->get_row($wpdb->prepare( - "SELECT cluster_name, cluster_term_id FROM {$wpdb->prefix}igny8_clusters WHERE id = %d", - $cluster_id - )); - - if (!$cluster) { - error_log("Igny8: Cluster not found for ID $cluster_id."); - return; - } - - // 2. Skip if no term linked - if (empty($cluster->cluster_term_id)) { - error_log("Igny8: Cluster $cluster_id has no linked taxonomy term."); - return; - } - - // 3. Ensure taxonomy exists - if (!taxonomy_exists('clusters') && function_exists('igny8_register_taxonomies')) { - igny8_register_taxonomies(); - } - - // 4. Get the existing term - $term = get_term($cluster->cluster_term_id, 'clusters'); - if (is_wp_error($term) || !$term) { - error_log("Igny8: Taxonomy term {$cluster->cluster_term_id} not found for cluster $cluster_id."); - return; - } - - // 5. Check if name has changed - if ($term->name === $cluster->cluster_name) { - error_log("Igny8: Cluster $cluster_id name unchanged, skipping taxonomy update."); - return; - } - - // 6. Update the term name and slug - $update_result = wp_update_term( - $cluster->cluster_term_id, - 'clusters', - [ - 'name' => sanitize_text_field($cluster->cluster_name), - 'slug' => sanitize_title($cluster->cluster_name) - ] - ); - - if (is_wp_error($update_result)) { - error_log("Igny8: Failed to update taxonomy term for cluster $cluster_id: " . $update_result->get_error_message()); - } else { - error_log("Igny8: Successfully updated taxonomy term {$cluster->cluster_term_id} for cluster $cluster_id."); - } -} - -// Hook registration for automatic cluster term creation -// Hook moved to sync-hooks.php - -/** - * Handle content association/disassociation with cluster taxonomy terms - */ -function igny8_handle_content_cluster_association($object_id, $terms, $tt_ids, $taxonomy) { - // Only process clusters taxonomy - if ($taxonomy !== 'clusters') { - return; - } - - global $wpdb; - - // Get all cluster IDs that have terms in this taxonomy update - $cluster_ids = []; - - foreach ($terms as $term) { - if (is_numeric($term)) { - $term_id = $term; - } else { - $term_obj = get_term_by('slug', $term, 'clusters'); - $term_id = $term_obj ? $term_obj->term_id : null; - } - - if ($term_id) { - // Find cluster that has this term_id - $cluster_id = $wpdb->get_var($wpdb->prepare(" - SELECT id FROM {$wpdb->prefix}igny8_clusters - WHERE cluster_term_id = %d - ", $term_id)); - - if ($cluster_id) { - $cluster_ids[] = $cluster_id; - } - } - } - - // Update metrics for all affected clusters - foreach (array_unique($cluster_ids) as $cluster_id) { - igny8_update_cluster_metrics($cluster_id); - } -} - -// Hook registration for automatic cluster metrics updates when content is associated/disassociated with cluster terms -// Hook moved to sync-hooks.php - -/** - * Update task metrics including word count and meta sync - */ -function igny8_update_task_metrics($task_id) { - global $wpdb; - $post_id = $wpdb->get_var($wpdb->prepare("SELECT assigned_post_id FROM {$wpdb->prefix}igny8_tasks WHERE id = %d", $task_id)); - - if ($post_id) { - $content = get_post_field('post_content', $post_id); - $word_count = str_word_count(strip_tags($content)); - update_post_meta($post_id, '_igny8_word_count', $word_count); - - $wpdb->update("{$wpdb->prefix}igny8_tasks", ['word_count' => $word_count], ['id' => $task_id]); - } -} \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/flows/sync-hooks.php b/igny8-ai-seo-wp-plugin/flows/sync-hooks.php deleted file mode 100644 index 7e2650c1..00000000 --- a/igny8-ai-seo-wp-plugin/flows/sync-hooks.php +++ /dev/null @@ -1,99 +0,0 @@ -"; - echo "Igny8 CRON: Request started
          "; - error_log("Igny8 CRON: Request started - " . date('Y-m-d H:i:s')); - - // Force load Igny8 plugin if not already loaded - echo "Igny8 CRON: Checking if plugin is active
          "; - if (!function_exists('igny8_get_ai_setting')) { - echo "Igny8 CRON: Plugin not loaded, attempting to load
          "; - - // Try to load the plugin manually - check multiple possible locations - $possible_paths = [ - 'igny8-ai-seo/igny8.php', - 'igny8/igny8.php', - 'igny8-ai-seo.php' - ]; - - $plugin_loaded = false; - foreach ($possible_paths as $plugin_file) { - $full_path = WP_PLUGIN_DIR . '/' . $plugin_file; - echo "Igny8 CRON: Checking path: " . $full_path . "
          "; - if (file_exists($full_path)) { - echo "Igny8 CRON: Plugin file found at: " . $full_path . ", loading
          "; - include_once $full_path; - $plugin_loaded = true; - break; - } - } - - if (!$plugin_loaded) { - echo "Igny8 CRON: Plugin file not found in any expected location
          "; - echo "Igny8 CRON: WP_PLUGIN_DIR: " . WP_PLUGIN_DIR . "
          "; - echo "Igny8 CRON: Available plugins: " . implode(', ', array_diff(scandir(WP_PLUGIN_DIR), array('.', '..'))) . "
          "; - } - - // Check again after manual load - if (!function_exists('igny8_get_ai_setting')) { - echo "Igny8 CRON: Plugin still not active after manual load, trying WordPress plugin loading
          "; - - // Try to trigger WordPress plugin loading - if (function_exists('do_action')) { - echo "Igny8 CRON: Triggering plugins_loaded action
          "; - do_action('plugins_loaded'); - } - - // Manually load cron handlers if still not found - if (!function_exists('igny8_auto_cluster_cron_handler')) { - echo "Igny8 CRON: Manually loading cron handlers
          "; - $cron_handlers_path = WP_PLUGIN_DIR . '/igny8-ai-seo/core/cron/igny8-cron-handlers.php'; - if (file_exists($cron_handlers_path)) { - echo "Igny8 CRON: Loading cron handlers from: " . $cron_handlers_path . "
          "; - include_once $cron_handlers_path; - } else { - echo "Igny8 CRON: Cron handlers file not found at: " . $cron_handlers_path . "
          "; - } - } - - // Final check - if (!function_exists('igny8_get_ai_setting')) { - echo "Igny8 CRON: Plugin still not active after all attempts
          "; - error_log("Igny8 CRON: Plugin not active - igny8_get_ai_setting function not found"); - http_response_code(500); - die(json_encode(['error' => 'Igny8 plugin not active'])); - } - } - } - echo "Igny8 CRON: Plugin is active
          "; - - // Ensure cron handlers are loaded - if (!function_exists('igny8_auto_cluster_cron_handler')) { - echo "Igny8 CRON: Loading cron handlers manually
          "; - $cron_handlers_path = WP_PLUGIN_DIR . '/igny8-ai-seo/core/cron/igny8-cron-handlers.php'; - if (file_exists($cron_handlers_path)) { - echo "Igny8 CRON: Loading cron handlers from: " . $cron_handlers_path . "
          "; - include_once $cron_handlers_path; - } else { - echo "Igny8 CRON: Cron handlers file not found at: " . $cron_handlers_path . "
          "; - } - } - echo "Igny8 CRON: Cron handlers loaded
          "; - - // Load global helpers for sector options function - if (!function_exists('igny8_get_sector_options')) { - echo "Igny8 CRON: Loading global helpers for sector options
          "; - $global_helpers_path = WP_PLUGIN_DIR . '/igny8-ai-seo/core/admin/global-helpers.php'; - if (file_exists($global_helpers_path)) { - echo "Igny8 CRON: Loading global helpers from: " . $global_helpers_path . "
          "; - include_once $global_helpers_path; - } else { - echo "Igny8 CRON: Global helpers file not found at: " . $global_helpers_path . "
          "; - } - } - echo "Igny8 CRON: Global helpers loaded
          "; - - // Load AJAX handlers for clustering function - if (!function_exists('igny8_ajax_ai_cluster_keywords')) { - echo "Igny8 CRON: Loading AJAX handlers for clustering
          "; - $ajax_handlers_path = WP_PLUGIN_DIR . '/igny8-ai-seo/core/admin/ajax.php'; - if (file_exists($ajax_handlers_path)) { - echo "Igny8 CRON: Loading AJAX handlers from: " . $ajax_handlers_path . "
          "; - include_once $ajax_handlers_path; - } else { - echo "Igny8 CRON: AJAX handlers file not found at: " . $ajax_handlers_path . "
          "; - } - } - echo "Igny8 CRON: AJAX handlers loaded
          "; - - // Load model rates configuration to prevent PHP warnings - echo "Igny8 CRON: Loading model rates configuration
          "; - $model_rates_path = WP_PLUGIN_DIR . '/igny8-ai-seo/ai/model-rates-config.php'; - if (file_exists($model_rates_path)) { - echo "Igny8 CRON: Loading model rates from: " . $model_rates_path . "
          "; - include_once $model_rates_path; - - // Verify the global variable is set - if (isset($GLOBALS['IGNY8_MODEL_RATES'])) { - echo "Igny8 CRON: Model rates global variable loaded successfully
          "; - } else { - echo "Igny8 CRON: WARNING - Model rates global variable not set
          "; - } - } else { - echo "Igny8 CRON: Model rates file not found at: " . $model_rates_path . "
          "; - } - echo "Igny8 CRON: Model rates loaded
          "; - - // Load database functions for taxonomy registration - if (!function_exists('igny8_register_taxonomies')) { - echo "Igny8 CRON: Loading database functions for taxonomy registration
          "; - $db_functions_path = WP_PLUGIN_DIR . '/igny8-ai-seo/core/db/db.php'; - if (file_exists($db_functions_path)) { - echo "Igny8 CRON: Loading database functions from: " . $db_functions_path . "
          "; - include_once $db_functions_path; - } else { - echo "Igny8 CRON: Database functions file not found at: " . $db_functions_path . "
          "; - } - } - echo "Igny8 CRON: Database functions loaded
          "; - - // Load master dispatcher for smart automation - if (!function_exists('igny8_master_dispatcher_run')) { - echo "Igny8 CRON: Loading master dispatcher
          "; - $master_dispatcher_path = WP_PLUGIN_DIR . '/igny8-ai-seo/core/cron/igny8-cron-master-dispatcher.php'; - if (file_exists($master_dispatcher_path)) { - echo "Igny8 CRON: Loading master dispatcher from: " . $master_dispatcher_path . "
          "; - include_once $master_dispatcher_path; - } else { - echo "Igny8 CRON: Master dispatcher file not found at: " . $master_dispatcher_path . "
          "; - } - } - echo "Igny8 CRON: Master dispatcher loaded
          "; - - // 🔧 PATCH: Enable full WordPress taxonomy and rewrite support in CRON context - echo "Igny8 CRON: Initializing WordPress rewrite and taxonomy system
          "; - - // Set globals for rewrite + taxonomy system - global $wp_rewrite, $wp_taxonomies; - - // ✅ Fix: Initialize rewrite system (prevents "add_rewrite_tag() on null" error) - if (!isset($wp_rewrite)) { - echo "Igny8 CRON: Initializing WP_Rewrite system
          "; - $wp_rewrite = new WP_Rewrite(); - $wp_rewrite->init(); - echo "Igny8 CRON: WP_Rewrite system initialized
          "; - } else { - echo "Igny8 CRON: WP_Rewrite system already initialized
          "; - } - - // ✅ Trigger key WP hooks (needed for taxonomy registration) - but be selective - echo "Igny8 CRON: Triggering WordPress hooks for taxonomy support
          "; - - // Only trigger plugins_loaded (safest for external cron) - if (!did_action('plugins_loaded')) { - do_action('plugins_loaded'); - echo "Igny8 CRON: plugins_loaded hook triggered
          "; - } else { - echo "Igny8 CRON: plugins_loaded hook already executed
          "; - } - - // Skip after_setup_theme and init to avoid widget system issues - echo "Igny8 CRON: Skipping after_setup_theme and init hooks to avoid widget system conflicts
          "; - - // ✅ Load and register Igny8 taxonomies manually - echo "Igny8 CRON: Registering Igny8 taxonomies
          "; - if (function_exists('igny8_register_taxonomies')) { - // Ensure WordPress global taxonomies array exists - global $wp_taxonomies; - if (!is_array($wp_taxonomies)) { - $wp_taxonomies = []; - echo "Igny8 CRON: Initialized wp_taxonomies global array
          "; - } - - igny8_register_taxonomies(); - echo "Igny8 CRON: Igny8 taxonomies registered
          "; - } else { - echo "Igny8 CRON: WARNING - igny8_register_taxonomies function not found
          "; - } - - // ✅ Verify taxonomy registration - if (!taxonomy_exists('clusters')) { - echo "Igny8 CRON: ❌ Taxonomy 'clusters' not registered
          "; - error_log('[CRON] ❌ Taxonomy "clusters" not registered'); - - // Fallback: Try to register clusters taxonomy directly - echo "Igny8 CRON: Attempting direct taxonomy registration as fallback
          "; - if (function_exists('register_taxonomy')) { - register_taxonomy('clusters', ['post', 'page', 'product'], [ - 'hierarchical' => true, - 'labels' => [ - 'name' => 'Content Clusters', - 'singular_name' => 'Cluster', - ], - 'public' => true, - 'show_ui' => true, - 'show_admin_column' => true, - 'show_in_nav_menus' => true, - 'show_tagcloud' => true, - 'show_in_rest' => true, - ]); - echo "Igny8 CRON: Direct clusters taxonomy registration attempted
          "; - } - } else { - echo "Igny8 CRON: ✅ Taxonomy 'clusters' registered successfully
          "; - error_log('[CRON] ✅ Taxonomy "clusters" registered successfully'); - } - - if (!taxonomy_exists('sectors')) { - echo "Igny8 CRON: ❌ Taxonomy 'sectors' not registered
          "; - error_log('[CRON] ❌ Taxonomy "sectors" not registered'); - - // Fallback: Try to register sectors taxonomy directly - if (function_exists('register_taxonomy')) { - register_taxonomy('sectors', ['post', 'page', 'product'], [ - 'hierarchical' => true, - 'labels' => [ - 'name' => 'Sectors', - 'singular_name' => 'Sector', - ], - 'public' => true, - 'show_ui' => true, - 'show_admin_column' => true, - 'show_in_nav_menus' => true, - 'show_tagcloud' => true, - 'show_in_rest' => true, - ]); - echo "Igny8 CRON: Direct sectors taxonomy registration attempted
          "; - } - } else { - echo "Igny8 CRON: ✅ Taxonomy 'sectors' registered successfully
          "; - error_log('[CRON] ✅ Taxonomy "sectors" registered successfully'); - } - - // === STEP 1: Validate Security Key === - echo "Igny8 CRON: Validating security key
          "; - $provided_key = isset($_GET['import_key']) ? sanitize_text_field($_GET['import_key']) : ''; - $stored_key = get_option('igny8_secure_cron_key'); - - echo "Igny8 CRON: Provided key: " . substr($provided_key, 0, 8) . "...
          "; - echo "Igny8 CRON: Stored key: " . substr($stored_key, 0, 8) . "...
          "; - - if (empty($stored_key) || $provided_key !== $stored_key) { - echo "Igny8 CRON: Security key validation failed
          "; - error_log("Igny8 CRON: Security key validation failed - provided: " . substr($provided_key, 0, 8) . ", stored: " . substr($stored_key, 0, 8)); - status_header(403); - echo json_encode(['error' => 'Invalid or missing security key']); - exit; - } - echo "Igny8 CRON: Security key validated
          "; - - // === STEP 2: Capture Action === - echo "Igny8 CRON: Capturing action parameter
          "; - $action = isset($_GET['action']) ? sanitize_text_field($_GET['action']) : ''; - echo "Igny8 CRON: Action: " . $action . "
          "; - - if (empty($action)) { - echo "Igny8 CRON: Missing action parameter
          "; - error_log("Igny8 CRON: Missing action parameter"); - status_header(400); - echo json_encode(['error' => 'Missing action parameter']); - exit; - } - - // === STEP 3: Execute CRON Function === - echo "Igny8 CRON: Building allowed actions list
          "; - $allowed_actions = [ - 'master_scheduler' => 'igny8_master_dispatcher_run', - 'auto_cluster' => 'igny8_auto_cluster_cron', - 'auto_ideas' => 'igny8_auto_generate_ideas_cron', - 'auto_queue' => 'igny8_auto_queue_cron', - 'auto_content' => 'igny8_auto_generate_content_cron', - 'auto_images' => 'igny8_auto_generate_images_cron', - 'auto_publish' => 'igny8_auto_publish_drafts_cron', - 'auto_optimizer' => 'igny8_auto_optimizer_cron', - 'auto_recalc' => 'igny8_auto_recalc_cron', - 'health_check' => 'igny8_health_check_cron', - 'test' => 'igny8_test_cron_endpoint', - ]; - echo "Igny8 CRON: Allowed actions: " . implode(', ', array_keys($allowed_actions)) . "
          "; - - if (!array_key_exists($action, $allowed_actions)) { - echo "Igny8 CRON: Invalid action name: " . $action . "
          "; - error_log("Igny8 CRON: Invalid action name: " . $action); - status_header(400); - echo json_encode(['error' => 'Invalid action name']); - exit; - } - echo "Igny8 CRON: Action validated: " . $action . "
          "; - - // Execute safely and catch errors - echo "Igny8 CRON: Starting execution
          "; - try { - // Handle test action specially - if ($action === 'test') { - echo "Igny8 CRON: Executing test action
          "; - status_header(200); - echo json_encode([ - 'success' => true, - 'message' => 'Igny8 CRON endpoint is working via wp-load.php', - 'timestamp' => date('Y-m-d H:i:s'), - 'server' => $_SERVER['SERVER_NAME'] ?? 'unknown', - 'url_structure' => 'wp-load.php?import_key=[KEY]&import_id=igny8_cron&action=[ACTION]' - ]); - exit; - } - - echo "Igny8 CRON: Executing action: " . $action . "
          "; - echo "Igny8 CRON: Hook to execute: " . $allowed_actions[$action] . "
          "; - - // Check if the hook function exists - $hook_function = $allowed_actions[$action]; - $handler_function = $hook_function . '_handler'; - - echo "Igny8 CRON: Checking hook function: " . $hook_function . "
          "; - echo "Igny8 CRON: Checking handler function: " . $handler_function . "
          "; - - if (!function_exists($hook_function) && !function_exists($handler_function)) { - echo "Igny8 CRON: Neither hook nor handler function found
          "; - echo "Igny8 CRON: Available functions containing 'igny8_auto_cluster':
          "; - $functions = get_defined_functions()['user']; - foreach ($functions as $func) { - if (strpos($func, 'igny8_auto_cluster') !== false) { - echo "- " . $func . "
          "; - } - } - error_log("Igny8 CRON: Hook function not found: " . $hook_function); - status_header(500); - echo json_encode(['error' => 'Hook function not found', 'hook' => $hook_function]); - exit; - } - echo "Igny8 CRON: Hook function exists: " . $allowed_actions[$action] . "
          "; - - // Execute the hook - echo "Igny8 CRON: Calling do_action(" . $allowed_actions[$action] . ")
          "; - do_action($allowed_actions[$action]); - echo "Igny8 CRON: do_action completed successfully
          "; - - status_header(200); - echo json_encode(['success' => true, 'message' => "CRON action '{$action}' executed successfully."]); - } catch (Throwable $e) { - echo "Igny8 CRON: Exception caught: " . $e->getMessage() . "
          "; - error_log("Igny8 CRON: Exception caught: " . $e->getMessage() . " in " . $e->getFile() . " line " . $e->getLine()); - status_header(500); - echo json_encode(['error' => 'Execution failed', 'details' => $e->getMessage()]); - } - echo "Igny8 CRON: Request completed
          "; - echo ""; // Close the main div - exit; -} diff --git a/igny8-ai-seo-wp-plugin/igny8.php b/igny8-ai-seo-wp-plugin/igny8.php deleted file mode 100644 index e0862f1f..00000000 --- a/igny8-ai-seo-wp-plugin/igny8.php +++ /dev/null @@ -1,246 +0,0 @@ - 'home', // Planner home only - 'igny8-writer' => 'home' // Writer home only - ]; - - // Check if current page needs CRON - if (isset($cron_pages[$current_page])) { - if (is_string($cron_pages[$current_page])) { - // Page has submodule requirement - return $current_submodule === $cron_pages[$current_page]; - } - return true; // Page needs CRON without submodule requirement - } - - return false; -} - -// --------------------------------------------------------------------- -// ACTIVATION / DEACTIVATION -// --------------------------------------------------------------------- -register_activation_hook(__FILE__, 'igny8_activate'); -register_deactivation_hook(__FILE__, 'igny8_deactivate'); - -function igny8_activate() { - require_once plugin_dir_path(__FILE__) . 'install.php'; - if (function_exists('igny8_install')) { - igny8_install(); - } - flush_rewrite_rules(); -} - -function igny8_deactivate() { - flush_rewrite_rules(); -} - -// --------------------------------------------------------------------- -// ADMIN ASSETS ENQUEUING -// --------------------------------------------------------------------- -add_action('admin_enqueue_scripts', 'igny8_admin_scripts'); - -function igny8_admin_scripts($hook) { - // Only load on Igny8 admin pages - if (strpos($hook, 'igny8') === false) return; - - $plugin_url = plugin_dir_url(__FILE__); - - // Enqueue core CSS with updated version - wp_enqueue_style('igny8-admin-style', $plugin_url . 'assets/css/core.css', [], '0.1'); - - // Enqueue Chart.js - wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js', [], '4.5.0', true); - wp_add_inline_script('chart-js', 'window.Chart = Chart;', 'after'); - - // Enqueue core JavaScript with dependencies and updated version - wp_enqueue_script('igny8-admin-js', $plugin_url . 'assets/js/core.js', ['jquery', 'chart-js'], '0.1', true); - - // Enqueue image queue processor - wp_enqueue_script('igny8-image-queue', $plugin_url . 'assets/js/image-queue-processor.js', ['igny8-admin-js'], '0.1', true); - - // AJAX localization - wp_localize_script('igny8-admin-js', 'igny8_ajax', [ - 'ajax_url' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_ajax_nonce'), - 'module' => igny8_get_current_module(), - 'debug_enabled' => false - ]); -} - -// --------------------------------------------------------------------- -// HELPER: Get current module from admin URL -// --------------------------------------------------------------------- -function igny8_get_current_module() { - if (isset($_GET['page']) && strpos($_GET['page'], 'igny8-') === 0) { - return str_replace('igny8-', '', sanitize_text_field($_GET['page'])); - } - return 'planner'; -} diff --git a/igny8-ai-seo-wp-plugin/install.php b/igny8-ai-seo-wp-plugin/install.php deleted file mode 100644 index 650ed56c..00000000 --- a/igny8-ai-seo-wp-plugin/install.php +++ /dev/null @@ -1,43 +0,0 @@ - -
          -

          Analytics & Reporting

          -

          Performance analytics and reporting functionality coming soon...

          -
          - \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/components/_README.php b/igny8-ai-seo-wp-plugin/modules/components/_README.php deleted file mode 100644 index b0551bbb..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/_README.php +++ /dev/null @@ -1,14 +0,0 @@ - - -
          -
          - 0 selected - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          -
          -
          - - - - - - - - - -
          -
          -
          - - - diff --git a/igny8-ai-seo-wp-plugin/modules/components/export-modal-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/export-modal-tpl.php deleted file mode 100644 index 5e0caa41..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/export-modal-tpl.php +++ /dev/null @@ -1,77 +0,0 @@ - - -
          -
          -
          -

          Export

          - -
          -
          -

          Export to CSV format.

          - - -
          -

          Export Options

          -
          - - - -
          -
          - - - -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/components/filters-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/filters-tpl.php deleted file mode 100644 index 3d53a88d..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/filters-tpl.php +++ /dev/null @@ -1,136 +0,0 @@ - - * - * - * @package Igny8Compact - * @since 1.0.0 - */ - -// Prevent direct access -if (!defined('ABSPATH')) { - exit; -} - -// Render filters function -function igny8_render_filters($table_id) { - // Load filters configuration - $filters_config = require plugin_dir_path(__FILE__) . '../config/filters-config.php'; - $filters = $filters_config[$table_id] ?? []; - - // Load table configuration to get humanize_columns - $tables_config = require plugin_dir_path(__FILE__) . '../config/tables-config.php'; - $GLOBALS['igny8_tables_config'] = $tables_config; - $table_config = igny8_get_dynamic_table_config($table_id); - $humanize_columns = $table_config['humanize_columns'] ?? []; - - // Set variables for component - $module = explode('_', $table_id)[0]; - $tab = explode('_', $table_id)[1] ?? ''; - - // Debug: Log filters array for verification - - // Start output buffering to capture HTML - ob_start(); - ?> - -
          -
          - $filter_config): ?> -
          - - - - - -
          - -
          -
          All
          - - - $label): ?> -
          - - - ' . esc_html($option['label']) . '
          '; - } - } - } - ?> - - -
          -
          - - -
          - -
          -
          - - -
          -
          - - -
          -
          - - -
          -
          -
          - -
          - - -
          - - -
          -
          - - diff --git a/igny8-ai-seo-wp-plugin/modules/components/forms-tpl.php b/igny8-ai-seo-wp-plugin/modules/components/forms-tpl.php deleted file mode 100644 index 372aee15..00000000 --- a/igny8-ai-seo-wp-plugin/modules/components/forms-tpl.php +++ /dev/null @@ -1,176 +0,0 @@ -
          Form not configured for table: ' . esc_html($table_id) . '
          - - - - - -
          -
          - -
          - - - - - - -
          -
          -
          -
          - -
          - - - -
          -
          -
          -
          - - - Manual - -
          -
          - - -
          - No records found -
          - - - - ↕' : ''; - ?> - - - - - - - - - - - - - - - - -
          - - - - Keyword Volume KD CPC Intent Status Cluster Actions
          - - 'content', 'label' => 'Content', 'sortable' => true], - ['key' => 'status', 'label' => 'Status', 'sortable' => true], - ['key' => 'date', 'label' => 'Date', 'sortable' => true] -]; -$module = $module ?? ''; -$tab = $tab ?? ''; - -// Debug state: Table HTML rendered -if (function_exists('igny8_debug_state')) { - igny8_debug_state('TABLE_HTML_RENDERED', true, 'Table HTML rendered for ' . $table_id); -} -?> diff --git a/igny8-ai-seo-wp-plugin/modules/config/_README.php b/igny8-ai-seo-wp-plugin/modules/config/_README.php deleted file mode 100644 index 445857da..00000000 --- a/igny8-ai-seo-wp-plugin/modules/config/_README.php +++ /dev/null @@ -1,14 +0,0 @@ - [ - 'keyword' => [ - 'type' => 'search', - 'placeholder' => 'Search keywords...', - 'field' => 'keyword' - ], - 'intent' => [ - 'type' => 'select', - 'label' => 'Intent', - 'field' => 'intent', - 'options' => [ - 'informational' => 'Informational', - 'navigational' => 'Navigational', - 'transactional' => 'Transactional', - 'commercial' => 'Commercial' - ] - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'unmapped' => 'Unmapped', - 'mapped' => 'Mapped', - 'queued' => 'Queued', - 'published' => 'Published' - ] - ], - 'difficulty' => [ - 'type' => 'select', - 'label' => 'Difficulty', - 'field' => 'difficulty', - 'options' => [ - 'Very Easy' => 'Very Easy', - 'Easy' => 'Easy', - 'Medium' => 'Medium', - 'Hard' => 'Hard', - 'Very Hard' => 'Very Hard' - ] - ], - 'search_volume' => [ - 'type' => 'range', - 'label' => 'Volume', - 'field' => 'search_volume', - 'min' => 0, - 'max' => 100000 - ] - ], - - // Clusters Filters - 'planner_clusters' => [ - 'cluster_name' => [ - 'type' => 'search', - 'placeholder' => 'Search clusters...', - 'field' => 'cluster_name' - ], - 'sector_id' => [ - 'type' => 'select', - 'label' => 'Sector', - 'field' => 'sector_id', - 'options' => 'dynamic_sectors' // Will be loaded via AJAX - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'active' => 'Active', - 'inactive' => 'Inactive', - 'archived' => 'Archived' - ] - ], - 'keyword_count' => [ - 'type' => 'range', - 'label' => 'Keywords Count', - 'field' => 'keyword_count', - 'min' => 0, - 'max' => 1000 - ] - ], - - // Ideas Filters - 'planner_ideas' => [ - 'idea_title' => [ - 'type' => 'search', - 'placeholder' => 'Search ideas...', - 'field' => 'idea_title' - ], - 'content_structure' => [ - 'type' => 'select', - 'label' => 'Content Structure', - 'field' => 'content_structure', - 'options' => [ - 'cluster_hub' => 'Cluster Hub', - 'landing_page' => 'Landing Page', - 'guide_tutorial' => 'Guide Tutorial', - 'how_to' => 'How To', - 'comparison' => 'Comparison', - 'review' => 'Review', - 'top_listicle' => 'Top Listicle', - 'question' => 'Question', - 'product_description' => 'Product Description', - 'service_page' => 'Service Page', - 'home_page' => 'Home Page' - ] - ], - 'source' => [ - 'type' => 'select', - 'label' => 'Source', - 'field' => 'source', - 'options' => [ - 'AI' => 'AI', - 'Manual' => 'Manual' - ] - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'new' => 'New', - 'scheduled' => 'Scheduled', - 'published' => 'Published' - ] - ], - 'keyword_cluster_id' => [ - 'type' => 'select', - 'label' => 'Cluster', - 'field' => 'keyword_cluster_id', - 'options' => 'dynamic_clusters' - ], - 'estimated_word_count' => [ - 'type' => 'range', - 'label' => 'Word Count', - 'field' => 'estimated_word_count', - 'min' => 0, - 'max' => 5000 - ] - ], - - - // Writer Tasks Filters (Content Queue / Tasks) - 'writer_tasks' => [ - 'title' => [ - 'type' => 'search', - 'placeholder' => 'Search tasks...', - 'field' => 'title' - ], - 'keywords' => [ - 'type' => 'search', - 'placeholder' => 'Search keywords...', - 'field' => 'keywords' - ], - 'cluster_id' => [ - 'type' => 'select', - 'label' => 'Cluster Name', - 'field' => 'cluster_id', - 'options' => 'dynamic_clusters' - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'queued' => 'Queued', - 'in_progress' => 'In Progress', - 'completed' => 'Completed', - 'cancelled' => 'Cancelled', - 'draft' => 'Draft', - 'review' => 'Review', - 'published' => 'Published' - ] - ], - 'content_type' => [ - 'type' => 'select', - 'label' => 'Content Type', - 'field' => 'content_type', - 'options' => [ - 'blog_post' => 'Blog Post', - 'landing_page' => 'Landing Page', - 'product_page' => 'Product Page', - 'guide_tutorial' => 'Guide Tutorial', - 'news_article' => 'News Article', - 'review' => 'Review', - 'comparison' => 'Comparison', - 'email' => 'Email', - 'social_media' => 'Social Media' - ] - ], - 'created_at' => [ - 'type' => 'date_range', - 'label' => 'Queued Date Range', - 'field' => 'created_at' - ] - ], - - // Writer Drafts Filters (Content Generated) - 'writer_drafts' => [ - 'title' => [ - 'type' => 'search', - 'placeholder' => 'Search drafts...', - 'field' => 'title' - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'draft' => 'Draft', - 'review' => 'Review' - ] - ], - 'content_type' => [ - 'type' => 'select', - 'label' => 'Content Type', - 'field' => 'content_type', - 'options' => [ - 'blog_post' => 'Blog Post', - 'landing_page' => 'Landing Page', - 'product_page' => 'Product Page', - 'guide_tutorial' => 'Guide Tutorial', - 'news_article' => 'News Article', - 'review' => 'Review', - 'comparison' => 'Comparison', - 'email' => 'Email', - 'social_media' => 'Social Media' - ] - ], - 'cluster_id' => [ - 'type' => 'select', - 'label' => 'Cluster', - 'field' => 'cluster_id', - 'options' => 'dynamic_clusters' - ], - 'meta_status' => [ - 'label' => 'Meta Status', - 'type' => 'select', - 'options' => [ - 'all' => 'All', - 'complete' => 'Meta Present', - 'missing' => 'Meta Missing' - ] - ], - 'keywords' => [ - 'label' => 'Keywords', - 'type' => 'text', - 'searchable' => true - ] - ], - - // Writer Published Filters (Live Content) - 'writer_published' => [ - 'title' => [ - 'type' => 'search', - 'placeholder' => 'Search published content...', - 'field' => 'title' - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'published' => 'Published' - ] - ], - 'content_type' => [ - 'type' => 'select', - 'label' => 'Content Type', - 'field' => 'content_type', - 'options' => [ - 'blog_post' => 'Blog Post', - 'landing_page' => 'Landing Page', - 'product_page' => 'Product Page', - 'guide_tutorial' => 'Guide Tutorial', - 'news_article' => 'News Article', - 'review' => 'Review', - 'comparison' => 'Comparison', - 'email' => 'Email', - 'social_media' => 'Social Media' - ] - ], - 'cluster_id' => [ - 'type' => 'select', - 'label' => 'Cluster', - 'field' => 'cluster_id', - 'options' => 'dynamic_clusters' - ], - 'meta_status' => [ - 'label' => 'Meta Status', - 'type' => 'select', - 'options' => [ - 'all' => 'All', - 'complete' => 'Meta Present', - 'missing' => 'Meta Missing' - ] - ], - 'keywords' => [ - 'label' => 'Keywords', - 'type' => 'text', - 'searchable' => true - ], - 'created_at' => [ - 'type' => 'date_range', - 'label' => 'Date Range', - 'field' => 'created_at' - ] - ], - - // Optimizer Audits Filters - 'optimizer_audits' => [ - 'page_url' => [ - 'type' => 'search', - 'placeholder' => 'Search pages...', - 'field' => 'page_url' - ], - 'audit_status' => [ - 'type' => 'select', - 'label' => 'Audit Status', - 'field' => 'audit_status', - 'options' => [ - 'pending' => 'Pending', - 'in_progress' => 'In Progress', - 'completed' => 'Completed', - 'failed' => 'Failed' - ] - ], - 'score_range' => [ - 'type' => 'range', - 'label' => 'SEO Score', - 'field' => 'seo_score', - 'min' => 0, - 'max' => 100 - ], - 'last_audit' => [ - 'type' => 'date_range', - 'label' => 'Last Audit', - 'field' => 'last_audit_date' - ] - ], - - // Linker Backlinks Filters - 'linker_backlinks' => [ - 'target_url' => [ - 'type' => 'search', - 'placeholder' => 'Search target URLs...', - 'field' => 'target_url' - ], - 'source_domain' => [ - 'type' => 'search', - 'placeholder' => 'Search source domains...', - 'field' => 'source_domain' - ], - 'link_type' => [ - 'type' => 'select', - 'label' => 'Link Type', - 'field' => 'link_type', - 'options' => [ - 'dofollow' => 'DoFollow', - 'nofollow' => 'NoFollow', - 'sponsored' => 'Sponsored', - 'ugc' => 'UGC' - ] - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'active' => 'Active', - 'lost' => 'Lost', - 'pending' => 'Pending' - ] - ], - 'domain_authority' => [ - 'type' => 'range', - 'label' => 'Domain Authority', - 'field' => 'domain_authority', - 'min' => 0, - 'max' => 100 - ] - ], - - // Writer Templates Filters - 'writer_templates' => [ - 'prompt_name' => [ - 'type' => 'search', - 'placeholder' => 'Search templates...', - 'field' => 'prompt_name' - ], - 'prompt_type' => [ - 'type' => 'select', - 'label' => 'Category', - 'field' => 'prompt_type', - 'options' => [ - 'content' => 'Blog', - 'optimization' => 'Review', - 'generation' => 'Product', - 'custom' => 'Custom' - ] - ], - 'is_active' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'is_active', - 'options' => [ - '1' => 'Active', - '0' => 'Draft' - ] - ], - 'created_at' => [ - 'type' => 'date_range', - 'label' => 'Created Date', - 'field' => 'created_at' - ] - ], - - // Optimizer Suggestions Filters - 'optimizer_suggestions' => [ - 'page_url' => [ - 'type' => 'search', - 'placeholder' => 'Search pages...', - 'field' => 'page_url' - ], - 'suggestion_type' => [ - 'type' => 'select', - 'label' => 'Suggestion Type', - 'field' => 'suggestion_type', - 'options' => [ - 'title_optimization' => 'Title Optimization', - 'meta_description' => 'Meta Description', - 'heading_structure' => 'Heading Structure', - 'content_improvement' => 'Content Improvement', - 'internal_linking' => 'Internal Linking' - ] - ], - 'priority' => [ - 'type' => 'select', - 'label' => 'Priority', - 'field' => 'priority', - 'options' => [ - 'high' => 'High', - 'medium' => 'Medium', - 'low' => 'Low' - ] - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'pending' => 'Pending', - 'in_progress' => 'In Progress', - 'completed' => 'Completed', - 'dismissed' => 'Dismissed' - ] - ], - 'impact_score' => [ - 'type' => 'range', - 'label' => 'Impact Score', - 'field' => 'impact_score', - 'min' => 0, - 'max' => 100 - ] - ], - - // Linker Campaigns Filters - 'linker_campaigns' => [ - 'campaign_name' => [ - 'type' => 'search', - 'placeholder' => 'Search campaigns...', - 'field' => 'campaign_name' - ], - 'target_url' => [ - 'type' => 'search', - 'placeholder' => 'Search target URLs...', - 'field' => 'target_url' - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'planning' => 'Planning', - 'active' => 'Active', - 'paused' => 'Paused', - 'completed' => 'Completed', - 'cancelled' => 'Cancelled' - ] - ], - 'completion_percentage' => [ - 'type' => 'range', - 'label' => 'Completion %', - 'field' => 'completion_percentage', - 'min' => 0, - 'max' => 100 - ], - 'start_date' => [ - 'type' => 'date_range', - 'label' => 'Start Date', - 'field' => 'start_date' - ] - ], - - // Personalize Rewrites Filters - 'personalize_rewrites' => [ - 'personalized_content' => [ - 'type' => 'search', - 'placeholder' => 'Search personalized content...', - 'field' => 'personalized_content' - ], - 'field_inputs' => [ - 'type' => 'search', - 'placeholder' => 'Search field inputs...', - 'field' => 'field_inputs' - ], - 'post_id' => [ - 'type' => 'search', - 'placeholder' => 'Search by post ID...', - 'field' => 'post_id' - ], - 'created_at' => [ - 'type' => 'date_range', - 'label' => 'Created Date', - 'field' => 'created_at' - ] - ], - - // Personalize Tones Filters - 'personalize_tones' => [ - 'tone_name' => [ - 'type' => 'search', - 'placeholder' => 'Search tones...', - 'field' => 'tone_name' - ], - 'category' => [ - 'type' => 'select', - 'label' => 'Category', - 'field' => 'category', - 'options' => [ - 'business' => 'Business', - 'creative' => 'Creative', - 'technical' => 'Technical', - 'marketing' => 'Marketing', - 'educational' => 'Educational' - ] - ], - 'status' => [ - 'type' => 'select', - 'label' => 'Status', - 'field' => 'status', - 'options' => [ - 'active' => 'Active', - 'inactive' => 'Inactive', - 'draft' => 'Draft' - ] - ], - 'usage_count' => [ - 'type' => 'range', - 'label' => 'Usage Count', - 'field' => 'usage_count', - 'min' => 0, - 'max' => 1000 - ] - ] -]; diff --git a/igny8-ai-seo-wp-plugin/modules/config/forms-config.php b/igny8-ai-seo-wp-plugin/modules/config/forms-config.php deleted file mode 100644 index 2e70863b..00000000 --- a/igny8-ai-seo-wp-plugin/modules/config/forms-config.php +++ /dev/null @@ -1,638 +0,0 @@ - [ - 'fields' => [ - [ - 'name' => 'keyword', - 'type' => 'text', - 'label' => 'Keyword', - 'required' => true - ], - [ - 'name' => 'search_volume', - 'type' => 'number', - 'label' => 'Search Volume', - 'required' => false - ], - [ - 'name' => 'difficulty', - 'type' => 'select', - 'label' => 'Difficulty', - 'options' => [ - 'Very Easy' => 'Very Easy', - 'Easy' => 'Easy', - 'Medium' => 'Medium', - 'Hard' => 'Hard', - 'Very Hard' => 'Very Hard' - ], - 'required' => false - ], - [ - 'name' => 'cpc', - 'type' => 'number', - 'label' => 'CPC', - 'required' => false, - 'step' => 0.01 - ], - [ - 'name' => 'intent', - 'type' => 'select', - 'label' => 'Intent', - 'options' => [ - 'informational' => 'Informational', - 'navigational' => 'Navigational', - 'transactional' => 'Transactional', - 'commercial' => 'Commercial' - ], - 'required' => false - ], - [ - 'name' => 'status', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - 'unmapped' => 'Unmapped', - 'mapped' => 'Mapped', - 'queued' => 'Queued', - 'published' => 'Published' - ], - 'required' => true, - 'default' => 'unmapped' - ], - [ - 'name' => 'cluster_id', - 'type' => 'select', - 'label' => 'Cluster', - 'source' => 'igny8_get_cluster_options', - 'required' => false - ] - ], - 'title' => 'Keyword', - 'submit_text' => 'Save Keyword' - ], - 'planner_clusters' => [ - 'fields' => [ - [ - 'name' => 'cluster_name', - 'type' => 'text', - 'label' => 'Cluster Name', - 'required' => true - ], - [ - 'name' => 'sector_id', - 'type' => 'select', - 'label' => 'Sector', - 'source' => 'igny8_get_sector_options', - 'required' => false - ], - [ - 'name' => 'status', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - 'active' => 'Active', - 'inactive' => 'Inactive', - 'archived' => 'Archived' - ], - 'required' => true, - 'default' => 'active' - ], - ], - 'title' => 'Cluster', - 'submit_text' => 'Save Cluster' - ], - 'planner_ideas' => [ - 'fields' => [ - [ - 'name' => 'idea_title', - 'type' => 'text', - 'label' => 'Idea Title', - 'required' => true - ], - [ - 'name' => 'idea_description', - 'type' => 'textarea', - 'label' => 'Description', - 'required' => false - ], - [ - 'name' => 'content_structure', - 'type' => 'select', - 'label' => 'Content Structure', - 'options' => [ - 'cluster_hub' => 'Cluster Hub', - 'landing_page' => 'Landing Page', - 'guide_tutorial' => 'Guide Tutorial', - 'how_to' => 'How To', - 'comparison' => 'Comparison', - 'review' => 'Review', - 'top_listicle' => 'Top Listicle', - 'question' => 'FAQ', - 'product_description' => 'Product Description', - 'service_page' => 'Service Page', - 'home_page' => 'Home Page' - ], - 'required' => true, - 'default' => 'cluster_hub' - ], - [ - 'name' => 'content_type', - 'type' => 'select', - 'label' => 'Content Type', - 'options' => [ - 'post' => 'Post', - 'product' => 'Product', - 'page' => 'Page', - 'CPT' => 'Custom Post Type' - ], - 'required' => true, - 'default' => 'post' - ], - [ - 'name' => 'target_keywords', - 'type' => 'textarea', - 'label' => 'Target Keywords (comma-separated)', - 'required' => false, - 'placeholder' => 'Enter target keywords for this idea, separated by commas...', - 'rows' => 3 - ], - [ - 'name' => 'image_prompts', - 'type' => 'textarea', - 'label' => 'Image Prompts (JSON)', - 'required' => false, - 'placeholder' => 'Enter image prompts as JSON...', - 'rows' => 4, - 'help_text' => 'JSON format: {"featured_image": "prompt", "in_article_image_1": "prompt", "in_article_image_2": "prompt"}' - ], - [ - 'name' => 'keyword_cluster_id', - 'type' => 'select', - 'label' => 'Cluster', - 'source' => 'igny8_get_cluster_options', - 'required' => false - ], - [ - 'name' => 'source', - 'type' => 'select', - 'label' => 'Source', - 'options' => [ - 'AI' => 'AI', - 'Manual' => 'Manual' - ], - 'required' => true, - 'default' => 'AI' - ], - [ - 'name' => 'status', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - 'new' => 'New', - 'scheduled' => 'Scheduled', - 'published' => 'Published' - ], - 'required' => true, - 'default' => 'new' - ], - [ - 'name' => 'estimated_word_count', - 'type' => 'number', - 'label' => 'Estimated Words', - 'required' => false - ] - ], - 'title' => 'Content Idea', - 'submit_text' => 'Save Idea' - ], - 'writer_tasks' => [ - 'fields' => [ - [ - 'name' => 'title', - 'type' => 'text', - 'label' => 'Task Title', - 'required' => true - ], - [ - 'name' => 'cluster_id', - 'type' => 'select', - 'label' => 'Cluster Name', - 'source' => 'igny8_get_cluster_options', - 'required' => false - ], - [ - 'name' => 'keywords', - 'type' => 'text', - 'label' => 'Keywords', - 'required' => false - ], - [ - 'name' => 'word_count', - 'type' => 'number', - 'label' => 'Word Count', - 'required' => false - ], - [ - 'name' => 'status', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - 'queued' => 'Queued', - 'in_progress' => 'In Progress', - 'completed' => 'Completed', - 'cancelled' => 'Cancelled', - 'draft' => 'Draft', - 'review' => 'Review', - 'published' => 'Published' - ], - 'required' => true, - 'default' => 'queued' - ], - [ - 'name' => 'content_structure', - 'type' => 'select', - 'label' => 'Content Structure', - 'options' => [ - 'cluster_hub' => 'Cluster Hub', - 'landing_page' => 'Landing Page', - 'guide_tutorial' => 'Guide Tutorial', - 'how_to' => 'How To', - 'comparison' => 'Comparison', - 'review' => 'Review', - 'top_listicle' => 'Top Listicle', - 'question' => 'FAQ', - 'product_description' => 'Product Description', - 'service_page' => 'Service Page', - 'home_page' => 'Home Page' - ], - 'required' => true, - 'default' => 'cluster_hub' - ], - [ - 'name' => 'content_type', - 'type' => 'select', - 'label' => 'Content Type', - 'options' => [ - 'post' => 'Post', - 'product' => 'Product', - 'page' => 'Page', - 'CPT' => 'Custom Post Type' - ], - 'required' => true, - 'default' => 'post' - ], - [ - 'name' => 'created_at', - 'type' => 'text', - 'label' => 'Created', - 'required' => false, - 'readonly' => true - ] - ], - 'title' => 'Queue Task', - 'submit_text' => 'Add to Queue' - ], - - 'writer_drafts' => [ - 'fields' => [ - [ - 'name' => 'title', - 'type' => 'text', - 'label' => 'Title', - 'required' => true - ], - [ - 'name' => 'status', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - 'draft' => 'Draft' - ], - 'required' => true, - 'default' => 'draft' - ], - [ - 'name' => 'cluster_id', - 'type' => 'select', - 'label' => 'Cluster', - 'source' => 'igny8_get_cluster_options', - 'required' => false - ], - [ - 'name' => 'content_structure', - 'type' => 'select', - 'label' => 'Content Structure', - 'options' => [ - 'cluster_hub' => 'Cluster Hub', - 'landing_page' => 'Landing Page', - 'guide_tutorial' => 'Guide Tutorial', - 'how_to' => 'How To', - 'comparison' => 'Comparison', - 'review' => 'Review', - 'top_listicle' => 'Top Listicle', - 'question' => 'FAQ', - 'product_description' => 'Product Description', - 'service_page' => 'Service Page', - 'home_page' => 'Home Page' - ], - 'required' => false, - 'default' => 'cluster_hub' - ], - [ - 'name' => 'content_type', - 'type' => 'select', - 'label' => 'Content Type', - 'options' => [ - 'post' => 'Post', - 'product' => 'Product', - 'page' => 'Page', - 'CPT' => 'Custom Post Type' - ], - 'required' => false, - 'default' => 'post' - ], - [ - 'name' => 'meta_title', - 'label' => 'Meta Title', - 'type' => 'text', - 'placeholder' => 'Enter SEO title...', - 'maxlength' => 60 - ], - [ - 'name' => 'meta_description', - 'label' => 'Meta Description', - 'type' => 'textarea', - 'placeholder' => 'Enter meta description...', - 'maxlength' => 160 - ], - [ - 'name' => 'keywords', - 'label' => 'Primary Keywords', - 'type' => 'text', - 'placeholder' => 'e.g., duvet covers, king size' - ], - [ - 'name' => 'word_count', - 'label' => 'Word Count', - 'type' => 'number', - 'readonly' => true - ], - [ - 'name' => 'updated_at', - 'type' => 'text', - 'label' => 'Updated', - 'required' => false, - 'readonly' => true - ] - ], - 'title' => 'Content Draft', - 'submit_text' => 'Save Draft' - ], - - 'writer_published' => [ - 'fields' => [ - [ - 'name' => 'title', - 'type' => 'text', - 'label' => 'Title', - 'required' => true - ], - [ - 'name' => 'status', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - 'published' => 'Published' - ], - 'required' => true, - 'default' => 'published' - ], - [ - 'name' => 'cluster_id', - 'type' => 'select', - 'label' => 'Cluster', - 'source' => 'igny8_get_cluster_options', - 'required' => false - ], - [ - 'name' => 'content_structure', - 'type' => 'select', - 'label' => 'Content Structure', - 'options' => [ - 'cluster_hub' => 'Cluster Hub', - 'landing_page' => 'Landing Page', - 'guide_tutorial' => 'Guide Tutorial', - 'how_to' => 'How To', - 'comparison' => 'Comparison', - 'review' => 'Review', - 'top_listicle' => 'Top Listicle', - 'question' => 'FAQ', - 'product_description' => 'Product Description', - 'service_page' => 'Service Page', - 'home_page' => 'Home Page' - ], - 'required' => false, - 'default' => 'cluster_hub' - ], - [ - 'name' => 'content_type', - 'type' => 'select', - 'label' => 'Content Type', - 'options' => [ - 'post' => 'Post', - 'product' => 'Product', - 'page' => 'Page', - 'CPT' => 'Custom Post Type' - ], - 'required' => false, - 'default' => 'post' - ], - [ - 'name' => 'meta_title', - 'label' => 'Meta Title', - 'type' => 'text', - 'placeholder' => 'Enter SEO title...', - 'maxlength' => 60 - ], - [ - 'name' => 'meta_description', - 'label' => 'Meta Description', - 'type' => 'textarea', - 'placeholder' => 'Enter meta description...', - 'maxlength' => 160 - ], - [ - 'name' => 'keywords', - 'label' => 'Primary Keywords', - 'type' => 'text', - 'placeholder' => 'e.g., duvet covers, king size' - ], - [ - 'name' => 'word_count', - 'label' => 'Word Count', - 'type' => 'number', - 'readonly' => true - ], - [ - 'name' => 'updated_at', - 'type' => 'text', - 'label' => 'Updated', - 'required' => false, - 'readonly' => true - ] - ], - 'title' => 'Published Content', - 'submit_text' => 'Save Published Content' - ], - - - 'writer_templates' => [ - 'fields' => [ - [ - 'name' => 'prompt_name', - 'type' => 'text', - 'label' => 'Template Name', - 'required' => true, - 'placeholder' => 'Enter template name...' - ], - [ - 'name' => 'prompt_type', - 'type' => 'select', - 'label' => 'Category', - 'options' => [ - 'content' => 'Blog', - 'optimization' => 'Review', - 'generation' => 'Product', - 'custom' => 'Custom' - ], - 'required' => true, - 'default' => 'content' - ], - [ - 'name' => 'is_active', - 'type' => 'select', - 'label' => 'Status', - 'options' => [ - '1' => 'Active', - '0' => 'Draft' - ], - 'required' => true, - 'default' => '1' - ], - [ - 'name' => 'prompt_text', - 'type' => 'textarea', - 'label' => 'Prompt Body', - 'required' => true, - 'rows' => 10, - 'placeholder' => 'Enter the prompt template...' - ], - [ - 'name' => 'variables', - 'type' => 'textarea', - 'label' => 'Variables (JSON)', - 'required' => false, - 'rows' => 5, - 'placeholder' => '{"label": "Custom Label", "description": "Template description"}' - ] - ], - 'title' => 'Content Template', - 'submit_text' => 'Save Template' - ], - - 'personalize_data' => [ - 'fields' => [ - [ - 'name' => 'post_id', - 'type' => 'number', - 'label' => 'Post ID', - 'required' => true - ], - [ - 'name' => 'data_type', - 'type' => 'text', - 'label' => 'Data Type', - 'required' => true - ], - [ - 'name' => 'data', - 'type' => 'textarea', - 'label' => 'Data (JSON)', - 'required' => true, - 'rows' => 10 - ] - ], - 'title' => 'Personalization Data', - 'submit_text' => 'Save Data' - ], - - 'personalize_variations' => [ - 'fields' => [ - [ - 'name' => 'post_id', - 'type' => 'number', - 'label' => 'Post ID', - 'required' => true - ], - [ - 'name' => 'fields_hash', - 'type' => 'text', - 'label' => 'Fields Hash', - 'required' => true - ], - [ - 'name' => 'fields_json', - 'type' => 'textarea', - 'label' => 'Fields JSON', - 'required' => true, - 'rows' => 5 - ], - [ - 'name' => 'content', - 'type' => 'textarea', - 'label' => 'Content', - 'required' => true, - 'rows' => 15 - ] - ], - 'title' => 'Content Variation', - 'submit_text' => 'Save Variation' - ] - ]; -} diff --git a/igny8-ai-seo-wp-plugin/modules/config/import-export-config.php b/igny8-ai-seo-wp-plugin/modules/config/import-export-config.php deleted file mode 100644 index 6dc0f46c..00000000 --- a/igny8-ai-seo-wp-plugin/modules/config/import-export-config.php +++ /dev/null @@ -1,150 +0,0 @@ - [ - 'type' => 'keywords', - 'singular' => 'Keyword', - 'plural' => 'Keywords', - 'template_file' => 'igny8_keywords_template.csv', - 'columns' => ['keyword', 'search_volume', 'difficulty', 'cpc', 'intent', 'status', 'sector_id', 'cluster_id'], - 'required_fields' => ['keyword'] - ], - 'planner_clusters' => [ - 'type' => 'clusters', - 'singular' => 'Cluster', - 'plural' => 'Clusters', - 'template_file' => 'igny8_clusters_template.csv', - 'columns' => ['cluster_name', 'sector_id', 'status', 'keyword_count', 'total_volume', 'avg_difficulty', 'mapped_pages_count'], - 'required_fields' => ['cluster_name'] - ], - 'planner_ideas' => [ - 'type' => 'ideas', - 'singular' => 'Idea', - 'plural' => 'Ideas', - 'template_file' => 'igny8_ideas_template.csv', - 'columns' => ['idea_title', 'idea_description', 'content_structure', 'content_type', 'keyword_cluster_id', 'target_keywords', 'status', 'estimated_word_count'], - 'required_fields' => ['idea_title'] - ], - - // WRITER MODULE (3 submodules) - 'writer_tasks' => [ - 'type' => 'tasks', - 'singular' => 'Task', - 'plural' => 'Tasks', - 'template_file' => 'igny8_tasks_template.csv', - 'columns' => ['title', 'description', 'content_type', 'cluster_id', 'priority', 'status', 'keywords', 'schedule_at'], - 'required_fields' => ['title'] - ], - 'writer_drafts' => [ - 'type' => 'tasks', - 'singular' => 'Draft', - 'plural' => 'Drafts', - 'template_file' => 'igny8_tasks_template.csv', - 'columns' => ['title', 'description', 'content_type', 'cluster_id', 'status', 'assigned_post_id'], - 'required_fields' => ['title'] - ], - 'writer_published' => [ - 'type' => 'tasks', - 'singular' => 'Published Content', - 'plural' => 'Published Content', - 'template_file' => 'igny8_tasks_template.csv', - 'columns' => ['title', 'description', 'content_type', 'cluster_id', 'status', 'assigned_post_id'], - 'required_fields' => ['title'] - ], - 'writer_templates' => [ - 'type' => 'templates', - 'singular' => 'Template', - 'plural' => 'Templates', - 'template_file' => 'igny8_templates_template.csv', - 'columns' => ['template_name', 'prompt_type', 'system_prompt', 'user_prompt', 'is_active'], - 'required_fields' => ['template_name'] - ], - - // OPTIMIZER MODULE (2 submodules) - 'optimizer_audits' => [ - 'type' => 'audits', - 'singular' => 'Audit', - 'plural' => 'Audits', - 'template_file' => 'igny8_audits_template.csv', - 'columns' => ['page_id', 'audit_status', 'seo_score', 'issues_found', 'recommendations'], - 'required_fields' => ['page_id'] - ], - 'optimizer_suggestions' => [ - 'type' => 'suggestions', - 'singular' => 'Suggestion', - 'plural' => 'Suggestions', - 'template_file' => 'igny8_suggestions_template.csv', - 'columns' => ['audit_id', 'suggestion_type', 'priority', 'status', 'impact_level'], - 'required_fields' => ['audit_id'] - ], - - // LINKER MODULE (2 submodules) - 'linker_backlinks' => [ - 'type' => 'backlinks', - 'singular' => 'Backlink', - 'plural' => 'Backlinks', - 'template_file' => 'igny8_backlinks_template.csv', - 'columns' => ['source_url', 'target_url', 'anchor_text', 'domain_authority', 'link_type', 'status'], - 'required_fields' => ['source_url', 'target_url'] - ], - 'linker_campaigns' => [ - 'type' => 'campaigns', - 'singular' => 'Campaign', - 'plural' => 'Campaigns', - 'template_file' => 'igny8_campaigns_template.csv', - 'columns' => ['campaign_name', 'target_url', 'status', 'backlink_count', 'live_links_count'], - 'required_fields' => ['campaign_name'] - ], - - // PERSONALIZE MODULE (4 submodules) - 'personalize_rewrites' => [ - 'type' => 'rewrites', - 'singular' => 'Rewrite', - 'plural' => 'Rewrites', - 'template_file' => 'igny8_rewrites_template.csv', - 'columns' => ['post_id', 'tone_id', 'variation_content', 'created_at'], - 'required_fields' => ['post_id'] - ], - 'personalize_tones' => [ - 'type' => 'tones', - 'singular' => 'Tone', - 'plural' => 'Tones', - 'template_file' => 'igny8_tones_template.csv', - 'columns' => ['tone_name', 'tone_type', 'description', 'status', 'usage_count'], - 'required_fields' => ['tone_name'] - ], - 'personalize_data' => [ - 'type' => 'personalization_data', - 'singular' => 'Data Entry', - 'plural' => 'Data Entries', - 'template_file' => 'igny8_personalization_data_template.csv', - 'columns' => ['data_key', 'data_value', 'data_type', 'created_at'], - 'required_fields' => ['data_key'] - ], - 'personalize_variations' => [ - 'type' => 'variations', - 'singular' => 'Variation', - 'plural' => 'Variations', - 'template_file' => 'igny8_variations_template.csv', - 'columns' => ['post_id', 'field_name', 'variation_content', 'tone_id'], - 'required_fields' => ['post_id', 'field_name'] - ] -]; diff --git a/igny8-ai-seo-wp-plugin/modules/config/kpi-config.php b/igny8-ai-seo-wp-plugin/modules/config/kpi-config.php deleted file mode 100644 index 55324462..00000000 --- a/igny8-ai-seo-wp-plugin/modules/config/kpi-config.php +++ /dev/null @@ -1,581 +0,0 @@ - [ - 'total_keywords' => [ - 'label' => 'Total Keywords', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'mapped_keywords' => [ - 'label' => 'Mapped', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "mapped"', - 'color' => 'green' - ], - 'unmapped_keywords' => [ - 'label' => 'Unmapped', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "unmapped"', - 'color' => 'amber' - ], - 'total_volume' => [ - 'label' => 'Total Volume', - 'query' => 'SELECT SUM(search_volume) as count FROM {table_name}', - 'color' => 'purple' - ], - 'avg_difficulty' => [ - 'label' => 'Avg Difficulty', - 'query' => 'SELECT ROUND(AVG(difficulty)) as count FROM {table_name}', - 'color' => 'blue' - ], - 'high_volume_keywords' => [ - 'label' => 'High Volume (>1K)', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE search_volume > 1000', - 'color' => 'green' - ] - ], - - // Clusters KPIs - 'planner_clusters' => [ - 'total_clusters' => [ - 'label' => 'Clusters', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => '' - ], - 'total_volume' => [ - 'label' => 'Volume', - 'query' => 'SELECT SUM(total_volume) as count FROM {table_name}', - 'color' => 'green' - ], - 'total_keywords' => [ - 'label' => 'Keywords', - 'query' => 'SELECT SUM(keyword_count) as count FROM {table_name}', - 'color' => 'amber' - ], - 'mapped_clusters' => [ - 'label' => 'Mapped', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE mapped_pages_count > 0', - 'color' => 'purple' - ], - 'avg_keywords_per_cluster' => [ - 'label' => 'Avg Keywords/Cluster', - 'query' => 'SELECT ROUND(AVG(keyword_count)) as count FROM {table_name}', - 'color' => 'blue' - ], - 'high_volume_clusters' => [ - 'label' => 'High Volume Clusters', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE total_volume > 10000', - 'color' => 'green' - ] - ], - - // Ideas KPIs - 'planner_ideas' => [ - 'total_ideas' => [ - 'label' => 'Ideas', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => '' - ], - 'new_ideas' => [ - 'label' => 'New', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "new"', - 'color' => 'green' - ], - 'scheduled_ideas' => [ - 'label' => 'Scheduled', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "scheduled"', - 'color' => 'amber' - ], - 'published_ideas' => [ - 'label' => 'Published', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "published"', - 'color' => 'purple' - ], - 'ai_generated' => [ - 'label' => 'AI Generated', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE source = "AI"', - 'color' => 'blue' - ], - 'recent_ideas' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ] - ], - - // Planner Home KPIs (Main Dashboard) - 'planner_home' => [ - 'total_keywords' => [ - 'label' => 'Keywords', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_keywords', - 'color' => '' - ], - 'total_volume' => [ - 'label' => 'Volume', - 'query' => 'SELECT SUM(search_volume) as count FROM {prefix}igny8_keywords', - 'color' => 'green' - ], - 'total_clusters' => [ - 'label' => 'Clusters', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_clusters', - 'color' => 'amber' - ], - 'total_ideas' => [ - 'label' => 'Ideas', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_content_ideas', - 'color' => 'purple' - ], - 'high_volume_keywords' => [ - 'label' => 'High Vol (>1K)', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_keywords WHERE search_volume > 1000', - 'color' => 'blue' - ], - 'recent_ideas' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_content_ideas WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'teal' - ], - 'mapped_keywords' => [ - 'label' => 'Mapped Keywords', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_keywords WHERE status = "mapped"', - 'color' => 'green' - ], - 'unmapped_keywords' => [ - 'label' => 'Unmapped Keywords', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_keywords WHERE status = "unmapped"', - 'color' => 'amber' - ], - 'clusters_with_ideas' => [ - 'label' => 'Clusters With Ideas', - 'query' => 'SELECT COUNT(DISTINCT keyword_cluster_id) as count FROM {prefix}igny8_content_ideas WHERE keyword_cluster_id IS NOT NULL', - 'color' => 'green' - ], - 'queued_ideas' => [ - 'label' => 'Queued Ideas', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_content_ideas WHERE status = "scheduled"', - 'color' => 'amber' - ] - ], - - // Writer Home KPIs (Main Dashboard) - 'writer_home' => [ - 'queued_tasks' => [ - 'label' => 'Queued Tasks', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("queued", "in_progress")', - 'color' => 'blue' - ], - 'draft_tasks' => [ - 'label' => 'Drafts', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("draft", "review")', - 'color' => 'amber' - ], - 'published_tasks' => [ - 'label' => 'Published', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status = "published"', - 'color' => 'green' - ], - 'total_tasks' => [ - 'label' => 'Total Tasks', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks', - 'color' => '' - ] - ], - - // Writer Tasks KPIs - 'writer_tasks' => [ - 'total_ideas' => [ - 'label' => 'Ideas', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_content_ideas', - 'color' => '' - ], - 'content_scheduled' => [ - 'label' => 'Content Scheduled', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("queued", "in_progress")', - 'color' => 'green' - ], - 'written' => [ - 'label' => 'Written', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("draft", "review")', - 'color' => 'amber' - ], - 'published' => [ - 'label' => 'Published', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status = "published"', - 'color' => 'purple' - ] - ], - - // Writer Drafts KPIs - 'writer_drafts' => [ - 'total_ideas' => [ - 'label' => 'Ideas', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_content_ideas', - 'color' => '' - ], - 'content_scheduled' => [ - 'label' => 'Content Scheduled', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("queued", "in_progress")', - 'color' => 'green' - ], - 'written' => [ - 'label' => 'Written', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("draft", "review")', - 'color' => 'amber' - ], - 'published' => [ - 'label' => 'Published', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status = "published"', - 'color' => 'purple' - ] - ], - - // Writer Published KPIs - 'writer_published' => [ - 'total_ideas' => [ - 'label' => 'Ideas', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_content_ideas', - 'color' => '' - ], - 'content_scheduled' => [ - 'label' => 'Content Scheduled', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("queued", "in_progress")', - 'color' => 'green' - ], - 'written' => [ - 'label' => 'Written', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status IN ("draft", "review")', - 'color' => 'amber' - ], - 'published' => [ - 'label' => 'Published', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_tasks WHERE status = "published"', - 'color' => 'purple' - ] - ], - - - // Optimizer Audits KPIs - 'optimizer_audits' => [ - 'total_audits' => [ - 'label' => 'Total Audits', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'completed_audits' => [ - 'label' => 'Completed', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE audit_status = "completed"', - 'color' => 'green' - ], - 'pending_audits' => [ - 'label' => 'Pending', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE audit_status = "pending"', - 'color' => 'amber' - ], - 'avg_seo_score' => [ - 'label' => 'Avg SEO Score', - 'query' => 'SELECT ROUND(AVG(seo_score)) as count FROM {table_name} WHERE audit_status = "completed"', - 'color' => 'purple' - ], - 'total_issues' => [ - 'label' => 'Total Issues', - 'query' => 'SELECT SUM(issues_found) as count FROM {table_name} WHERE audit_status = "completed"', - 'color' => 'blue' - ], - 'recent_audits' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ] - ], - - // Writer Templates KPIs - 'writer_templates' => [ - 'total_templates' => [ - 'label' => 'Total Templates', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'active_templates' => [ - 'label' => 'Active', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE is_active = 1', - 'color' => 'green' - ], - 'draft_templates' => [ - 'label' => 'Draft', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE is_active = 0', - 'color' => 'gray' - ], - 'content_templates' => [ - 'label' => 'Blog Templates', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE prompt_type = "content"', - 'color' => 'blue' - ], - 'product_templates' => [ - 'label' => 'Product Templates', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE prompt_type = "generation"', - 'color' => 'green' - ], - 'popular_templates' => [ - 'label' => 'Popular (>10 uses)', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE usage_count > 10', - 'color' => 'purple' - ] - ], - - - // Optimizer Suggestions KPIs - 'optimizer_suggestions' => [ - 'total_suggestions' => [ - 'label' => 'Total Suggestions', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'implemented' => [ - 'label' => 'Implemented', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "implemented"', - 'color' => 'green' - ], - 'pending' => [ - 'label' => 'Pending', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "pending"', - 'color' => 'amber' - ], - 'high_impact' => [ - 'label' => 'High Impact', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE impact_level = "high"', - 'color' => 'purple' - ], - 'avg_improvement' => [ - 'label' => 'Avg Improvement', - 'query' => 'SELECT ROUND(AVG(improvement_score)) as count FROM {table_name} WHERE status = "implemented"', - 'color' => 'blue' - ], - 'recent_suggestions' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ] - ], - - // Linker Backlinks KPIs - 'linker_backlinks' => [ - 'total_backlinks' => [ - 'label' => 'Total Backlinks', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'active_backlinks' => [ - 'label' => 'Active', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "active"', - 'color' => 'green' - ], - 'lost_backlinks' => [ - 'label' => 'Lost', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "lost"', - 'color' => 'amber' - ], - 'avg_domain_authority' => [ - 'label' => 'Avg DA', - 'query' => 'SELECT ROUND(AVG(domain_authority)) as count FROM {table_name} WHERE status = "active"', - 'color' => 'purple' - ], - 'dofollow_links' => [ - 'label' => 'DoFollow Links', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE link_type = "dofollow" AND status = "active"', - 'color' => 'blue' - ], - 'recent_backlinks' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ] - ], - - // Linker Campaigns KPIs - 'linker_campaigns' => [ - 'total_campaigns' => [ - 'label' => 'Total Campaigns', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'active_campaigns' => [ - 'label' => 'Active', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "active"', - 'color' => 'green' - ], - 'completed_campaigns' => [ - 'label' => 'Completed', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "completed"', - 'color' => 'amber' - ], - 'avg_links_per_campaign' => [ - 'label' => 'Avg Links/Campaign', - 'query' => 'SELECT ROUND(AVG(links_acquired)) as count FROM {table_name}', - 'color' => 'purple' - ], - 'success_rate' => [ - 'label' => 'Success Rate %', - 'query' => 'SELECT ROUND((COUNT(CASE WHEN status = "completed" THEN 1 END) * 100.0 / COUNT(*))) as count FROM {table_name}', - 'color' => 'blue' - ], - 'recent_campaigns' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ] - ], - - // Personalize Rewrites KPIs - 'personalize_rewrites' => [ - 'total_rewrites' => [ - 'label' => 'Total Variations', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'this_month_rewrites' => [ - 'label' => 'This Month', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_FORMAT(NOW(), "%Y-%m-01")', - 'color' => 'amber' - ], - 'total_ai_sessions' => [ - 'label' => 'AI Sessions', - 'query' => 'SELECT COUNT(*) as count FROM {prefix}igny8_logs WHERE log_type = "field_detection" OR log_type = "content_generation"', - 'color' => 'green' - ], - 'avg_sessions_per_rewrite' => [ - 'label' => 'Avg Sessions/Rewrite', - 'query' => 'SELECT ROUND((SELECT COUNT(*) FROM {prefix}igny8_logs WHERE log_type = "field_detection" OR log_type = "content_generation") / GREATEST(COUNT(*), 1), 1) as count FROM {table_name}', - 'color' => 'purple' - ], - 'recent_rewrites' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ], - 'successful_rewrites' => [ - 'label' => 'Successful', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "completed"', - 'color' => 'blue' - ] - ], - - // Personalize Tones KPIs - 'personalize_tones' => [ - 'total_tones' => [ - 'label' => 'Total Tones', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'active_tones' => [ - 'label' => 'Active', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "active"', - 'color' => 'green' - ], - 'custom_tones' => [ - 'label' => 'Custom', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE tone_type = "custom"', - 'color' => 'amber' - ], - 'avg_usage_frequency' => [ - 'label' => 'Avg Usage', - 'query' => 'SELECT ROUND(AVG(usage_count)) as count FROM {table_name}', - 'color' => 'purple' - ], - 'popular_tones' => [ - 'label' => 'Popular (>50 uses)', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE usage_count > 50', - 'color' => 'blue' - ], - 'recent_tones' => [ - 'label' => 'This Week', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'green' - ] - ], - - // Personalization Data KPIs - 'personalize_data' => [ - 'total_data_entries' => [ - 'label' => 'Total Data Entries', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'personalization_data' => [ - 'label' => 'Personalization Data', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE data_type = "personalization"', - 'color' => 'green' - ], - 'field_data' => [ - 'label' => 'Field Data', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE data_type = "fields"', - 'color' => 'amber' - ], - 'recent_entries' => [ - 'label' => 'Recent (7 days)', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'purple' - ], - 'active_entries' => [ - 'label' => 'Active', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "active"', - 'color' => 'green' - ], - 'avg_usage_per_entry' => [ - 'label' => 'Avg Usage/Entry', - 'query' => 'SELECT ROUND(AVG(usage_count)) as count FROM {table_name}', - 'color' => 'blue' - ] - ], - - // Personalization Variations KPIs - 'personalize_variations' => [ - 'total_variations' => [ - 'label' => 'Total Variations', - 'query' => 'SELECT COUNT(*) as count FROM {table_name}', - 'color' => 'blue' - ], - 'unique_posts' => [ - 'label' => 'Unique Posts', - 'query' => 'SELECT COUNT(DISTINCT post_id) as count FROM {table_name}', - 'color' => 'green' - ], - 'recent_variations' => [ - 'label' => 'Recent (7 days)', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)', - 'color' => 'amber' - ], - 'avg_variations_per_post' => [ - 'label' => 'Avg Variations/Post', - 'query' => 'SELECT ROUND(COUNT(*) / COUNT(DISTINCT post_id), 2) as count FROM {table_name}', - 'color' => 'purple' - ], - 'published_variations' => [ - 'label' => 'Published', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE status = "published"', - 'color' => 'green' - ], - 'high_performing_variations' => [ - 'label' => 'High Performing', - 'query' => 'SELECT COUNT(*) as count FROM {table_name} WHERE performance_score > 80', - 'color' => 'blue' - ] - ] -]; - diff --git a/igny8-ai-seo-wp-plugin/modules/config/tables-config.php b/igny8-ai-seo-wp-plugin/modules/config/tables-config.php deleted file mode 100644 index 3803fd87..00000000 --- a/igny8-ai-seo-wp-plugin/modules/config/tables-config.php +++ /dev/null @@ -1,989 +0,0 @@ - [ - 'table' => 'igny8_keywords', - 'title' => 'Keywords Management', - 'humanize_columns' => ['keyword', 'search_volume', 'difficulty', 'cpc', 'intent', 'status', 'cluster_id'], - 'columns' => [ - 'keyword' => [ - 'label' => 'Keyword', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'search_volume' => [ - 'label' => 'Volume', - 'type' => 'number', - 'sortable' => true, - 'format' => 'number' - ], - 'difficulty' => [ - 'label' => 'Difficulty', - 'type' => 'number', - 'sortable' => true, - 'format' => 'difficulty_label' - ], - 'cpc' => [ - 'label' => 'CPC', - 'type' => 'number', - 'sortable' => true, - 'decimal' => true - ], - 'intent' => [ - 'label' => 'Intent', - 'type' => 'enum', - 'options' => ['informational', 'navigational', 'transactional', 'commercial'], - 'sortable' => true - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['unmapped', 'mapped', 'queued', 'published'], - 'sortable' => true - ], - 'cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'lookup', - 'source_field' => 'cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'keyword', - 'search_placeholder' => 'Search keywords...', - 'actions' => ['delete_selected', 'export_selected', 'import', 'add_new'], - 'bulk_actions' => ['delete', 'map'], - 'row_actions' => ['edit', 'delete'] - ], - - // Clusters Table - 'planner_clusters' => [ - 'table' => 'igny8_clusters', - 'title' => 'Clusters Management', - 'humanize_columns' => ['cluster_name', 'sector_id', 'status', 'keyword_count', 'total_volume', 'avg_difficulty', 'mapped_pages_count', 'created_at'], - 'columns' => [ - 'cluster_name' => [ - 'label' => 'Cluster Name', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'sector_id' => [ - 'label' => 'Sectors', - 'type' => 'text', - 'source_field' => 'sector_id', - 'display_field' => 'sector_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_sectors s ON {table_name}.sector_id = s.id', - 'select_field' => 's.sector_name as sector_name' - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['active', 'inactive', 'archived'], - 'sortable' => true - ], - 'keyword_count' => [ - 'label' => 'Keywords', - 'type' => 'number', - 'sortable' => true, - 'calculated' => false - //'calculation_query' => 'SELECT COUNT(k.id) FROM {prefix}igny8_keywords k WHERE k.cluster_id = {table_name}.id' - ], - 'total_volume' => [ - 'label' => 'Total Volume', - 'type' => 'number', - 'sortable' => true, - 'format' => 'number' - ], - 'avg_difficulty' => [ - 'label' => 'Avg KD', - 'type' => 'number', - 'sortable' => true, - 'format' => 'difficulty_label' - ], - 'mapped_pages_count' => [ - 'label' => 'Mapped Pages', - 'type' => 'number', - 'sortable' => true, - 'calculated' => false - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'cluster_name', - 'search_placeholder' => 'Search clusters...', - 'actions' => ['delete_selected', 'export_selected', 'import', 'add_new'], - 'bulk_actions' => ['delete', 'activate', 'deactivate'], - 'row_actions' => ['edit', 'delete', 'view_keywords'] - ], - - // Ideas Table - 'planner_ideas' => [ - 'table' => 'igny8_content_ideas', - 'title' => 'Content Ideas Management', - 'humanize_columns' => ['idea_title', 'content_structure', 'content_type', 'target_keywords', 'keyword_cluster_id', 'status', 'estimated_word_count', 'created_at'], - 'columns' => [ - 'idea_title' => [ - 'label' => 'Title', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'content_structure' => [ - 'label' => 'Structure', - 'type' => 'enum', - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'], - 'sortable' => true - ], - 'content_type' => [ - 'label' => 'Type', - 'type' => 'enum', - 'options' => ['post', 'product', 'page', 'CPT'], - 'sortable' => true - ], - 'target_keywords' => [ - 'label' => 'Target Keywords', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'keyword_cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'lookup', - 'source_field' => 'keyword_cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.keyword_cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['new', 'scheduled', 'published'], - 'sortable' => true - ], - 'estimated_word_count' => [ - 'label' => 'Words', - 'type' => 'number', - 'sortable' => true - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'idea_title', - 'search_placeholder' => 'Search ideas...', - 'actions' => ['delete_selected', 'export_selected', 'import', 'add_new'], - 'bulk_actions' => ['delete', 'change_status', 'bulk_queue_to_writer'], - 'row_actions' => ['edit', 'delete', 'create_draft', 'queue_to_writer'] - ], - - - // Writer Tasks Table (Content Queue / Tasks) - 'writer_tasks' => [ - 'table' => 'igny8_tasks', - 'title' => 'Content Queue / Tasks', - 'humanize_columns' => ['title', 'cluster_id', 'keywords', 'word_count', 'status', 'content_structure', 'content_type', 'created_at'], - 'default_filter' => [ - 'status' => ['queued', 'in_progress'] - ], - 'columns' => [ - 'title' => [ - 'label' => 'Task Title', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'cluster_id' => [ - 'label' => 'Cluster Name', - 'type' => 'lookup', - 'source_field' => 'cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ], - 'keywords' => [ - 'label' => 'Keywords', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'word_count' => [ - 'label' => 'Word Count', - 'type' => 'number', - 'sortable' => true, - 'format' => 'number' - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['queued', 'in_progress', 'completed', 'cancelled', 'draft', 'review', 'published'] - ], - 'content_structure' => [ - 'label' => 'Structure', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'label' => 'Type', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['post', 'product', 'page', 'CPT'] - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'datetime', - 'sortable' => true, - 'format' => 'time_ago_created' - ] - ], - 'pagination' => ['per_page' => 20, 'enabled' => true], - 'search_field' => 'title', - 'search_placeholder' => 'Search tasks...', - 'filters' => [ - 'status' => [ - 'label' => 'Status', - 'type' => 'select', - 'options' => ['queued', 'in_progress'] - ], - 'priority' => [ - 'label' => 'Priority', - 'type' => 'select', - 'options' => ['urgent', 'high', 'medium', 'low'] - ], - 'content_structure' => [ - 'label' => 'Content Structure', - 'type' => 'select', - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'label' => 'Content Type', - 'type' => 'select', - 'options' => ['post', 'product', 'page', 'CPT'] - ], - 'cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'select', - 'options' => 'dynamic_clusters' - ] - ], - 'actions' => ['delete_selected', 'export_selected', 'add_new'], - 'bulk_actions' => ['delete', 'mark_in_progress', 'move_to_drafts'], - 'row_actions' => ['edit', 'delete'] - ], - - // Writer Drafts Table (Content Generated) - 'writer_drafts' => [ - 'table' => 'igny8_tasks', - 'title' => 'Content Generated', - 'humanize_columns' => ['title', 'cluster_id', 'status', 'content_structure', 'content_type', 'meta_title', 'meta_description', 'keywords', 'word_count', 'updated_at'], - 'default_filter' => [ - 'status' => ['draft', 'review'] - ], - 'columns' => [ - 'title' => [ - 'label' => 'Title', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'lookup', - 'source_field' => 'cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['draft', 'review'] - ], - 'content_structure' => [ - 'label' => 'Structure', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'label' => 'Type', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['post', 'product', 'page', 'CPT'] - ], - 'meta_title' => [ - 'label' => 'Meta Title', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'meta_description' => [ - 'label' => 'Meta Description', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'keywords' => [ - 'label' => 'Keywords', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'word_count' => [ - 'label' => 'Word Count', - 'type' => 'number', - 'sortable' => true - ], - 'updated_at' => [ - 'label' => 'Updated', - 'type' => 'datetime', - 'sortable' => true, - 'format' => 'time_ago_updated' - ] - ], - 'pagination' => ['per_page' => 20, 'enabled' => true], - 'search_field' => 'title', - 'search_placeholder' => 'Search drafts...', - 'filters' => [ - 'status' => [ - 'label' => 'Status', - 'type' => 'select', - 'options' => ['draft', 'review'] - ], - 'content_structure' => [ - 'label' => 'Content Structure', - 'type' => 'select', - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'label' => 'Content Type', - 'type' => 'select', - 'options' => ['post', 'product', 'page', 'CPT'] - ], - 'cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'select', - 'options' => 'dynamic_clusters' - ] - ], - 'actions' => ['delete_selected', 'publish_selected', 'export_selected', 'add_new'], - 'bulk_actions' => ['delete', 'move_to_queue', 'publish'], - 'row_actions' => ['edit', 'publish', 'delete'] - ], - - // Writer Published Table (Live Content) - 'writer_published' => [ - 'table' => 'igny8_tasks', - 'title' => 'Live Content', - 'humanize_columns' => ['title', 'status', 'cluster_id', 'content_structure', 'content_type', 'meta_title', 'meta_description', 'keywords', 'word_count', 'updated_at'], - 'default_filter' => [ - 'status' => ['published'] - ], - 'columns' => [ - 'title' => [ - 'label' => 'Title', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['published'] - ], - 'cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'lookup', - 'source_field' => 'cluster_id', - 'display_field' => 'cluster_name', - 'sortable' => true, - 'join_query' => 'LEFT JOIN {prefix}igny8_clusters c ON {table_name}.cluster_id = c.id', - 'select_field' => 'c.cluster_name as cluster_name' - ], - 'content_structure' => [ - 'label' => 'Structure', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'label' => 'Type', - 'type' => 'enum', - 'sortable' => true, - 'options' => ['post', 'product', 'page', 'CPT'] - ], - 'meta_title' => [ - 'label' => 'Meta Title', - 'type' => 'text', - 'source_meta' => '_igny8_meta_title' - ], - 'meta_description' => [ - 'label' => 'Meta Description', - 'type' => 'text', - 'source_meta' => '_igny8_meta_description' - ], - 'keywords' => [ - 'label' => 'Keywords', - 'type' => 'text', - 'source_meta' => '_igny8_primary_keywords' - ], - 'word_count' => [ - 'label' => 'Word Count', - 'type' => 'number', - 'source_meta' => '_igny8_word_count', - 'sortable' => true - ], - 'updated_at' => [ - 'label' => 'Updated', - 'type' => 'datetime', - 'sortable' => true, - 'format' => 'time_ago_updated' - ] - ], - 'pagination' => ['per_page' => 20, 'enabled' => true], - 'search_field' => 'title', - 'search_placeholder' => 'Search published content...', - 'filters' => [ - 'status' => [ - 'label' => 'Status', - 'type' => 'select', - 'options' => ['published'] - ], - 'content_structure' => [ - 'label' => 'Content Structure', - 'type' => 'select', - 'options' => ['cluster_hub', 'landing_page', 'guide_tutorial', 'how_to', 'comparison', 'review', 'top_listicle', 'question', 'product_description', 'service_page', 'home_page'] - ], - 'content_type' => [ - 'label' => 'Content Type', - 'type' => 'select', - 'options' => ['post', 'product', 'page', 'CPT'] - ], - 'cluster_id' => [ - 'label' => 'Cluster', - 'type' => 'select', - 'options' => 'dynamic_clusters' - ], - 'created_at' => [ - 'label' => 'Date Range', - 'type' => 'date_range', - 'field' => 'created_at' - ] - ], - 'actions' => ['delete_selected', 'export_selected', 'add_new'], - 'bulk_actions' => ['delete', 'move_to_draft', 'unpublish'], - 'row_actions' => ['edit', 'unpublish', 'delete'] - ], - - // Optimizer Audits Table - 'optimizer_audits' => [ - 'table' => 'igny8_audits', - 'title' => 'SEO Audits Management', - 'columns' => [ - 'page_url' => [ - 'label' => 'Page URL', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'format' => 'url' - ], - 'seo_score' => [ - 'label' => 'SEO Score', - 'type' => 'number', - 'sortable' => true, - 'format' => 'score' - ], - 'audit_status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['pending', 'in_progress', 'completed', 'failed'], - 'sortable' => true - ], - 'issues_found' => [ - 'label' => 'Issues', - 'type' => 'number', - 'sortable' => true - ], - 'last_audit' => [ - 'label' => 'Last Audit', - 'type' => 'date', - 'sortable' => true, - 'format' => 'datetime' - ], - 'next_audit' => [ - 'label' => 'Next Audit', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'page_url', - 'search_placeholder' => 'Search pages...', - 'actions' => ['delete_selected', 'export_selected', 'bulk_audit'], - 'bulk_actions' => ['run_audit', 'schedule_audit'], - 'row_actions' => ['view_details', 'run_audit', 'delete'] - ], - - // Linker Backlinks Table - 'linker_backlinks' => [ - 'table' => 'igny8_backlinks', - 'title' => 'Backlinks Management', - 'columns' => [ - 'target_url' => [ - 'label' => 'Target URL', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'format' => 'url' - ], - 'source_domain' => [ - 'label' => 'Source Domain', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'link_type' => [ - 'label' => 'Link Type', - 'type' => 'enum', - 'options' => ['dofollow', 'nofollow', 'sponsored', 'ugc'], - 'sortable' => true - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['active', 'lost', 'pending'], - 'sortable' => true - ], - 'domain_authority' => [ - 'label' => 'DA', - 'type' => 'number', - 'sortable' => true - ], - 'anchor_text' => [ - 'label' => 'Anchor Text', - 'type' => 'text', - 'sortable' => true - ], - 'discovered_date' => [ - 'label' => 'Discovered', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'target_url', - 'search_placeholder' => 'Search backlinks...', - 'actions' => ['delete_selected', 'export_selected', 'recheck_links'], - 'bulk_actions' => ['recheck', 'mark_lost'], - 'row_actions' => ['edit', 'delete', 'recheck', 'view_source'] - ], - - // Writer Templates Table (Prompts) - 'writer_templates' => [ - 'table' => 'igny8_prompts', - 'title' => 'Content Templates Management', - 'columns' => [ - 'prompt_name' => [ - 'label' => 'Template Name', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'source_field' => 'prompt_name', - 'editable' => true - ], - 'category' => [ - 'label' => 'Category', - 'type' => 'text', - 'sortable' => true, - 'source_field' => 'prompt_type', - 'options' => [ - 'content' => 'Blog', - 'optimization' => 'Review', - 'generation' => 'Product', - 'custom' => 'Custom' - ] - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'badge', - 'sortable' => true, - 'source_field' => 'is_active', - 'options' => [ - '1' => ['label' => 'Active', 'color' => 'green'], - '0' => ['label' => 'Draft', 'color' => 'gray'] - ] - ], - 'label' => [ - 'label' => 'Label', - 'type' => 'text', - 'sortable' => true, - 'source_field' => 'variables', - 'format' => 'json_extract', - 'json_path' => '$.label' - ], - 'prompt_text' => [ - 'label' => 'Prompt Body', - 'type' => 'truncated_text', - 'sortable' => false, - 'source_field' => 'prompt_text', - 'truncate_length' => 100, - 'tooltip' => true - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'datetime', - 'sortable' => true, - 'format' => 'datetime' - ] - ], - 'pagination' => ['per_page' => 15, 'enabled' => true], - 'search_field' => 'prompt_name', - 'search_placeholder' => 'Search templates...', - 'actions' => ['delete_selected', 'export_selected', 'import', 'add_new'], - 'bulk_actions' => ['delete', 'activate', 'deactivate'], - 'row_actions' => ['edit', 'duplicate', 'delete'] - ], - - // Optimizer Suggestions Table - 'optimizer_suggestions' => [ - 'table' => 'igny8_suggestions', - 'title' => 'SEO Suggestions Management', - 'columns' => [ - 'page_url' => [ - 'label' => 'Page URL', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'format' => 'url' - ], - 'suggestion_type' => [ - 'label' => 'Type', - 'type' => 'enum', - 'options' => ['title_optimization', 'meta_description', 'heading_structure', 'content_improvement', 'internal_linking'], - 'sortable' => true - ], - 'priority' => [ - 'label' => 'Priority', - 'type' => 'enum', - 'options' => ['high', 'medium', 'low'], - 'sortable' => true - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['pending', 'in_progress', 'completed', 'dismissed'], - 'sortable' => true - ], - 'impact_score' => [ - 'label' => 'Impact Score', - 'type' => 'number', - 'sortable' => true, - 'format' => 'score' - ], - 'created_date' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'page_url', - 'search_placeholder' => 'Search pages...', - 'actions' => ['delete_selected', 'export_selected', 'bulk_apply'], - 'bulk_actions' => ['apply', 'dismiss', 'change_priority'], - 'row_actions' => ['view_details', 'apply', 'dismiss'] - ], - - // Linker Campaigns Table - 'linker_campaigns' => [ - 'table' => 'igny8_campaigns', - 'title' => 'Link Building Campaigns', - 'columns' => [ - 'campaign_name' => [ - 'label' => 'Campaign Name', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'target_url' => [ - 'label' => 'Target URL', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'format' => 'url' - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['planning', 'active', 'paused', 'completed', 'cancelled'], - 'sortable' => true - ], - 'links_acquired' => [ - 'label' => 'Links Acquired', - 'type' => 'number', - 'sortable' => true - ], - 'target_links' => [ - 'label' => 'Target Links', - 'type' => 'number', - 'sortable' => true - ], - 'completion_percentage' => [ - 'label' => 'Completion %', - 'type' => 'number', - 'sortable' => true, - 'format' => 'percentage' - ], - 'start_date' => [ - 'label' => 'Start Date', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ], - 'end_date' => [ - 'label' => 'End Date', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'campaign_name', - 'search_placeholder' => 'Search campaigns...', - 'actions' => ['delete_selected', 'export_selected', 'import', 'add_new'], - 'bulk_actions' => ['delete', 'activate', 'pause', 'complete'], - 'row_actions' => ['edit', 'delete', 'view_progress', 'duplicate'] - ], - - // Personalize Rewrites Table - 'personalize_rewrites' => [ - 'table' => 'igny8_variations', - 'title' => 'Content Variations Management', - 'humanize_columns' => ['post_id', 'field_inputs', 'personalized_content', 'fields_hash', 'created_at'], - 'columns' => [ - 'post_id' => [ - 'label' => 'Post', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'join_query' => 'LEFT JOIN {prefix}posts p ON {table_name}.post_id = p.ID', - 'select_field' => 'p.post_title as post_title', - 'display_field' => 'post_title' - ], - 'field_inputs' => [ - 'label' => 'Field Inputs', - 'type' => 'text', - 'sortable' => false, - 'searchable' => true, - 'truncate' => 100 - ], - 'personalized_content' => [ - 'label' => 'Personalized Content', - 'type' => 'text', - 'sortable' => false, - 'searchable' => true, - 'truncate' => 150 - ], - 'fields_hash' => [ - 'label' => 'Fields Hash', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true, - 'truncate' => 20 - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'datetime' - ] - ], - 'pagination' => ['per_page' => 20, 'enabled' => true], - 'search_field' => 'personalized_content', - 'search_placeholder' => 'Search personalized content...', - 'actions' => ['delete_selected', 'export_selected', 'bulk_delete'], - 'bulk_actions' => ['delete'], - 'row_actions' => ['edit', 'delete', 'preview'] - ], - - // Personalize Tones Table - 'personalize_tones' => [ - 'table' => 'igny8_tones', - 'title' => 'Tone Management', - 'columns' => [ - 'tone_name' => [ - 'label' => 'Tone Name', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'description' => [ - 'label' => 'Description', - 'type' => 'text', - 'sortable' => false, - 'searchable' => true, - 'truncate' => 150 - ], - 'category' => [ - 'label' => 'Category', - 'type' => 'enum', - 'options' => ['business', 'creative', 'technical', 'marketing', 'educational'], - 'sortable' => true - ], - 'status' => [ - 'label' => 'Status', - 'type' => 'enum', - 'options' => ['active', 'inactive', 'draft'], - 'sortable' => true - ], - 'usage_count' => [ - 'label' => 'Usage Count', - 'type' => 'number', - 'sortable' => true - ], - 'created_date' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'date' - ] - ], - 'pagination' => ['per_page' => 10, 'enabled' => true], - 'search_field' => 'tone_name', - 'search_placeholder' => 'Search tones...', - 'actions' => ['delete_selected', 'export_selected', 'import', 'add_new'], - 'bulk_actions' => ['delete', 'activate', 'deactivate'], - 'row_actions' => ['edit', 'delete', 'duplicate', 'preview'] - ], - - // Personalization Data Table - 'personalize_data' => [ - 'table' => 'igny8_data', - 'title' => 'Personalization Data', - 'humanize_columns' => ['post_id', 'data_type', 'data', 'created_at'], - 'columns' => [ - 'post_id' => [ - 'label' => 'Post ID', - 'type' => 'number', - 'sortable' => true, - 'display_field' => 'post_title', - 'join' => [ - 'table' => 'posts', - 'on' => 'igny8_data.post_id = posts.ID', - 'type' => 'LEFT' - ] - ], - 'data_type' => [ - 'label' => 'Data Type', - 'type' => 'text', - 'sortable' => true, - 'searchable' => true - ], - 'data' => [ - 'label' => 'Data', - 'type' => 'json', - 'sortable' => false, - 'format' => 'json_preview' - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'datetime' - ] - ], - 'pagination' => ['per_page' => 20, 'enabled' => true], - 'search_field' => 'data_type', - 'search_placeholder' => 'Search data types...', - 'actions' => ['delete_selected', 'export_selected'], - 'bulk_actions' => ['delete'], - 'row_actions' => ['view', 'delete'] - ], - - // Personalization Variations Table - 'personalize_variations' => [ - 'table' => 'igny8_variations', - 'title' => 'Content Variations', - 'humanize_columns' => ['post_id', 'fields_hash', 'content', 'created_at'], - 'columns' => [ - 'post_id' => [ - 'label' => 'Post ID', - 'type' => 'number', - 'sortable' => true, - 'display_field' => 'post_title', - 'join' => [ - 'table' => 'posts', - 'on' => 'igny8_variations.post_id = posts.ID', - 'type' => 'LEFT' - ] - ], - 'fields_hash' => [ - 'label' => 'Fields Hash', - 'type' => 'text', - 'sortable' => true, - 'format' => 'hash_preview' - ], - 'content' => [ - 'label' => 'Content', - 'type' => 'text', - 'sortable' => false, - 'format' => 'content_preview' - ], - 'created_at' => [ - 'label' => 'Created', - 'type' => 'date', - 'sortable' => true, - 'format' => 'datetime' - ] - ], - 'pagination' => ['per_page' => 20, 'enabled' => true], - 'search_field' => 'fields_hash', - 'search_placeholder' => 'Search variations...', - 'actions' => ['delete_selected', 'export_selected'], - 'bulk_actions' => ['delete'], - 'row_actions' => ['view', 'edit', 'delete'] - ] -]; - diff --git a/igny8-ai-seo-wp-plugin/modules/help/docs.php b/igny8-ai-seo-wp-plugin/modules/help/docs.php deleted file mode 100644 index 494fe4f1..00000000 --- a/igny8-ai-seo-wp-plugin/modules/help/docs.php +++ /dev/null @@ -1,841 +0,0 @@ - - - -
          -
          -
          -

          Igny8 AI SEO - Complete Technical Snapshot

          -

          Comprehensive AI-powered SEO operating system for WordPress

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          System Architecture

          -

          Igny8 is a sophisticated WordPress plugin that combines AI automation with configuration-driven architecture to deliver enterprise-level SEO functionality.

          -
            -
          • Modular Design: Independent modules with clear interfaces
          • -
          • AI-Centric: OpenAI integration for intelligent automation
          • -
          • Configuration-Driven: All UI components render from configuration files
          • -
          • Automation-First: CRON-based workflows for hands-off operation
          • -
          -
          - -
          -

          Core Modules

          -

          Eight main modules provide comprehensive SEO functionality across the entire content lifecycle.

          -
            -
          • Planner: Keyword research, clustering, and content planning
          • -
          • Writer: AI-powered content generation and task management
          • -
          • Analytics: Performance tracking and SEO analytics
          • -
          • Schedules: Automated task scheduling and CRON management
          • -
          -
          - -
          -

          Technical Stack

          -

          Built on WordPress with advanced AI integration and modern web technologies.

          -
            -
          • Backend: PHP 7.4+, WordPress 5.0+, MySQL 5.7+
          • -
          • AI Integration: OpenAI GPT-4, GPT-3.5-turbo
          • -
          • Frontend: Vanilla JavaScript, CSS3, HTML5
          • -
          • Database: 15 custom tables, WordPress integration
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Complete File Structure

          -

          Organized file tree with detailed descriptions

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          igny8-ai-seo/
          -├── igny8.php                          # Main plugin bootstrap and initialization
          -├── install.php                        # Database setup and plugin activation
          -├── uninstall.php                      # Plugin cleanup and data removal
          -├── igny8-wp-load-handler.php          # CRON endpoint handler
          -├── CHANGELOG.md                       # Version history and changes
          -│
          -├── ai/                                # AI Integration System (5 files)
          -│   ├── integration.php                # API key setup and connection management
          -│   ├── modules-ai.php                 # Common AI interface for modules
          -│   ├── model-rates-config.php         # AI model pricing and rate limits
          -│   ├── openai-api.php                 # OpenAI API integration and AI functions
          -│   └── prompts-library.php            # AI prompts library and templates
          -│
          -├── assets/                            # Frontend Assets
          -│   ├── css/
          -│   │   └── core.css                   # Main stylesheet (2000+ lines)
          -│   ├── js/
          -│   │   └── core.js                    # Main JavaScript (1000+ lines)
          -│   ├── templates/                     # CSV templates for import/export
          -│   │   ├── igny8_clusters_template.csv
          -│   │   ├── igny8_ideas_template.csv
          -│   │   └── igny8_keywords_template.csv
          -│   └── ai-images/                     # AI-generated images
          -│
          -├── core/                              # Core System Files
          -│   ├── admin/                         # Admin Interface System (7 files)
          -│   │   ├── ajax.php                   # Centralized AJAX endpoint management
          -│   │   ├── global-helpers.php         # Global utility functions (50+ helpers)
          -│   │   ├── init.php                   # Admin initialization and settings registration
          -│   │   ├── menu.php                   # WordPress admin menu registration
          -│   │   ├── meta-boxes.php             # WordPress meta boxes integration
          -│   │   ├── module-manager-class.php   # Module management system
          -│   │   └── routing.php                # Admin page routing and content rendering
          -│   ├── cron/                          # CRON System (2 files)
          -│   │   ├── igny8-cron-handlers.php    # CRON task handlers
          -│   │   └── igny8-cron-master-dispatcher.php # CRON master dispatcher
          -│   ├── db/                            # Database System (2 files)
          -│   │   ├── db.php                     # Database operations, schema, and utilities
          -│   │   └── db-migration.php           # Version-based migration system
          -│   ├── pages/                        # Admin Page Templates (organized by module)
          -│   │   ├── analytics/                 # Analytics module pages (2 files)
          -│   │   │   ├── analytics.php          # Analytics and reporting interface
          -│   │   │   └── status.php             # System status and health monitoring
          -│   │   ├── cron/                      # CRON management pages
          -│   │   ├── help/                      # Help and documentation pages (2 files)
          -│   │   │   ├── docs.php               # Technical documentation page
          -│   │   │   └── help.php               # User guide and support page
          -│   │   ├── settings/                  # Settings module pages (4 files)
          -│   │   │   ├── general-settings.php   # General plugin settings interface
          -│   │   │   ├── import-export.php      # Data import/export interface
          -│   │   │   ├── integration.php        # API integration settings interface
          -│   │   │   └── schedules.php          # Scheduling and automation interface
          -│   │   └── thinker/                   # Thinker module pages (5 files)
          -│   │       ├── image-testing.php      # Image testing interface
          -│   │       ├── main.php               # Thinker main interface
          -│   │       ├── profile.php            # Thinker profile interface
          -│   │       ├── prompts.php            # Prompts management interface
          -│   │       └── strategies.php         # Strategies interface
          -│   └── global-layout.php              # Master UI layout template
          -│
          -├── debug/                             # Debug & Monitoring System (5 files)
          -│   ├── debug.php                      # Debug functionality (redirected to status)
          -│   ├── module-debug.php               # Module-specific debugging utilities
          -│   ├── monitor-helpers.php            # Monitoring helper functions
          -│   ├── system-testing.php             # System testing utilities
          -│   └── temp-function-testing.php      # Function testing utilities
          -│
          -├── docs/                              # Documentation System (8 files)
          -│   ├── HOW_TO_ADD_COLUMN.md           # Database column addition guide
          -│   ├── IGNY8_SNAPSHOT_V0.1.md       # Complete plugin snapshot
          -│   ├── MASTER_ARCHITECTURE.md         # Master architecture documentation
          -│   ├── how-tos/                       # How-to guides (5 files)
          -│   │   ├── 01-adding-new-pages-and-modules.md
          -│   │   ├── 02-adding-new-modules-to-module-manager.md
          -│   │   ├── 03-auto-clustering-system.md
          -│   │   ├── cron-management.md
          -│   │   └── HOW_TO_ADD_COLUMN.md
          -│   └── parts/                         # Architecture parts (2 files)
          -│       ├── AI_INTEGRATION_ARCHITECTURE.md
          -│       └── AUTOMATION_FLOWS.md
          -│
          -├── flows/                             # Automation & Workflow System (3 files)
          -│   ├── sync-ajax.php                  # Automation-specific AJAX handlers
          -│   ├── sync-functions.php             # Core automation logic and workflow functions
          -│   └── sync-hooks.php                 # Workflow hook definitions and registration
          -│
          -└── modules/                           # Module System
          -    ├── components/                    # Reusable UI Components (8 files)
          -    │   ├── actions-tpl.php            # Action buttons template
          -    │   ├── export-modal-tpl.php       # Export modal template
          -    │   ├── filters-tpl.php           # Filter controls template
          -    │   ├── forms-tpl.php              # Form rendering template
          -    │   ├── import-modal-tpl.php       # Import modal template
          -    │   ├── kpi-tpl.php                # KPI display template
          -    │   ├── pagination-tpl.php         # Pagination controls template
          -    │   └── table-tpl.php              # Data table template
          -    ├── config/                        # Configuration Files (5 files)
          -    │   ├── filters-config.php         # Filter configuration definitions
          -    │   ├── forms-config.php           # Form configuration definitions
          -    │   ├── import-export-config.php    # Import/export configuration
          -    │   ├── kpi-config.php             # KPI configuration definitions
          -    │   └── tables-config.php          # Table configuration definitions
          -    └── modules-pages/                 # Module Page Interfaces
          -        ├── linker.php                 # Linker module interface
          -        ├── optimizer.php              # Optimizer module interface
          -        ├── planner.php                # Planner module interface
          -        ├── writer.php                 # Writer module interface
          -        └── personalize/               # Personalization Module (7 files)
          -            ├── content-generation.php # Content generation interface
          -            ├── front-end.php          # Frontend personalization
          -            ├── personalize.ajax       # Personalization AJAX handlers
          -            ├── personalize.js         # Personalization JavaScript
          -            ├── personalize.php        # Personalize module main interface
          -            ├── rewrites.php           # Content rewriting interface
          -            └── Settings.php           # Personalization settings
          -
          -
          -
          - - -
          -
          -
          -

          Database Architecture

          -

          15 custom tables with comprehensive relationships

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Core Data Tables

          -
            -
          • igny8_keywords - Keyword research data with metrics
          • -
          • igny8_clusters - Content topic groupings with stored metrics
          • -
          • igny8_content_ideas - AI-generated content concepts
          • -
          • igny8_tasks - Writer workflow management
          • -
          • igny8_variations - Personalization content cache
          • -
          -
          - -
          -

          Analytics & Tracking

          -
            -
          • igny8_logs - System audit trail and AI event logging
          • -
          • igny8_ai_queue - AI processing queue with retry logic
          • -
          • igny8_campaigns - Link building campaign management
          • -
          • igny8_backlinks - Backlink monitoring and tracking
          • -
          -
          - -
          -

          WordPress Integration

          -
            -
          • wp_options - Plugin settings (38+ options)
          • -
          • wp_posts - Generated WordPress content
          • -
          • wp_postmeta - Custom post meta fields (6 fields)
          • -
          • wp_terms - Custom taxonomies (sectors, clusters)
          • -
          -
          -
          - -
          -

          Data Flow Architecture

          -
          Keywords → Clusters → Ideas → Tasks → WordPress Posts
          -    ↓         ↓        ↓       ↓
          -  Mapping → Posts ← Variations (Personalization)
          -    ↓
          -Campaigns → Sites → Backlinks
          -
          -
          -
          - - -
          -
          -
          -

          AI Integration System

          -

          OpenAI integration with cost tracking and automation

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          AI Functions

          -
            -
          • Content Generation: Blog posts, landing pages, product descriptions
          • -
          • Keyword Analysis: Intent detection, difficulty scoring, clustering
          • -
          • SEO Optimization: Meta descriptions, title optimization
          • -
          • Personalization: Audience-specific content variations
          • -
          -
          - -
          -

          Model Configuration

          -
            -
          • GPT-4: Primary model for complex tasks
          • -
          • GPT-3.5-turbo: Fallback for cost optimization
          • -
          • Rate Limiting: Automatic retry with exponential backoff
          • -
          • Cost Tracking: Daily budget limits and usage monitoring
          • -
          -
          - -
          -

          AI Queue System

          -
            -
          • Queue Processing: Background AI task processing
          • -
          • Retry Logic: Automatic retry for failed requests
          • -
          • Priority System: Task prioritization for efficient processing
          • -
          • Error Handling: Comprehensive error logging and recovery
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Automation Workflows

          -

          Event-driven automation with CRON scheduling

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Keyword Processing Workflow

          -
          // When keywords are imported/updated
          -igny8_handle_keyword_cluster_update($keyword_id) {
          -    // Update cluster metrics
          -    igny8_update_cluster_metrics($cluster_id);
          -    
          -    // Trigger AI clustering if enabled
          -    if (ai_enabled) {
          -        igny8_ajax_ai_cluster_keywords($keyword_ids);
          -    }
          -}
          -
          - -
          -

          Content Generation Workflow

          -
          // When content ideas are created
          -igny8_create_task_from_idea($idea_id) {
          -    // Create writer task
          -    $task_id = create_task($idea_data);
          -    
          -    // Generate content if AI enabled
          -    if (ai_enabled) {
          -        igny8_ajax_ai_generate_content($task_id);
          -    }
          -    
          -    // Update metrics
          -    igny8_update_idea_metrics($idea_id);
          -}
          -
          - -
          -

          Cluster Management Workflow

          -
          // When clusters are created/updated
          -igny8_auto_create_cluster_term($cluster_id) {
          -    // Create WordPress taxonomy term
          -    $term_id = wp_insert_term($cluster_name, 'clusters');
          -    
          -    // Link cluster to term
          -    update_cluster_term_id($cluster_id, $term_id);
          -    
          -    // Update metrics
          -    igny8_update_cluster_metrics($cluster_id);
          -}
          -
          -
          -
          -
          - - -
          -
          -
          -

          Configuration System

          -

          Configuration-driven UI with reusable components

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Table Configuration

          -

          Dynamic table rendering with sorting, filtering, and pagination based on configuration files.

          -
            -
          • Column Definitions: Field types, labels, and display options
          • -
          • Sorting & Filtering: Configurable sort and filter options
          • -
          • Actions: Bulk operations and individual record actions
          • -
          • Pagination: Configurable page sizes and navigation
          • -
          -
          - -
          -

          Form Configuration

          -

          Dynamic form generation with validation and field types based on configuration.

          -
            -
          • Field Types: Text, number, select, textarea, date, etc.
          • -
          • Validation: Required fields, data types, and custom validation
          • -
          • Lookup Fields: Foreign key relationships and dropdown options
          • -
          • Conditional Logic: Show/hide fields based on other field values
          • -
          -
          - -
          -

          KPI Configuration

          -

          Dynamic metrics display with charts and trend indicators based on configuration.

          -
            -
          • Metric Types: Count, sum, average, percentage calculations
          • -
          • Visualization: Charts, graphs, and trend indicators
          • -
          • Filtering: Date ranges and conditional filtering
          • -
          • Real-time Updates: Live data updates and caching
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Security & Performance

          -

          Enterprise-level security and optimization

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Security Measures

          -
            -
          • Nonce Verification: All AJAX requests protected with WordPress nonces
          • -
          • Capability Checks: User permission validation for all operations
          • -
          • Data Sanitization: All input data sanitized and validated
          • -
          • SQL Injection Protection: Prepared statements for all database queries
          • -
          -
          - -
          -

          Performance Optimizations

          -
            -
          • Conditional Loading: Admin assets only loaded when needed
          • -
          • Database Indexing: Optimized indexes on frequently queried fields
          • -
          • Caching: WordPress transients for expensive operations
          • -
          • Lazy Loading: AJAX-based data loading for large datasets
          • -
          -
          - -
          -

          Monitoring & Debugging

          -
            -
          • Real-time Monitoring: Live system health monitoring
          • -
          • Module Debug: Individual module performance tracking
          • -
          • Error Logging: Comprehensive error tracking and reporting
          • -
          • Performance Metrics: Response times and resource usage
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          API Reference

          -

          Complete function and endpoint documentation

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Core Functions

          -
          -
          -
          Database Functions
          -
            -
          • igny8_create_all_tables() - Create all database tables
          • -
          • igny8_register_taxonomies() - Register custom taxonomies
          • -
          • igny8_register_post_meta() - Register custom post meta
          • -
          • igny8_install_database() - Complete plugin installation
          • -
          -
          -
          -
          Admin Functions
          -
            -
          • igny8_get_cluster_options() - Get cluster dropdown options
          • -
          • igny8_get_sector_options() - Get sector dropdown options
          • -
          • igny8_render_table() - Render dynamic tables
          • -
          • igny8_render_filters() - Render filter controls
          • -
          -
          -
          -
          - -
          -

          AI Functions

          -
          -
          -
          Content Generation
          -
            -
          • igny8_generate_blog_post() - Generate blog post content
          • -
          • igny8_generate_landing_page() - Generate landing page content
          • -
          • igny8_generate_product_description() - Generate product content
          • -
          • igny8_generate_seo_meta() - Generate SEO meta data
          • -
          -
          -
          -
          AI Analysis
          -
            -
          • igny8_ai_analyze_keywords() - Analyze keywords using AI
          • -
          • igny8_ai_cluster_keywords() - Cluster keywords using AI
          • -
          • igny8_ai_generate_ideas() - Generate content ideas
          • -
          • igny8_ai_optimize_content() - Optimize existing content
          • -
          -
          -
          -
          - -
          -

          Automation Functions

          -
          -
          -
          Workflow Functions
          -
            -
          • igny8_update_cluster_metrics() - Update cluster metrics
          • -
          • igny8_update_idea_metrics() - Update idea metrics
          • -
          • igny8_workflow_triggers() - Trigger workflow automation
          • -
          • igny8_bulk_delete_keywords() - Bulk delete keywords
          • -
          -
          -
          -
          AJAX Endpoints
          -
            -
          • wp_ajax_igny8_get_table_data - Get table data
          • -
          • wp_ajax_igny8_save_record - Save/update record
          • -
          • wp_ajax_igny8_ai_generate_content - AI content generation
          • -
          • wp_ajax_igny8_bulk_action - Perform bulk actions
          • -
          -
          -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Development Workflow

          -

          Guidelines for extending and maintaining the plugin

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Adding New Modules

          -
            -
          1. Create module page in modules/modules-pages/
          2. -
          3. Update module manager in core/admin/module-manager-class.php
          4. -
          5. Add table, form, and filter configurations
          6. -
          7. Register routes in core/admin/routing.php
          8. -
          9. Add menu items in core/admin/menu.php
          10. -
          -
          - -
          -

          Adding New Tables

          -
            -
          1. Add database schema to core/db/db.php
          2. -
          3. Create migration in core/db/db-migration.php
          4. -
          5. Add table configuration to modules/config/tables-config.php
          6. -
          7. Add form configuration to modules/config/forms-config.php
          8. -
          9. Add filter configuration to modules/config/filters-config.php
          10. -
          -
          - -
          -

          Adding AI Features

          -
            -
          1. Add prompt template to ai/prompts-library.php
          2. -
          3. Add AI handler to ai/modules-ai.php
          4. -
          5. Add queue processing to flows/sync-functions.php
          6. -
          7. Add AJAX endpoint to flows/sync-ajax.php
          8. -
          -
          -
          -
          -
          - - diff --git a/igny8-ai-seo-wp-plugin/modules/help/function-testing.php b/igny8-ai-seo-wp-plugin/modules/help/function-testing.php deleted file mode 100644 index ac4a891b..00000000 --- a/igny8-ai-seo-wp-plugin/modules/help/function-testing.php +++ /dev/null @@ -1,122 +0,0 @@ - -
          -

          Test Page

          -

          This is a test page

          - - -

          AJAX Text Input Test

          -
          - - - - - - - -
          - - - -

          This text will be saved using AJAX without page reload

          -
          - - - - -
          - -
          - - -
          - - - diff --git a/igny8-ai-seo-wp-plugin/modules/help/help.php b/igny8-ai-seo-wp-plugin/modules/help/help.php deleted file mode 100644 index 5abdd24f..00000000 --- a/igny8-ai-seo-wp-plugin/modules/help/help.php +++ /dev/null @@ -1,834 +0,0 @@ - - -
          - - -
          -
          -
          -

          Welcome to Igny8 AI SEO

          -

          Your complete AI-powered SEO solution for WordPress

          -
          -
          - -
          -
          -
          - -
          -
          -

          Igny8 is a comprehensive AI-powered SEO plugin that helps you research keywords, plan content, and optimize your website for search engines using artificial intelligence. Transform your content strategy with intelligent automation and AI-driven insights.

          - -
          -
          - - Smart Keyword Research - AI-powered keyword analysis and clustering -
          -
          - - Content Generation - Create high-quality content with AI assistance -
          -
          - - Performance Tracking - Monitor your SEO progress and results -
          -
          -
          -
          - - -
          -
          -
          -

          Getting Started

          -

          Set up your AI-powered SEO workflow in minutes

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -
          - 1 -

          Configure AI Integration

          -
          -

          Go to Settings > AI Integration and enter your OpenAI API key. Choose your preferred AI model (GPT-4 recommended for best results).

          -
            -
          • API Key Setup: Enter your OpenAI API key for AI functionality
          • -
          • Model Selection: Choose between GPT-4, GPT-3.5-turbo, or other available models
          • -
          • Cost Management: Set daily limits to control API usage costs
          • -
          • Testing: Test your AI integration to ensure everything works properly
          • -
          -
          - 💡 Tip: You can get an OpenAI API key from platform.openai.com -
          -
          - -
          -
          - 2 -

          Import Your Keywords

          -
          -

          Navigate to Planner > Keywords and import your keyword list or add keywords manually. Set search volume, difficulty, and intent for each keyword.

          -
            -
          • Bulk Import: Upload CSV files with keyword data
          • -
          • Manual Entry: Add keywords one by one with detailed metrics
          • -
          • Data Enrichment: Set search volume, difficulty, and CPC data
          • -
          • Intent Classification: Categorize keywords by user intent
          • -
          -
          - 💡 Tip: Use the bulk import feature to add multiple keywords at once -
          -
          - -
          -
          - 3 -

          Create Content Clusters

          -
          -

          Go to Planner > Clusters and group related keywords into content topics. Use AI clustering to automatically organize your keywords.

          -
            -
          • AI Clustering: Automatically group related keywords using AI
          • -
          • Manual Organization: Create custom clusters for specific topics
          • -
          • Cluster Metrics: Track keyword count, volume, and difficulty
          • -
          • Content Mapping: Link clusters to published content
          • -
          -
          - 💡 Tip: Let AI suggest cluster groupings for faster organization -
          -
          - -
          -
          - 4 -

          Generate Content Ideas

          -
          -

          Visit Planner > Ideas to generate AI-powered content ideas based on your clusters. Refine and prepare ideas for content creation.

          -
            -
          • AI Generation: Create content ideas using artificial intelligence
          • -
          • Keyword Integration: Ideas include target keywords and topics
          • -
          • Content Types: Generate ideas for blog posts, guides, and more
          • -
          • Idea Management: Organize and prioritize content ideas
          • -
          -
          - 💡 Tip: Generate multiple ideas per cluster for content variety -
          -
          - -
          -
          - 5 -

          Create and Publish Content

          -
          -

          Go to Writer > Tasks to create content tasks from your ideas. Use AI to generate content or write manually, then publish directly to your site.

          -
            -
          • Task Creation: Convert ideas into actionable content tasks
          • -
          • AI Content Generation: Generate high-quality content using AI
          • -
          • Content Review: Edit and refine content before publishing
          • -
          • Direct Publishing: Publish content directly to your WordPress site
          • -
          -
          - 💡 Tip: Review AI-generated content before publishing to ensure quality -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Planner Module

          -

          Research keywords, create clusters, and generate content ideas

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Keywords Management

          -

          Research and organize keywords by search volume, difficulty, and intent. Import keywords from various sources or add them manually.

          -
            -
          • Import Keywords: Upload CSV files or paste keyword lists
          • -
          • Set Metrics: Add search volume, difficulty, and CPC data
          • -
          • Intent Classification: Categorize keywords by user intent
          • -
          • Status Tracking: Monitor keyword mapping and usage
          • -
          -
          - -
          -

          Content Clusters

          -

          Group related keywords into content topics for better content planning and SEO strategy.

          -
            -
          • AI Clustering: Automatically group related keywords
          • -
          • Manual Organization: Create custom clusters for specific topics
          • -
          • Cluster Metrics: Track keyword count, volume, and difficulty
          • -
          • Content Mapping: Link clusters to published content
          • -
          -
          - -
          -

          Content Ideas

          -

          Generate AI-powered content ideas based on your keyword clusters and research.

          -
            -
          • AI Generation: Create content ideas using artificial intelligence
          • -
          • Keyword Integration: Ideas include target keywords and topics
          • -
          • Content Types: Generate ideas for blog posts, guides, and more
          • -
          • Idea Management: Organize and prioritize content ideas
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Writer Module

          -

          Create, manage, and publish content with AI assistance

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Content Tasks

          -

          Create and manage content writing tasks with detailed specifications and deadlines.

          -
            -
          • Task Creation: Convert ideas into actionable content tasks
          • -
          • Priority Setting: Organize tasks by importance and urgency
          • -
          • Deadline Management: Set and track content deadlines
          • -
          • Progress Tracking: Monitor task completion status
          • -
          -
          - -
          -

          AI Content Generation

          -

          Generate high-quality content using AI based on your research and specifications.

          -
            -
          • Blog Posts: Create complete blog post content
          • -
          • Landing Pages: Generate optimized landing page copy
          • -
          • Product Descriptions: Write compelling product content
          • -
          • SEO Meta: Generate titles, descriptions, and meta tags
          • -
          -
          - -
          -

          Content Workflow

          -

          Track content from idea to publication with automated workflows and status updates.

          -
            -
          • Draft Management: Create and manage content drafts
          • -
          • Review Process: Track content review and approval
          • -
          • Publishing: Publish content directly to your WordPress site
          • -
          • Status Updates: Automatic status updates throughout the workflow
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Analytics Module

          -

          Track performance and monitor your SEO progress

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Performance Metrics

          -

          Monitor key SEO metrics and track your content performance over time.

          -
            -
          • Keyword Rankings: Track keyword position changes
          • -
          • Content Performance: Monitor page views and engagement
          • -
          • SEO Scores: Track overall SEO improvement
          • -
          • Traffic Analysis: Monitor organic traffic growth
          • -
          -
          - -
          -

          Content Analytics

          -

          Analyze your content performance and identify optimization opportunities.

          -
            -
          • Top Performing Content: Identify your best-performing pages
          • -
          • Content Gaps: Find opportunities for new content
          • -
          • Keyword Performance: Track which keywords drive traffic
          • -
          • Conversion Tracking: Monitor content conversion rates
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Schedules Module

          -

          Automate your SEO tasks with intelligent scheduling

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Automated Tasks

          -

          Set up automated tasks to run keyword research, content generation, and optimization tasks.

          -
            -
          • Keyword Research: Automatically discover new keywords
          • -
          • Content Generation: Schedule AI content creation
          • -
          • SEO Audits: Regular automated SEO analysis
          • -
          • Performance Reports: Scheduled performance reports
          • -
          -
          - -
          -

          Workflow Automation

          -

          Create automated workflows that trigger based on specific conditions and schedules.

          -
            -
          • Trigger Conditions: Set up conditions for task execution
          • -
          • Schedule Management: Configure when tasks should run
          • -
          • Notification System: Get alerts when tasks complete
          • -
          • Error Handling: Automatic retry and error management
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          AI-Powered Features

          -

          Leverage artificial intelligence for smarter SEO

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Intelligent Keyword Analysis

          -

          AI analyzes your keywords to determine search intent, difficulty, and optimization opportunities.

          -
            -
          • Automatic intent classification (informational, commercial, navigational)
          • -
          • AI-powered difficulty scoring
          • -
          • Keyword clustering and grouping
          • -
          • Competition analysis and insights
          • -
          -
          - -
          -

          Smart Content Generation

          -

          Generate high-quality, SEO-optimized content using advanced AI models.

          -
            -
          • Context-aware content creation
          • -
          • SEO optimization built-in
          • -
          • Multiple content formats (blog posts, landing pages, product descriptions)
          • -
          • Automatic keyword integration
          • -
          -
          - -
          -

          Automated Content Optimization

          -

          AI continuously optimizes your content for better search engine performance.

          -
            -
          • Automatic SEO scoring and suggestions
          • -
          • Content improvement recommendations
          • -
          • Keyword density optimization
          • -
          • Readability enhancement
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Best Practices

          -

          Get the most out of your AI-powered SEO workflow

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          Content Strategy

          -
            -
          • Plan Before You Write: Use the Planner module to research keywords and create clusters before writing
          • -
          • Quality Over Quantity: Focus on creating high-quality, comprehensive content rather than many short posts
          • -
          • Regular Content Updates: Keep your content fresh and updated to maintain search rankings
          • -
          • User Intent Focus: Always consider what users are looking for when creating content
          • -
          -
          - -
          -

          AI Usage

          -
            -
          • Review AI Content: Always review and edit AI-generated content before publishing
          • -
          • Use AI as a Starting Point: Let AI generate ideas and drafts, then add your unique perspective
          • -
          • Monitor API Usage: Keep track of your OpenAI API usage to manage costs
          • -
          • Test Different Prompts: Experiment with different AI prompts to get better results
          • -
          -
          - -
          -

          SEO Optimization

          -
            -
          • Keyword Research First: Always start with thorough keyword research
          • -
          • Monitor Performance: Regularly check your analytics to see what's working
          • -
          • Optimize for Users: Write for humans first, search engines second
          • -
          • Build Authority: Focus on creating content that establishes your expertise
          • -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Troubleshooting

          -

          Common issues and solutions

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          AI Integration Issues

          -
          - Problem: AI features not working -
          Solution: Check your OpenAI API key in Settings > AI Integration. Ensure you have sufficient API credits and a stable internet connection. -
          -
          - Problem: Slow AI responses -
          Solution: Try switching to a faster model like GPT-3.5-turbo or check your internet connection speed. -
          -
          - -
          -

          Performance Issues

          -
          - Problem: Slow page loading -
          Solution: Reduce the number of records per page in table settings, clear your browser cache, or check for plugin conflicts. -
          -
          - Problem: Missing data -
          Solution: Check that database tables are created correctly by visiting Settings > Status and running a system check. -
          -
          - -
          -

          Content Issues

          -
          - Problem: AI content quality issues -
          Solution: Try different prompts, provide more specific instructions, or use the content as a starting point for manual editing. -
          -
          - Problem: Keywords not being used properly -
          Solution: Ensure keywords are properly imported and mapped to clusters before generating content. -
          -
          -
          -
          -
          - - -
          -
          -
          -

          Support & Resources

          -

          Get help when you need it

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -

          System Status

          -

          Check your system health and configuration at Settings > Status. This page shows database status, AI integration, and system performance.

          -
          - -
          -

          Debug Information

          -

          Use the built-in debug tools to monitor real-time system status and identify any issues with your setup.

          -
          - -
          -

          Regular Backups

          -

          Always backup your WordPress site before making major changes or updates to ensure you can restore if needed.

          -
          - -
          -

          API Monitoring

          -

          Keep track of your OpenAI API usage to manage costs and ensure you don't exceed your limits.

          -
          -
          -
          -
          - -
          - - - - - - \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/help/system-testing.php b/igny8-ai-seo-wp-plugin/modules/help/system-testing.php deleted file mode 100644 index d9d98cf3..00000000 --- a/igny8-ai-seo-wp-plugin/modules/help/system-testing.php +++ /dev/null @@ -1,329 +0,0 @@ - -
          -

          Testing Interface: Basic functionality verification for taxonomy, schema, and UI components.

          -
          - -
          - - -
          -

          Database Schema Tests

          - -
          - - - -
          - -
          -

          Click a test button to see results...

          -
          -
          - - -
          -

          Taxonomy Tests

          - -
          - - - -
          - -
          -

          Click a test button to see results...

          -
          -
          - - -
          -

          AJAX & API Tests

          - -
          - - - -
          - -
          -

          Click a test button to see results...

          -
          -
          -
          - - -
          -

          Quick Record Test

          -

          Test basic record operations:

          - -
          - - - - -
          - -
          -

          Create or list records to see results...

          -
          -
          - - - - \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/home.php b/igny8-ai-seo-wp-plugin/modules/home.php deleted file mode 100644 index 0650dd8e..00000000 --- a/igny8-ai-seo-wp-plugin/modules/home.php +++ /dev/null @@ -1,231 +0,0 @@ - -
          - -
          -
          - -
          -

          Complete AI Content Workflow

          -

          Track your progress through the entire content creation and optimization pipeline

          -
          -
          -
          - -
          onclick="window.location.href=''"> -
          1
          -
          Add Keywords
          -
          - - -
          -
          - 0): ?> - keywords added - - No keywords yet - -
          - - - -
          -
          - - -
          onclick="window.location.href=''"> -
          2
          -
          Select Sector
          -
          - - -
          -
          - - Sector configured - - Required for AI workflows - -
          - -
          - Configure -
          - -
          -
          - - -
          onclick="window.location.href=''"> -
          3
          -
          Auto Cluster
          -
          - - -
          -
          - 0): ?> - unmapped keywords - 0): ?> - clusters created - - No clusters yet - -
          - - - -
          -
          - - -
          onclick="window.location.href=''"> -
          4
          -
          Generate Ideas
          -
          - - -
          -
          - 0): ?> - ideas generated - - No ideas yet - -
          - - - -
          -
          - - -
          onclick="window.location.href=''"> -
          5
          -
          Queue to Writer
          -
          - - -
          -
          - 0): ?> - ideas ready to queue - - All ideas queued - -
          - - - -
          -
          - - -
          onclick="window.location.href=''"> -
          6
          -
          Generate Drafts
          -
          - - -
          -
          - 0): ?> - tasks ready for AI - 0): ?> - drafts generated - - No drafts yet - -
          - - - -
          -
          - - -
          onclick="window.location.href=''"> -
          7
          -
          Publish Content
          -
          - - -
          -
          - 0): ?> - content published - 0): ?> - drafts ready to publish - - No content to publish - -
          - - - -
          -
          -
          -
          - - -
          -
          -

          Welcome to Igny8 AI SEO OS

          -
          -
          -

          Your comprehensive SEO management platform. Use the workflow guide above to track your progress through the complete content creation and optimization pipeline.

          -

          Each step shows your current status and provides direct links to the relevant modules. Click on any step to navigate to the appropriate section.

          -
          -
          -
          - - diff --git a/igny8-ai-seo-wp-plugin/modules/planner/clusters.php b/igny8-ai-seo-wp-plugin/modules/planner/clusters.php deleted file mode 100644 index 38a15de4..00000000 --- a/igny8-ai-seo-wp-plugin/modules/planner/clusters.php +++ /dev/null @@ -1,84 +0,0 @@ - -
          -
          - - -
          -
          -
          -
          -

          Keyword Clusters

          -

          Organize keywords into content clusters for better SEO strategy

          -
          -
          - -
          -
          -
          - - - $table_id, - 'module' => 'planner', - 'submodule' => 'clusters', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_planner_settings'), - 'defaultPerPage' => get_option('igny8_records_per_page', 20), - 'clusterOptions' => $cluster_options, - 'filtersConfig' => $filters_config, - 'cronKey' => igny8_needs_cron_functionality() ? igny8_get_or_generate_cron_key() : null - ]); - ?> -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/planner/ideas.php b/igny8-ai-seo-wp-plugin/modules/planner/ideas.php deleted file mode 100644 index ad9e4aea..00000000 --- a/igny8-ai-seo-wp-plugin/modules/planner/ideas.php +++ /dev/null @@ -1,84 +0,0 @@ - -
          -
          - - -
          -
          -
          -
          -

          Content Ideas

          -

          Generate and manage content ideas for your clusters

          -
          -
          - -
          -
          -
          - - - $table_id, - 'module' => 'planner', - 'submodule' => 'ideas', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_planner_settings'), - 'defaultPerPage' => get_option('igny8_records_per_page', 20), - 'clusterOptions' => $cluster_options, - 'filtersConfig' => $filters_config, - 'cronKey' => igny8_needs_cron_functionality() ? igny8_get_or_generate_cron_key() : null - ]); - ?> -
          - -
          -
          \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/planner/keywords.php b/igny8-ai-seo-wp-plugin/modules/planner/keywords.php deleted file mode 100644 index 3fc67974..00000000 --- a/igny8-ai-seo-wp-plugin/modules/planner/keywords.php +++ /dev/null @@ -1,84 +0,0 @@ - -
          -
          - - -
          -
          -
          -
          -

          Keywords Management

          -

          Import, analyze, and organize your keywords

          -
          -
          - -
          -
          -
          - - - $table_id, - 'module' => 'planner', - 'submodule' => 'keywords', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_planner_settings'), - 'defaultPerPage' => get_option('igny8_records_per_page', 20), - 'clusterOptions' => $cluster_options, - 'filtersConfig' => $filters_config, - 'cronKey' => igny8_needs_cron_functionality() ? igny8_get_or_generate_cron_key() : null - ]); - ?> -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/planner/planner.php b/igny8-ai-seo-wp-plugin/modules/planner/planner.php deleted file mode 100644 index e8d9068d..00000000 --- a/igny8-ai-seo-wp-plugin/modules/planner/planner.php +++ /dev/null @@ -1,653 +0,0 @@ -get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_keywords"); - $unmapped_keywords = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_keywords WHERE cluster_id IS NULL OR cluster_id = 0"); - $clusters_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_clusters"); - $ideas_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_content_ideas"); - $queued_ideas = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_content_ideas WHERE status = 'new'"); - - // Check sector selection - $sector_selected = !empty(igny8_get_saved_sector_selection()); - - return [ - 'keywords' => [ - 'count' => $keywords_count, - 'unmapped' => $unmapped_keywords, - 'status' => $keywords_count > 0 ? 'completed' : 'pending' - ], - 'sector' => [ - 'selected' => $sector_selected, - 'status' => $sector_selected ? 'completed' : 'current' - ], - 'clusters' => [ - 'count' => $clusters_count, - 'unmapped_keywords' => $unmapped_keywords, - 'status' => $unmapped_keywords == 0 && $clusters_count > 0 ? 'completed' : ($unmapped_keywords > 0 ? 'current' : 'pending') - ], - 'ideas' => [ - 'count' => $ideas_count, - 'status' => $ideas_count > 0 ? 'completed' : 'pending' - ], - 'queue' => [ - 'queued_ideas' => $queued_ideas, - 'status' => $queued_ideas == 0 && $ideas_count > 0 ? 'completed' : ($queued_ideas > 0 ? 'current' : 'pending') - ] - ]; -} - -// Handle URL parameters for subpages -$subpage = $_GET['sm'] ?? 'home'; -$GLOBALS['current_submodule'] = $subpage; -$GLOBALS['current_module'] = 'planner'; - -// Start output buffering -ob_start(); - -switch ($subpage) { - case 'keywords': - include plugin_dir_path(__FILE__) . 'keywords.php'; - break; - case 'clusters': - include plugin_dir_path(__FILE__) . 'clusters.php'; - break; - case 'ideas': - include plugin_dir_path(__FILE__) . 'ideas.php'; - break; - case 'home': - default: - // Home dashboard content - ?> -
          - - - -
          -
          - - Please select a Sector to continue. Sector selection is required to start AI-based workflows. -
          -
          - - - 0 ? round(($mapped_keywords / $total_keywords) * 100) : 0; - $clusters_ideas_pct = $total_clusters > 0 ? round(($clusters_with_ideas / $total_clusters) * 100) : 0; - $ideas_queued_pct = $total_ideas > 0 ? round(($queued_ideas / $total_ideas) * 100) : 0; - - // Use fixed colors matching top metric cards - $keyword_color = 'blue'; // Keywords Ready card is blue - $cluster_color = 'green'; // Clusters Built card is green - $idea_color = 'amber'; // Ideas Generated card is amber - ?> - - -
          -
          - -
          -
          -
          - - Keywords Ready - Research, analyze, and manage keywords strategy -
          -
          - -
          -
          -
          - - -
          -
          -
          - - Clusters Built - Organize keywords into strategic topical clusters -
          -
          - -
          -
          -
          - - -
          -
          -
          - - Ideas Generated - Generate creative content ideas based on semantic strategy -
          -
          - -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -

          Planner Workflow Steps

          -

          Track your planning progress

          -
          -
          - -
          -
          -
          - -
          - -
          -
          -
          -
          1
          -
          Add Keywords
          -
          -
          - - -
          -
          - 0): ?> - keywords added - - No keywords yet - -
          - -
          - Start Now -
          - -
          -
          - -
          -
          -
          -
          2
          -
          Select Sector
          -
          -
          - - -
          -
          - - Sector configured - - Required for AI workflows - -
          - -
          - Configure -
          - -
          -
          - -
          -
          -
          -
          3
          -
          Auto Cluster
          -
          -
          - - -
          -
          - 0): ?> - unmapped keywords - 0): ?> - clusters created - - No clusters yet - -
          - -
          - Start Now -
          - -
          -
          - -
          -
          -
          -
          4
          -
          Generate Ideas
          -
          -
          - - -
          -
          - 0): ?> - ideas generated - - No ideas yet - -
          - 0): ?> -
          - Start Now -
          - -
          -
          -
          -
          - - -
          -
          - -
          -
          -
          -
          -

          Workflow Status & AI Settings

          -

          Workflow Status & AI Settings

          -
          -
          - -
          - -
          - -
          - Manual - - AI Mode -
          - -
          -
          - -
          - -
          -
          -
          - - -
          -
          -
          -
          -

          Workflow Status & AI Settings

          -

          Workflow Status & AI Settings

          -
          -
          - -
          - -
          - - - -
          - - - - - - -
          -
          -
          -
          -
          - - -
          - -
          -
          -
          -
          -
          -

          Progress & Readiness Summary

          -

          Planning workflow progress tracking

          -
          -
          - -
          -
          -
          -
          - - -
          -
          - Keyword Mapping - % -
          -
          -
          -
          -
          of keywords mapped
          -
          - - -
          -
          - Clusters With Ideas - % -
          -
          -
          -
          -
          of clusters have ideas
          -
          - - -
          -
          - Ideas Queued to Writer - % -
          -
          -
          -
          -
          of ideas queued
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Top 5 Clusters by Volume

          -

          Highest volume keyword clusters

          -
          -
          - -
          -
          -
          -
          - get_results(" - SELECT - c.cluster_name, - c.total_volume, - c.keyword_count - FROM {$wpdb->prefix}igny8_clusters c - WHERE c.status = 'active' AND c.total_volume > 0 - ORDER BY c.total_volume DESC - LIMIT 5 - "); - - if ($top_clusters): - $max_volume = $top_clusters[0]->total_volume; // Highest volume for percentage calculation - $color_classes = ['igny8-progress-blue', 'igny8-progress-green', 'igny8-progress-amber', 'igny8-progress-purple', 'igny8-progress-text-dim']; - ?> -
          - $cluster): - $percentage = $max_volume > 0 ? round(($cluster->total_volume / $max_volume) * 100) : 0; - $color_class = $color_classes[$index % 5]; - ?> -
          -
          -
          cluster_name); ?>
          -
          total_volume); ?>
          -
          -
          -
          -
          -
          - % -
          -
          - -
          - -
          - -

          No clusters found yet

          - View Clusters -
          - -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Ideas by Status

          -

          Content ideas workflow status

          -
          -
          - -
          -
          -
          -
          - get_results(" - SELECT - status, - COUNT(*) as count - FROM {$wpdb->prefix}igny8_content_ideas - GROUP BY status - ORDER BY count DESC - "); - - if ($ideas_by_status): - $total_ideas_status = array_sum(array_column($ideas_by_status, 'count')); - $status_colors = [ - 'new' => 'igny8-progress-blue', - 'scheduled' => 'igny8-progress-amber', - 'published' => 'igny8-progress-green', - 'draft' => 'igny8-progress-purple', - 'completed' => 'igny8-progress-green' - ]; - ?> -
          - 0 ? round(($status->count / $total_ideas_status) * 100) : 0; - $color_class = $status_colors[$status->status] ?? 'igny8-progress-text-dim'; - $status_display = ucfirst(str_replace('_', ' ', $status->status)); - ?> -
          -
          -
          -
          count); ?>
          -
          -
          -
          -
          -
          - % -
          -
          - -
          - -
          - -

          No ideas found yet

          - View Ideas -
          - -
          -
          -
          -
          - -
          -
          -
          -
          -
          -

          Next Actions

          -

          Actionable items requiring attention

          -
          -
          - -
          -
          -
          -
          -
          - - - 0): ?> -
          - keywords unmapped - Map Keywords -
          - - - 0): ?> -
          - clusters without ideas - Generate Ideas -
          - - - 0): ?> -
          - ideas not queued to writer - Queue to Writer -
          - - -
          - Import new keywords to expand your strategy - Import Keywords -
          - - -
          - All planning tasks completed! - ✓ Ready for content creation -
          - -
          -
          -
          -
          - - - 'planner', - 'submodule' => 'home', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_planner_settings'), - 'cronKey' => $cron_key - ]); - break; -} - -// Capture page content -$igny8_page_content = ob_get_clean(); - -// Include global layout -include_once plugin_dir_path(__FILE__) . '../../core/global-layout.php'; diff --git a/igny8-ai-seo-wp-plugin/modules/settings/general-settings.php b/igny8-ai-seo-wp-plugin/modules/settings/general-settings.php deleted file mode 100644 index 41958afa..00000000 --- a/igny8-ai-seo-wp-plugin/modules/settings/general-settings.php +++ /dev/null @@ -1,676 +0,0 @@ -

          Image generation settings saved successfully!

          '; -} elseif (isset($_POST['igny8_image_settings_nonce'])) { - echo '

          Security check failed. Please try again.

          '; -} - -// Handle editor type settings form submission -if (isset($_POST['igny8_editor_type_nonce']) && wp_verify_nonce($_POST['igny8_editor_type_nonce'], 'igny8_editor_type_settings')) { - $editor_type = isset($_POST['igny8_editor_type']) ? sanitize_text_field($_POST['igny8_editor_type']) : 'block'; - update_option('igny8_editor_type', $editor_type); - echo '

          Editor type settings saved successfully! Selected: ' . esc_html($editor_type) . '

          '; -} elseif (isset($_POST['igny8_editor_type_nonce'])) { - echo '

          Security check failed. Please try again.

          '; -} - -// Handle image metabox settings form submission -if (isset($_POST['igny8_image_metabox_nonce']) && wp_verify_nonce($_POST['igny8_image_metabox_nonce'], 'igny8_image_metabox_settings')) { - $enabled_types = isset($_POST['igny8_enable_image_metabox']) ? $_POST['igny8_enable_image_metabox'] : []; - $enabled_types = array_map('sanitize_text_field', $enabled_types); - update_option('igny8_enable_image_metabox', $enabled_types); - echo '

          Image metabox settings saved successfully!

          '; -} - -// Handle module settings form submission (only if not editor type or image metabox form) -if (isset($_POST['submit']) && !isset($_POST['igny8_editor_type_nonce']) && !isset($_POST['igny8_image_metabox_nonce'])) { - $module_manager->save_module_settings(); -} - -// Debug: Log form submission data (remove in production) -if (isset($_POST['submit']) && current_user_can('manage_options')) { - error_log('Igny8 Settings Debug - POST data: ' . print_r($_POST, true)); -} - - -$settings = get_option('igny8_module_settings', []); -?> -
          - -
          -
          -
          -
          -

          Module Manager

          -

          Enable or disable plugin modules and features

          -
          -
          - -
          -
          -
          - - -
          -
          -
          - - - -
          - - -
          - get_modules() as $module_key => $module): ?> - -
          -
          -
          - -
          -
          -
          - -
          -
          - -
          -

          -
          -
          - - -
          -
          - - -
          -
          -
          -
          -

          Admin & Analytics

          -

          Administrative tools and analytics modules

          -
          -
          - -
          -
          -
          - -
          - get_modules() as $module_key => $module): ?> - -
          -
          -
          - -
          -
          -
          - -
          -
          - -
          -

          -
          -
          - - -
          -
          - - 'margin-top: 20px;']); ?> -
          -
          -
          -
          - - -
          -
          -
          -
          -

          Content Editor Type

          -

          Choose between Classic editor or Block (Gutenberg) editor for AI-generated content

          -
          -
          - -
          -
          -
          - -
          -
          - - -
          - -
          - - - -
          - Current Setting: - - - -
          - - - - -
          - -
          -

          📝 How this affects your content:

          -
            -
          • Block Editor: AI content will be converted to WordPress blocks for better formatting and structure
          • -
          • Classic Editor: AI content will be saved as HTML and displayed in the classic editor
          • -
          • You can change this setting anytime and it will apply to all new AI-generated content
          • -
          -
          -
          - - 'margin-top: 20px;']); ?> -
          -
          -
          - - - - -
          -
          -
          -
          -

          Image Generation

          -

          Configure AI image generation settings and preferences

          -
          -
          - -
          -
          -
          - -
          -
          - - -
          - -
          -
          - - -
          - -
          - - -
          - -
          - - -
          -
          -
          -
          - -
          - -
          - 'DALL·E 3', - 'dall-e-2' => 'DALL·E 2', - 'gpt-image-1' => 'GPT Image 1 (Full)', - 'gpt-image-1-mini' => 'GPT Image 1 Mini' - ]; - $model_name = $model_names[$current_model] ?? $current_model; - echo '
          '; - echo 'Provider:'; - echo 'OpenAI'; - echo '
          '; - echo '
          '; - echo 'Model:'; - echo '' . esc_html($model_name) . ''; - echo '
          '; - } else { - echo '
          '; - echo 'Provider:'; - echo 'Runware'; - echo '
          '; - echo '
          '; - echo 'Model:'; - echo 'HiDream-I1 Full'; - echo '
          '; - } - ?> -
          - - Note: To change the image provider or model, go to - Integration Settings - -
          -
          -
          - -
          - -
          - - - -
          - -
          1024×1024 pixels
          -
          - -
          - -
          960×1280 pixels
          -
          -
          - Choose which image sizes to generate and how many of each type. -
          - - 'margin-top: 20px;']); ?> -
          -
          -
          - - - - - - -
          -
          -
          -
          -

          In-Article Image Meta Box

          -

          Enable image metabox for specific post types

          -
          -
          - -
          -
          -
          - -
          -
          - - -
          - -
          - true], 'objects'); - $enabled_types = (array) get_option('igny8_enable_image_metabox', []); - - foreach ($all_post_types as $pt => $obj) { - $checked = in_array($pt, $enabled_types) ? 'checked' : ''; - echo ''; - } - ?> -
          -

          Select which post types should display the In-Article Image metabox in the WordPress editor.

          -
          - - -
          - -
          -

          Use these shortcodes in your posts/pages to display the selected images:

          - -
          - [igny8-images] - Display all images -
          - -
          - [igny8-image id="desktop-1"] - Display specific image by ID -
          - -
          - [igny8-desktop-images] - Display only desktop images -
          - -
          - [igny8-mobile-images] - Display only mobile images -
          - -
          - [igny8-responsive-gallery] - Responsive gallery (desktop on large screens, mobile on small screens) -
          - -
          - [igny8-image-count] - Display count of images -
          - -

          - 💡 Tip: After selecting images in the metabox, use these shortcodes in your post content to display them on the frontend. -

          -
          -
          - - 'margin-top: 20px;']); ?> -
          -
          -
          - - -
          -
          - - - - - - -
          - - - -

          Default number of records to display per page across all tables in the plugin.

          -
          - -
          -
          - -
          - - \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/settings/import-export.php b/igny8-ai-seo-wp-plugin/modules/settings/import-export.php deleted file mode 100644 index 62b2cdc8..00000000 --- a/igny8-ai-seo-wp-plugin/modules/settings/import-export.php +++ /dev/null @@ -1,267 +0,0 @@ - 'csv', - 'overwrite_existing' => false, - 'include_metrics' => true, - 'file_naming_pattern' => 'igny8_export_[date].csv' -]); - -// Get recent import/export logs -$recent_logs = get_option('igny8_import_export_logs', []); -$recent_logs = array_slice($recent_logs, 0, 10); // Last 10 entries - -// Script localization will be done in the JavaScript section below -?> -
          -
          - - - -
          -
          -
          -
          -

          Import Data

          -
          Download CSV templates and import your data into the Planner module
          -
          -
          - -
          -
          -
          -
          - - - - - -
          - - -
          - - -

          Upload a CSV file with your data. Use the templates above for proper format.

          -
          - -
          - - -
          - - -
          - -
          -
          - - - -
          -
          - - -
          -
          -
          -
          -

          Export Data

          -
          Export your data in various formats for backup and migration
          -
          -
          - -
          -
          -
          -
          - -
          - - -
          - - -
          - -
          -

          Export Options

          -
          - - - -
          -
          - -
          - -
          -
          - - - -
          -
          - - -
          -
          -
          -
          -

          Import / Export Preferences

          -
          Configure import/export settings and view operation logs
          -
          -
          - -
          -
          -
          -
          -
          - - -
          -
          - - -
          - -
          - - -

          Use [date], [type], [time] placeholders

          -
          -
          - -
          -

          Default Options

          -
          - - -
          -
          - -
          - -
          -
          - - - -
          -

          Recent Import/Export Activity

          -
          - -
          -
          - - - - - -
          -
          - - -
          - -
          -
          - -
          -
          - -
          -
          - -
          -
          - - - diff --git a/igny8-ai-seo-wp-plugin/modules/settings/integration.php b/igny8-ai-seo-wp-plugin/modules/settings/integration.php deleted file mode 100644 index aa11d0a6..00000000 --- a/igny8-ai-seo-wp-plugin/modules/settings/integration.php +++ /dev/null @@ -1,744 +0,0 @@ - - -
          -
          -
          -
          -

          API Integration

          -

          Configure external API connections and integrations

          -
          -
          - -
          -
          -
          - - -
          - - -
          -
          -
          -
          -

          OpenAI API

          -

          AI-powered content generation and analysis

          -
          -
          - -
          -
          -
          -
          -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          - - - -

          Your OpenAI API key for DALL-E 3 image generation and text AI features.

          -
          - - - -

          Your Runware API key for high-quality image generation. Get your API key here.

          -
          - - -
          - - - -
          -

          Select the AI model to use for content generation. Pricing shown per 1M tokens.

          -
          Image Generation Service -
          - - -
          -

          Select the image generation service to use. Each service requires its own API key.

          -
          API Validation - - -
          -
          - -
          -
          -
          - - -
          -
          -
          -
          -

          Google Search Console API

          -

          Search performance and ranking data integration

          -
          -
          - -
          -
          -
          -
          -
          -
          - ⚠️ -
          - Coming Soon
          - - Google Search Console API integration is currently in development. - This will provide search performance data and ranking insights. - -
          -
          -
          - -
          - Planned Features: -
            -
          • Search performance metrics
          • -
          • Keyword ranking data
          • -
          • Click-through rate analysis
          • -
          • Search appearance insights
          • -
          -
          - -
          - - Integration will be available in a future update - -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -

          API Request Logs

          -

          Monitor API usage and performance metrics

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -
          - - -
          -
          - - 0 API calls - -
          -
          - - - - - - - - - - - - - - get_results(" - SELECT * FROM {$wpdb->prefix}igny8_logs - WHERE source = 'openai_api' - ORDER BY created_at DESC - LIMIT 20 - "); - - if ($logs) { - foreach ($logs as $log) { - $context = json_decode($log->context, true); - $status_class = $log->status === 'success' ? 'success' : 'error'; - $status_icon = $log->status === 'success' ? '✅' : '❌'; - - // Debug logging for cost display - error_log("Igny8 Display Debug: Log ID=" . $log->id . ", total_cost=" . ($context['total_cost'] ?? 'null') . ", formatted=" . igny8_format_cost($context['total_cost'] ?? 0)); - - echo " - - - - - - - "; - } - } else { - echo ''; - } - ?> - -
          TimestampStatusModelTokens (In/Out)CostAPI ID
          " . esc_html($log->created_at) . "{$status_icon} " . esc_html($log->status) . "" . esc_html($context['model'] ?? 'Unknown') . "" . intval($context['input_tokens'] ?? 0) . " / " . intval($context['output_tokens'] ?? 0) . "" . igny8_format_cost($context['total_cost'] ?? 0) . "" . esc_html($log->api_id ? substr($log->api_id, 0, 12) . '...' : 'N/A') . "
          No API logs found.
          -
          -
          -
          - - -
          -
          -
          -
          -

          Image Request Logs

          -

          Monitor AI image generation requests and performance

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -
          - - -
          -
          - - 0 image requests - -
          -
          - - - - - - - - - - - - - - - get_results(" - SELECT * FROM {$wpdb->prefix}igny8_logs - WHERE source = 'openai_image' - ORDER BY created_at DESC - LIMIT 20 - "); - - if ($image_logs) { - foreach ($image_logs as $log) { - $context = json_decode($log->context, true); - $status_class = $log->status === 'success' ? 'success' : 'error'; - $status_icon = $log->status === 'success' ? '✅' : '❌'; - - echo " - - - - - - - - "; - } - } else { - echo ''; - } - ?> - -
          TimestampStatusModelPrompt LengthCostImage SizeAPI ID
          " . esc_html($log->created_at) . "{$status_icon} " . esc_html($log->status) . "" . esc_html($context['model'] ?? 'dall-e-3') . "" . intval($context['prompt_length'] ?? 0) . " chars" . igny8_format_cost($context['total_cost'] ?? 0) . "" . esc_html($context['image_size'] ?? '1024x1024') . "" . esc_html($log->api_id ? substr($log->api_id, 0, 12) . '...' : 'N/A') . "
          No image request logs found.
          -
          -
          -
          - - -
          -
          -
          -
          -

          Content Engine Settings

          -

          Personalization and content generation configuration

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          - ℹ️ -
          - Settings Moved
          - - Content Engine settings have been moved to the Personalize module for better organization. - -
          -
          -
          - -

          Note: Content Engine settings have been moved to the Personalize module for better organization.

          -

          Please configure personalization settings in the Personalize → Settings section.

          -
          -
          -
          - - -
          -
          -
          -
          -

          Third-party Integrations

          -

          Additional SEO tools and data sources

          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          - ⚠️ -
          - Coming Soon
          - - Additional third-party integrations are currently in development. - -
          -
          -
          - -
          -
          -

          Ahrefs API

          -

          Integration with Ahrefs for keyword and backlink data.

          -
          -
          -

          SEMrush API

          -

          Integration with SEMrush for competitive analysis.

          -
          -
          -
          -
          -
          - - - - - diff --git a/igny8-ai-seo-wp-plugin/modules/settings/schedules.php b/igny8-ai-seo-wp-plugin/modules/settings/schedules.php deleted file mode 100644 index b932c5cf..00000000 --- a/igny8-ai-seo-wp-plugin/modules/settings/schedules.php +++ /dev/null @@ -1,297 +0,0 @@ - $job_data) { - $cron_settings[$job_name] = [ - 'enabled' => isset($job_data['enabled']), - 'last_run' => $cron_settings[$job_name]['last_run'] ?? 0 - ]; - - // Update limits - if (isset($job_data['limit'])) { - $cron_limits[$job_name] = intval($job_data['limit']); - } - } - - update_option('igny8_cron_settings', $cron_settings); - update_option('igny8_cron_limits', $cron_limits); - - echo '

          Settings saved successfully!

          '; -} - -// Get current data -$defined_jobs = igny8_get_defined_cron_jobs(); -$cron_settings = get_option('igny8_cron_settings', []); -$cron_limits = get_option('igny8_cron_limits', []); -$last_execution = get_option('igny8_cron_last_execution', []); - -// Initialize defaults if needed -if (empty($cron_settings)) { - $cron_settings = igny8_get_default_cron_settings(); - update_option('igny8_cron_settings', $cron_settings); -} - -if (empty($cron_limits)) { - $cron_limits = igny8_get_default_cron_limits(); - update_option('igny8_cron_limits', $cron_limits); -} - -// Get health status for all jobs -$health_status = []; -foreach ($defined_jobs as $job_name => $job_config) { - $health_status[$job_name] = igny8_get_job_health_status($job_name); -} -?> - -
          - - -
          -
          -
          -
          -

          Smart Automation Jobs

          -

          Configure and manage all automation jobs

          -
          -
          - -
          -
          -
          -
          -
          - - - - - - - - - - - - - - - - $job_config): - // Skip crons if their respective modules are disabled - $analytics_crons = ['igny8_process_ai_queue_cron', 'igny8_auto_recalc_cron', 'igny8_health_check_cron']; - $writer_crons = ['igny8_auto_generate_content_cron', 'igny8_auto_generate_images_cron', 'igny8_auto_publish_drafts_cron']; - $optimizer_crons = ['igny8_auto_optimizer_cron']; - - if (in_array($job_name, $analytics_crons) && !igny8_is_module_enabled('analytics')) { - continue; - } - if (in_array($job_name, $writer_crons) && !igny8_is_module_enabled('writer')) { - continue; - } - if (in_array($job_name, $optimizer_crons) && !igny8_is_module_enabled('optimizer')) { - continue; - } - - $job_settings = $cron_settings[$job_name] ?? []; - $job_status = igny8_get_cron_job_status($job_name); - $job_health = $health_status[$job_name]; - ?> - - - - - - - - - - - -
          Job NameModuleEnableMax ItemsLast RunExecution TimeActions
          - - - - - - - - - - ' . $item_text . ''; - ?> - - - -
          - - - ✓ - - ✗ - - - -
          - 0 ? number_format($execution_time, 2) . 's' : 'N/A'; - ?> - -
          - via - - -
          - -
          - -

          - -

          -
          -
          -
          - - -
          -
          -
          -
          -

          Master Scheduler Configuration

          -

          Single cron job manages all automation

          -
          -
          - -
          -
          -
          -
          -

          Single cPanel Configuration Required:

          - */5 * * * * curl -s "https:///wp-load.php?import_key=&import_id=igny8_cron&action=master_scheduler" > /dev/null 2>&1 -

          This single cron job will intelligently manage all automation based on your settings below.

          -
          -
          - - -
          -
          -
          -
          -

          System Overview

          -

          Current automation system status

          -
          -
          - -
          -
          -
          -
          - -
          -
          -

          Total Jobs

          -

          -
          -
          -

          Enabled Jobs

          -

          -
          -
          -

          Scheduled Jobs

          -

          -
          -
          -

          Failed Jobs

          -

          -
          -
          -
          -
          -
          - - \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/settings/status.php b/igny8-ai-seo-wp-plugin/modules/settings/status.php deleted file mode 100644 index 63b3d9e0..00000000 --- a/igny8-ai-seo-wp-plugin/modules/settings/status.php +++ /dev/null @@ -1,353 +0,0 @@ - - -
          -
          -
          -
          -

          System Status

          -

          Monitor system health and component status

          -
          -
          - -
          -
          -
          - - -
          - -
          -
          -
          -
          -

          Debug Monitoring

          -

          Real-time debug monitoring controls and status

          -
          -
          - -
          -
          -
          -
          -

          Enable or disable submodule debug monitoring for Planner, Writer, Linker, Optimizer and Personalize

          - - -
          -
          - Real-time Monitoring -
          -
          -
          - > - -
          -
          - > - -
          - -
          -
          - - - -
          -
          - -
          - Debug Monitoring:
          - - - Real-time debug monitoring is active for all submodules. Debug cards and status indicators are visible on submodule pages. - - Debug monitoring is disabled. Debug cards and status indicators are hidden on submodule pages. - - -
          -
          -
          -
          -
          - -
          -
          -
          -
          -

          System Layers

          -

          System layer health and operational metrics

          -
          -
          - -
          -
          -
          -
          - -
          -
          -
          100%
          -
          Layer Health
          -
          -
          -
          6
          -
          Operational
          -
          -
          -
          0
          -
          Failed
          -
          -
          -
          6
          -
          Total Layers
          -
          -
          - - -
          - 'DB', - 'configuration_system' => 'CFG', - 'rendering_system' => 'RND', - 'javascript_ajax' => 'JS', - 'component_functionality' => 'CMP', - 'data_flow' => 'FLW' - ]; - - foreach ($layer_labels as $layer_key => $label): - $layer_status = true; // Simplified - always true for basic layer summary - $status_class = $layer_status ? 'bg-success' : 'bg-error'; - ?> -
          - -
          - -
          - -
          -
          - -
          - All Layers Operational
          - - All 6 layers are functioning correctly. - The Igny8 plugin is operating at full capacity. - -
          -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -

          System Information & Database Status

          -

          System details, database tables, and module status

          -
          -
          - -
          -
          -
          - - -
          - - -
          -
          -
          -
          -

          System & API Info

          -

          System information and API configuration status

          -
          -
          - -
          -
          -
          -
          -
          -
          System Information
          -
          Plugin:
          -
          WordPress:
          -
          PHP:
          -
          MySQL: db_version(); ?>
          -
          - -
          -
          API Status
          - -
          - OpenAI API: -
          -
          Model:
          -
          -
          -
          - - -
          -
          -
          -
          -

          Database Tables 1

          -

          Core database tables status and health

          -
          -
          - -
          -
          -
          -
          - 'Keywords', - 'igny8_tasks' => 'Tasks', - 'igny8_data' => 'Data', - 'igny8_variations' => 'Variations', - 'igny8_rankings' => 'Rankings', - 'igny8_suggestions' => 'Suggestions', - 'igny8_campaigns' => 'Campaigns' - ]; - - foreach ($tables_part1 as $table => $name) { - $table_name = $wpdb->prefix . $table; - $exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name; - $status_color = $exists ? 'green' : 'red'; - $status_icon = $exists ? '✓' : '✗'; - echo "
          $status_icon $name
          "; - } - ?> -
          -
          - - -
          -
          -
          -
          -

          Database Tables 2

          -

          Extended database tables status and health

          -
          -
          - -
          -
          -
          -
          - 'Content Ideas', - 'igny8_clusters' => 'Clusters', - 'igny8_sites' => 'Sites', - 'igny8_backlinks' => 'Backlinks', - 'igny8_mapping' => 'Mapping', - 'igny8_prompts' => 'Prompts', - 'igny8_logs' => 'Logs' - ]; - - foreach ($tables_part2 as $table => $name) { - $table_name = $wpdb->prefix . $table; - $exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name; - $status_color = $exists ? 'green' : 'red'; - $status_icon = $exists ? '✓' : '✗'; - echo "
          $status_icon $name
          "; - } - ?> -
          -
          - - -
          -
          -
          -
          -

          Module Status

          -

          Plugin modules activation and status

          -
          -
          - -
          -
          -
          -
          - 'Planner', - 'writer' => 'Writer', - 'personalize' => 'Personalize', - 'optimizer' => 'Optimizer', - 'linker' => 'Linker' - ]; - - foreach ($modules as $module => $name) { - $enabled = get_option("igny8_{$module}_enabled", true); - $status_color = $enabled ? 'green' : 'red'; - $status_icon = $enabled ? '✓' : '✗'; - echo "
          $status_icon $name
          "; - } - ?> -
          -
          - -
          -
          -
          - - \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/thinker/image-testing.php b/igny8-ai-seo-wp-plugin/modules/thinker/image-testing.php deleted file mode 100644 index 9f7baa87..00000000 --- a/igny8-ai-seo-wp-plugin/modules/thinker/image-testing.php +++ /dev/null @@ -1,917 +0,0 @@ - 0) { - $t = " "; - while ($t != "\n") { - if (fseek($handle, $pos, SEEK_END) == -1) { - $beginning = true; - break; - } - $t = fgetc($handle); - $pos--; - } - $linecounter--; - if ($beginning) { - rewind($handle); - } - $text[$lines - $linecounter - 1] = fgets($handle); - if ($beginning) break; - } - fclose($handle); - return array_reverse($text); -} - -// Image Testing content -ob_start(); - -// Handle Form Submit -$image_url = ''; -$response_error = ''; -$save_path = ''; -$generated_image_data = null; - -// Handle saving prompt settings -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_prompt_nonce']) && wp_verify_nonce($_POST['save_prompt_nonce'], 'save_prompt')) { - $prompt_template = sanitize_textarea_field(wp_unslash($_POST['prompt_template'])); - update_option('igny8_image_prompt_template', $prompt_template); - $settings_saved = true; -} - -// Handle image generation -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_image_nonce']) && wp_verify_nonce($_POST['generate_image_nonce'], 'generate_image')) { - $title = sanitize_text_field(wp_unslash($_POST['title'])); - $desc = sanitize_textarea_field(wp_unslash($_POST['desc'])); - $image_type = sanitize_text_field(wp_unslash($_POST['image_type'])); - $image_provider = sanitize_text_field(wp_unslash($_POST['image_provider'] ?? 'openai')); - $negative_prompt = sanitize_textarea_field(wp_unslash($_POST['negative_prompt'] ?? 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title')); - $image_width = intval($_POST['image_width'] ?? 1024); - $image_height = intval($_POST['image_height'] ?? 1024); - $image_format = sanitize_text_field(wp_unslash($_POST['image_format'] ?? 'jpg')); - - // Get custom prompt template or use default - $prompt_template = wp_unslash(get_option('igny8_image_prompt_template', 'Generate a {image_type} image for a blog post titled "{title}". Description: {description}')); - - // Replace placeholders in prompt template - $prompt = str_replace( - ['{image_type}', '{title}', '{description}'], - [$image_type, $title, $desc], - $prompt_template - ); - - // Get API keys - $openai_key = get_option('igny8_api_key', ''); - $runware_key = get_option('igny8_runware_api_key', ''); - - // Check if the required API key is configured - $required_key = ($image_provider === 'runware') ? $runware_key : $openai_key; - $service_name = ($image_provider === 'runware') ? 'Runware' : 'OpenAI'; - - if (empty($required_key)) { - $response_error = $service_name . ' API key not configured. Please set your API key in the settings.'; - } else { - // Debug: Log the request details - error_log('Igny8 Image Generation - Starting request'); - error_log('Igny8 Image Generation - Prompt: ' . $prompt); - error_log('Igny8 Image Generation - Provider: ' . $image_provider); - error_log('Igny8 Image Generation - API Key: ' . substr($required_key, 0, 10) . '...'); - - try { - if ($image_provider === 'runware') { - // Runware API Call - error_log('Igny8 Image Generation - Using Runware service'); - - // Prepare Runware API payload - $payload = [ - [ - 'taskType' => 'authentication', - 'apiKey' => $runware_key - ], - [ - 'taskType' => 'imageInference', - 'taskUUID' => wp_generate_uuid4(), - 'positivePrompt' => $prompt, - 'negativePrompt' => $negative_prompt, - 'model' => 'runware:97@1', - 'width' => $image_width, - 'height' => $image_height, - 'steps' => 30, - 'CFGScale' => 7.5, - 'numberResults' => 1, - 'outputFormat' => $image_format - ] - ]; - - // Make API request - $response = wp_remote_post('https://api.runware.ai/v1', [ - 'headers' => ['Content-Type' => 'application/json'], - 'body' => json_encode($payload), - 'timeout' => 60 - ]); - - if (is_wp_error($response)) { - error_log('Igny8 Image Generation - Runware Error: ' . $response->get_error_message()); - $response_error = 'Runware API Error: ' . $response->get_error_message(); - } else { - $response_body = wp_remote_retrieve_body($response); - $response_data = json_decode($response_body, true); - - if (isset($response_data['data'][0]['imageURL'])) { - // Download and save the image - $image_url = $response_data['data'][0]['imageURL']; - $filename = sanitize_file_name($title) . '_' . time() . '.' . $image_format; - - // Create uploads directory - $upload_dir = wp_upload_dir(); - $igny8_dir = $upload_dir['basedir'] . '/igny8-ai-images/'; - - if (!file_exists($igny8_dir)) { - wp_mkdir_p($igny8_dir); - } - - // Download image - $image_response = wp_remote_get($image_url); - - if (is_wp_error($image_response)) { - $response_error = 'Failed to download Runware image: ' . $image_response->get_error_message(); - } else { - $image_data = wp_remote_retrieve_body($image_response); - $file_path = $igny8_dir . $filename; - - $saved = file_put_contents($file_path, $image_data); - - if ($saved === false) { - $response_error = 'Failed to save Runware image file'; - } else { - $response_success = true; - $response_image_url = str_replace(ABSPATH, home_url('/'), $file_path); - $response_message = 'Image generated successfully using Runware!'; - - // Set generated image data for consistent display - $generated_image_data = [ - 'model' => 'runware:97@1', - 'provider' => 'runware', - 'negative_prompt' => $negative_prompt - ]; - - error_log('Igny8 Image Generation - Runware image saved to: ' . $file_path); - } - } - } elseif (isset($response_data['errors'][0]['message'])) { - $response_error = 'Runware API Error: ' . $response_data['errors'][0]['message']; - } else { - $response_error = 'Unknown response from Runware API'; - } - } - } else { - // OpenAI DALL-E 3 API Call - error_log('Igny8 Image Generation - Using OpenAI DALL-E 3 service'); - $data = [ - 'model' => 'dall-e-3', - 'prompt' => $prompt, - 'n' => 1, - 'size' => $image_width . 'x' . $image_height - ]; - - // Add negative prompt if supported (DALL-E 3 doesn't support negative prompts, but we'll log it) - if (!empty($negative_prompt)) { - error_log('Igny8 Image Generation - Negative prompt provided but DALL-E 3 does not support negative prompts: ' . $negative_prompt); - } - - $args = [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . $openai_key, - 'OpenAI-Beta' => 'assistants=v2' - ], - 'body' => json_encode($data), - 'timeout' => 60, - 'sslverify' => true - ]; - - error_log('Igny8 Image Generation - Making API request to OpenAI'); - $api_response = wp_remote_post('https://api.openai.com/v1/images/generations', $args); - - // Debug: Log response details - if (is_wp_error($api_response)) { - error_log('Igny8 Image Generation - WP Error: ' . $api_response->get_error_message()); - error_log('Igny8 Image Generation - Error Code: ' . $api_response->get_error_code()); - $response_error = 'WordPress HTTP Error: ' . $api_response->get_error_message(); - } else { - $response_code = wp_remote_retrieve_response_code($api_response); - $response_body = wp_remote_retrieve_body($api_response); - - error_log('Igny8 Image Generation - Response Code: ' . $response_code); - error_log('Igny8 Image Generation - Response Body: ' . substr($response_body, 0, 500)); - - if ($response_code === 200) { - $body = json_decode($response_body, true); - if (isset($body['data'][0]['url'])) { - $image_url = $body['data'][0]['url']; - $generated_image_data = $body['data'][0]; - error_log('Igny8 Image Generation - Image URL received: ' . $image_url); - - // Log successful image generation - global $wpdb; - $wpdb->insert($wpdb->prefix . 'igny8_logs', [ - 'level' => 'info', - 'message' => 'Image generation successful', - 'context' => wp_json_encode([ - 'model' => 'dall-e-3', - 'prompt_length' => strlen($prompt), - 'image_size' => '1024x1024', - 'total_cost' => 0.040, // DALL-E 3 standard cost - 'title' => $title, - 'image_type' => $image_type - ]), - 'source' => 'openai_image', - 'status' => 'success', - 'api_id' => $body['data'][0]['id'] ?? null, - 'user_id' => get_current_user_id(), - 'created_at' => current_time('mysql') - ], ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s']); - - // Try to save to plugin directory first, fallback to WordPress uploads - $plugin_upload_dir = plugin_dir_path(__FILE__) . '../../assets/ai-images/'; - $wp_upload_dir = wp_upload_dir(); - $wp_upload_path = $wp_upload_dir['basedir'] . '/igny8-ai-images/'; - - $slug = sanitize_title($title); - $upload_dir = $plugin_upload_dir; - $folder = $upload_dir . $slug . '/'; - $save_location = 'plugin'; - - // Check if plugin directory is writable - if (!file_exists($plugin_upload_dir)) { - if (wp_mkdir_p($plugin_upload_dir)) { - error_log('Igny8 Image Generation - Created plugin directory: ' . $plugin_upload_dir); - } else { - error_log('Igny8 Image Generation - Failed to create plugin directory: ' . $plugin_upload_dir); - // Fallback to WordPress uploads directory - $upload_dir = $wp_upload_path; - $folder = $upload_dir . $slug . '/'; - $save_location = 'wordpress'; - error_log('Igny8 Image Generation - Using WordPress uploads as fallback: ' . $wp_upload_path); - } - } - - if (file_exists($plugin_upload_dir) && !is_writable($plugin_upload_dir)) { - // Try to fix permissions - chmod($plugin_upload_dir, 0755); - error_log('Igny8 Image Generation - Attempted to fix plugin directory permissions: ' . $plugin_upload_dir); - - if (!is_writable($plugin_upload_dir)) { - // Fallback to WordPress uploads directory - $upload_dir = $wp_upload_path; - $folder = $upload_dir . $slug . '/'; - $save_location = 'wordpress'; - error_log('Igny8 Image Generation - Plugin directory not writable, using WordPress uploads: ' . $wp_upload_path); - } - } - - // Ensure target directory exists - if (!file_exists($folder)) { - wp_mkdir_p($folder); - error_log('Igny8 Image Generation - Created directory: ' . $folder); - } - - // Final check if directory is writable - if (file_exists($folder) && !is_writable($folder)) { - // Try to fix permissions one more time - chmod($folder, 0755); - error_log('Igny8 Image Generation - Attempted to fix final permissions for: ' . $folder); - - if (!is_writable($folder)) { - $response_error = 'Directory is not writable. Please check file permissions for: ' . $folder; - error_log('Igny8 Image Generation - Directory still not writable: ' . $folder); - error_log('Igny8 Image Generation - Directory permissions: ' . substr(sprintf('%o', fileperms($folder)), -4)); - } - } elseif (!file_exists($folder)) { - $response_error = 'Directory does not exist and could not be created: ' . $folder; - error_log('Igny8 Image Generation - Directory does not exist: ' . $folder); - } - - if (empty($response_error)) { - // Download image data - $image_data = file_get_contents($image_url); - - if ($image_data === false) { - $response_error = 'Failed to download image from URL: ' . $image_url; - error_log('Igny8 Image Generation - Failed to download image from: ' . $image_url); - } else { - $filename = $folder . 'image.png'; - - // Try to write the file - $bytes_written = file_put_contents($filename, $image_data); - - if ($bytes_written === false) { - $response_error = 'Failed to save image file. Check file permissions.'; - error_log('Igny8 Image Generation - Failed to write file: ' . $filename); - error_log('Igny8 Image Generation - Directory permissions: ' . substr(sprintf('%o', fileperms($folder)), -4)); - error_log('Igny8 Image Generation - Directory writable: ' . (is_writable($folder) ? 'yes' : 'no')); - } else { - // Determine the correct path for display - if ($save_location === 'wordpress') { - $display_path = '/wp-content/uploads/igny8-ai-images/' . $slug . '/image.png'; - $log_path = $display_path; - } else { - $display_path = '/assets/ai-images/' . $slug . '/image.png'; - $log_path = $display_path; - } - - $save_path = 'Saved to: ' . $display_path . ' (' . $bytes_written . ' bytes)'; - error_log('Igny8 Image Generation - Image saved successfully: ' . $filename . ' (' . $bytes_written . ' bytes)'); - error_log('Igny8 Image Generation - Save location: ' . $save_location); - - // Update log with file path - $wpdb->update( - $wpdb->prefix . 'igny8_logs', - [ - 'context' => wp_json_encode([ - 'model' => 'dall-e-3', - 'prompt_length' => strlen($prompt), - 'image_size' => '1024x1024', - 'total_cost' => 0.040, - 'title' => $title, - 'image_type' => $image_type, - 'file_path' => $log_path, - 'file_size' => $bytes_written, - 'save_location' => $save_location - ]) - ], - ['api_id' => $body['data'][0]['id'] ?? null], - ['%s'], - ['%s'] - ); - } - } - } - } else { - $response_error = 'Image URL not returned from API. Response: ' . substr($response_body, 0, 200); - if (isset($body['error']['message'])) { - $response_error .= ' Error: ' . $body['error']['message']; - } - - // Log failed image generation - global $wpdb; - $wpdb->insert($wpdb->prefix . 'igny8_logs', [ - 'level' => 'error', - 'message' => 'Image generation failed - no URL returned', - 'context' => wp_json_encode([ - 'model' => 'dall-e-3', - 'prompt_length' => strlen($prompt), - 'image_size' => '1024x1024', - 'error_response' => substr($response_body, 0, 500), - 'title' => $title, - 'image_type' => $image_type - ]), - 'source' => 'openai_image', - 'status' => 'error', - 'user_id' => get_current_user_id(), - 'created_at' => current_time('mysql') - ], ['%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s']); - } - } else { - $response_error = 'API returned error code ' . $response_code . '. Response: ' . substr($response_body, 0, 200); - - // Log failed image generation - global $wpdb; - $wpdb->insert($wpdb->prefix . 'igny8_logs', [ - 'level' => 'error', - 'message' => 'Image generation failed - HTTP error', - 'context' => wp_json_encode([ - 'model' => 'dall-e-3', - 'prompt_length' => strlen($prompt), - 'image_size' => '1024x1024', - 'http_code' => $response_code, - 'error_response' => substr($response_body, 0, 500), - 'title' => $title, - 'image_type' => $image_type - ]), - 'source' => 'openai_image', - 'status' => 'error', - 'user_id' => get_current_user_id(), - 'created_at' => current_time('mysql') - ], ['%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s']); - } - } - } // End of OpenAI else block - } catch (Exception $e) { - error_log('Igny8 Image Generation - Exception: ' . $e->getMessage()); - $response_error = 'Exception occurred: ' . $e->getMessage(); - - // Log exception - global $wpdb; - $wpdb->insert($wpdb->prefix . 'igny8_logs', [ - 'level' => 'error', - 'message' => 'Image generation failed - exception', - 'context' => wp_json_encode([ - 'model' => 'dall-e-3', - 'prompt_length' => strlen($prompt), - 'image_size' => '1024x1024', - 'exception_message' => $e->getMessage(), - 'title' => $title, - 'image_type' => $image_type - ]), - 'source' => 'openai_image', - 'status' => 'error', - 'user_id' => get_current_user_id(), - 'created_at' => current_time('mysql') - ], ['%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s']); - } - } -} - -?> - - -
          - -
          -
          -
          -
          -
          -

          AI Image Generation Test

          -

          Test OpenAI DALL·E 3 image generation with your content data

          -
          -
          - -
          -
          -
          -
          -

          Generate AI-powered images for your content using OpenAI's DALL·E 3. Enter your post details below to create custom images based on your title, description, keywords, and cluster information.

          -
          -
          -
          - -
          - -
          -
          -
          -
          -
          -

          Generate Image

          -

          Configure image generation parameters

          -
          -
          - -
          -
          -
          -
          -
          - - -
          - - -
          - -
          - - - Describe the visual elements, style, mood, and composition you want in the image. -
          - -
          - - - Specify elements to avoid in the generated image (text, watermarks, logos, etc.). -
          - -
          - - -
          - -
          - - - Choose the AI image generation service to use. -
          - -
          - - - Choose the image dimensions for your generated image. -
          -
          - -
          - - - Choose the image file format for your generated image. -
          -
          - - - - - -
          - -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Generated Image

          -

          AI-generated image results

          -
          -
          - -
          -
          -
          -
          - -
          -
          - Generated Image -
          - - -
          -

          Image Details

          -
            -
          • Size: pixels
          • -
          • Format:
          • -
          • Model:
          • - -
          • Revised Prompt:
          • - - -
          • Negative Prompt:
          • - -
          -
          - - - -
          -

          - - -

          -
          - - -
          - - - View Original - - -
          -
          - -
          - -

          Generation Failed

          -

          -
          - -
          - -

          No image generated yet. Fill out the form and click "Generate Image" to create your first AI image.

          -
          - -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Prompt Settings

          -

          Customize your image generation prompt template

          -
          -
          - -
          -
          -
          -
          - -
          - - Prompt template saved successfully! -
          - - -
          - - -
          - - - - Available placeholders:
          - {title} - Post title
          - {description} - Prompt description
          - {image_type} - Selected image type -
          -
          - -
          - -
          -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          API Configuration

          -

          OpenAI API settings and requirements

          -
          -
          - -
          -
          -
          -
          -
          -
          -
          -

          OpenAI API Key

          -

          Your OpenAI API key is configured' : 'not set'; ?>

          - - - Configure API Key - -
          - -
          -

          Image Service

          -

          (1024x1024 resolution)

          -
          - -
          -

          Image Storage

          -

          Generated images are saved to /assets/ai-images/[slug]/image.png

          -
          - -
          -

          Usage Limits

          -

          Subject to your OpenAI account limits and billing

          -
          -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Debug Information

          -

          Technical details and troubleshooting

          -
          -
          - -
          -
          -
          -
          -
          -
          -
          -

          Server Information

          -
            -
          • PHP Version:
          • -
          • WordPress Version:
          • -
          • cURL Available:
          • -
          • SSL Support:
          • -
          • Image Save Directory:
          • -
          • Image Dir Writable: - -
          • -
          • WordPress Uploads:
          • -
          • WP Uploads Writable: - -
          • -
          -
          - -
          -

          API Configuration

          -
            -
          • API Key Status:
          • -
          • API Key Length: characters
          • -
          • Prompt Template: characters
          • -
          -
          - -
          -

          Recent Errors

          -
          - '; - foreach ($recent_errors as $error) { - if (strpos($error, 'Igny8 Image Generation') !== false) { - echo esc_html($error) . "\n"; - } - } - echo ''; - } else { - echo '

          No recent Igny8 errors found.

          '; - } - } else { - echo '

          Error log not accessible.

          '; - } - ?> -
          -
          - -
          -

          Test Connection

          - -
          -
          -
          -
          -
          -
          -
          -
          - - - 'thinker', - 'subpage' => 'image-testing', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_image_testing_settings') -]); -?> \ No newline at end of file diff --git a/igny8-ai-seo-wp-plugin/modules/thinker/profile.php b/igny8-ai-seo-wp-plugin/modules/thinker/profile.php deleted file mode 100644 index b76dd280..00000000 --- a/igny8-ai-seo-wp-plugin/modules/thinker/profile.php +++ /dev/null @@ -1,344 +0,0 @@ - -
          -
          - - - -
          -
          -
          -
          -
          -

          Writing Style Preferences

          -

          Configure AI writing tone, style, and behavior

          -
          -
          - -
          -
          -
          -
          -
          - -
          - - - Choose the default tone for AI-generated content -
          - - -
          - - - Select the complexity level for AI-generated content -
          - - -
          - - - Choose the preferred length for AI-generated content -
          - - -
          - - - These instructions will be added to all AI prompts to customize behavior. -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          SEO Preferences

          -

          Configure SEO optimization settings and preferences

          -
          -
          - -
          -
          -
          -
          -
          - -
          - - - Choose the keyword density for SEO optimization -
          - - -
          - - - Choose the preferred length for meta descriptions -
          - - -
          - - Enable structured data recommendations for better SEO -
          - - -
          - - Enable internal linking suggestions for better site structure -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Content Structure Preferences

          -

          Configure content structure and formatting preferences

          -
          -
          - -
          -
          -
          -
          -
          - -
          - - - Choose the heading structure for content organization -
          - - -
          - - Automatically generate table of contents for lengthy content -
          - - -
          - - Add call-to-action sections to engage readers -
          - - -
          - - - Choose the preferred list style for content -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Profile Actions

          -

          Save, reset, or test your AI profile settings

          -
          -
          - -
          -
          -
          -
          -
          - - - -
          -
          -
          -
          -
          -
          - - - - - -
          -
          - - - -
          -
          -
          -
          -

          Planner Prompts

          -

          Configure AI prompt templates for clustering and idea generation

          -
          -
          - -
          -
          -
          - - - -
          -
          -
          -
          -

          Clustering Prompt

          -

          Group keywords into topic clusters

          -
          -
          - -
          -
          -
          -
          -

          This prompt is used to group keywords into topic clusters. Use [IGNY8_KEYWORDS] to inject keyword data.

          - -
          - - -
          -
          -
          - - -
          -
          -
          -
          -

          Ideas Generation Prompt

          -

          Generate content ideas from clusters

          -
          -
          - -
          -
          -
          -
          -

          This prompt generates content ideas from clusters. Use [IGNY8_CLUSTERS] to inject cluster data.

          - -
          - - -
          -
          -
          - -
          -
          -
          -

          Note: Planner prompts are only available when AI-Powered SEO Mode is enabled in the Planner module.

          - Go to Planner Settings -
          -
          -
          - -
          - - -
          -
          -
          -
          -

          Writer Prompts

          -

          Configure AI prompt templates for content writing

          -
          -
          - -
          -
          -
          - - - -
          -
          -
          -
          -

          Content Generation Prompt

          -

          Generate content from ideas

          -
          -
          - -
          -
          -
          -
          - -

          This prompt is used to generate content from ideas. Use [IGNY8_IDEA] to inject idea data, [IGNY8_CLUSTER] for cluster data, and [IGNY8_KEYWORDS] for keywords.

          - -
          - - -
          -
          -
          - -
          -
          -
          -

          Note: Writer prompts are only available when AI-Powered Mode is enabled in the Writer module.

          - Go to Writer Settings -
          -
          -
          - -
          - - -
          -
          -
          -
          -

          Image Generation

          -

          Test and configure AI image generation with DALL·E 3 and Runware

          -
          -
          - -
          -
          -
          - -
          -
          -
          - - - - - - -
          -
          - - -
          - - - - Available placeholders:
          - {post_title} - Post title
          - {image_prompt} - Image prompt (featured or in-article based on request)
          - {image_type} - Selected image type (from settings) -
          -
          - - -
          -
          - -
          - - - Specify elements to avoid in the generated image (text, watermarks, logos, etc.). -
          - -
          -
          - -
          -
          - -
          -
          -
          -
          - 'thinker', - 'subpage' => 'prompts', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_thinker_settings') - ]); - - // Add JavaScript for prompt management - ?> - - -// Global notification system is handled by core.js - diff --git a/igny8-ai-seo-wp-plugin/modules/thinker/strategies.php b/igny8-ai-seo-wp-plugin/modules/thinker/strategies.php deleted file mode 100644 index a659907d..00000000 --- a/igny8-ai-seo-wp-plugin/modules/thinker/strategies.php +++ /dev/null @@ -1,408 +0,0 @@ - -
          -
          - - - -
          -
          -
          -
          -

          Strategy Templates

          -

          Pre-built content strategies for different use cases

          -
          -
          - -
          -
          -
          -
          - -
          -
          -
          -
          -

          Blog Post Strategy

          -

          Comprehensive blog post creation with SEO optimization

          -
          -
          - -
          -
          -
          -
          -

          Comprehensive blog post creation with SEO optimization, internal linking, and engagement elements.

          -
          - SEO Optimized - Internal Links - CTA Included -
          -
          - - -
          -
          -
          - - -
          -
          -
          -
          -

          Product Description Strategy

          -

          E-commerce focused product descriptions with conversion optimization

          -
          -
          - -
          -
          -
          -
          -

          E-commerce focused product descriptions with conversion optimization and feature highlighting.

          -
          - Conversion Focused - Feature Rich - Benefit Driven -
          -
          - - -
          -
          -
          - - -
          -
          -
          -
          -

          Landing Page Strategy

          -

          High-converting landing pages with persuasive copy and social proof

          -
          -
          - -
          -
          -
          -
          -

          High-converting landing pages with persuasive copy, social proof, and clear value propositions.

          -
          - Persuasive Copy - Social Proof - Clear CTA -
          -
          - - -
          -
          -
          - - -
          -
          -
          -
          -

          Email Campaign Strategy

          -

          Engaging email sequences with personalization and automation

          -
          -
          - -
          -
          -
          -
          -

          Engaging email sequences with personalization, segmentation, and automated follow-ups.

          -
          - Personalized - Segmented - Automated -
          -
          - - -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -

          Custom Strategy Builder

          -

          Create custom content strategies tailored to your needs

          -
          -
          - -
          -
          -
          -
          -
          -
          -
          -
          - - - Choose a descriptive name for your strategy -
          -
          - - - Select the category for your strategy -
          -
          - -
          - - - Provide a clear description of what this strategy aims to achieve -
          - -
          - - - Define the specific goals and measurable objectives -
          - -
          - - - Detailed instructions for AI behavior and approach -
          - -
          - - -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -

          Strategy Performance

          -

          Track the performance and success of your strategies

          -
          -
          - -
          -
          -
          -
          -
          -
          -
          -
          -
          Strategies Created
          -
          -
          -
          -
          Success Rate
          -
          -
          -
          -
          Content Generated
          -
          -
          -
          -
          Avg Engagement
          -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -

          Recent Strategy Activity

          -

          Monitor recent strategy usage and activity

          -
          -
          - -
          -
          -
          -
          -
          -
          -
          -

          No recent strategy activity. Create and use strategies to see activity here.

          -
          -
          -
          -
          -
          -
          -
          - - - - - - -
          -
          - -
          -
          -
          -
          -

          Thinker Workflow Steps

          -

          Track your AI configuration progress

          -
          -
          - -
          -
          -
          - -
          - -
          -
          -
          -
          1
          -
          AI Prompts
          -
          -
          - - Configured -
          -
          - Manage AI prompt templates -
          -
          - Configure -
          -
          -
          - - -
          -
          -
          -
          2
          -
          AI Profile
          -
          -
          - - Ready -
          -
          - Set AI behavior and tone -
          -
          - Configure -
          -
          -
          - - -
          -
          -
          -
          3
          -
          Content Strategies
          -
          -
          - - Pending -
          -
          - Define content strategies -
          -
          - Configure -
          -
          -
          - - -
          -
          -
          -
          4
          -
          Image Testing
          -
          -
          - - Pending -
          -
          - Test DALL·E 3 integration -
          -
          - Test -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          AI Settings & Quick Actions

          -

          Manage AI configuration and test connections

          -
          -
          - -
          -
          -
          -
          -
          - - - -
          -
          -
          -
          - - -
          -
          - -
          -
          - - -
          -
          -
          -
          -

          Content Drafts

          -

          Review and manage your content drafts

          -
          -
          - -
          -
          -
          - - - $table_id, - 'module' => 'writer', - 'submodule' => 'drafts', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_writer_settings'), - 'parseNonce' => wp_create_nonce('igny8_parse_content'), - 'defaultPerPage' => get_option('igny8_records_per_page', 20), - 'clusterOptions' => $cluster_options, - 'filtersConfig' => $filters_config, - 'cronKey' => igny8_needs_cron_functionality() ? igny8_get_or_generate_cron_key() : null, - 'imageSettings' => [ - 'desktop_enabled' => get_option('igny8_desktop_enabled', '1') === '1', - 'mobile_enabled' => get_option('igny8_mobile_enabled', '0') === '1', - 'max_in_article_images' => intval(get_option('igny8_max_in_article_images', 1)) - ] - ]); - ?> -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/writer/published.php b/igny8-ai-seo-wp-plugin/modules/writer/published.php deleted file mode 100644 index 6208938d..00000000 --- a/igny8-ai-seo-wp-plugin/modules/writer/published.php +++ /dev/null @@ -1,90 +0,0 @@ - -
          -
          - - -
          -
          -
          -
          -

          Published Content

          -

          View and manage your published content

          -
          -
          - -
          -
          -
          - - - $table_id, - 'module' => 'writer', - 'submodule' => 'published', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_writer_settings'), - 'parseNonce' => wp_create_nonce('igny8_parse_content'), - 'defaultPerPage' => get_option('igny8_records_per_page', 20), - 'clusterOptions' => $cluster_options, - 'filtersConfig' => $filters_config, - 'cronKey' => igny8_needs_cron_functionality() ? igny8_get_or_generate_cron_key() : null, - 'imageSettings' => [ - 'desktop_enabled' => get_option('igny8_desktop_enabled', '1') === '1', - 'mobile_enabled' => get_option('igny8_mobile_enabled', '0') === '1', - 'max_in_article_images' => intval(get_option('igny8_max_in_article_images', 1)) - ] - ]); - ?> -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/writer/tasks.php b/igny8-ai-seo-wp-plugin/modules/writer/tasks.php deleted file mode 100644 index efeb103d..00000000 --- a/igny8-ai-seo-wp-plugin/modules/writer/tasks.php +++ /dev/null @@ -1,90 +0,0 @@ - -
          -
          - - -
          -
          -
          -
          -

          Writing Tasks

          -

          Manage your content writing tasks and workflow

          -
          -
          - -
          -
          -
          - - - $table_id, - 'module' => 'writer', - 'submodule' => 'tasks', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_writer_settings'), - 'parseNonce' => wp_create_nonce('igny8_parse_content'), - 'defaultPerPage' => get_option('igny8_records_per_page', 20), - 'clusterOptions' => $cluster_options, - 'filtersConfig' => $filters_config, - 'cronKey' => igny8_needs_cron_functionality() ? igny8_get_or_generate_cron_key() : null, - 'imageSettings' => [ - 'desktop_enabled' => get_option('igny8_desktop_enabled', '1') === '1', - 'mobile_enabled' => get_option('igny8_mobile_enabled', '0') === '1', - 'max_in_article_images' => intval(get_option('igny8_max_in_article_images', 1)) - ] - ]); - ?> -
          - -
          -
          diff --git a/igny8-ai-seo-wp-plugin/modules/writer/writer.php b/igny8-ai-seo-wp-plugin/modules/writer/writer.php deleted file mode 100644 index 9126ccc5..00000000 --- a/igny8-ai-seo-wp-plugin/modules/writer/writer.php +++ /dev/null @@ -1,660 +0,0 @@ -get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status IN ('pending', 'queued', 'new')"); - $draft_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status IN ('draft', 'in_progress', 'review')"); - $published_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed'"); - $total_tasks = $queued_tasks + $draft_tasks + $published_tasks; - - return [ - 'view_tasks' => [ - 'queued' => $queued_tasks, - 'total' => $total_tasks, - 'status' => $total_tasks > 0 ? 'completed' : 'pending' - ], - 'generate_drafts' => [ - 'queued' => $queued_tasks, - 'status' => $queued_tasks > 0 ? 'current' : ($draft_tasks > 0 ? 'completed' : 'pending') - ], - 'review_drafts' => [ - 'drafts' => $draft_tasks, - 'status' => $draft_tasks > 0 ? 'current' : ($published_tasks > 0 ? 'completed' : 'pending') - ], - 'publish' => [ - 'published' => $published_tasks, - 'drafts' => $draft_tasks, - 'status' => $published_tasks > 0 ? 'completed' : ($draft_tasks > 0 ? 'current' : 'pending') - ] - ]; -} - -// Handle URL parameters for subpages -$subpage = $_GET['sm'] ?? 'home'; -$GLOBALS['current_submodule'] = $subpage; -$GLOBALS['current_module'] = 'writer'; - -// Start output buffering -ob_start(); - -switch ($subpage) { - case 'tasks': - include plugin_dir_path(__FILE__) . 'tasks.php'; - break; - case 'drafts': - include plugin_dir_path(__FILE__) . 'drafts.php'; - break; - case 'published': - include plugin_dir_path(__FILE__) . 'published.php'; - break; - case 'home': - default: - // Home dashboard content - // Load KPI data for dashboard calculations - if (!defined('IGNY8_INCLUDE_CONFIG')) { - define('IGNY8_INCLUDE_CONFIG', true); - } - $kpi_config = $GLOBALS['igny8_kpi_config'] ?? []; // Loaded globally in igny8.php - $kpi_data = igny8_get_kpi_data_safe('writer_home', $kpi_config['writer_home'] ?? []); - - // Calculate dashboard metrics - Updated queries for correct status - global $wpdb; - - // Queued tasks - tasks that are pending/not started - $queued_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status IN ('pending', 'queued', 'new')"); - - // Draft tasks - content generated and saved as draft, awaiting review/publish - $draft_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status IN ('draft', 'in_progress', 'review')"); - - // Published tasks - status is 'completed' in tasks table - $published_tasks = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed'"); - - $total_tasks = $queued_tasks + $draft_tasks + $published_tasks; - - // Calculate percentages for progress bars - $queued_pct = $total_tasks > 0 ? round(($queued_tasks / $total_tasks) * 100) : 0; - $draft_pct = $total_tasks > 0 ? round(($draft_tasks / $total_tasks) * 100) : 0; - $published_pct = $total_tasks > 0 ? round(($published_tasks / $total_tasks) * 100) : 0; - - // Home page content - ?> -
          - -
          -
          - -
          -
          -
          - - Tasks Queued - awaiting writing start -
          -
          - -
          -
          -
          - - -
          -
          -
          - - Drafts - currently being written -
          -
          - -
          -
          -
          - - -
          -
          -
          - - Published - live on your site -
          -
          - -
          -
          -
          -
          -
          - - - - - -
          -
          -
          -
          -

          Writer Workflow Steps

          -

          Track your content creation progress

          -
          -
          - -
          -
          -
          - -
          - -
          -
          -
          -
          1
          -
          View Queued Tasks
          -
          -
          - - -
          -
          - 0): ?> - tasks queued - - No tasks available - -
          - - - -
          -
          - -
          -
          -
          -
          2
          -
          Generate Drafts (AI)
          -
          -
          - - -
          -
          - 0): ?> - tasks ready for AI - 0): ?> - All tasks processed - - No tasks to process - -
          - - - -
          -
          - -
          -
          -
          -
          3
          -
          Review Drafts
          -
          -
          - - -
          -
          - 0): ?> - drafts to review - 0): ?> - All drafts reviewed - - No drafts to review - -
          - - - -
          -
          - -
          -
          -
          -
          4
          -
          Publish Content
          -
          -
          - - -
          -
          - 0): ?> - drafts ready to publish - 0): ?> - content published - - No content to publish - -
          - - - -
          -
          -
          - -
          - - - - - - - - -
          - -
          -
          -
          -
          -

          Workflow Status & AI Settings

          -

          Workflow Status & AI Settings

          -
          - - - -
          -
          -
          -
          -
          - Manual - - AI Mode -
          -
          -
          -
          - -
          -
          -
          - -
          -
          -
          - - - -
          -
          -
          -
          -

          Workflow Status & AI Settings

          -

          Workflow Status & AI Settings

          -
          - - - - - - -
          -
          -
          -
          - - -
          -
          -
          - -
          -
          -
          - - - -
          -
          -
          - -
          - - -
          - -
          -
          -
          -
          -
          -

          Content Published by Type

          -

          Content distribution analysis

          -
          -
          - -
          -
          -
          - get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed'"); - echo '' . number_format($total_published) . ''; - echo 'Total Published'; - ?> -
          -
          -
          - get_results(" - SELECT content_type, COUNT(*) as count - FROM {$wpdb->prefix}igny8_tasks - WHERE status = 'completed' - GROUP BY content_type - ORDER BY count DESC - "); - - if ($content_by_type): - ?> -
          - content_type)); - $percentage = $total_published > 0 ? round(($type->count / $total_published) * 100) : 0; - $color_class = $color_classes[$color_index % 4]; - $color_index++; - ?> -
          -
          -
          -
          count); ?>
          -
          -
          -
          -
          -
          - % -
          -
          - -
          - -
          - -

          No published content found yet

          - View Tasks -
          - -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Publishing Activity

          -

          Content publishing timeline

          -
          -
          - -
          -
          -
          - get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed' AND updated_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)"); - echo '' . number_format($recent_7_days) . ''; - echo 'This Week'; - ?> -
          -
          -
          - get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed' AND DATE(updated_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)"); - $last_7_days = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed' AND updated_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)"); - $last_20_days = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed' AND updated_at >= DATE_SUB(NOW(), INTERVAL 20 DAY)"); - $all_time = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}igny8_tasks WHERE status = 'completed'"); - ?> -
          -
          -
          -
          Yesterday
          -
          -
          -
          -
          -
          -
          - 0 ? round(($yesterday_count / $all_time) * 100) : 0; ?>% -
          -
          - -
          -
          -
          Last 7 Days
          -
          -
          -
          -
          -
          -
          - 0 ? round(($last_7_days / $all_time) * 100) : 0; ?>% -
          -
          - -
          -
          -
          Last 20 Days
          -
          -
          -
          -
          -
          -
          - 0 ? round(($last_20_days / $all_time) * 100) : 0; ?>% -
          -
          - -
          -
          -
          All Time
          -
          -
          -
          -
          -
          -
          - 100% -
          -
          -
          -
          -
          -
          - - -
          -
          -
          -
          -
          -

          Content Workflow Summary

          -

          Content distribution analysis

          -
          -
          - -
          -
          -
          -
          -
          -
          - Queued Tasks - % -
          -
          -
          -
          -
          tasks waiting to start
          -
          - -
          -
          - Draft / Review - % -
          -
          -
          -
          -
          drafts awaiting review and publish
          -
          - -
          -
          - Published - % -
          -
          -
          -
          -
          tasks completed and published
          -
          -
          -
          -
          - - -
          - -
          - - - - - -
          -
          -
          -
          -
          -

          Pending Actions

          -

          Action items requiring attention

          -
          -
          - -
          -
          -
          -
          -
          - 0): ?> -
          - tasks not yet started - Start Writing -
          - - - 0): ?> -
          - drafts awaiting review and publish - Review Drafts -
          - - - 0): ?> -
          - posts ready to publish - View Published -
          - - - -
          - All caught up! Consider adding new content ideas. - Plan New Content -
          - -
          -
          -
          -
          - -
          - - - 'writer', - 'submodule' => 'home', - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('igny8_writer_settings'), - 'cronKey' => $cron_key - ]); - break; -} - -// Capture page content -$igny8_page_content = ob_get_clean(); - -// Include global layout -include_once plugin_dir_path(__FILE__) . '../../core/global-layout.php'; diff --git a/igny8-ai-seo-wp-plugin/shortcodes/ai-shortcodes.php b/igny8-ai-seo-wp-plugin/shortcodes/ai-shortcodes.php deleted file mode 100644 index 1222b7c0..00000000 --- a/igny8-ai-seo-wp-plugin/shortcodes/ai-shortcodes.php +++ /dev/null @@ -1,147 +0,0 @@ - $value) { - $atts[$key] = do_shortcode($value); - } - - // Store normalized field list - $form_fields_value = $atts['form_fields'] ?? ''; - - // Generate inline JavaScript for personalization functionality - $ajax_url = admin_url('admin-ajax.php'); - $nonce = wp_create_nonce('igny8_ajax_nonce'); - - // Fetch context values - $post_id = get_queried_object_id(); - - // Debug: Log PHP variables - error_log('IGNY8 Debug - PHP Variables:'); - error_log('ajax_url: ' . $ajax_url); - error_log('nonce: ' . $nonce); - error_log('post_id: ' . $post_id); - - // Check if Content Engine is enabled - $content_engine_status = get_option('igny8_content_engine_global_status', 'enabled'); - $post_type = get_post_type(); - $enabled_post_types = get_option('igny8_content_engine_enabled_post_types', []); - - if ($content_engine_status === 'enabled' && in_array($post_type, $enabled_post_types)) { - $teaser = esc_html(get_option('igny8_content_engine_teaser_text', 'Want to read this as if it was written exclusively about you?')); - $display_mode = get_option('igny8_content_engine_display_mode', 'button'); - } else { - $teaser = esc_html(get_option('igny8_teaser_text', 'Want to read this as if it was written exclusively about you?')); - $display_mode = 'button'; - } - - // Start output buffering - ob_start(); - ?> - - - -
          $val) { - if (!in_array($key, ['form_fields', 'form-fields'])) { - echo ' data-' . esc_attr($key) . '="' . esc_attr($val) . '"'; - } - } - ?> - > -

          -
          Generating personalized content...
          -
          -
          - - -
          -

          -
          -
          -
          - - -
          -

          - -
          - - - - ' . esc_html($context_raw) . '
          '; - } - ?> - - - - ''], $atts); - $post_id = get_the_ID(); - - if (empty($post_id)) { - return ''; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images)) { - return ''; - } - - // Display specific image by ID - if (!empty($atts['id']) && isset($images[$atts['id']])) { - $image_data = $images[$atts['id']]; - - // Device detection - only show if device matches - $is_mobile = wp_is_mobile(); - $is_desktop = !$is_mobile; - - // Check if image should be displayed based on device - $should_display = false; - if (strpos($atts['id'], 'desktop-') === 0 && $is_desktop) { - $should_display = true; - } elseif (strpos($atts['id'], 'mobile-') === 0 && $is_mobile) { - $should_display = true; - } - - if (!$should_display) { - return ''; - } - - $attachment_id = intval($image_data['attachment_id']); - - if ($attachment_id > 0) { - return wp_get_attachment_image($attachment_id, 'large', false, [ - 'class' => 'igny8-inarticle-image', - 'data-image-id' => esc_attr($atts['id']), - 'data-device' => esc_attr($image_data['device']), - 'alt' => esc_attr($image_data['label']) - ]); - } - } - - return ''; -}); - -/** - * Display all in-article images - * - * Usage: [igny8-images] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-images', function($atts) { - $atts = shortcode_atts([ - 'device' => '', // Filter by device type (desktop/mobile) - 'size' => 'large', // Image size - 'class' => 'igny8-image-gallery' // CSS class - ], $atts); - - $post_id = get_the_ID(); - - if (empty($post_id)) { - return ''; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images) || empty($images)) { - return ''; - } - - $output = '
          '; - $output .= '

          This is coming from shortcode

          '; - - foreach ($images as $label => $image_data) { - // Filter by device if specified - if (!empty($atts['device']) && $image_data['device'] !== $atts['device']) { - continue; - } - - $attachment_id = intval($image_data['attachment_id']); - - if ($attachment_id > 0) { - $output .= wp_get_attachment_image($attachment_id, $atts['size'], false, [ - 'class' => 'igny8-inarticle-image', - 'data-image-id' => esc_attr($label), - 'data-device' => esc_attr($image_data['device']), - 'alt' => esc_attr($image_data['label']) - ]); - } - } - - $output .= '
          '; - - return $output; -}); - -/** - * Display desktop images only - * - * Usage: [igny8-desktop-images] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-desktop-images', function($atts) { - $atts = shortcode_atts([ - 'size' => 'large', - 'class' => 'igny8-desktop-gallery' - ], $atts); - - return do_shortcode('[igny8-images device="desktop" size="' . $atts['size'] . '" class="' . $atts['class'] . '"]'); -}); - -/** - * Display mobile images only - * - * Usage: [igny8-mobile-images] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-mobile-images', function($atts) { - $atts = shortcode_atts([ - 'size' => 'large', - 'class' => 'igny8-mobile-gallery' - ], $atts); - - return do_shortcode('[igny8-images device="mobile" size="' . $atts['size'] . '" class="' . $atts['class'] . '"]'); -}); - -/** - * Display image count - * - * Usage: [igny8-image-count] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-image-count', function($atts) { - $atts = shortcode_atts(['device' => ''], $atts); - - $post_id = get_the_ID(); - - if (empty($post_id)) { - return '0'; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images)) { - return '0'; - } - - if (!empty($atts['device'])) { - $count = 0; - foreach ($images as $image_data) { - if ($image_data['device'] === $atts['device']) { - $count++; - } - } - return (string) $count; - } - - return (string) count($images); -}); - -/** - * Display image gallery with responsive design - * - * Usage: [igny8-responsive-gallery] - * - * @param array $atts Shortcode attributes - * @return string HTML output - */ -add_shortcode('igny8-responsive-gallery', function($atts) { - $atts = shortcode_atts([ - 'desktop_size' => 'large', - 'mobile_size' => 'medium', - 'class' => 'igny8-responsive-gallery' - ], $atts); - - $post_id = get_the_ID(); - - if (empty($post_id)) { - return ''; - } - - $images = get_post_meta($post_id, '_igny8_inarticle_images', true); - if (!is_array($images) || empty($images)) { - return ''; - } - - $output = '
          '; - - // Desktop images - $desktop_images = array_filter($images, function($img) { - return $img['device'] === 'desktop'; - }); - - if (!empty($desktop_images)) { - $output .= '
          '; - foreach ($desktop_images as $label => $image_data) { - $attachment_id = intval($image_data['attachment_id']); - if ($attachment_id > 0) { - $output .= wp_get_attachment_image($attachment_id, $atts['desktop_size'], false, [ - 'class' => 'igny8-desktop-image', - 'data-image-id' => esc_attr($label) - ]); - } - } - $output .= '
          '; - } - - // Mobile images - $mobile_images = array_filter($images, function($img) { - return $img['device'] === 'mobile'; - }); - - if (!empty($mobile_images)) { - $output .= ''; - } - - $output .= '
          '; - - // Add responsive CSS - $output .= ''; - - return $output; -}); diff --git a/igny8-ai-seo-wp-plugin/uninstall.php b/igny8-ai-seo-wp-plugin/uninstall.php deleted file mode 100644 index 72b86c2d..00000000 --- a/igny8-ai-seo-wp-plugin/uninstall.php +++ /dev/null @@ -1,208 +0,0 @@ -prefix . $table; - $wpdb->query("DROP TABLE IF EXISTS $table_name"); - error_log("Igny8 Compact: Dropped table $table_name"); - } -} - -/** - * Remove all plugin options - */ -function igny8_remove_plugin_options() { - // List of plugin options - $options = [ - 'igny8_version', - 'igny8_installed', - 'igny8_activated', - 'igny8_status', - 'igny8_settings', - 'igny8_last_update', - 'igny8_notifications', - 'igny8_cache_version', - 'igny8_license_key', - 'igny8_license_status' - ]; - - // Remove each option - foreach ($options as $option) { - delete_option($option); - error_log("Igny8 Compact: Removed option $option"); - } - - // Remove any transients - delete_transient('igny8_cache_data'); - delete_transient('igny8_api_data'); - delete_transient('igny8_stats_cache'); -} - -/** - * Remove user meta data related to the plugin - */ -function igny8_remove_user_meta() { - global $wpdb; - - // Remove user meta keys that start with igny8_ - $wpdb->query( - "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'igny8_%'" - ); - - error_log('Igny8 Compact: Removed user meta data'); -} - -/** - * Remove custom taxonomies and their terms - */ -function igny8_remove_taxonomies() { - // Get all terms for our custom taxonomies - $taxonomies = ['sectors', 'clusters']; - - foreach ($taxonomies as $taxonomy) { - // Get all terms - $terms = get_terms([ - 'taxonomy' => $taxonomy, - 'hide_empty' => false - ]); - - // Delete each term - foreach ($terms as $term) { - wp_delete_term($term->term_id, $taxonomy); - } - - // Unregister taxonomy - unregister_taxonomy($taxonomy); - - error_log("Igny8 Compact: Removed taxonomy $taxonomy"); - } -} - -/** - * Remove post meta data related to the plugin - */ -function igny8_remove_post_meta() { - global $wpdb; - - // List of post meta keys to remove - $meta_keys = [ - '_igny8_cluster_id', - '_igny8_keyword_ids', - '_igny8_task_id', - '_igny8_campaign_ids', - '_igny8_backlink_count', - '_igny8_last_optimized', - '_igny8_seo_score', - '_igny8_optimization_status', - '_igny8_content_score', - '_igny8_readability_score' - ]; - - // Remove each meta key - foreach ($meta_keys as $meta_key) { - $wpdb->query( - $wpdb->prepare( - "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s", - $meta_key - ) - ); - } - - error_log('Igny8 Compact: Removed post meta data'); -} - -/** - * Clear any cached data - */ -function igny8_clear_cache() { - // Clear WordPress object cache - wp_cache_flush(); - - // Clear any plugin-specific cache - if (function_exists('wp_cache_delete_group')) { - wp_cache_delete_group('igny8'); - } - - // Clear rewrite rules - flush_rewrite_rules(); - - error_log('Igny8 Compact: Cleared cache and rewrite rules'); -} - -// Run the uninstallation -igny8_uninstall(); diff --git a/scripts/auto-pull.sh b/scripts/auto-pull.sh deleted file mode 100644 index 82e93db8..00000000 --- a/scripts/auto-pull.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -cd /data/app/igny8 -git pull origin main diff --git a/scripts/cleanup-repo.sh b/scripts/cleanup-repo.sh deleted file mode 100644 index 48e41acc..00000000 --- a/scripts/cleanup-repo.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -# ============================================================================= -# Cleanup Remote Repository - Remove Architecture-Level Files -# ============================================================================= -# This script removes files from git tracking that shouldn't be in the repo -# Files are removed from git but kept locally (needed for builds) -# ============================================================================= - -set -e - -echo "🧹 Cleaning up repository..." -echo "" - -# Remove frontend node_modules from git -echo "📦 Removing frontend/node_modules/ from git tracking..." -git rm -r --cached frontend/node_modules/ 2>/dev/null || echo " (already removed or not tracked)" - -# Remove frontend dist from git -echo "📦 Removing frontend/dist/ from git tracking..." -git rm -r --cached frontend/dist/ 2>/dev/null || echo " (already removed or not tracked)" - -# Remove all __pycache__ directories from git -echo "🐍 Removing Python __pycache__/ directories from git tracking..." -git ls-files | grep "__pycache__" | xargs -r git rm --cached 2>/dev/null || echo " (already removed or not tracked)" - -# Remove .vite cache if tracked -echo "⚡ Removing .vite/ cache from git tracking..." -git rm -r --cached frontend/.vite/ 2>/dev/null || echo " (not tracked)" -git ls-files | grep "\.vite" | xargs -r git rm --cached 2>/dev/null || echo " (already removed)" - -# Remove any .pyc files -echo "🐍 Removing .pyc files from git tracking..." -git ls-files | grep "\.pyc$" | xargs -r git rm --cached 2>/dev/null || echo " (already removed)" - -# Remove any .pyo files -echo "🐍 Removing .pyo files from git tracking..." -git ls-files | grep "\.pyo$" | xargs -r git rm --cached 2>/dev/null || echo " (already removed)" - -# Remove any .env files (shouldn't be in repo) -echo "🔐 Removing .env files from git tracking..." -git ls-files | grep "\.env$" | xargs -r git rm --cached 2>/dev/null || echo " (not tracked)" - -# Remove log files -echo "📝 Removing log files from git tracking..." -git ls-files | grep "\.log$" | xargs -r git rm --cached 2>/dev/null || echo " (not tracked)" - -echo "" -echo "✅ Cleanup complete!" -echo "" -echo "📊 Summary of changes:" -git status --short | head -20 -echo "" -echo "💡 Next steps:" -echo " 1. Review the changes: git status" -echo " 2. Commit the cleanup: git commit -m 'chore: remove architecture-level files from repo'" -echo " 3. Push to remote: git push" -echo "" -echo "⚠️ Note: Files are removed from git but kept locally (needed for builds)" - diff --git a/scripts/extract_ai_elements.py b/scripts/extract_ai_elements.py deleted file mode 100644 index 0dc6d155..00000000 --- a/scripts/extract_ai_elements.py +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/env python3 -""" -IGNY8 AI Data Mapping Script -Extracts complete reference table for all AI-related elements (functions, models, prompts, limits, calls) -to eliminate assumptions during restructuring. - -Output: Markdown table with all AI Elements for cluster, idea, content, image -""" -import os -import re -import json -import ast -from pathlib import Path -from typing import Dict, List, Any, Optional - -# Project root (assuming script is in scripts/ directory) -PROJECT_ROOT = Path(__file__).parent.parent -BACKEND_ROOT = PROJECT_ROOT / "backend" / "igny8_core" - - -def extract_function_info(file_path: Path, function_name: str) -> Dict[str, Any]: - """Extract information about a function from a Python file""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - except Exception as e: - return {"error": f"Could not read file: {e}"} - - # Try to parse AST - try: - tree = ast.parse(content) - except SyntaxError: - return {"error": "Syntax error in file"} - - info = { - "file": str(file_path.relative_to(PROJECT_ROOT)), - "function_name": function_name, - "found": False, - "line_number": None, - "uses_ai_processor": False, - "uses_celery": False, - "has_progress_callback": False, - "has_request_steps": False, - "has_response_steps": False, - "prompt_source": "Unknown", - "model_source": "Unknown", - "validation_checks": [], - "limit_checks": [], - } - - # Search for function definition - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef) and node.name == function_name: - info["found"] = True - info["line_number"] = node.lineno - - # Check function body for patterns - func_content = ast.get_source_segment(content, node) or "" - - # Check for AIProcessor usage - if "AIProcessor" in func_content or "ai_processor" in func_content: - info["uses_ai_processor"] = True - - # Check for Celery - if "self.request" in func_content or "@shared_task" in content[:node.lineno * 100]: - info["uses_celery"] = True - - # Check for progress tracking - if "progress_callback" in func_content or "progress_tracker" in func_content: - info["has_progress_callback"] = True - - # Check for step tracking - if "request_steps" in func_content: - info["has_request_steps"] = True - if "response_steps" in func_content: - info["has_response_steps"] = True - - # Check for prompt sources - if "get_prompt_value" in func_content: - info["prompt_source"] = "Database (get_prompt_value)" - elif "get_default_prompt" in func_content: - info["prompt_source"] = "Default (get_default_prompt)" - elif "prompt_template" in func_content.lower(): - info["prompt_source"] = "Inline/Hardcoded" - - # Check for model selection - if "default_model" in func_content or "self.default_model" in func_content: - info["model_source"] = "AIProcessor.default_model" - elif "get_model" in func_content: - info["model_source"] = "Function.get_model()" - elif "IntegrationSettings" in func_content: - info["model_source"] = "IntegrationSettings.config['model']" - - # Check for validation - if "validate" in func_content.lower(): - info["validation_checks"].append("Has validate() call") - if "check_credits" in func_content: - info["limit_checks"].append("Credit check") - if "daily_cluster_limit" in func_content or "max_clusters" in func_content: - info["limit_checks"].append("Plan limits") - - break - - return info - - -def extract_class_info(file_path: Path, class_name: str) -> Dict[str, Any]: - """Extract information about a class from a Python file""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - except Exception as e: - return {"error": f"Could not read file: {e}"} - - try: - tree = ast.parse(content) - except SyntaxError: - return {"error": "Syntax error in file"} - - info = { - "file": str(file_path.relative_to(PROJECT_ROOT)), - "class_name": class_name, - "found": False, - "line_number": None, - "methods": [], - "inherits_from": [], - } - - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef) and node.name == class_name: - info["found"] = True - info["line_number"] = node.lineno - - # Get base classes - for base in node.bases: - if isinstance(base, ast.Name): - info["inherits_from"].append(base.id) - - # Get methods - for item in node.body: - if isinstance(item, ast.FunctionDef): - info["methods"].append(item.name) - - break - - return info - - -def find_ai_functions() -> List[Dict[str, Any]]: - """Find all AI-related functions in the codebase""" - functions = [] - - # Define AI functions to search for - ai_function_definitions = [ - { - "name": "_auto_cluster_keywords_core", - "file": BACKEND_ROOT / "modules" / "planner" / "tasks.py", - "type": "core_function", - "category": "cluster" - }, - { - "name": "_generate_single_idea_core", - "file": BACKEND_ROOT / "modules" / "planner" / "tasks.py", - "type": "core_function", - "category": "ideas" - }, - { - "name": "auto_generate_content_task", - "file": BACKEND_ROOT / "modules" / "writer" / "tasks.py", - "type": "celery_task", - "category": "content" - }, - { - "name": "AutoClusterFunction", - "file": BACKEND_ROOT / "ai" / "functions" / "auto_cluster.py", - "type": "class", - "category": "cluster" - }, - { - "name": "cluster_keywords", - "file": BACKEND_ROOT / "utils" / "ai_processor.py", - "type": "method", - "category": "cluster" - }, - { - "name": "generate_ideas", - "file": BACKEND_ROOT / "utils" / "ai_processor.py", - "type": "method", - "category": "ideas" - }, - { - "name": "generate_content", - "file": BACKEND_ROOT / "utils" / "ai_processor.py", - "type": "method", - "category": "content" - }, - { - "name": "generate_image", - "file": BACKEND_ROOT / "utils" / "ai_processor.py", - "type": "method", - "category": "image" - }, - { - "name": "run_ai_task", - "file": BACKEND_ROOT / "ai" / "tasks.py", - "type": "celery_task", - "category": "unified" - }, - { - "name": "execute", - "file": BACKEND_ROOT / "ai" / "engine.py", - "type": "method", - "category": "unified" - }, - ] - - for func_def in ai_function_definitions: - file_path = func_def["file"] - if not file_path.exists(): - continue - - if func_def["type"] == "class": - info = extract_class_info(file_path, func_def["name"]) - else: - info = extract_function_info(file_path, func_def["name"]) - - info.update({ - "type": func_def["type"], - "category": func_def["category"] - }) - - functions.append(info) - - return functions - - -def extract_prompt_info() -> List[Dict[str, Any]]: - """Extract prompt information""" - prompts = [] - - utils_file = BACKEND_ROOT / "modules" / "system" / "utils.py" - if utils_file.exists(): - with open(utils_file, 'r', encoding='utf-8') as f: - content = f.read() - - # Find prompt types in get_default_prompt - prompt_types = re.findall(r"'(\w+)':\s*\"\"\"", content) - for prompt_type in prompt_types: - prompts.append({ - "prompt_type": prompt_type, - "source": "Hardcoded in get_default_prompt()", - "file": "modules/system/utils.py", - "retrieval": "get_prompt_value() -> AIPrompt model or default" - }) - - return prompts - - -def extract_model_info() -> List[Dict[str, Any]]: - """Extract model configuration information""" - models = [] - - processor_file = BACKEND_ROOT / "utils" / "ai_processor.py" - if processor_file.exists(): - with open(processor_file, 'r', encoding='utf-8') as f: - content = f.read() - - # Find MODEL_RATES - model_rates_match = re.search(r'MODEL_RATES\s*=\s*\{([^}]+)\}', content, re.DOTALL) - if model_rates_match: - models_text = model_rates_match.group(1) - model_names = re.findall(r"'([^']+)'", models_text) - for model in model_names: - models.append({ - "model_name": model, - "source": "MODEL_RATES constant", - "file": "utils/ai_processor.py", - "selection": "AIProcessor._get_model() -> IntegrationSettings or Django settings" - }) - - return models - - -def generate_markdown_table(functions: List[Dict], prompts: List[Dict], models: List[Dict]) -> str: - """Generate markdown table from extracted data""" - output = [] - output.append("# IGNY8 AI Elements Reference Table\n") - output.append("Generated by extract_ai_elements.py\n") - output.append("---\n\n") - - # Functions table - output.append("## 🧠 AI Core Functions\n\n") - output.append("| Function Name | Category | Type | File | Line | Uses AIProcessor | Celery | Progress | Steps | Prompt Source | Model Source |\n") - output.append("|---------------|----------|------|------|------|------------------|--------|----------|-------|---------------|--------------|\n") - - for func in functions: - if func.get("error"): - continue - - name = func.get("function_name") or func.get("class_name", "N/A") - category = func.get("category", "N/A") - func_type = func.get("type", "N/A") - file = func.get("file", "N/A") - line = str(func.get("line_number", "N/A")) - uses_ai = "✅" if func.get("uses_ai_processor") else "❌" - celery = "✅" if func.get("uses_celery") else "❌" - progress = "✅" if func.get("has_progress_callback") else "❌" - steps = "✅" if (func.get("has_request_steps") or func.get("has_response_steps")) else "❌" - prompt = func.get("prompt_source", "Unknown") - model = func.get("model_source", "Unknown") - - output.append(f"| {name} | {category} | {func_type} | `{file}` | {line} | {uses_ai} | {celery} | {progress} | {steps} | {prompt} | {model} |\n") - - # Prompts table - output.append("\n## 🧱 Prompt Sources\n\n") - output.append("| Prompt Type | Source | File | Retrieval Method |\n") - output.append("|-------------|--------|------|------------------|\n") - - for prompt in prompts: - output.append(f"| {prompt['prompt_type']} | {prompt['source']} | `{prompt['file']}` | {prompt['retrieval']} |\n") - - # Models table - output.append("\n## 🧾 Model Configuration\n\n") - output.append("| Model Name | Source | File | Selection Method |\n") - output.append("|------------|--------|------|------------------|\n") - - for model in models: - output.append(f"| {model['model_name']} | {model['source']} | `{model['file']}` | {model['selection']} |\n") - - # Validation and Limits - output.append("\n## ⚠️ Validation & Limits\n\n") - output.append("| Function | Validation Checks | Limit Checks |\n") - output.append("|----------|-------------------|--------------|\n") - - for func in functions: - if func.get("error") or not func.get("found"): - continue - - name = func.get("function_name") or func.get("class_name", "N/A") - validations = ", ".join(func.get("validation_checks", [])) or "None" - limits = ", ".join(func.get("limit_checks", [])) or "None" - - output.append(f"| {name} | {validations} | {limits} |\n") - - return "".join(output) - - -def main(): - """Main execution""" - print("🔍 Extracting AI elements from codebase...") - - functions = find_ai_functions() - prompts = extract_prompt_info() - models = extract_model_info() - - print(f"✅ Found {len(functions)} functions") - print(f"✅ Found {len(prompts)} prompt types") - print(f"✅ Found {len(models)} models") - - # Generate markdown - markdown = generate_markdown_table(functions, prompts, models) - - # Save to file - output_file = PROJECT_ROOT / "docs" / "ActiveDocs" / "AI-ELEMENTS-EXTRACTED.md" - output_file.parent.mkdir(parents=True, exist_ok=True) - - with open(output_file, 'w', encoding='utf-8') as f: - f.write(markdown) - - print(f"\n✅ Table saved to: {output_file.relative_to(PROJECT_ROOT)}") - - # Also print to console - print("\n" + "="*80) - print(markdown) - print("="*80) - - -if __name__ == "__main__": - main() - diff --git a/scripts/git-pull.sh b/scripts/git-pull.sh deleted file mode 100644 index cd0d2829..00000000 --- a/scripts/git-pull.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# Git pull triggered by Gitea webhook - -LOG_FILE="/tmp/git-pull.log" -{ - echo "$(date '+%Y-%m-%d %H:%M:%S') - Git pull started" - cd /data/app/igny8 || exit 0 - git config --global --add safe.directory /data/app/igny8 2>/dev/null || true - git config --global --add safe.directory /data/app/igny8/backend 2>/dev/null || true - git config --global --add safe.directory /data/app/igny8/frontend 2>/dev/null || true - git pull origin main - echo "$(date '+%Y-%m-%d %H:%M:%S') - Git pull completed" -} >> "$LOG_FILE" 2>&1