From f4d62448cf13196b3600718bd41e3a9621fe8d16 Mon Sep 17 00:00:00 2001 From: Desktop Date: Tue, 11 Nov 2025 21:16:37 +0500 Subject: [PATCH] reference plugin and image gen analysis --- IGNY8_AI_AUDIT_PLAN.md | 247 - IGNY8_AI_SYSTEM_AUDIT_BASELINE_REPORT.md | 771 --- IGNY8_AI_UNIFICATION_MIGRATION_PLAN.md | 649 -- docs/AI-FUNCTION-FILES.md | 271 - docs/AI-NEW-DESIGN/07-AI-FRAMEWORK.md | 274 - .../STAGE1-REORGANIZATION-COMPLETE.md | 191 - .../STAGE2-EXECUTION-LOGGING-COMPLETE.md | 220 - docs/AI-NEW-DESIGN/STAGE3-LOGGING-COMPLETE.md | 171 - .../STAGE4-PROMPT-REGISTRY-COMPLETE.md | 220 - docs/Architecture/01-ARCHITECTURE.md | 749 --- docs/GIT-AUTO-SYNC-ARCHITECTURE.md | 225 - .../{Architecture => }/MASTER-ARCHITECTURE.md | 0 .../ai-docs/AI_FILES_ANALYSIS.md | 0 .../ai-docs/AI_FUNCTIONS_AUDIT_REPORT.md | 0 .../ai-docs/AI_MASTER_ARCHITECTURE.md | 0 igny8-ai-seo-wp-plugin/CHANGELOG_live.md | 59 + igny8-ai-seo-wp-plugin/ai/_README.php | 14 + igny8-ai-seo-wp-plugin/ai/integration.php | 21 + .../ai/model-rates-config.php | 192 + igny8-ai-seo-wp-plugin/ai/modules-ai.php | 1809 ++++++ igny8-ai-seo-wp-plugin/ai/openai-api.php | 1729 ++++++ igny8-ai-seo-wp-plugin/ai/prompts-library.php | 310 + igny8-ai-seo-wp-plugin/ai/runware-api.php | 191 + .../ai/writer/images/image-generation.php | 534 ++ .../assets/css/core-backup.css | 3039 ++++++++++ igny8-ai-seo-wp-plugin/assets/css/core.css | 3039 ++++++++++ .../assets/css/image-injection.css | 51 + igny8-ai-seo-wp-plugin/assets/js/core.js | 4961 ++++++++++++++++ .../assets/js/image-queue-processor.js | 436 ++ .../assets/shortcodes/_README.php | 14 + .../assets/shortcodes/image-gallery.php | 278 + .../templates/igny8_clusters_template.csv | 4 + .../assets/templates/igny8_ideas_template.csv | 4 + .../templates/igny8_keywords_template.csv | 4 + igny8-ai-seo-wp-plugin/core/_README.php | 14 + igny8-ai-seo-wp-plugin/core/admin/ajax.php | 5223 +++++++++++++++++ .../core/admin/global-helpers.php | 1084 ++++ igny8-ai-seo-wp-plugin/core/admin/init.php | 135 + igny8-ai-seo-wp-plugin/core/admin/menu.php | 356 ++ .../core/admin/meta-boxes.php | 387 ++ .../core/admin/module-manager-class.php | 181 + .../core/cron/igny8-cron-handlers.php | 1610 +++++ .../cron/igny8-cron-master-dispatcher.php | 384 ++ .../core/db/db-migration.php | 253 + igny8-ai-seo-wp-plugin/core/db/db.php | 970 +++ igny8-ai-seo-wp-plugin/core/global-layout.php | 463 ++ igny8-ai-seo-wp-plugin/debug/_README.php | 14 + igny8-ai-seo-wp-plugin/debug/debug.php | 47 + igny8-ai-seo-wp-plugin/debug/module-debug.php | 1623 +++++ .../debug/monitor-helpers.php | 23 + .../docs/COMPLETE_FEATURES_LIST.md | 348 ++ .../docs/COMPLETE_FUNCTION_REFERENCE.md | 726 +++ .../docs/COMPLETE_IMAGE_GENERATION_AUDIT.md | 476 ++ .../docs/COMPLETE_WORKFLOWS_DOCUMENTATION.md | 723 +++ igny8-ai-seo-wp-plugin/docs/FILE_TREE.txt | 122 + .../docs/IGNY8_PAGES_TABLE.md | 162 + .../docs/IGNY8_SNAPSHOT_V5.2.0.md | 523 ++ ...WP Plugin to Igny8 App Migration Plan.docx | Bin 0 -> 130736 bytes .../docs/MIGRATION_PLAN_EXTRACTED.txt | 524 ++ ...to_blocks_and_image_shortcode_injection.md | 308 + igny8-ai-seo-wp-plugin/docs/_README.php | 14 + igny8-ai-seo-wp-plugin/extract_docx.ps1 | 24 + igny8-ai-seo-wp-plugin/flows/sync-ajax.php | 485 ++ .../flows/sync-functions.php | 673 +++ igny8-ai-seo-wp-plugin/flows/sync-hooks.php | 99 + .../igny8-wp-load-handler.php | 384 ++ igny8-ai-seo-wp-plugin/igny8.php | 246 + igny8-ai-seo-wp-plugin/install.php | 43 + igny8-ai-seo-wp-plugin/modules/_README.php | 14 + .../modules/analytics/analytics.php | 44 + .../modules/components/_README.php | 14 + .../modules/components/actions-tpl.php | 111 + .../modules/components/export-modal-tpl.php | 77 + .../modules/components/filters-tpl.php | 136 + .../modules/components/forms-tpl.php | 176 + .../modules/components/import-modal-tpl.php | 67 + .../modules/components/kpi-tpl.php | 140 + .../modules/components/pagination-tpl.php | 136 + .../modules/components/table-tpl.php | 870 +++ .../modules/config/_README.php | 14 + .../modules/config/filters-config.php | 586 ++ .../modules/config/forms-config.php | 638 ++ .../modules/config/import-export-config.php | 150 + .../modules/config/kpi-config.php | 581 ++ .../modules/config/tables-config.php | 989 ++++ igny8-ai-seo-wp-plugin/modules/help/docs.php | 841 +++ .../modules/help/function-testing.php | 122 + igny8-ai-seo-wp-plugin/modules/help/help.php | 834 +++ .../modules/help/system-testing.php | 329 ++ igny8-ai-seo-wp-plugin/modules/home.php | 231 + .../modules/planner/clusters.php | 84 + .../modules/planner/ideas.php | 84 + .../modules/planner/keywords.php | 84 + .../modules/planner/planner.php | 653 +++ .../modules/settings/general-settings.php | 676 +++ .../modules/settings/import-export.php | 267 + .../modules/settings/integration.php | 744 +++ .../modules/settings/schedules.php | 297 + .../modules/settings/status.php | 353 ++ .../modules/thinker/image-testing.php | 917 +++ .../modules/thinker/profile.php | 344 ++ .../modules/thinker/prompts.php | 461 ++ .../modules/thinker/strategies.php | 408 ++ .../modules/thinker/thinker.php | 224 + .../modules/writer/drafts.php | 90 + .../modules/writer/published.php | 90 + .../modules/writer/tasks.php | 90 + .../modules/writer/writer.php | 660 +++ .../shortcodes/ai-shortcodes.php | 147 + .../shortcodes/writer-shortcodes.php | 278 + igny8-ai-seo-wp-plugin/uninstall.php | 208 + 111 files changed, 49595 insertions(+), 3988 deletions(-) delete mode 100644 IGNY8_AI_AUDIT_PLAN.md delete mode 100644 IGNY8_AI_SYSTEM_AUDIT_BASELINE_REPORT.md delete mode 100644 IGNY8_AI_UNIFICATION_MIGRATION_PLAN.md delete mode 100644 docs/AI-FUNCTION-FILES.md delete mode 100644 docs/AI-NEW-DESIGN/07-AI-FRAMEWORK.md delete mode 100644 docs/AI-NEW-DESIGN/STAGE1-REORGANIZATION-COMPLETE.md delete mode 100644 docs/AI-NEW-DESIGN/STAGE2-EXECUTION-LOGGING-COMPLETE.md delete mode 100644 docs/AI-NEW-DESIGN/STAGE3-LOGGING-COMPLETE.md delete mode 100644 docs/AI-NEW-DESIGN/STAGE4-PROMPT-REGISTRY-COMPLETE.md delete mode 100644 docs/Architecture/01-ARCHITECTURE.md delete mode 100644 docs/GIT-AUTO-SYNC-ARCHITECTURE.md rename docs/{Architecture => }/MASTER-ARCHITECTURE.md (100%) rename AI_FILES_ANALYSIS.md => docs/ai-docs/AI_FILES_ANALYSIS.md (100%) rename AI_FUNCTIONS_AUDIT_REPORT.md => docs/ai-docs/AI_FUNCTIONS_AUDIT_REPORT.md (100%) rename AI_MASTER_ARCHITECTURE.md => docs/ai-docs/AI_MASTER_ARCHITECTURE.md (100%) create mode 100644 igny8-ai-seo-wp-plugin/CHANGELOG_live.md create mode 100644 igny8-ai-seo-wp-plugin/ai/_README.php create mode 100644 igny8-ai-seo-wp-plugin/ai/integration.php create mode 100644 igny8-ai-seo-wp-plugin/ai/model-rates-config.php create mode 100644 igny8-ai-seo-wp-plugin/ai/modules-ai.php create mode 100644 igny8-ai-seo-wp-plugin/ai/openai-api.php create mode 100644 igny8-ai-seo-wp-plugin/ai/prompts-library.php create mode 100644 igny8-ai-seo-wp-plugin/ai/runware-api.php create mode 100644 igny8-ai-seo-wp-plugin/ai/writer/images/image-generation.php create mode 100644 igny8-ai-seo-wp-plugin/assets/css/core-backup.css create mode 100644 igny8-ai-seo-wp-plugin/assets/css/core.css create mode 100644 igny8-ai-seo-wp-plugin/assets/css/image-injection.css create mode 100644 igny8-ai-seo-wp-plugin/assets/js/core.js create mode 100644 igny8-ai-seo-wp-plugin/assets/js/image-queue-processor.js create mode 100644 igny8-ai-seo-wp-plugin/assets/shortcodes/_README.php create mode 100644 igny8-ai-seo-wp-plugin/assets/shortcodes/image-gallery.php create mode 100644 igny8-ai-seo-wp-plugin/assets/templates/igny8_clusters_template.csv create mode 100644 igny8-ai-seo-wp-plugin/assets/templates/igny8_ideas_template.csv create mode 100644 igny8-ai-seo-wp-plugin/assets/templates/igny8_keywords_template.csv create mode 100644 igny8-ai-seo-wp-plugin/core/_README.php create mode 100644 igny8-ai-seo-wp-plugin/core/admin/ajax.php create mode 100644 igny8-ai-seo-wp-plugin/core/admin/global-helpers.php create mode 100644 igny8-ai-seo-wp-plugin/core/admin/init.php create mode 100644 igny8-ai-seo-wp-plugin/core/admin/menu.php create mode 100644 igny8-ai-seo-wp-plugin/core/admin/meta-boxes.php create mode 100644 igny8-ai-seo-wp-plugin/core/admin/module-manager-class.php create mode 100644 igny8-ai-seo-wp-plugin/core/cron/igny8-cron-handlers.php create mode 100644 igny8-ai-seo-wp-plugin/core/cron/igny8-cron-master-dispatcher.php create mode 100644 igny8-ai-seo-wp-plugin/core/db/db-migration.php create mode 100644 igny8-ai-seo-wp-plugin/core/db/db.php create mode 100644 igny8-ai-seo-wp-plugin/core/global-layout.php create mode 100644 igny8-ai-seo-wp-plugin/debug/_README.php create mode 100644 igny8-ai-seo-wp-plugin/debug/debug.php create mode 100644 igny8-ai-seo-wp-plugin/debug/module-debug.php create mode 100644 igny8-ai-seo-wp-plugin/debug/monitor-helpers.php create mode 100644 igny8-ai-seo-wp-plugin/docs/COMPLETE_FEATURES_LIST.md create mode 100644 igny8-ai-seo-wp-plugin/docs/COMPLETE_FUNCTION_REFERENCE.md create mode 100644 igny8-ai-seo-wp-plugin/docs/COMPLETE_IMAGE_GENERATION_AUDIT.md create mode 100644 igny8-ai-seo-wp-plugin/docs/COMPLETE_WORKFLOWS_DOCUMENTATION.md create mode 100644 igny8-ai-seo-wp-plugin/docs/FILE_TREE.txt create mode 100644 igny8-ai-seo-wp-plugin/docs/IGNY8_PAGES_TABLE.md create mode 100644 igny8-ai-seo-wp-plugin/docs/IGNY8_SNAPSHOT_V5.2.0.md create mode 100644 igny8-ai-seo-wp-plugin/docs/Igny8 WP Plugin to Igny8 App Migration Plan.docx create mode 100644 igny8-ai-seo-wp-plugin/docs/MIGRATION_PLAN_EXTRACTED.txt create mode 100644 igny8-ai-seo-wp-plugin/docs/TROUBLESHOOTING_Converting_to_blocks_and_image_shortcode_injection.md create mode 100644 igny8-ai-seo-wp-plugin/docs/_README.php create mode 100644 igny8-ai-seo-wp-plugin/extract_docx.ps1 create mode 100644 igny8-ai-seo-wp-plugin/flows/sync-ajax.php create mode 100644 igny8-ai-seo-wp-plugin/flows/sync-functions.php create mode 100644 igny8-ai-seo-wp-plugin/flows/sync-hooks.php create mode 100644 igny8-ai-seo-wp-plugin/igny8-wp-load-handler.php create mode 100644 igny8-ai-seo-wp-plugin/igny8.php create mode 100644 igny8-ai-seo-wp-plugin/install.php create mode 100644 igny8-ai-seo-wp-plugin/modules/_README.php create mode 100644 igny8-ai-seo-wp-plugin/modules/analytics/analytics.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/_README.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/actions-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/export-modal-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/filters-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/forms-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/import-modal-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/kpi-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/pagination-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/components/table-tpl.php create mode 100644 igny8-ai-seo-wp-plugin/modules/config/_README.php create mode 100644 igny8-ai-seo-wp-plugin/modules/config/filters-config.php create mode 100644 igny8-ai-seo-wp-plugin/modules/config/forms-config.php create mode 100644 igny8-ai-seo-wp-plugin/modules/config/import-export-config.php create mode 100644 igny8-ai-seo-wp-plugin/modules/config/kpi-config.php create mode 100644 igny8-ai-seo-wp-plugin/modules/config/tables-config.php create mode 100644 igny8-ai-seo-wp-plugin/modules/help/docs.php create mode 100644 igny8-ai-seo-wp-plugin/modules/help/function-testing.php create mode 100644 igny8-ai-seo-wp-plugin/modules/help/help.php create mode 100644 igny8-ai-seo-wp-plugin/modules/help/system-testing.php create mode 100644 igny8-ai-seo-wp-plugin/modules/home.php create mode 100644 igny8-ai-seo-wp-plugin/modules/planner/clusters.php create mode 100644 igny8-ai-seo-wp-plugin/modules/planner/ideas.php create mode 100644 igny8-ai-seo-wp-plugin/modules/planner/keywords.php create mode 100644 igny8-ai-seo-wp-plugin/modules/planner/planner.php create mode 100644 igny8-ai-seo-wp-plugin/modules/settings/general-settings.php create mode 100644 igny8-ai-seo-wp-plugin/modules/settings/import-export.php create mode 100644 igny8-ai-seo-wp-plugin/modules/settings/integration.php create mode 100644 igny8-ai-seo-wp-plugin/modules/settings/schedules.php create mode 100644 igny8-ai-seo-wp-plugin/modules/settings/status.php create mode 100644 igny8-ai-seo-wp-plugin/modules/thinker/image-testing.php create mode 100644 igny8-ai-seo-wp-plugin/modules/thinker/profile.php create mode 100644 igny8-ai-seo-wp-plugin/modules/thinker/prompts.php create mode 100644 igny8-ai-seo-wp-plugin/modules/thinker/strategies.php create mode 100644 igny8-ai-seo-wp-plugin/modules/thinker/thinker.php create mode 100644 igny8-ai-seo-wp-plugin/modules/writer/drafts.php create mode 100644 igny8-ai-seo-wp-plugin/modules/writer/published.php create mode 100644 igny8-ai-seo-wp-plugin/modules/writer/tasks.php create mode 100644 igny8-ai-seo-wp-plugin/modules/writer/writer.php create mode 100644 igny8-ai-seo-wp-plugin/shortcodes/ai-shortcodes.php create mode 100644 igny8-ai-seo-wp-plugin/shortcodes/writer-shortcodes.php create mode 100644 igny8-ai-seo-wp-plugin/uninstall.php diff --git a/IGNY8_AI_AUDIT_PLAN.md b/IGNY8_AI_AUDIT_PLAN.md deleted file mode 100644 index 84edefe2..00000000 --- a/IGNY8_AI_AUDIT_PLAN.md +++ /dev/null @@ -1,247 +0,0 @@ -# IGNY8 AI System Audit — Execution Plan - -## Objective -Perform a complete structural and functional audit of the IGNY8 AI subsystem exactly as it exists, without any modifications, renaming, or assumptions. Document all findings in a baseline report. - -## Scope - -### Primary Directory: `backend/igny8_core/ai/` -**Core AI Files (15 files):** -- `__init__.py` - Package initialization and exports -- `admin.py` - Django admin configuration for AI models -- `ai_core.py` - Core AI functionality -- `apps.py` - Django app configuration -- `base.py` - Base classes or utilities -- `constants.py` - AI-related constants -- `engine.py` - AI engine implementation -- `models.py` - Database models for AI entities -- `processor.py` - AI processing logic -- `prompts.py` - Prompt templates and management -- `registry.py` - Function/component registry -- `settings.py` - AI-specific settings -- `tasks.py` - Celery task definitions -- `tracker.py` - Progress tracking and state management -- `types.py` - Type definitions and schemas -- `validators.py` - Validation logic - -**AI Functions Subdirectory (5 files):** -- `functions/__init__.py` - Function package exports -- `functions/auto_cluster.py` - Automatic clustering functionality -- `functions/generate_content.py` - Content generation logic -- `functions/generate_ideas.py` - Idea generation logic -- `functions/generate_images.py` - Image generation logic - -### Related Directories - -**`backend/igny8_core/utils/` (4 files):** -- `ai_processor.py` - AI processing utilities -- `content_normalizer.py` - Content normalization utilities -- `queue_manager.py` - Queue management utilities -- `wordpress.py` - WordPress integration utilities - -**`backend/igny8_core/modules/` (AI-related files):** -- `planner/tasks.py` - Planner module Celery tasks -- `writer/tasks.py` - Writer module Celery tasks -- `system/models.py` - System models (may contain AI settings) -- `system/settings_models.py` - Settings models -- `system/settings_views.py` - Settings views -- `system/views.py` - System views -- `system/utils.py` - System utilities - -**Configuration Files:** -- `backend/igny8_core/celery.py` - Celery configuration and task registration -- `backend/igny8_core/settings.py` - Django settings (AI configuration loading) - -## Audit Methodology - -### Phase 1: File Inventory and Initial Reading -1. Read all files in `backend/igny8_core/ai/` directory -2. Read all files in `backend/igny8_core/ai/functions/` directory -3. Read AI-related files in `backend/igny8_core/utils/` -4. Read AI-related task files in `backend/igny8_core/modules/` -5. Read configuration and integration files - -### Phase 2: Function and Class Analysis -1. Extract all function definitions with: - - Function name - - Parameters and types - - Return values - - Docstrings/documentation - - Decorators (especially Celery tasks) -2. Extract all class definitions with: - - Class name - - Inheritance hierarchy - - Methods and their purposes - - Class-level attributes -3. Identify call sites for each function/class method - -### Phase 3: Dependency Mapping -1. Map import relationships: - - Which files import from which files - - External dependencies (libraries, Django, Celery) - - Circular dependencies (if any) -2. Create dependency graph/table showing: - - Direct imports - - Indirect dependencies - - Shared utilities - -### Phase 4: System Flow Analysis -1. Trace request flow: - - Frontend API endpoints → Views/Serializers - - Views → Celery tasks - - Celery tasks → AI functions - - AI functions → External APIs/Models - - Results → Database storage - - Results → Response to frontend -2. Document: - - Entry points (API endpoints, admin actions, management commands) - - Task queue flow (Celery task registration and execution) - - State management (tracker, progress updates) - - Error handling paths - - Logging and debug output - -### Phase 5: Integration Points Analysis -1. **Celery Integration:** - - Task registration in `celery.py` - - Task decorators and configurations - - Task routing and queues - - Async execution patterns - -2. **Database Integration:** - - Models used by AI subsystem - - Model relationships - - Data persistence patterns - - Query patterns - -3. **Frontend Integration:** - - API endpoints that trigger AI tasks - - Serializers for AI data - - Response formats - - WebSocket/SSE for progress updates (if any) - -4. **Configuration Integration:** - - Settings loading (Django settings, environment variables) - - Model/provider configuration - - API key management - - Feature flags or switches - -5. **Debug Panel Integration:** - - Debug logging mechanisms - - Progress tracking - - State inspection tools - -### Phase 6: Redundancy and Pattern Identification -1. Identify: - - Duplicated code blocks - - Similar functions with slight variations - - Repeated patterns that could indicate consolidation opportunities - - Unused or dead code - - Overlapping responsibilities - -2. Document patterns: - - Common error handling approaches - - Repeated validation logic - - Similar processing pipelines - - Shared utility patterns - -### Phase 7: Documentation Compilation -Create structured document with sections: -1. **Current File Inventory** - List all files with brief role descriptions -2. **Function Inventory** - Comprehensive list of all functions with descriptions -3. **Class Inventory** - All classes and their purposes -4. **Dependency Graph/Table** - Import relationships and dependencies -5. **System Flow Description** - End-to-end flow documentation -6. **Integration Points** - Detailed integration documentation -7. **Identified Redundancies** - Patterns and duplications found -8. **Summary of Potential Consolidation Areas** - Observations only (no refactoring proposals) - -## Execution Rules - -### Strict Guidelines: -- ✅ **DO:** Read all code exactly as written -- ✅ **DO:** Document what exists without modification -- ✅ **DO:** Label any assumptions explicitly -- ✅ **DO:** Trace actual code paths, not theoretical ones -- ✅ **DO:** Include line numbers and file paths for references - -### Prohibited Actions: -- ❌ **DON'T:** Rename anything -- ❌ **DON'T:** Merge or consolidate code -- ❌ **DON'T:** Propose new architecture -- ❌ **DON'T:** Suggest simplifications -- ❌ **DON'T:** Make any code changes -- ❌ **DON'T:** Create new files (except the audit document) -- ❌ **DON'T:** Assume functionality without reading code - -## Deliverable - -**Document Title:** `IGNY8_AI_SYSTEM_AUDIT_BASELINE_REPORT.md` - -**Structure:** -```markdown -# IGNY8 AI System Audit — Current Structure & Flow Mapping (Baseline Report) - -## Executive Summary -[Brief overview of findings] - -## 1. Current File Inventory -[Complete list with descriptions] - -## 2. Function Inventory -[All functions documented] - -## 3. Class Inventory -[All classes documented] - -## 4. Dependency Graph/Table -[Import relationships] - -## 5. System Flow Description -[End-to-end flows] - -## 6. Integration Points -[Celery, Database, Frontend, Configuration, Debug] - -## 7. Identified Redundancies or Repetition -[Patterns found] - -## 8. Summary of Potential Consolidation Areas -[Observations only] - -## 9. Assumptions Made -[Any assumptions explicitly labeled] - -## 10. Appendix -[Additional details, code snippets, etc.] -``` - -## Execution Checklist - -- [ ] Phase 1: Read all AI core files -- [ ] Phase 1: Read all AI function files -- [ ] Phase 1: Read all utility files -- [ ] Phase 1: Read all module task files -- [ ] Phase 1: Read configuration files -- [ ] Phase 2: Extract and document all functions -- [ ] Phase 2: Extract and document all classes -- [ ] Phase 3: Map all import dependencies -- [ ] Phase 4: Trace system flows -- [ ] Phase 5: Document integration points -- [ ] Phase 6: Identify redundancies -- [ ] Phase 7: Compile final audit document - -## Estimated File Count -- **AI Core Files:** 15 files -- **AI Functions:** 5 files -- **Utilities:** 4 files -- **Module Tasks:** 2 files -- **System Module:** ~5 files -- **Configuration:** 2 files -- **Total:** ~33 files to analyze - -## Notes -- This is a discovery phase only -- All findings must be based on actual code -- No refactoring or improvements will be proposed -- The goal is to understand the current state completely - diff --git a/IGNY8_AI_SYSTEM_AUDIT_BASELINE_REPORT.md b/IGNY8_AI_SYSTEM_AUDIT_BASELINE_REPORT.md deleted file mode 100644 index 6bdbcf58..00000000 --- a/IGNY8_AI_SYSTEM_AUDIT_BASELINE_REPORT.md +++ /dev/null @@ -1,771 +0,0 @@ -# IGNY8 AI System Audit — Current Structure & Flow Mapping (Baseline Report) - -**Date:** 2024-12-19 -**Scope:** Complete structural and functional audit of the IGNY8 AI subsystem -**Methodology:** Code analysis without modifications or assumptions - ---- - -## Executive Summary - -The IGNY8 AI subsystem is a comprehensive framework for AI-powered content operations including keyword clustering, idea generation, content generation, and image generation. The system uses a unified framework architecture with a centralized execution engine, but also maintains legacy code paths for backward compatibility. - -**Key Findings:** -- **20 core AI files** in `backend/igny8_core/ai/` -- **4 AI function implementations** following BaseAIFunction pattern -- **Dual execution paths:** New unified framework (`run_ai_task` → `AIEngine`) and legacy paths (direct task calls) -- **Centralized AI request handling** via `AICore.run_ai_request()` -- **Comprehensive tracking** via `StepTracker`, `ProgressTracker`, `CostTracker`, and `ConsoleStepTracker` -- **Multiple prompt management systems:** `PromptRegistry` (new) and direct database queries (legacy) - ---- - -## 1. Current File Inventory - -### 1.1 Core AI Framework (`backend/igny8_core/ai/`) - -| File | Lines | Purpose | -|------|-------|---------| -| `__init__.py` | 73 | Package exports - exposes main classes and functions | -| `admin.py` | 60 | Django admin configuration for `AITaskLog` model | -| `ai_core.py` | 756 | Centralized AI request handler - `AICore` class with `run_ai_request()` and `generate_image()` | -| `apps.py` | 21 | Django app configuration | -| `base.py` | 95 | Abstract base class `BaseAIFunction` - defines function interface | -| `constants.py` | 42 | Model pricing, valid models, configuration constants | -| `engine.py` | 375 | Central orchestrator `AIEngine` - manages function lifecycle | -| `models.py` | 52 | Database model `AITaskLog` for unified logging | -| `processor.py` | 76 | **DEPRECATED** wrapper around `AICore` for backward compatibility | -| `prompts.py` | 432 | `PromptRegistry` - centralized prompt management with hierarchical resolution | -| `registry.py` | 97 | Function registry with lazy loading - `register_function()`, `get_function()` | -| `settings.py` | 117 | Model configurations per function - `MODEL_CONFIG`, `get_model_config()` | -| `tasks.py` | 131 | Unified Celery task entrypoint `run_ai_task()` - single entry point for all AI functions | -| `tracker.py` | 348 | Progress tracking utilities - `StepTracker`, `ProgressTracker`, `CostTracker`, `ConsoleStepTracker` | -| `types.py` | 44 | Type definitions - `StepLog`, `ProgressState`, `AITaskResult` dataclasses | -| `validators.py` | 187 | Validation functions - `validate_ids()`, `validate_keywords_exist()`, etc. | - -### 1.2 AI Function Implementations (`backend/igny8_core/ai/functions/`) - -| File | Lines | Purpose | -|------|-------|---------| -| `__init__.py` | 18 | Function package exports | -| `auto_cluster.py` | 330 | `AutoClusterFunction` - Groups keywords into semantic clusters | -| `generate_content.py` | 388 | `GenerateContentFunction` + `generate_content_core()` - Generates article content | -| `generate_ideas.py` | 335 | `GenerateIdeasFunction` + `generate_ideas_core()` - Generates content ideas from clusters | -| `generate_images.py` | 279 | `GenerateImagesFunction` + `generate_images_core()` - Generates images for tasks | - -### 1.3 Utility Files (`backend/igny8_core/utils/`) - -| File | Lines | Purpose | -|------|-------|---------| -| `ai_processor.py` | 1407 | **LEGACY** Unified AI interface - `AIProcessor` class with OpenAI/Runware support. Contains duplicate constants and methods. | -| `content_normalizer.py` | 273 | Content normalization - converts AI responses to HTML format | -| `queue_manager.py` | 90 | Queue abstraction (currently placeholder, not fully implemented) | -| `wordpress.py` | (not read) | WordPress integration utilities | - -### 1.4 Module Task Files - -| File | Lines | Purpose | -|------|-------|---------| -| `modules/planner/tasks.py` | 736 | **DEPRECATED** Legacy clustering task `auto_cluster_keywords_task()` - uses old `AIProcessor` | -| `modules/writer/tasks.py` | 1156 | Legacy content/image generation tasks - `auto_generate_content_task()`, `auto_generate_images_task()` | - -### 1.5 Configuration Files - -| File | Purpose | -|------|---------| -| `celery.py` | Celery app configuration - auto-discovers tasks from all Django apps | -| `modules/system/models.py` | `AIPrompt`, `IntegrationSettings` models for AI configuration | - ---- - -## 2. Function Inventory - -### 2.1 Core Framework Functions - -#### `AICore` (ai_core.py) -- **`__init__(account=None)`** - Initialize with account context, loads API keys and model from `IntegrationSettings` -- **`_load_account_settings()`** - Loads OpenAI/Runware API keys and model from `IntegrationSettings` or Django settings -- **`get_api_key(integration_type='openai')`** - Returns API key for integration type -- **`get_model(integration_type='openai')`** - Returns model name for integration type -- **`run_ai_request(prompt, model=None, max_tokens=4000, temperature=0.7, response_format=None, api_key=None, function_name='ai_request', function_id=None, tracker=None)`** - **CENTRAL METHOD** - Handles all OpenAI text generation requests with console logging -- **`extract_json(response_text)`** - Extracts JSON from response text (handles markdown code blocks) -- **`generate_image(prompt, provider='openai', model=None, size='1024x1024', n=1, api_key=None, negative_prompt=None, function_name='generate_image')`** - Generates images via OpenAI DALL-E or Runware -- **`_generate_image_openai(...)`** - Internal method for OpenAI image generation -- **`_generate_image_runware(...)`** - Internal method for Runware image generation -- **`calculate_cost(model, input_tokens, output_tokens, model_type='text')`** - Calculates API cost -- **`call_openai(...)`** - **LEGACY** - Redirects to `run_ai_request()` - -#### `AIEngine` (engine.py) -- **`__init__(celery_task=None, account=None)`** - Initialize with Celery task and account -- **`execute(fn: BaseAIFunction, payload: dict)`** - **CENTRAL ORCHESTRATOR** - Unified execution pipeline: - - Phase 1: INIT (0-10%) - Validation - - Phase 2: PREP (10-25%) - Data loading & prompt building - - Phase 3: AI_CALL (25-70%) - API call to provider - - Phase 4: PARSE (70-85%) - Response parsing - - Phase 5: SAVE (85-98%) - Database operations - - Phase 6: DONE (98-100%) - Finalization -- **`_handle_error(error, fn=None, exc_info=False)`** - Centralized error handling -- **`_log_to_database(fn, payload, parsed, save_result, error=None)`** - Logs to `AITaskLog` model -- **`_calculate_credits_for_clustering(keyword_count, tokens, cost)`** - Calculates credits for clustering operations - -#### `PromptRegistry` (prompts.py) -- **`get_prompt(function_name, account=None, task=None, context=None)`** - Hierarchical prompt resolution: - 1. Task-level `prompt_override` (if exists) - 2. DB prompt for (account, function) - 3. Default fallback from registry -- **`_render_prompt(prompt_template, context)`** - Renders template with `[IGNY8_*]` placeholders and `{variable}` format -- **`get_image_prompt_template(account=None)`** - Gets image prompt template -- **`get_negative_prompt(account=None)`** - Gets negative prompt - -#### `BaseAIFunction` (base.py) - Abstract Interface -- **`get_name()`** - Returns function name (abstract) -- **`get_metadata()`** - Returns function metadata (display name, description, phases) -- **`validate(payload, account=None)`** - Validates input payload (default: checks for 'ids') -- **`get_max_items()`** - Returns max items limit (optional) -- **`prepare(payload, account=None)`** - Loads and prepares data (abstract) -- **`build_prompt(data, account=None)`** - Builds AI prompt (abstract) -- **`get_model(account=None)`** - Returns model override (optional) -- **`parse_response(response, step_tracker=None)`** - Parses AI response (abstract) -- **`save_output(parsed, original_data, account=None, progress_tracker=None, step_tracker=None)`** - Saves results to database (abstract) - -### 2.2 AI Function Implementations - -#### `AutoClusterFunction` (functions/auto_cluster.py) -- **`get_name()`** - Returns `'auto_cluster'` -- **`validate(payload, account=None)`** - Validates keyword IDs exist -- **`prepare(payload, account=None)`** - Loads keywords with relationships -- **`build_prompt(data, account=None)`** - Builds clustering prompt using `PromptRegistry` -- **`parse_response(response, step_tracker=None)`** - Parses JSON cluster data -- **`save_output(parsed, original_data, account=None, ...)`** - Creates/updates clusters and assigns keywords - -#### `GenerateIdeasFunction` (functions/generate_ideas.py) -- **`get_name()`** - Returns `'generate_ideas'` -- **`validate(payload, account=None)`** - Validates cluster IDs exist -- **`prepare(payload, account=None)`** - Loads clusters with keywords -- **`build_prompt(data, account=None)`** - Builds ideas generation prompt -- **`parse_response(response, step_tracker=None)`** - Parses JSON ideas data -- **`save_output(parsed, original_data, account=None, ...)`** - Creates `ContentIdeas` records - -#### `GenerateContentFunction` (functions/generate_content.py) -- **`get_name()`** - Returns `'generate_content'` -- **`validate(payload, account=None)`** - Validates task IDs exist -- **`prepare(payload, account=None)`** - Loads tasks with relationships -- **`build_prompt(data, account=None)`** - Builds content generation prompt -- **`parse_response(response, step_tracker=None)`** - Parses JSON or plain text content -- **`save_output(parsed, original_data, account=None, ...)`** - Saves content to `Content` model - -#### `GenerateImagesFunction` (functions/generate_images.py) -- **`get_name()`** - Returns `'generate_images'` -- **`validate(payload, account=None)`** - Validates task IDs exist -- **`prepare(payload, account=None)`** - Loads tasks and image settings -- **`build_prompt(data, account=None)`** - Extracts image prompts from task content (calls AI) -- **`parse_response(response, step_tracker=None)`** - Returns parsed response (already parsed) -- **`save_output(parsed, original_data, account=None, ...)`** - Creates `Images` records - -### 2.3 Tracking Functions - -#### `StepTracker` (tracker.py) -- **`add_request_step(step_name, status='success', message='', error=None, duration=None)`** - Adds request step -- **`add_response_step(step_name, status='success', message='', error=None, duration=None)`** - Adds response step -- **`get_meta()`** - Returns metadata dict with request/response steps - -#### `ProgressTracker` (tracker.py) -- **`update(phase, percentage, message, current=None, total=None, current_item=None, meta=None)`** - Updates Celery task state -- **`set_phase(phase, percentage, message, meta=None)`** - Sets progress phase -- **`complete(message='Task complete!', meta=None)`** - Marks task as complete -- **`error(error_message, meta=None)`** - Marks task as failed -- **`get_duration()`** - Returns elapsed time in milliseconds - -#### `ConsoleStepTracker` (tracker.py) -- **`init(message='Task started')`** - Logs initialization -- **`prep(message)`** - Logs preparation phase -- **`ai_call(message)`** - Logs AI call phase -- **`parse(message)`** - Logs parsing phase -- **`save(message)`** - Logs save phase -- **`done(message='Execution completed')`** - Logs completion -- **`error(error_type, message, exception=None)`** - Logs error -- **`retry(attempt, max_attempts, reason='')`** - Logs retry -- **`timeout(timeout_seconds)`** - Logs timeout -- **`rate_limit(retry_after)`** - Logs rate limit -- **`malformed_json(details='')`** - Logs JSON parsing error - -#### `CostTracker` (tracker.py) -- **`record(function_name, cost, tokens, model=None)`** - Records API call cost -- **`get_total()`** - Returns total cost -- **`get_total_tokens()`** - Returns total tokens -- **`get_operations()`** - Returns all operations list - -### 2.4 Celery Tasks - -#### `run_ai_task` (ai/tasks.py) -- **`run_ai_task(self, function_name: str, payload: dict, account_id: int = None)`** - **UNIFIED ENTRYPOINT** - Dynamically loads and executes AI functions via `AIEngine` - -#### Legacy Tasks (modules/*/tasks.py) -- **`auto_cluster_keywords_task`** (planner/tasks.py) - **DEPRECATED** - Uses old `AIProcessor` -- **`auto_generate_content_task`** (writer/tasks.py) - Uses `AIProcessor` directly (not via framework) -- **`auto_generate_images_task`** (writer/tasks.py) - Uses `AIProcessor` directly - -### 2.5 Legacy Functions (ai_processor.py) - -#### `AIProcessor` - **LEGACY/DEPRECATED** -- **`_call_openai(prompt, model=None, max_tokens=4000, temperature=0.7, response_format=None, api_key=None, function_id=None, response_steps=None)`** - Internal OpenAI API caller -- **`_extract_json_from_response(response_text)`** - JSON extraction (duplicate of `AICore.extract_json()`) -- **`generate_content(prompt, model=None, max_tokens=4000, temperature=0.7, **kwargs)`** - Generates text content -- **`extract_image_prompts(content, title, max_images=3, account=None)`** - Extracts image prompts from content -- **`check_moderation(text, api_key=None)`** - Checks content moderation -- **`generate_image(prompt, provider='openai', model=None, size='1024x1024', n=1, api_key=None, **kwargs)`** - Generates images -- **`cluster_keywords(keywords, sector_name=None, account=None, response_steps=None, progress_callback=None, tracker=None, **kwargs)`** - **DEPRECATED** - Clusters keywords (old method) -- **`generate_ideas(clusters, account=None, **kwargs)`** - Generates ideas (old method) -- **`get_prompt(prompt_type, account=None)`** - Gets prompt from database (old method) -- **`estimate_cost(operation, tokens_or_prompt, model=None)`** - Estimates cost (not implemented) - ---- - -## 3. Class Inventory - -### 3.1 Core Classes - -| Class | File | Purpose | Inheritance | -|-------|------|---------|-------------| -| `AICore` | ai_core.py | Centralized AI request handler | - | -| `AIEngine` | engine.py | Central orchestrator for AI functions | - | -| `BaseAIFunction` | base.py | Abstract base for all AI functions | ABC | -| `PromptRegistry` | prompts.py | Centralized prompt management | - | -| `StepTracker` | tracker.py | Tracks request/response steps | - | -| `ProgressTracker` | tracker.py | Tracks Celery progress updates | - | -| `CostTracker` | tracker.py | Tracks API costs and tokens | - | -| `ConsoleStepTracker` | tracker.py | Console-based step logging | - | -| `AITaskLog` | models.py | Database model for AI task logging | AccountBaseModel | -| `AIProcessor` | utils/ai_processor.py | **LEGACY** Unified AI interface | - | - -### 3.2 Function Classes - -| Class | File | Purpose | Inheritance | -|-------|------|---------|-------------| -| `AutoClusterFunction` | functions/auto_cluster.py | Keyword clustering | BaseAIFunction | -| `GenerateIdeasFunction` | functions/generate_ideas.py | Idea generation | BaseAIFunction | -| `GenerateContentFunction` | functions/generate_content.py | Content generation | BaseAIFunction | -| `GenerateImagesFunction` | functions/generate_images.py | Image generation | BaseAIFunction | - -### 3.3 Data Classes - -| Class | File | Purpose | -|-------|------|---------| -| `StepLog` | types.py | Single step in request/response tracking | -| `ProgressState` | types.py | Progress state for AI tasks | -| `AITaskResult` | types.py | Result from AI function execution | - ---- - -## 4. Dependency Graph/Table - -### 4.1 Import Relationships - -``` -ai/__init__.py - ├─> registry.py (register_function, get_function, list_functions) - ├─> engine.py (AIEngine) - ├─> base.py (BaseAIFunction) - ├─> ai_core.py (AICore) - ├─> validators.py (all validators) - ├─> constants.py (all constants) - ├─> prompts.py (PromptRegistry, get_prompt) - └─> settings.py (MODEL_CONFIG, get_model_config, etc.) - -ai/tasks.py - ├─> engine.py (AIEngine) - └─> registry.py (get_function_instance) - -ai/engine.py - ├─> base.py (BaseAIFunction) - ├─> tracker.py (StepTracker, ProgressTracker, CostTracker, ConsoleStepTracker) - ├─> ai_core.py (AICore) - └─> settings.py (get_model_config) - -ai/ai_core.py - ├─> constants.py (MODEL_RATES, IMAGE_MODEL_RATES, etc.) - └─> tracker.py (ConsoleStepTracker) - -ai/functions/auto_cluster.py - ├─> base.py (BaseAIFunction) - ├─> ai_core.py (AICore) - ├─> prompts.py (PromptRegistry) - └─> settings.py (get_model_config) - -ai/functions/generate_content.py - ├─> base.py (BaseAIFunction) - ├─> ai_core.py (AICore) - ├─> prompts.py (PromptRegistry) - └─> settings.py (get_model_config) - -ai/functions/generate_ideas.py - ├─> base.py (BaseAIFunction) - ├─> ai_core.py (AICore) - ├─> prompts.py (PromptRegistry) - └─> settings.py (get_model_config) - -ai/functions/generate_images.py - ├─> base.py (BaseAIFunction) - ├─> ai_core.py (AICore) - ├─> prompts.py (PromptRegistry) - └─> settings.py (get_model_config) - -utils/ai_processor.py - ├─> modules/system/models.py (IntegrationSettings) - └─> modules/system/utils.py (get_prompt_value, get_default_prompt) - -modules/planner/tasks.py - └─> utils/ai_processor.py (AIProcessor) [DEPRECATED PATH] - -modules/writer/tasks.py - ├─> utils/ai_processor.py (AIProcessor) [LEGACY PATH] - └─> ai/functions/generate_content.py (generate_content_core) -``` - -### 4.2 External Dependencies - -| Dependency | Used By | Purpose | -|------------|---------|---------| -| `django` | All files | Django ORM, models, settings | -| `celery` | tasks.py, engine.py | Async task execution | -| `requests` | ai_core.py, ai_processor.py | HTTP requests to OpenAI/Runware APIs | -| `json` | Multiple files | JSON parsing | -| `re` | ai_core.py, ai_processor.py, content_normalizer.py | Regex for JSON extraction | -| `logging` | All files | Logging | -| `time` | tracker.py, ai_core.py | Timing and duration tracking | -| `bs4` (BeautifulSoup) | content_normalizer.py | HTML parsing (optional) | - -### 4.3 Database Models Dependencies - -| Model | Used By | Purpose | -|-------|---------|---------| -| `AITaskLog` | engine.py | Unified AI task logging | -| `IntegrationSettings` | ai_core.py, ai_processor.py, settings.py | API keys and model configuration | -| `AIPrompt` | prompts.py | Custom prompt templates | -| `Keywords` | auto_cluster.py, validators.py | Keyword data | -| `Clusters` | auto_cluster.py, generate_ideas.py | Cluster data | -| `ContentIdeas` | generate_ideas.py | Content ideas | -| `Tasks` | generate_content.py, generate_images.py | Writer tasks | -| `Content` | generate_content.py | Generated content | -| `Images` | generate_images.py | Generated images | - ---- - -## 5. System Flow Description - -### 5.1 New Unified Framework Flow (Recommended Path) - -``` -Frontend API Call - ↓ -ViewSet Action (e.g., planner/views.py::auto_cluster) - ↓ -run_ai_task.delay(function_name='auto_cluster', payload={ids: [...]}, account_id=123) - ↓ -Celery Worker: run_ai_task (ai/tasks.py) - ├─> Load Account - ├─> get_function_instance('auto_cluster') → AutoClusterFunction - └─> AIEngine.execute(AutoClusterFunction, payload) - ├─> Phase 1: INIT (0-10%) - │ └─> fn.validate(payload, account) - ├─> Phase 2: PREP (10-25%) - │ ├─> fn.prepare(payload, account) → Load keywords - │ └─> fn.build_prompt(data, account) → PromptRegistry.get_prompt() - ├─> Phase 3: AI_CALL (25-70%) - │ ├─> AICore.run_ai_request(prompt, model, ...) - │ │ ├─> Load API key from IntegrationSettings - │ │ ├─> Validate model - │ │ ├─> Build OpenAI request - │ │ ├─> Send HTTP request - │ │ ├─> Parse response - │ │ └─> Calculate cost - │ └─> Track cost via CostTracker - ├─> Phase 4: PARSE (70-85%) - │ └─> fn.parse_response(response_content, step_tracker) - ├─> Phase 5: SAVE (85-98%) - │ └─> fn.save_output(parsed, original_data, account, ...) - │ └─> Database transaction: Create/update clusters - └─> Phase 6: DONE (98-100%) - ├─> Log to AITaskLog - └─> Return result dict - ↓ -Celery Task State Update (SUCCESS/FAILURE) - ↓ -Frontend Polls Task Status - ↓ -Progress Modal Displays Steps -``` - -### 5.2 Legacy Content Generation Flow (Still Active) - -``` -Frontend API Call - ↓ -ViewSet Action (writer/views.py::auto_generate_content) - ↓ -auto_generate_content_task.delay(task_ids, account_id) - ↓ -Celery Worker: auto_generate_content_task (modules/writer/tasks.py) - ├─> Load Tasks from database - ├─> For each task: - │ ├─> Load prompt template (get_prompt_value) - │ ├─> Format prompt with task data - │ ├─> AIProcessor.generate_content(prompt) - │ │ └─> AIProcessor._call_openai() [DUPLICATE OF AICore.run_ai_request] - │ ├─> Parse response (GenerateContentFunction.parse_response) - │ └─> Save content (GenerateContentFunction.save_output) - └─> Return result -``` - -### 5.3 Legacy Clustering Flow (Deprecated) - -``` -Frontend API Call - ↓ -ViewSet Action (planner/views.py::auto_cluster) [OLD PATH] - ↓ -_auto_cluster_keywords_core() (modules/planner/tasks.py) - ├─> Load keywords - ├─> AIProcessor.cluster_keywords() [DEPRECATED] - │ └─> AIProcessor._call_openai() [DUPLICATE] - ├─> Parse clusters - └─> Save to database -``` - -### 5.4 Image Generation Flow - -``` -Frontend API Call - ↓ -ViewSet Action (writer/views.py::auto_generate_images) - ↓ -auto_generate_images_task.delay(task_ids, account_id) - ↓ -Celery Worker: auto_generate_images_task (modules/writer/tasks.py) - ├─> Load tasks - ├─> For each task: - │ ├─> Extract image prompts (AIProcessor.extract_image_prompts) - │ │ └─> Calls AI to extract prompts from content - │ ├─> Generate featured image (AIProcessor.generate_image) - │ ├─> Generate desktop images (if enabled) - │ └─> Generate mobile images (if enabled) - └─> Save Images records -``` - ---- - -## 6. Integration Points - -### 6.1 Celery Integration - -**Task Registration:** -- `celery.py` uses `app.autodiscover_tasks()` to auto-discover tasks from all Django apps -- Tasks are registered via `@shared_task` decorator - -**Registered Tasks:** -1. `ai.tasks.run_ai_task` - Unified entrypoint (NEW) -2. `planner.tasks.auto_cluster_keywords_task` - **DEPRECATED** -3. `writer.tasks.auto_generate_content_task` - Legacy (still active) -4. `writer.tasks.auto_generate_images_task` - Legacy (still active) - -**Task State Management:** -- `ProgressTracker.update()` calls `task.update_state(state='PROGRESS', meta={...})` -- Progress metadata includes: `phase`, `percentage`, `message`, `request_steps`, `response_steps` -- Final states: `SUCCESS`, `FAILURE` - -### 6.2 Database Integration - -**Models:** -- `AITaskLog` - Unified logging table (used by `AIEngine._log_to_database()`) -- `IntegrationSettings` - API keys and model configuration (used by `AICore._load_account_settings()`) -- `AIPrompt` - Custom prompt templates (used by `PromptRegistry.get_prompt()`) - -**Data Models:** -- `Keywords` - Input for clustering -- `Clusters` - Output of clustering -- `ContentIdeas` - Output of idea generation -- `Tasks` - Input for content/image generation -- `Content` - Output of content generation -- `Images` - Output of image generation - -### 6.3 Frontend API Integration - -**Endpoints:** -1. `POST /v1/planner/keywords/auto_cluster/` → `planner.views.ClusterViewSet.auto_cluster()` - - Calls `run_ai_task.delay(function_name='auto_cluster', ...)` - -2. `POST /v1/planner/clusters/auto_generate_ideas/` → `planner.views.ClusterViewSet.auto_generate_ideas()` - - Calls `run_ai_task.delay(function_name='auto_generate_ideas', ...)` - -3. `POST /v1/writer/tasks/auto_generate_content/` → `writer.views.TasksViewSet.auto_generate_content()` - - Calls `auto_generate_content_task.delay(...)` [LEGACY PATH] - -4. `POST /v1/writer/tasks/auto_generate_images/` → `writer.views.TasksViewSet.auto_generate_images()` - - Calls `auto_generate_images_task.delay(...)` [LEGACY PATH] - -**Response Format:** -- Success: `{success: true, task_id: "...", message: "..."}` -- Error: `{success: false, error: "..."}` - -**Progress Tracking:** -- Frontend polls Celery task status via `GET /v1/system/tasks/{task_id}/status/` -- Progress modal displays `request_steps` and `response_steps` from task meta - -### 6.4 Configuration Integration - -**Settings Loading Hierarchy:** -1. **Account-level** (`IntegrationSettings` model): - - API keys: `IntegrationSettings.config['apiKey']` - - Model: `IntegrationSettings.config['model']` - - Image settings: `IntegrationSettings.config` (for image_generation type) - -2. **Django Settings** (fallback): - - `OPENAI_API_KEY` - - `RUNWARE_API_KEY` - - `DEFAULT_AI_MODEL` - -3. **Function-level** (`ai/settings.py`): - - `MODEL_CONFIG[function_name]` - Default model, max_tokens, temperature per function - -**Prompt Loading Hierarchy:** -1. Task-level override: `task.prompt_override` (if exists) -2. Account-level: `AIPrompt` model (account, prompt_type) -3. Default: `PromptRegistry.DEFAULT_PROMPTS[prompt_type]` - -### 6.5 Debug Panel Integration - -**Console Logging:** -- `ConsoleStepTracker` logs to stdout/stderr (only if `DEBUG_MODE=True`) -- Logs include timestamps, phases, messages, errors - -**Step Tracking:** -- `StepTracker` maintains `request_steps` and `response_steps` arrays -- Steps include: `stepNumber`, `stepName`, `status`, `message`, `duration`, `error` -- Steps are included in Celery task meta and displayed in progress modal - -**Database Logging:** -- `AITaskLog` records all AI task executions -- Fields: `task_id`, `function_name`, `phase`, `status`, `cost`, `tokens`, `request_steps`, `response_steps`, `error`, `payload`, `result` - ---- - -## 7. Identified Redundancies or Repetition - -### 7.1 Duplicate Constants - -**Location 1:** `ai/constants.py` -- `MODEL_RATES`, `IMAGE_MODEL_RATES`, `VALID_OPENAI_IMAGE_MODELS`, `VALID_SIZES_BY_MODEL`, `DEFAULT_AI_MODEL`, `JSON_MODE_MODELS` - -**Location 2:** `utils/ai_processor.py` (lines 18-44) -- **EXACT DUPLICATE** of all constants from `constants.py` - -**Impact:** Constants are defined in two places, risking inconsistency. - -### 7.2 Duplicate JSON Extraction Logic - -**Location 1:** `ai/ai_core.py::extract_json()` (lines 391-429) -- Handles markdown code blocks, multiline JSON, direct JSON - -**Location 2:** `utils/ai_processor.py::_extract_json_from_response()` (lines 342-449) -- **MORE COMPREHENSIVE** - handles more edge cases, balanced brace matching - -**Impact:** Two implementations with different capabilities. `ai_processor.py` version is more robust. - -### 7.3 Duplicate OpenAI API Calling Logic - -**Location 1:** `ai/ai_core.py::run_ai_request()` (lines 106-389) -- Centralized method with console logging via `ConsoleStepTracker` -- Handles validation, model selection, cost calculation -- Returns standardized dict format - -**Location 2:** `utils/ai_processor.py::_call_openai()` (lines 125-340) -- **SIMILAR LOGIC** but with `response_steps` parameter instead of `tracker` -- Less comprehensive error handling -- Different return format - -**Impact:** Two code paths for the same operation. New code should use `AICore.run_ai_request()`. - -### 7.4 Duplicate Image Generation Logic - -**Location 1:** `ai/ai_core.py::generate_image()` + `_generate_image_openai()` + `_generate_image_runware()` (lines 431-728) -- Uses `print()` statements for logging (inconsistent with console tracker) - -**Location 2:** `utils/ai_processor.py::generate_image()` (lines 667-1043) -- **MORE COMPREHENSIVE** - extensive logging, better error handling -- Handles Runware authentication flow - -**Impact:** Two implementations. `ai_processor.py` version has better logging. - -### 7.5 Duplicate Prompt Loading Logic - -**Location 1:** `ai/prompts.py::PromptRegistry.get_prompt()` (lines 280-333) -- Hierarchical resolution: task override → DB prompt → default -- Supports `[IGNY8_*]` placeholders and `{variable}` format - -**Location 2:** `utils/ai_processor.py::get_prompt()` (lines 1044-1057) -- Simple database lookup via `modules/system/utils.get_prompt_value()` -- No hierarchical resolution - -**Location 3:** Direct calls in `modules/writer/tasks.py` (lines 343, 959, 964) -- Uses `get_prompt_value()` and `get_default_prompt()` directly - -**Impact:** Three different ways to load prompts. New code should use `PromptRegistry`. - -### 7.6 Duplicate Model Configuration Logic - -**Location 1:** `ai/settings.py::get_model_config()` (lines 49-97) -- Reads from `IntegrationSettings` if account provided -- Falls back to `MODEL_CONFIG` defaults - -**Location 2:** `ai/ai_core.py::_load_account_settings()` (lines 46-90) -- Reads model from `IntegrationSettings` directly -- Similar logic but embedded in `AICore.__init__()` - -**Location 3:** `utils/ai_processor.py::_get_model()` (lines 98-123) -- Reads model from `IntegrationSettings` directly -- Similar logic but embedded in `AIProcessor.__init__()` - -**Impact:** Model loading logic duplicated in three places. - -### 7.7 Duplicate API Key Loading Logic - -**Location 1:** `ai/ai_core.py::_load_account_settings()` (lines 46-90) -- Loads OpenAI and Runware keys from `IntegrationSettings` - -**Location 2:** `utils/ai_processor.py::_get_api_key()` (lines 73-96) -- **EXACT SAME LOGIC** for loading API keys - -**Impact:** Identical code in two places. - -### 7.8 Repeated Error Handling Patterns - -**Pattern:** Multiple files have similar try/except blocks for: -- API request errors -- JSON parsing errors -- Database errors -- Validation errors - -**Impact:** Error handling is not centralized, making it harder to maintain consistent error messages and logging. - -### 7.9 Repeated Progress Update Patterns - -**Pattern:** Multiple places manually build progress update dicts: -- `modules/writer/tasks.py` (lines 62-73, 220-231, etc.) -- `modules/planner/tasks.py` (lines 59-71, 203-215, etc.) -- `ai/engine.py` (lines 57, 79, 141, etc.) - -**Impact:** Progress update format is not standardized, though `ProgressTracker` exists to handle this. - ---- - -## 8. Summary of Potential Consolidation Areas - -### 8.1 Constants Consolidation - -**Observation:** Model rates, valid models, and configuration constants are duplicated between `ai/constants.py` and `utils/ai_processor.py`. - -**Potential Action:** Remove constants from `ai_processor.py` and import from `constants.py`. However, `ai_processor.py` is marked as legacy, so this may not be necessary if it's being phased out. - -### 8.2 JSON Extraction Consolidation - -**Observation:** Two JSON extraction methods exist with different capabilities. `ai_processor.py::_extract_json_from_response()` is more comprehensive. - -**Potential Action:** Enhance `AICore.extract_json()` with logic from `ai_processor.py`, or create a shared utility function. - -### 8.3 API Request Consolidation - -**Observation:** `AICore.run_ai_request()` and `AIProcessor._call_openai()` perform the same operation with different interfaces. - -**Potential Action:** All new code should use `AICore.run_ai_request()`. Legacy code in `ai_processor.py` can remain for backward compatibility but should be marked as deprecated. - -### 8.4 Image Generation Consolidation - -**Observation:** Two image generation implementations exist. `ai_processor.py` version has better logging. - -**Potential Action:** Enhance `AICore.generate_image()` with logging improvements from `ai_processor.py`, or migrate all code to use `AICore.generate_image()`. - -### 8.5 Prompt Loading Consolidation - -**Observation:** Three different methods exist for loading prompts: `PromptRegistry.get_prompt()`, `AIProcessor.get_prompt()`, and direct `get_prompt_value()` calls. - -**Potential Action:** Migrate all code to use `PromptRegistry.get_prompt()` for consistency. Update legacy code paths gradually. - -### 8.6 Model/API Key Loading Consolidation - -**Observation:** Model and API key loading logic is duplicated in `AICore`, `AIProcessor`, and `settings.py`. - -**Potential Action:** Create shared utility functions for loading settings from `IntegrationSettings`, used by all classes. - -### 8.7 Error Handling Consolidation - -**Observation:** Error handling patterns are repeated across multiple files. - -**Potential Action:** Create centralized error handling utilities or enhance `AIEngine._handle_error()` to be more reusable. - -### 8.8 Progress Tracking Consolidation - -**Observation:** Some code manually builds progress update dicts instead of using `ProgressTracker`. - -**Potential Action:** Migrate all progress updates to use `ProgressTracker.update()` for consistency. - -### 8.9 Legacy Code Path Elimination - -**Observation:** Multiple execution paths exist: -- New: `run_ai_task` → `AIEngine` → `BaseAIFunction` implementations -- Legacy: Direct `AIProcessor` calls in `modules/*/tasks.py` - -**Potential Action:** Gradually migrate all legacy tasks to use the new framework. Mark legacy code as deprecated. - ---- - -## 9. Assumptions Made - -1. **File `wordpress.py`** was not read - assumed to be unrelated to AI processing based on name. - -2. **Frontend code** was partially analyzed via search results - full frontend audit not performed. - -3. **Database migrations** were not analyzed - assumed to be standard Django migrations. - -4. **Test files** were not analyzed - `ai/tests/test_run.py` exists but was not read. - -5. **Settings file** (`backend/igny8_core/settings.py`) was not read - assumed to contain standard Django settings. - -6. **System module utilities** (`modules/system/utils.py`) were referenced but not fully read - assumed to contain `get_prompt_value()` and `get_default_prompt()` functions. - ---- - -## 10. Appendix - -### 10.1 File Line Counts - -| Directory | Files | Total Lines | -|-----------|-------|-------------| -| `ai/` | 15 | ~2,500 | -| `ai/functions/` | 5 | ~1,300 | -| `utils/` (AI-related) | 3 | ~1,800 | -| `modules/planner/tasks.py` | 1 | 736 | -| `modules/writer/tasks.py` | 1 | 1,156 | -| **Total** | **25** | **~7,500** | - -### 10.2 Function Count Summary - -- **Core Framework Functions:** ~30 -- **AI Function Implementations:** 4 classes × ~6 methods = ~24 methods -- **Tracking Functions:** ~20 -- **Legacy Functions:** ~15 -- **Celery Tasks:** 4 -- **Total:** ~90 functions/methods - -### 10.3 Key Design Patterns - -1. **Template Method Pattern:** `BaseAIFunction` defines algorithm skeleton, subclasses implement steps -2. **Registry Pattern:** `FunctionRegistry` for dynamic function discovery -3. **Factory Pattern:** `get_function_instance()` creates function instances -4. **Strategy Pattern:** Different AI functions implement same interface -5. **Observer Pattern:** `ProgressTracker` updates Celery task state -6. **Facade Pattern:** `AICore` provides simplified interface to OpenAI/Runware APIs - ---- - -**End of Report** - diff --git a/IGNY8_AI_UNIFICATION_MIGRATION_PLAN.md b/IGNY8_AI_UNIFICATION_MIGRATION_PLAN.md deleted file mode 100644 index e0fa6146..00000000 --- a/IGNY8_AI_UNIFICATION_MIGRATION_PLAN.md +++ /dev/null @@ -1,649 +0,0 @@ -# IGNY8 AI System Unification — Complete Migration Plan - -**Date:** 2024-12-19 -**Goal:** Unify all AI functions into single structure, remove all redundancy, implement checklist-style progress UI -**Estimated Time:** 5 stages, ~2-3 days total - ---- - -## Overview - -This migration plan unifies the IGNY8 AI system by: -1. Standardizing backend progress messages with input data -2. Implementing checklist-style progress UI with 3 states (pending/in-progress/completed) -3. Migrating all views to use unified `run_ai_task` entrypoint -4. Removing duplicate code and deprecated files -5. Final cleanup and verification - ---- - -## Stage 1: Backend — Standardize Progress Messages with Input Data - -**Goal:** Update `AIEngine` to send user-friendly messages with actual input data -**Files to Modify:** `backend/igny8_core/ai/engine.py` -**Estimated Time:** 1-2 hours - -### Step 1.1: Add Helper Methods to AIEngine - -**File:** `backend/igny8_core/ai/engine.py` - -Add these helper methods to the `AIEngine` class (after `__init__` method): - -```python -def _get_input_description(self, function_name: str, payload: dict, count: int) -> str: - """Get user-friendly input description""" - if function_name == 'auto_cluster': - return f"{count} keyword{'s' if count != 1 else ''}" - elif function_name == 'generate_ideas': - return f"{count} cluster{'s' if count != 1 else ''}" - elif function_name == 'generate_content': - return f"{count} task{'s' if count != 1 else ''}" - elif function_name == 'generate_images': - return f"{count} task{'s' if count != 1 else ''}" - return f"{count} item{'s' if count != 1 else ''}" - -def _get_prep_message(self, function_name: str, count: int, data: Any) -> str: - """Get user-friendly prep message""" - if function_name == 'auto_cluster': - return f"Loading {count} keyword{'s' if count != 1 else ''}" - elif function_name == 'generate_ideas': - return f"Loading {count} cluster{'s' if count != 1 else ''}" - elif function_name == 'generate_content': - return f"Preparing {count} content idea{'s' if count != 1 else ''}" - elif function_name == 'generate_images': - return f"Extracting image prompts from {count} task{'s' if count != 1 else ''}" - return f"Preparing {count} item{'s' if count != 1 else ''}" - -def _get_ai_call_message(self, function_name: str, count: int) -> str: - """Get user-friendly AI call message""" - if function_name == 'auto_cluster': - return f"Grouping {count} keyword{'s' if count != 1 else ''} into clusters" - elif function_name == 'generate_ideas': - return f"Generating content ideas for {count} cluster{'s' if count != 1 else ''}" - elif function_name == 'generate_content': - return f"Writing article{'s' if count != 1 else ''} with AI" - elif function_name == 'generate_images': - return f"Creating image{'s' if count != 1 else ''} with AI" - return f"Processing with AI" - -def _get_parse_message(self, function_name: str) -> str: - """Get user-friendly parse message""" - if function_name == 'auto_cluster': - return "Organizing clusters" - elif function_name == 'generate_ideas': - return "Structuring outlines" - elif function_name == 'generate_content': - return "Formatting content" - elif function_name == 'generate_images': - return "Processing images" - return "Processing results" - -def _get_save_message(self, function_name: str, count: int) -> str: - """Get user-friendly save message""" - if function_name == 'auto_cluster': - return f"Saving {count} cluster{'s' if count != 1 else ''}" - elif function_name == 'generate_ideas': - return f"Saving {count} idea{'s' if count != 1 else ''}" - elif function_name == 'generate_content': - return f"Saving {count} article{'s' if count != 1 else ''}" - elif function_name == 'generate_images': - return f"Saving {count} image{'s' if count != 1 else ''}" - return f"Saving {count} item{'s' if count != 1 else ''}" -``` - -### Step 1.2: Update execute() Method to Use Helper Methods - -**File:** `backend/igny8_core/ai/engine.py` - -In the `execute()` method, replace step tracking messages: - -**Replace lines 48-57 (INIT phase):** -```python -# OLD: -self.console_tracker.prep("Validating input payload") -validated = fn.validate(payload, self.account) -if not validated['valid']: - self.console_tracker.error('ValidationError', validated['error']) - return self._handle_error(validated['error'], fn) - -self.console_tracker.prep("Validation complete") -self.step_tracker.add_request_step("INIT", "success", "Validation complete") -self.tracker.update("INIT", 10, "Validation complete", meta=self.step_tracker.get_meta()) - -# NEW: -# Extract input data for user-friendly messages -ids = payload.get('ids', []) -input_count = len(ids) if ids else 0 -input_description = self._get_input_description(function_name, payload, input_count) - -self.console_tracker.prep(f"Validating {input_description}") -validated = fn.validate(payload, self.account) -if not validated['valid']: - self.console_tracker.error('ValidationError', validated['error']) - return self._handle_error(validated['error'], fn) - -validation_message = f"Validating {input_description}" -self.console_tracker.prep("Validation complete") -self.step_tracker.add_request_step("INIT", "success", validation_message) -self.tracker.update("INIT", 10, validation_message, meta=self.step_tracker.get_meta()) -``` - -**Replace lines 59-79 (PREP phase):** -```python -# OLD: -self.console_tracker.prep("Loading data from database") -data = fn.prepare(payload, self.account) -# ... existing data_count logic ... -self.console_tracker.prep(f"Building prompt from {data_count} items") -prompt = fn.build_prompt(data, self.account) -self.console_tracker.prep(f"Prompt built: {len(prompt)} characters") -self.step_tracker.add_request_step("PREP", "success", f"Loaded {data_count} items, built prompt ({len(prompt)} chars)") -self.tracker.update("PREP", 25, f"Data prepared: {data_count} items", meta=self.step_tracker.get_meta()) - -# NEW: -prep_message = self._get_prep_message(function_name, input_count, payload) -self.console_tracker.prep(prep_message) -data = fn.prepare(payload, self.account) -# ... existing data_count logic ... -prompt = fn.build_prompt(data, self.account) -self.console_tracker.prep(f"Prompt built: {len(prompt)} characters") -self.step_tracker.add_request_step("PREP", "success", prep_message) -self.tracker.update("PREP", 25, prep_message, meta=self.step_tracker.get_meta()) -``` - -**Replace lines 136-141 (AI_CALL phase):** -```python -# OLD: -self.step_tracker.add_response_step( - "AI_CALL", - "success", - f"Calling {model or 'default'} model..." -) -self.tracker.update("AI_CALL", 30, f"Sending to {model or 'default'}...", meta=self.step_tracker.get_meta()) - -# NEW: -ai_call_message = self._get_ai_call_message(function_name, data_count) -self.step_tracker.add_response_step("AI_CALL", "success", ai_call_message) -self.tracker.update("AI_CALL", 50, ai_call_message, meta=self.step_tracker.get_meta()) -``` - -**Find PARSE phase (around line 200-210) and replace:** -```python -# OLD: -self.step_tracker.add_response_step("PARSE", "success", "Parsing response...") -self.tracker.update("PARSE", 70, "Parsing response...", meta=self.step_tracker.get_meta()) - -# NEW: -parse_message = self._get_parse_message(function_name) -self.step_tracker.add_response_step("PARSE", "success", parse_message) -self.tracker.update("PARSE", 70, parse_message, meta=self.step_tracker.get_meta()) -``` - -**Find SAVE phase (around line 250-260) and replace:** -```python -# OLD: -self.step_tracker.add_response_step("SAVE", "success", "Saving results...") -self.tracker.update("SAVE", 85, "Saving results...", meta=self.step_tracker.get_meta()) - -# NEW: -save_message = self._get_save_message(function_name, data_count) -self.step_tracker.add_response_step("SAVE", "success", save_message) -self.tracker.update("SAVE", 85, save_message, meta=self.step_tracker.get_meta()) -``` - -### Step 1.3: Remove Technical Debug Messages - -**File:** `backend/igny8_core/ai/engine.py` - -Remove or comment out lines 115-124 (model configuration tracking in step tracker): -```python -# REMOVE these lines (keep console logging, but not step tracker): -# self.step_tracker.add_request_step( -# "PREP", -# "success", -# f"AI model in settings: {model_from_integration or 'Not set'}" -# ) -# self.step_tracker.add_request_step( -# "PREP", -# "success", -# f"AI model selected for request: {model or 'default'}" -# ) -``` - -### Verification Checklist for Stage 1: -- [ ] Helper methods added to `AIEngine` class -- [ ] All phase messages updated to use helper methods -- [ ] Messages include actual input counts (e.g., "Validating 5 keywords") -- [ ] No technical terms like "database" or "parsing" in user-facing messages -- [ ] Test: Run `auto_cluster` and verify messages in step logs - ---- - -## Stage 2: Frontend — Implement Checklist-Style Progress Modal - -**Goal:** Replace progress bar with checklist UI showing 3 states (pending/in-progress/completed) -**Files to Modify:** `frontend/src/components/common/ProgressModal.tsx` -**Files to Create:** None -**Estimated Time:** 2-3 hours - -### Step 2.1: Replace ProgressModal Component - -**File:** `frontend/src/components/common/ProgressModal.tsx` - -Replace the entire file with the new checklist-style implementation (see previous response for full code). - -Key changes: -- Remove progress bar component -- Add checklist-style step display -- Add 3-state logic (pending/in-progress/completed) -- Add success alert in same modal when completed -- Use step logs to determine current phase - -### Step 2.2: Update useProgressModal Hook (Simplify) - -**File:** `frontend/src/hooks/useProgressModal.ts` - -Remove all `aiRequestLogsStore` references: -- Remove lines 501-642 (all store-related code) -- Keep only polling logic and state management -- Simplify step mapping logic - -### Verification Checklist for Stage 2: -- [ ] ProgressModal shows checklist instead of progress bar -- [ ] Steps show as pending (gray/disabled) initially -- [ ] Steps show as in-progress (blue/spinner) when active -- [ ] Steps show as completed (green/checkmark) when done -- [ ] Success alert appears in same modal when completed -- [ ] No errors in browser console -- [ ] Test: Run `auto_cluster` and verify checklist UI - ---- - -## Stage 3: Migrate Views to Unified Entrypoint - -**Goal:** Update all views to use `run_ai_task` instead of legacy task functions -**Files to Modify:** -- `backend/igny8_core/modules/writer/views.py` -- `backend/igny8_core/modules/planner/views.py` (verify already migrated) - -**Estimated Time:** 2-3 hours - -### Step 3.1: Migrate auto_generate_content View - -**File:** `backend/igny8_core/modules/writer/views.py` - -**Replace lines 180-228** (the entire try/except block for Celery task): - -```python -# OLD: -from .tasks import auto_generate_content_task -if hasattr(auto_generate_content_task, 'delay'): - task = auto_generate_content_task.delay(ids, account_id=account_id) - # ... rest of old code - -# NEW: -from igny8_core.ai.tasks import run_ai_task -from kombu.exceptions import OperationalError as KombuOperationalError - -try: - if hasattr(run_ai_task, 'delay'): - task = run_ai_task.delay( - function_name='generate_content', - payload={'ids': ids}, - account_id=account_id - ) - logger.info(f"Task queued: {task.id}") - return Response({ - 'success': True, - 'task_id': str(task.id), - 'message': 'Content generation started' - }, status=status.HTTP_200_OK) - else: - # Celery not available - execute synchronously - logger.info("auto_generate_content: Executing synchronously (Celery not available)") - result = run_ai_task( - function_name='generate_content', - payload={'ids': ids}, - account_id=account_id - ) - if result.get('success'): - return Response({ - 'success': True, - 'tasks_updated': result.get('count', 0), - 'message': 'Content generated successfully' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'error': result.get('error', 'Content generation failed'), - 'type': 'TaskExecutionError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -except KombuOperationalError as e: - logger.error(f"Celery connection error: {str(e)}") - return Response({ - 'error': 'Task queue unavailable. Please try again.', - 'type': 'QueueError' - }, status=status.HTTP_503_SERVICE_UNAVAILABLE) -except Exception as e: - logger.error(f"Error queuing content generation task: {str(e)}", exc_info=True) - return Response({ - 'error': f'Failed to start content generation: {str(e)}', - 'type': 'TaskError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -``` - -### Step 3.2: Migrate auto_generate_images View - -**File:** `backend/igny8_core/modules/writer/views.py` - -**Replace lines 358-377** (the entire try/except block): - -```python -# OLD: -from .tasks import auto_generate_images_task -if hasattr(auto_generate_images_task, 'delay'): - task = auto_generate_images_task.delay(task_ids, account_id=account_id) - # ... rest of old code - -# NEW: -from igny8_core.ai.tasks import run_ai_task -from kombu.exceptions import OperationalError as KombuOperationalError - -try: - if hasattr(run_ai_task, 'delay'): - task = run_ai_task.delay( - function_name='generate_images', - payload={'ids': task_ids}, - account_id=account_id - ) - return Response({ - 'success': True, - 'task_id': str(task.id), - 'message': 'Image generation started' - }, status=status.HTTP_200_OK) - else: - # Celery not available - execute synchronously - result = run_ai_task( - function_name='generate_images', - payload={'ids': task_ids}, - account_id=account_id - ) - if result.get('success'): - return Response({ - 'success': True, - 'images_created': result.get('count', 0), - 'message': result.get('message', 'Image generation completed') - }, status=status.HTTP_200_OK) - else: - return Response({ - 'error': result.get('error', 'Image generation failed'), - 'type': 'TaskExecutionError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -except KombuOperationalError as e: - logger.error(f"Celery connection error: {str(e)}") - return Response({ - 'error': 'Task queue unavailable. Please try again.', - 'type': 'QueueError' - }, status=status.HTTP_503_SERVICE_UNAVAILABLE) -except Exception as e: - logger.error(f"Error queuing image generation task: {str(e)}", exc_info=True) - return Response({ - 'error': f'Failed to start image generation: {str(e)}', - 'type': 'TaskError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -``` - -### Step 3.3: Verify Planner Views Already Migrated - -**File:** `backend/igny8_core/modules/planner/views.py` - -Verify that `auto_cluster` and `auto_generate_ideas` already use `run_ai_task`: -- [ ] `auto_cluster` uses `run_ai_task` (line 480) -- [ ] `auto_generate_ideas` uses `run_ai_task` (line 756) - -### Verification Checklist for Stage 3: -- [ ] `auto_generate_content` uses `run_ai_task` -- [ ] `auto_generate_images` uses `run_ai_task` -- [ ] All views return consistent response format -- [ ] Error handling is consistent -- [ ] Test: Generate content and verify it works -- [ ] Test: Generate images and verify it works - ---- - -## Stage 4: Remove Duplicate Code and Deprecated Files - -**Goal:** Clean up duplicate constants, remove deprecated files, simplify code -**Files to Delete:** 6 files -**Files to Modify:** 5 files -**Estimated Time:** 2-3 hours - -### Step 4.1: Remove Duplicate Constants - -**File:** `backend/igny8_core/utils/ai_processor.py` - -**Replace lines 18-44** (duplicate constants): - -```python -# OLD: -MODEL_RATES = { ... } -IMAGE_MODEL_RATES = { ... } -VALID_OPENAI_IMAGE_MODELS = { ... } -VALID_SIZES_BY_MODEL = { ... } - -# NEW: -from igny8_core.ai.constants import ( - MODEL_RATES, - IMAGE_MODEL_RATES, - VALID_OPENAI_IMAGE_MODELS, - VALID_SIZES_BY_MODEL, - DEFAULT_AI_MODEL, - JSON_MODE_MODELS, -) -``` - -### Step 4.2: Remove response_steps Parameter - -**File:** `backend/igny8_core/utils/ai_processor.py` - -Remove `response_steps` parameter from all methods: -- Find all method signatures with `response_steps=None` (lines 1064, 1135, etc.) -- Remove the parameter -- Remove all `response_steps.append()` calls (lines 1135-1299) -- Keep the file (still used by legacy code temporarily) - -### Step 4.3: Simplify task_progress Endpoint - -**File:** `backend/igny8_core/modules/system/integration_views.py` - -**Simplify `task_progress()` method** (lines 734-1163): - -Replace complex extraction logic with simple meta retrieval: -```python -# In task_progress method, replace lines 784-1100 with: -meta = {} -request_steps = [] -response_steps = [] - -try: - if hasattr(task, 'info') and task.info: - if isinstance(task.info, dict): - meta = task.info.get('meta', {}) - if isinstance(meta, dict): - request_steps = meta.get('request_steps', []) - response_steps = meta.get('response_steps', []) -except Exception as e: - logger.debug(f"Error extracting meta: {str(e)}") - -# Use request_steps and response_steps in response -``` - -### Step 4.4: Remove Deprecated Store References - -**File:** `frontend/src/services/api.ts` - -Remove all `aiRequestLogsStore` imports and references: -- Remove line 4: `import { useAIRequestLogsStore } from '../store/aiRequestLogsStore';` -- Remove lines 579, 601, 671-672, 730-731, 812-813, 1185-1186, 1265-1266 (all store references) - -**File:** `frontend/src/hooks/useProgressModal.ts` - -Remove all `aiRequestLogsStore` references (lines 501-642) - -**File:** `frontend/src/templates/TablePageTemplate.tsx` - -Remove commented import (line 44) - -### Step 4.5: Delete Deprecated Files - -**Delete these files:** - -1. `backend/igny8_core/modules/planner/tasks.py` - - Already deprecated, no longer used - -2. `backend/igny8_core/modules/writer/tasks.py` - - No longer used after Stage 3 migration - -3. `backend/igny8_core/ai/processor.py` - - Deprecated wrapper, redirects to AICore - -4. `frontend/src/store/aiRequestLogsStore.ts` - - Deprecated debug store - -5. `frontend/src/components/debug/ResourceDebugOverlay.tsx` - - Optional: Delete if not needed - -6. `frontend/src/components/debug/ResourceDebugToggle.tsx` - - Optional: Delete if not needed (or keep if still used) - -### Verification Checklist for Stage 4: -- [ ] Duplicate constants removed from `ai_processor.py` -- [ ] `response_steps` parameter removed from all methods -- [ ] `task_progress` endpoint simplified -- [ ] All deprecated store references removed -- [ ] Deprecated files deleted -- [ ] No import errors after deletions -- [ ] Test: Verify all AI functions still work - ---- - -## Stage 5: Final Cleanup and Verification - -**Goal:** Final testing, documentation, and cleanup -**Estimated Time:** 1-2 hours - -### Step 5.1: Remove Debug Overlay from Layout (if deleted) - -**File:** `frontend/src/layout/AppLayout.tsx` - -If you deleted debug components, remove: -- Line 12: `import ResourceDebugOverlay from "../components/debug/ResourceDebugOverlay";` -- Lines 166-180: Debug toggle listener -- Lines 197-198: `` - -### Step 5.2: Update Function Metadata - -**File:** `backend/igny8_core/ai/functions/auto_cluster.py` - -Verify `get_metadata()` returns correct phase messages (should already be correct) - -**File:** `backend/igny8_core/ai/functions/generate_ideas.py` - -Verify `get_metadata()` returns correct phase messages - -**File:** `backend/igny8_core/ai/functions/generate_content.py` - -Verify `get_metadata()` returns correct phase messages - -**File:** `backend/igny8_core/ai/functions/generate_images.py` - -Verify `get_metadata()` returns correct phase messages - -### Step 5.3: Comprehensive Testing - -Test each AI function end-to-end: - -**Test 1: Keyword Clustering** -- [ ] Select 5-10 keywords -- [ ] Click "Auto Cluster" -- [ ] Verify checklist shows: "Validating 5 keywords" → "Loading 5 keywords" → etc. -- [ ] Verify success message: "Clustering complete — keywords grouped into meaningful clusters." -- [ ] Verify clusters created in database - -**Test 2: Idea Generation** -- [ ] Select 1-2 clusters -- [ ] Click "Generate Ideas" -- [ ] Verify checklist shows correct steps -- [ ] Verify success message: "Content ideas and outlines created successfully." -- [ ] Verify ideas created in database - -**Test 3: Content Generation** -- [ ] Select 1-2 tasks -- [ ] Click "Generate Content" -- [ ] Verify checklist shows correct steps -- [ ] Verify success message: "Article drafted successfully." -- [ ] Verify content saved to tasks - -**Test 4: Image Generation** -- [ ] Select 1-2 tasks with content -- [ ] Click "Generate Images" -- [ ] Verify checklist shows correct steps -- [ ] Verify success message: "Images created and saved successfully." -- [ ] Verify images created in database - -### Step 5.4: Code Review Checklist - -- [ ] All AI functions use `run_ai_task` entrypoint -- [ ] All progress messages include input data -- [ ] No duplicate constants -- [ ] No deprecated code references -- [ ] Frontend shows checklist UI correctly -- [ ] Success messages appear in modal -- [ ] No console errors -- [ ] No TypeScript errors -- [ ] No Python linting errors - -### Step 5.5: Documentation Update - -Update any relevant documentation: -- [ ] Update API documentation if needed -- [ ] Update developer guide if needed -- [ ] Mark this migration as complete - ---- - -## Rollback Plan - -If issues occur during migration: - -1. **Stage 1-2 Issues:** Revert `engine.py` and `ProgressModal.tsx` changes -2. **Stage 3 Issues:** Revert view changes, keep using legacy tasks temporarily -3. **Stage 4 Issues:** Restore deleted files from git history -4. **Stage 5 Issues:** Fix specific issues without rolling back - ---- - -## Success Criteria - -Migration is complete when: -- ✅ All AI functions use unified `run_ai_task` entrypoint -- ✅ All progress messages are user-friendly with input data -- ✅ Frontend shows checklist-style progress UI -- ✅ Success messages appear in modal -- ✅ No duplicate code remains -- ✅ All deprecated files deleted -- ✅ All tests pass -- ✅ No console/terminal errors - ---- - -## Notes - -- **Backward Compatibility:** Legacy code in `utils/ai_processor.py` is kept temporarily for any remaining references -- **Debug Components:** ResourceDebugOverlay can be kept if still needed for other debugging -- **Testing:** Test each stage before moving to next stage -- **Git:** Commit after each stage for easy rollback - ---- - -**End of Migration Plan** - diff --git a/docs/AI-FUNCTION-FILES.md b/docs/AI-FUNCTION-FILES.md deleted file mode 100644 index 036b9268..00000000 --- a/docs/AI-FUNCTION-FILES.md +++ /dev/null @@ -1,271 +0,0 @@ -# AI Function Related Files - -This document lists all files containing code related to: -- Auto Cluster Keywords -- Auto Generate Ideas -- Auto Generate Content -- Auto Generate Images - ---- - -## Backend Files - -### Auto Cluster Keywords - -#### Core Implementation -- `backend/igny8_core/ai/functions/auto_cluster.py` - **Main AI function implementation** (BaseAIFunction) -- `backend/igny8_core/ai/base.py` - Base AI function class -- `backend/igny8_core/ai/engine.py` - AI engine orchestrator -- `backend/igny8_core/ai/processor.py` - AI processor wrapper -- `backend/igny8_core/ai/tasks.py` - Unified Celery task entrypoint -- `backend/igny8_core/ai/registry.py` - Function registry -- `backend/igny8_core/ai/tracker.py` - Progress and cost tracking - -#### API Endpoints & Views -- `backend/igny8_core/modules/planner/views.py` - **KeywordViewSet.auto_cluster()** action -- `backend/igny8_core/modules/planner/urls.py` - URL routing - -#### Celery Tasks (Legacy/Alternative) -- `backend/igny8_core/modules/planner/tasks.py` - **auto_cluster_keywords_task()** (legacy implementation) - -#### AI Processor -- `backend/igny8_core/utils/ai_processor.py` - **AIProcessor.cluster_keywords()** method - -#### Models -- `backend/igny8_core/modules/planner/models.py` - Keywords, Clusters models - -#### Serializers -- `backend/igny8_core/modules/planner/serializers.py` - Keyword, Cluster serializers -- `backend/igny8_core/modules/planner/cluster_serializers.py` - Cluster-specific serializers - -#### System Integration -- `backend/igny8_core/modules/system/schemas.py` - Schema definitions -- `backend/igny8_core/modules/system/utils.py` - Prompt loading utilities - ---- - -### Auto Generate Ideas - -#### Core Implementation -- `backend/igny8_core/utils/ai_processor.py` - **AIProcessor.generate_ideas()** method - -#### API Endpoints & Views -- `backend/igny8_core/modules/planner/views.py` - **ClusterViewSet.auto_generate_ideas()** action -- `backend/igny8_core/modules/planner/urls.py` - URL routing - -#### Celery Tasks -- `backend/igny8_core/modules/planner/tasks.py` - **auto_generate_ideas_task()** and **generate_single_idea_core()** - -#### Models -- `backend/igny8_core/modules/planner/models.py` - Clusters, ContentIdeas models - -#### Serializers -- `backend/igny8_core/modules/planner/serializers.py` - Cluster, ContentIdeas serializers - -#### System Integration -- `backend/igny8_core/modules/system/utils.py` - Prompt loading utilities - ---- - -### Auto Generate Content - -#### Core Implementation -- `backend/igny8_core/utils/ai_processor.py` - **AIProcessor.generate_content()** method - -#### API Endpoints & Views -- `backend/igny8_core/modules/writer/views.py` - **TasksViewSet.auto_generate_content()** action -- `backend/igny8_core/modules/writer/urls.py` - URL routing - -#### Celery Tasks -- `backend/igny8_core/modules/writer/tasks.py` - **auto_generate_content_task()** - -#### Models -- `backend/igny8_core/modules/writer/models.py` - Tasks, Content models - -#### Serializers -- `backend/igny8_core/modules/writer/serializers.py` - Task, Content serializers - -#### System Integration -- `backend/igny8_core/modules/system/schemas.py` - Schema definitions -- `backend/igny8_core/modules/system/utils.py` - Prompt loading utilities - ---- - -### Auto Generate Images - -#### Core Implementation -- `backend/igny8_core/utils/ai_processor.py` - **AIProcessor.extract_image_prompts()** and **AIProcessor.generate_image()** methods - -#### API Endpoints & Views -- `backend/igny8_core/modules/writer/views.py` - **TasksViewSet.auto_generate_images()** action -- `backend/igny8_core/modules/writer/urls.py` - URL routing - -#### Celery Tasks -- `backend/igny8_core/modules/writer/tasks.py` - **auto_generate_images_task()** - -#### Models -- `backend/igny8_core/modules/writer/models.py` - Tasks, Images models - -#### Serializers -- `backend/igny8_core/modules/writer/serializers.py` - Task, Images serializers - -#### System Integration -- `backend/igny8_core/modules/system/schemas.py` - Schema definitions -- `backend/igny8_core/modules/system/integration_views.py` - Integration settings for image generation - ---- - -## Frontend Files - -### Auto Cluster Keywords - -#### Pages -- `frontend/src/pages/Planner/Keywords.tsx` - **Main page component** with auto cluster functionality - -#### API Services -- `frontend/src/services/api.ts` - **autoClusterKeywords()** function - -#### Configuration -- `frontend/src/config/pages/keywords.config.tsx` - Page configuration -- `frontend/src/config/pages/table-actions.config.tsx` - Action button configurations - -#### State Management -- `frontend/src/store/aiRequestLogsStore.ts` - AI request/response logs store - -#### Hooks -- `frontend/src/hooks/useProgressModal.ts` - Progress modal hook for tracking task progress - ---- - -### Auto Generate Ideas - -#### Pages -- `frontend/src/pages/Planner/Clusters.tsx` - **Main page component** with auto generate ideas functionality - -#### API Services -- `frontend/src/services/api.ts` - **autoGenerateIdeas()** function - -#### Configuration -- `frontend/src/config/pages/clusters.config.tsx` - Page configuration -- `frontend/src/config/pages/table-actions.config.tsx` - Action button configurations - -#### State Management -- `frontend/src/store/aiRequestLogsStore.ts` - AI request/response logs store - -#### Hooks -- `frontend/src/hooks/useProgressModal.ts` - Progress modal hook for tracking task progress - ---- - -### Auto Generate Content - -#### Pages -- `frontend/src/pages/Writer/Tasks.tsx` - **Main page component** with auto generate content functionality - -#### API Services -- `frontend/src/services/api.ts` - **autoGenerateContent()** function - -#### Configuration -- `frontend/src/config/pages/tasks.config.tsx` - Page configuration -- `frontend/src/config/pages/table-actions.config.tsx` - Action button configurations - -#### State Management -- `frontend/src/store/aiRequestLogsStore.ts` - AI request/response logs store - -#### Hooks -- `frontend/src/hooks/useProgressModal.ts` - Progress modal hook for tracking task progress - ---- - -### Auto Generate Images - -#### Pages -- `frontend/src/pages/Writer/Tasks.tsx` - **Main page component** with auto generate images functionality - -#### API Services -- `frontend/src/services/api.ts` - **autoGenerateImages()** function - -#### Configuration -- `frontend/src/config/pages/tasks.config.tsx` - Page configuration -- `frontend/src/config/pages/images.config.tsx` - Image-related configuration -- `frontend/src/config/pages/table-actions.config.tsx` - Action button configurations - -#### State Management -- `frontend/src/store/aiRequestLogsStore.ts` - AI request/response logs store - -#### Hooks -- `frontend/src/hooks/useProgressModal.ts` - Progress modal hook for tracking task progress - ---- - -## Shared/Common Files - -### AI Framework (Backend) -- `backend/igny8_core/ai/__init__.py` - AI module initialization and auto-registration -- `backend/igny8_core/ai/models.py` - AITaskLog model for unified logging -- `backend/igny8_core/ai/types.py` - Shared dataclasses and types -- `backend/igny8_core/ai/admin.py` - Admin interface for AITaskLog - -### Progress Tracking (Backend) -- `backend/igny8_core/modules/system/integration_views.py` - **task_progress()** endpoint - -### Progress Tracking (Frontend) -- `frontend/src/components/common/ProgressModal.tsx` - Progress modal component -- `frontend/src/hooks/useProgressModal.ts` - Progress modal hook - -### Common Components (Frontend) -- `frontend/src/components/common/FormModal.tsx` - Form modal used in AI function pages - ---- - -## Summary by Function - -### Auto Cluster Keywords -**Backend**: 15 files -**Frontend**: 5 files -**Total**: 20 files - -### Auto Generate Ideas -**Backend**: 7 files -**Frontend**: 5 files -**Total**: 12 files - -### Auto Generate Content -**Backend**: 7 files -**Frontend**: 5 files -**Total**: 12 files - -### Auto Generate Images -**Backend**: 7 files -**Frontend**: 5 files -**Total**: 12 files - -### Shared/Common -**Backend**: 4 files -**Frontend**: 3 files -**Total**: 7 files - ---- - -## Key Files (Most Important) - -### Backend Core Files -1. `backend/igny8_core/ai/functions/auto_cluster.py` - Auto cluster AI function -2. `backend/igny8_core/utils/ai_processor.py` - Unified AI processor (all functions) -3. `backend/igny8_core/modules/planner/views.py` - Auto cluster & ideas API endpoints -4. `backend/igny8_core/modules/writer/views.py` - Content & images API endpoints -5. `backend/igny8_core/modules/planner/tasks.py` - Planner Celery tasks -6. `backend/igny8_core/modules/writer/tasks.py` - Writer Celery tasks -7. `backend/igny8_core/ai/tasks.py` - Unified AI task entrypoint - -### Frontend Core Files -1. `frontend/src/pages/Planner/Keywords.tsx` - Auto cluster UI -2. `frontend/src/pages/Planner/Clusters.tsx` - Auto generate ideas UI -3. `frontend/src/pages/Writer/Tasks.tsx` - Content & images generation UI -4. `frontend/src/services/api.ts` - All API functions -5. `frontend/src/hooks/useProgressModal.ts` - Progress tracking hook - ---- - -**Last Updated**: 2025-01-XX - diff --git a/docs/AI-NEW-DESIGN/07-AI-FRAMEWORK.md b/docs/AI-NEW-DESIGN/07-AI-FRAMEWORK.md deleted file mode 100644 index a29d7be3..00000000 --- a/docs/AI-NEW-DESIGN/07-AI-FRAMEWORK.md +++ /dev/null @@ -1,274 +0,0 @@ -# IGNY8 AI Framework Documentation - -**Version:** 1.0 -**Last Updated:** 2025-01-XX -**Purpose:** Complete documentation of the unified AI framework architecture. - ---- - -## Overview - -The IGNY8 AI Framework provides a unified, consistent architecture for all AI functions. It eliminates code duplication, standardizes progress tracking, and provides a single interface for all AI operations. - -### Key Benefits - -- **90% Code Reduction**: Functions are now ~100 lines instead of ~600 -- **Consistent UX**: All functions use the same progress modal and tracking -- **Unified Logging**: Single `AITaskLog` table for all AI operations -- **Easy Extension**: Add new functions by creating one class -- **Better Debugging**: Detailed step-by-step tracking for all operations - ---- - -## Architecture - -### Directory Structure - -``` -igny8_core/ai/ -├── __init__.py # Auto-registers all functions -├── apps.py # Django app configuration -├── admin.py # Admin interface for AITaskLog -├── base.py # BaseAIFunction abstract class -├── engine.py # AIEngine orchestrator -├── processor.py # AIProcessor wrapper -├── registry.py # Function registry -├── tracker.py # StepTracker, ProgressTracker, CostTracker -├── tasks.py # Unified Celery task entrypoint -├── types.py # Shared dataclasses -├── models.py # AITaskLog model -└── functions/ # Function implementations - ├── __init__.py - └── auto_cluster.py # Auto cluster function -``` - ---- - -## Core Components - -### 1. BaseAIFunction - -Abstract base class that all AI functions inherit from. - -**Methods to implement:** -- `get_name()`: Return function name -- `prepare()`: Load and prepare data -- `build_prompt()`: Build AI prompt -- `parse_response()`: Parse AI response -- `save_output()`: Save results to database - -**Optional overrides:** -- `validate()`: Custom validation -- `get_max_items()`: Set item limit -- `get_model()`: Specify AI model -- `get_metadata()`: Function metadata - -### 2. AIEngine - -Central orchestrator that manages the execution pipeline. - -**Phases:** -- INIT (0-10%): Validation & setup -- PREP (10-25%): Data loading & prompt building -- AI_CALL (25-60%): API call to provider -- PARSE (60-80%): Response parsing -- SAVE (80-95%): Database operations -- DONE (95-100%): Finalization - -### 3. Function Registry - -Dynamic function discovery system. - -**Usage:** -```python -from igny8_core.ai.registry import register_function, get_function - -# Register function -register_function('auto_cluster', AutoClusterFunction) - -# Get function -fn = get_function('auto_cluster') -``` - -### 4. Unified Celery Task - -Single entrypoint for all AI functions. - -**Endpoint:** `run_ai_task(function_name, payload, account_id)` - -**Example:** -```python -from igny8_core.ai.tasks import run_ai_task - -task = run_ai_task.delay( - function_name='auto_cluster', - payload={'ids': [1, 2, 3], 'sector_id': 1}, - account_id=1 -) -``` - ---- - -## Function Implementation Example - -### Auto Cluster Function - -```python -from igny8_core.ai.base import BaseAIFunction - -class AutoClusterFunction(BaseAIFunction): - def get_name(self) -> str: - return 'auto_cluster' - - def get_max_items(self) -> int: - return 20 - - def prepare(self, payload: dict, account=None) -> Dict: - # Load keywords - ids = payload.get('ids', []) - keywords = Keywords.objects.filter(id__in=ids) - return {'keywords': keywords, ...} - - def build_prompt(self, data: Dict, account=None) -> str: - # Build clustering prompt - return prompt_template.replace('[IGNY8_KEYWORDS]', keywords_text) - - def parse_response(self, response: str, step_tracker=None) -> List[Dict]: - # Parse AI response - return clusters - - def save_output(self, parsed, original_data, account, progress_tracker) -> Dict: - # Save clusters to database - return {'clusters_created': 5, 'keywords_updated': 20} -``` - ---- - -## API Endpoint Example - -### Before (Old): ~300 lines - -### After (New): ~50 lines - -```python -@action(detail=False, methods=['post'], url_path='auto_cluster') -def auto_cluster(self, request): - from igny8_core.ai.tasks import run_ai_task - - account = getattr(request, 'account', None) - account_id = account.id if account else None - - payload = { - 'ids': request.data.get('ids', []), - 'sector_id': request.data.get('sector_id') - } - - task = run_ai_task.delay( - function_name='auto_cluster', - payload=payload, - account_id=account_id - ) - - return Response({ - 'success': True, - 'task_id': str(task.id), - 'message': 'Clustering started' - }) -``` - ---- - -## Progress Tracking - -### Unified Progress Endpoint - -**URL:** `/api/v1/system/settings/task_progress//` - -**Response:** -```json -{ - "state": "PROGRESS", - "meta": { - "phase": "AI_CALL", - "percentage": 45, - "message": "Analyzing keyword relationships...", - "request_steps": [...], - "response_steps": [...], - "cost": 0.000123, - "tokens": 1500 - } -} -``` - -### Frontend Integration - -All AI functions use the same progress modal: -- Single `useProgressModal` hook -- Unified progress endpoint -- Consistent phase labels -- Step-by-step logs - ---- - -## Database Logging - -### AITaskLog Model - -Unified logging table for all AI operations. - -**Fields:** -- `task_id`: Celery task ID -- `function_name`: Function name -- `account`: Account (required) -- `phase`: Current phase -- `status`: success/error/pending -- `cost`: API cost -- `tokens`: Token usage -- `request_steps`: Request step logs -- `response_steps`: Response step logs -- `error`: Error message (if any) - ---- - -## Migration Guide - -### Migrating Existing Functions - -1. Create function class inheriting `BaseAIFunction` -2. Implement required methods -3. Register function in `ai/__init__.py` -4. Update API endpoint to use `run_ai_task` -5. Test and remove old code - -### Example Migration - -**Old code:** -```python -@action(...) -def auto_cluster(self, request): - # 300 lines of code -``` - -**New code:** -```python -@action(...) -def auto_cluster(self, request): - # 20 lines using framework -``` - ---- - -## Summary - -The AI Framework provides: - -1. **Unified Architecture**: Single framework for all AI functions -2. **Code Reduction**: 90% less code per function -3. **Consistent UX**: Same progress modal for all functions -4. **Better Debugging**: Detailed step tracking -5. **Easy Extension**: Add functions quickly -6. **Unified Logging**: Single log table -7. **Cost Tracking**: Automatic cost calculation - -This architecture ensures maintainability, consistency, and extensibility while dramatically reducing code duplication. - diff --git a/docs/AI-NEW-DESIGN/STAGE1-REORGANIZATION-COMPLETE.md b/docs/AI-NEW-DESIGN/STAGE1-REORGANIZATION-COMPLETE.md deleted file mode 100644 index c0eeeadc..00000000 --- a/docs/AI-NEW-DESIGN/STAGE1-REORGANIZATION-COMPLETE.md +++ /dev/null @@ -1,191 +0,0 @@ -# Stage 1 - AI Folder Structure & Functional Split - COMPLETE ✅ - -## Summary - -Successfully reorganized the AI backend into a clean, modular structure where every AI function lives inside its own file within `/ai/functions/`. - -## ✅ Completed Deliverables - -### 1. Folder Structure Created - -``` -backend/igny8_core/ai/ -├── functions/ -│ ├── __init__.py ✅ -│ ├── auto_cluster.py ✅ -│ ├── generate_ideas.py ✅ -│ ├── generate_content.py ✅ -│ └── generate_images.py ✅ -├── ai_core.py ✅ (Shared operations) -├── validators.py ✅ (Consolidated validation) -├── constants.py ✅ (Model pricing, valid models) -├── engine.py ✅ (Updated to use AICore) -├── tracker.py ✅ (Existing) -├── base.py ✅ (Existing) -├── processor.py ✅ (Existing wrapper) -├── registry.py ✅ (Updated with new functions) -└── __init__.py ✅ (Updated exports) -``` - -### 2. Shared Modules Created - -#### `ai_core.py` -- **Purpose**: Shared operations for all AI functions -- **Features**: - - API call construction (`call_openai`) - - Model selection (`get_model`, `get_api_key`) - - Response parsing (`extract_json`) - - Image generation (`generate_image`) - - Cost calculation (`calculate_cost`) -- **Status**: ✅ Complete - -#### `validators.py` -- **Purpose**: Consolidated validation logic -- **Functions**: - - `validate_ids()` - Base ID validation - - `validate_keywords_exist()` - Keyword existence check - - `validate_cluster_limits()` - Plan limit checks - - `validate_cluster_exists()` - Cluster existence - - `validate_tasks_exist()` - Task existence - - `validate_api_key()` - API key validation - - `validate_model()` - Model validation - - `validate_image_size()` - Image size validation -- **Status**: ✅ Complete - -#### `constants.py` -- **Purpose**: AI-related constants -- **Constants**: - - `MODEL_RATES` - Text model pricing - - `IMAGE_MODEL_RATES` - Image model pricing - - `VALID_OPENAI_IMAGE_MODELS` - Valid image models - - `VALID_SIZES_BY_MODEL` - Valid sizes per model - - `DEFAULT_AI_MODEL` - Default model name - - `JSON_MODE_MODELS` - Models supporting JSON mode -- **Status**: ✅ Complete - -### 3. Function Files Created - -#### `functions/auto_cluster.py` -- **Status**: ✅ Updated to use new validators and AICore -- **Changes**: - - Uses `validate_ids()`, `validate_keywords_exist()`, `validate_cluster_limits()` from validators - - Uses `AICore.extract_json()` for JSON parsing - - Maintains backward compatibility - -#### `functions/generate_ideas.py` -- **Status**: ✅ Created -- **Features**: - - `GenerateIdeasFunction` class (BaseAIFunction) - - `generate_ideas_core()` legacy function for backward compatibility - - Uses AICore for API calls - - Uses validators for validation - -#### `functions/generate_content.py` -- **Status**: ✅ Created -- **Features**: - - `GenerateContentFunction` class (BaseAIFunction) - - `generate_content_core()` legacy function for backward compatibility - - Uses AICore for API calls - - Uses validators for validation - -#### `functions/generate_images.py` -- **Status**: ✅ Created -- **Features**: - - `GenerateImagesFunction` class (BaseAIFunction) - - `generate_images_core()` legacy function for backward compatibility - - Uses AICore for image generation - - Uses validators for validation - -### 4. Import Paths Updated - -#### Updated Files: -- ✅ `modules/planner/views.py` - Uses `generate_ideas_core` from new location -- ✅ `modules/planner/tasks.py` - Imports `generate_ideas_core` from new location -- ✅ `modules/writer/tasks.py` - Imports `generate_content_core` and `generate_images_core` from new locations -- ✅ `ai/engine.py` - Uses `AICore` instead of `AIProcessor` -- ✅ `ai/functions/auto_cluster.py` - Uses new validators and AICore -- ✅ `ai/registry.py` - Registered all new functions -- ✅ `ai/__init__.py` - Exports all new modules - -### 5. Dependencies Verified - -#### No Circular Dependencies ✅ -- Functions depend on: `ai_core`, `validators`, `constants`, `base` -- `ai_core` depends on: `utils.ai_processor` (legacy, will be refactored later) -- `validators` depends on: `constants`, models -- `engine` depends on: `ai_core`, `base`, `tracker` -- All imports are clean and modular - -#### Modular Structure ✅ -- Each function file is self-contained -- Shared logic in `ai_core.py` -- Validation logic in `validators.py` -- Constants in `constants.py` -- No scattered or duplicated logic - -## 📋 File Structure Details - -### Core AI Modules - -| File | Purpose | Dependencies | -|------|---------|--------------| -| `ai_core.py` | Shared AI operations | `utils.ai_processor` (legacy) | -| `validators.py` | All validation logic | `constants`, models | -| `constants.py` | AI constants | None | -| `engine.py` | Execution orchestrator | `ai_core`, `base`, `tracker` | -| `base.py` | Base function class | None | -| `tracker.py` | Progress/step tracking | None | -| `registry.py` | Function registry | `base`, function modules | - -### Function Files - -| File | Function Class | Legacy Function | Status | -|------|----------------|-----------------|--------| -| `auto_cluster.py` | `AutoClusterFunction` | N/A (uses engine) | ✅ Updated | -| `generate_ideas.py` | `GenerateIdeasFunction` | `generate_ideas_core()` | ✅ Created | -| `generate_content.py` | `GenerateContentFunction` | `generate_content_core()` | ✅ Created | -| `generate_images.py` | `GenerateImagesFunction` | `generate_images_core()` | ✅ Created | - -## 🔄 Import Path Changes - -### Old Imports (Still work, but deprecated) -```python -from igny8_core.utils.ai_processor import AIProcessor -from igny8_core.modules.planner.tasks import _generate_single_idea_core -``` - -### New Imports (Recommended) -```python -from igny8_core.ai.functions.generate_ideas import generate_ideas_core -from igny8_core.ai.functions.generate_content import generate_content_core -from igny8_core.ai.functions.generate_images import generate_images_core -from igny8_core.ai.ai_core import AICore -from igny8_core.ai.validators import validate_ids, validate_cluster_limits -from igny8_core.ai.constants import MODEL_RATES, DEFAULT_AI_MODEL -``` - -## ✅ Verification Checklist - -- [x] All function files created in `ai/functions/` -- [x] Shared modules (`ai_core`, `validators`, `constants`) created -- [x] No circular dependencies -- [x] All imports updated in views and tasks -- [x] Functions registered in registry -- [x] `__init__.py` files updated -- [x] Backward compatibility maintained (legacy functions still work) -- [x] No linting errors -- [x] Structure matches required layout - -## 🎯 Next Steps (Future Stages) - -- **Stage 2**: Inject tracker into all functions -- **Stage 3**: Simplify logging -- **Stage 4**: Clean up legacy code - -## 📝 Notes - -- Legacy `AIProcessor` from `utils.ai_processor` is still used by `ai_core.py` as a wrapper -- This will be refactored in later stages -- All existing API endpoints continue to work -- No functional changes - only structural reorganization - diff --git a/docs/AI-NEW-DESIGN/STAGE2-EXECUTION-LOGGING-COMPLETE.md b/docs/AI-NEW-DESIGN/STAGE2-EXECUTION-LOGGING-COMPLETE.md deleted file mode 100644 index 31fd5ac7..00000000 --- a/docs/AI-NEW-DESIGN/STAGE2-EXECUTION-LOGGING-COMPLETE.md +++ /dev/null @@ -1,220 +0,0 @@ -# Stage 2 - AI Execution & Logging Layer - COMPLETE ✅ - -## Summary - -Successfully created a centralized, consistent, and traceable execution layer for all AI requests with unified request handler and clean console-based logging. - -## ✅ Completed Deliverables - -### 1. Centralized Execution in `ai_core.py` - -#### `run_ai_request()` Method -- **Purpose**: Single entry point for all AI text generation requests -- **Features**: - - Step-by-step console logging with `print()` statements - - Standardized request payload construction - - Error handling with detailed logging - - Token counting and cost calculation - - Rate limit detection and logging - - Timeout handling - - JSON mode auto-enablement for supported models - -#### Console Logging Format -``` -[AI][function_name] Step 1: Preparing request... -[AI][function_name] Step 2: Using model: gpt-4o -[AI][function_name] Step 3: Auto-enabled JSON mode for gpt-4o -[AI][function_name] Step 4: Prompt length: 1234 characters -[AI][function_name] Step 5: Request payload prepared (model=gpt-4o, max_tokens=4000, temp=0.7) -[AI][function_name] Step 6: Sending request to OpenAI API... -[AI][function_name] Step 7: Received response in 2.34s (status=200) -[AI][function_name] Step 8: Received 150 tokens (input: 50, output: 100) -[AI][function_name] Step 9: Content length: 450 characters -[AI][function_name] Step 10: Cost calculated: $0.000123 -[AI][function_name][Success] Request completed successfully -``` - -#### Error Logging Format -``` -[AI][function_name][Error] OpenAI Rate Limit - waiting 60s -[AI][function_name][Error] HTTP 429 error: Rate limit exceeded (Rate limit - retry after 60s) -[AI][function_name][Error] Request timeout (60s exceeded) -[AI][function_name][Error] Failed to parse JSON response: ... -``` - -### 2. Image Generation with Logging - -#### `generate_image()` Method -- **Purpose**: Centralized image generation with console logging -- **Features**: - - Supports OpenAI DALL-E and Runware - - Model and size validation - - Step-by-step console logging - - Error handling with detailed messages - - Cost calculation - -#### Console Logging Format -``` -[AI][generate_images] Step 1: Preparing image generation request... -[AI][generate_images] Provider: OpenAI -[AI][generate_images] Step 2: Using model: dall-e-3, size: 1024x1024 -[AI][generate_images] Step 3: Sending request to OpenAI Images API... -[AI][generate_images] Step 4: Received response in 5.67s (status=200) -[AI][generate_images] Step 5: Image generated successfully -[AI][generate_images] Step 6: Cost: $0.0400 -[AI][generate_images][Success] Image generation completed -``` - -### 3. Updated All Function Files - -#### `functions/auto_cluster.py` -- ✅ Uses `AICore.extract_json()` for JSON parsing -- ✅ Engine calls `run_ai_request()` (via engine.py) - -#### `functions/generate_ideas.py` -- ✅ Updated `generate_ideas_core()` to use `run_ai_request()` -- ✅ Console logging enabled with function name - -#### `functions/generate_content.py` -- ✅ Updated `generate_content_core()` to use `run_ai_request()` -- ✅ Console logging enabled with function name - -#### `functions/generate_images.py` -- ✅ Updated to use `run_ai_request()` for prompt extraction -- ✅ Updated to use `generate_image()` with logging -- ✅ Console logging enabled - -### 4. Updated Engine - -#### `engine.py` -- ✅ Updated to use `run_ai_request()` instead of `call_openai()` -- ✅ Passes function name for logging context -- ✅ Maintains backward compatibility - -### 5. Deprecated Old Code - -#### `processor.py` -- ✅ Marked as DEPRECATED -- ✅ Redirects all calls to `AICore` -- ✅ Kept for backward compatibility only -- ✅ All methods now use `AICore` internally - -### 6. Edge Case Handling - -#### Implemented in `run_ai_request()`: -- ✅ **API Key Validation**: Logs error if not configured -- ✅ **Prompt Length**: Logs character count -- ✅ **Rate Limits**: Detects and logs retry-after time -- ✅ **Timeouts**: Handles 60s timeout with clear error -- ✅ **JSON Parsing Errors**: Logs decode errors with context -- ✅ **Empty Responses**: Validates content exists -- ✅ **Token Overflow**: Max tokens enforced -- ✅ **Model Validation**: Auto-selects JSON mode for supported models - -### 7. Standardized Request Schema - -#### OpenAI Request Payload -```python -{ - "model": "gpt-4o", - "messages": [{"role": "user", "content": prompt}], - "temperature": 0.7, - "max_tokens": 4000, - "response_format": {"type": "json_object"} # Auto-enabled for supported models -} -``` - -#### All Functions Use Same Logic: -- Model selection (account default or override) -- JSON mode auto-enablement -- Token limits -- Temperature settings -- Error handling - -### 8. Test Script Created - -#### `ai/tests/test_run.py` -- ✅ Test script for all AI functions -- ✅ Tests `run_ai_request()` directly -- ✅ Tests JSON extraction -- ✅ Placeholder tests for all functions -- ✅ Can be run standalone to verify logging - -## 📋 File Changes Summary - -| File | Changes | Status | -|------|---------|--------| -| `ai_core.py` | Complete rewrite with `run_ai_request()` and console logging | ✅ Complete | -| `engine.py` | Updated to use `run_ai_request()` | ✅ Complete | -| `processor.py` | Marked deprecated, redirects to AICore | ✅ Complete | -| `functions/auto_cluster.py` | Uses AICore methods | ✅ Complete | -| `functions/generate_ideas.py` | Uses `run_ai_request()` | ✅ Complete | -| `functions/generate_content.py` | Uses `run_ai_request()` | ✅ Complete | -| `functions/generate_images.py` | Uses `run_ai_request()` and `generate_image()` | ✅ Complete | -| `tests/test_run.py` | Test script created | ✅ Complete | - -## 🔄 Migration Path - -### Old Code (Deprecated) -```python -from igny8_core.utils.ai_processor import AIProcessor -processor = AIProcessor(account=account) -result = processor._call_openai(prompt, model=model) -``` - -### New Code (Recommended) -```python -from igny8_core.ai.ai_core import AICore -ai_core = AICore(account=account) -result = ai_core.run_ai_request( - prompt=prompt, - model=model, - function_name='my_function' -) -``` - -## ✅ Verification Checklist - -- [x] `run_ai_request()` created with console logging -- [x] All function files updated to use `run_ai_request()` -- [x] Engine updated to use `run_ai_request()` -- [x] Old processor code deprecated -- [x] Edge cases handled with logging -- [x] Request schema standardized -- [x] Test script created -- [x] No linting errors -- [x] Backward compatibility maintained - -## 🎯 Benefits Achieved - -1. **Centralized Execution**: All AI requests go through one method -2. **Consistent Logging**: Every request logs steps to console -3. **Better Debugging**: Clear step-by-step visibility -4. **Error Handling**: Comprehensive error detection and logging -5. **Reduced Duplication**: No scattered AI call logic -6. **Easy Testing**: Single point to test/mock -7. **Future Ready**: Easy to add retry logic, backoff, etc. - -## 📝 Console Output Example - -When running any AI function, you'll see: -``` -[AI][generate_ideas] Step 1: Preparing request... -[AI][generate_ideas] Step 2: Using model: gpt-4o -[AI][generate_ideas] Step 3: Auto-enabled JSON mode for gpt-4o -[AI][generate_ideas] Step 4: Prompt length: 2345 characters -[AI][generate_ideas] Step 5: Request payload prepared (model=gpt-4o, max_tokens=4000, temp=0.7) -[AI][generate_ideas] Step 6: Sending request to OpenAI API... -[AI][generate_ideas] Step 7: Received response in 3.45s (status=200) -[AI][generate_ideas] Step 8: Received 250 tokens (input: 100, output: 150) -[AI][generate_ideas] Step 9: Content length: 600 characters -[AI][generate_ideas] Step 10: Cost calculated: $0.000250 -[AI][generate_ideas][Success] Request completed successfully -``` - -## 🚀 Next Steps (Future Stages) - -- **Stage 3**: Simplify logging (optional - console logging already implemented) -- **Stage 4**: Clean up legacy code (remove old processor completely) -- **Future**: Add retry logic, exponential backoff, request queuing - diff --git a/docs/AI-NEW-DESIGN/STAGE3-LOGGING-COMPLETE.md b/docs/AI-NEW-DESIGN/STAGE3-LOGGING-COMPLETE.md deleted file mode 100644 index 43512dc2..00000000 --- a/docs/AI-NEW-DESIGN/STAGE3-LOGGING-COMPLETE.md +++ /dev/null @@ -1,171 +0,0 @@ -# Stage 3 - Clean Logging, Unified Debug Flow & Step Traceability - COMPLETE ✅ - -## Summary - -Successfully replaced all fragmented or frontend-based debugging systems with a consistent, lightweight backend-only logging flow. All AI activity is now tracked via structured console messages with no UI panels, no Zustand state, and no silent failures. - -## ✅ Completed Deliverables - -### 1. ConsoleStepTracker Created - -#### `tracker.py` - ConsoleStepTracker Class -- **Purpose**: Lightweight console-based step tracker for AI functions -- **Features**: - - Logs each step to console with timestamps and clear labels - - Only logs if `DEBUG_MODE` is True - - Standardized phase methods: `init()`, `prep()`, `ai_call()`, `parse()`, `save()`, `done()` - - Error logging: `error()`, `timeout()`, `rate_limit()`, `malformed_json()` - - Retry logging: `retry()` - - Duration tracking - -#### Log Format -``` -[HH:MM:SS] [function_name] [PHASE] message -[HH:MM:SS] [function_name] [PHASE] ✅ success message -[HH:MM:SS] [function_name] [PHASE] [ERROR] error message -[function_name] === AI Task Complete === -``` - -### 2. DEBUG_MODE Constant Added - -#### `constants.py` -- Added `DEBUG_MODE = True` constant -- Controls all console logging -- Can be set to `False` in production to disable verbose logging -- All print statements check `DEBUG_MODE` before logging - -### 3. Integrated Tracker into AI Functions - -#### `generate_ideas.py` -- ✅ Added `ConsoleStepTracker` initialization -- ✅ Logs: INIT → PREP → AI_CALL → PARSE → SAVE → DONE -- ✅ Error handling with tracker.error() -- ✅ Passes tracker to `run_ai_request()` - -#### `ai_core.py` -- ✅ Updated `run_ai_request()` to accept optional tracker parameter -- ✅ All logging now uses tracker methods -- ✅ Replaced all `print()` statements with tracker calls -- ✅ Standardized error logging format - -### 4. Frontend Debug Systems Deprecated - -#### `TablePageTemplate.tsx` -- ✅ Commented out `AIRequestLogsSection` component -- ✅ Commented out import of `useAIRequestLogsStore` -- ✅ Added deprecation comments - -#### Frontend Store (Kept for now, but unused) -- `aiRequestLogsStore.ts` - Still exists but no longer used -- All calls to `addLog`, `updateLog`, `addRequestStep`, `addResponseStep` are deprecated - -### 5. Error Standardization - -#### Standardized Error Format -``` -[ERROR] {function_name}: {error_type} – {message} -``` - -#### Error Types -- `ConfigurationError` - API key not configured -- `ValidationError` - Input validation failed -- `HTTPError` - HTTP request failed -- `Timeout` - Request timeout -- `RateLimit` - Rate limit hit -- `MalformedJSON` - JSON parsing failed -- `EmptyResponse` - No content in response -- `ParseError` - Response parsing failed -- `Exception` - Unexpected exception - -### 6. Example Console Output - -#### Successful Execution -``` -[14:23:45] [generate_ideas] [INIT] Task started -[14:23:45] [generate_ideas] [PREP] Loading account and cluster data... -[14:23:45] [generate_ideas] [PREP] Validating input... -[14:23:45] [generate_ideas] [PREP] Loading cluster with keywords... -[14:23:45] [generate_ideas] [PREP] Building prompt... -[14:23:45] [generate_ideas] [AI_CALL] Preparing request... -[14:23:45] [generate_ideas] [AI_CALL] Using model: gpt-4o -[14:23:45] [generate_ideas] [AI_CALL] Auto-enabled JSON mode for gpt-4o -[14:23:45] [generate_ideas] [AI_CALL] Prompt length: 1234 characters -[14:23:45] [generate_ideas] [AI_CALL] Request payload prepared (model=gpt-4o, max_tokens=4000, temp=0.7) -[14:23:45] [generate_ideas] [AI_CALL] Sending request to OpenAI API... -[14:23:48] [generate_ideas] [AI_CALL] Received response in 2.34s (status=200) -[14:23:48] [generate_ideas] [PARSE] Received 250 tokens (input: 100, output: 150) -[14:23:48] [generate_ideas] [PARSE] Content length: 600 characters -[14:23:48] [generate_ideas] [PARSE] Cost calculated: $0.000250 -[14:23:48] [generate_ideas] [DONE] ✅ Request completed successfully (Duration: 3.12s) -[14:23:48] [generate_ideas] [PARSE] Parsing AI response... -[14:23:48] [generate_ideas] [PARSE] Parsed 1 idea(s) -[14:23:48] [generate_ideas] [SAVE] Saving idea to database... -[14:23:48] [generate_ideas] [SAVE] Saved 1 idea(s) -[14:23:48] [generate_ideas] [DONE] ✅ Idea 'My Great Idea' created successfully (Duration: 3.15s) -[generate_ideas] === AI Task Complete === -``` - -#### Error Execution -``` -[14:25:10] [generate_ideas] [INIT] Task started -[14:25:10] [generate_ideas] [PREP] Loading account and cluster data... -[14:25:10] [generate_ideas] [PREP] Validating input... -[14:25:10] [generate_ideas] [PREP] [ERROR] ValidationError – No cluster found -``` - -## 📋 File Changes Summary - -| File | Changes | Status | -|------|---------|--------| -| `tracker.py` | Added `ConsoleStepTracker` class | ✅ Complete | -| `constants.py` | Added `DEBUG_MODE` constant | ✅ Complete | -| `ai_core.py` | Updated to use tracker, removed print() statements | ✅ Complete | -| `generate_ideas.py` | Integrated ConsoleStepTracker | ✅ Complete | -| `TablePageTemplate.tsx` | Commented out frontend debug UI | ✅ Complete | - -## 🔄 Remaining Work - -### Functions Still Need Tracker Integration -- [ ] `auto_cluster.py` - Add tracker to core function -- [ ] `generate_content.py` - Add tracker to core function -- [ ] `generate_images.py` - Add tracker to core function - -### Image Generation Logging -- [ ] Update `_generate_image_openai()` to use tracker -- [ ] Update `_generate_image_runware()` to use tracker -- [ ] Replace all print() statements with tracker calls - -### Frontend Cleanup -- [ ] Remove or fully comment out `AIRequestLogsSection` function body -- [ ] Remove unused imports from `api.ts` and `useProgressModal.ts` -- [ ] Optionally delete `aiRequestLogsStore.ts` (or keep for reference) - -## ✅ Verification Checklist - -- [x] ConsoleStepTracker created with all methods -- [x] DEBUG_MODE constant added -- [x] `run_ai_request()` updated to use tracker -- [x] `generate_ideas.py` integrated with tracker -- [x] Frontend debug UI commented out -- [x] Error logging standardized -- [ ] All function files integrated (partial) -- [ ] Image generation logging updated (pending) -- [ ] All print() statements replaced (partial) - -## 🎯 Benefits Achieved - -1. **Unified Logging**: All AI functions use same logging format -2. **Backend-Only**: No frontend state management needed -3. **Production Ready**: Can disable logs via DEBUG_MODE -4. **Clear Traceability**: Every step visible in console -5. **Error Visibility**: All errors clearly labeled and logged -6. **No Silent Failures**: Every failure prints its cause - -## 📝 Next Steps - -1. Complete tracker integration in remaining functions -2. Update image generation methods -3. Remove remaining print() statements -4. Test end-to-end with all four AI flows -5. Optionally clean up frontend debug code completely - diff --git a/docs/AI-NEW-DESIGN/STAGE4-PROMPT-REGISTRY-COMPLETE.md b/docs/AI-NEW-DESIGN/STAGE4-PROMPT-REGISTRY-COMPLETE.md deleted file mode 100644 index a37da666..00000000 --- a/docs/AI-NEW-DESIGN/STAGE4-PROMPT-REGISTRY-COMPLETE.md +++ /dev/null @@ -1,220 +0,0 @@ -# Stage 4 - Prompt Registry, Model Unification, and Final Function Hooks - COMPLETE ✅ - -## Summary - -Successfully created a centralized prompt registry system, unified model configurations, and standardized all AI function execution with clean, minimal function files. - -## ✅ Completed Deliverables - -### 1. Prompt Registry System Created - -#### `ai/prompts.py` - PromptRegistry Class -- **Purpose**: Centralized prompt management with hierarchical resolution -- **Features**: - - Hierarchical prompt resolution: - 1. Task-level `prompt_override` (if exists) - 2. DB prompt for (account, function) - 3. Default fallback from registry - - Supports both `.format()` style and `[IGNY8_*]` placeholder replacement - - Function-to-prompt-type mapping - - Convenience methods: `get_image_prompt_template()`, `get_negative_prompt()` - -#### Prompt Resolution Priority -```python -# Priority 1: Task override -if task.prompt_override: - use task.prompt_override - -# Priority 2: DB prompt -elif DB prompt for (account, function) exists: - use DB prompt - -# Priority 3: Default fallback -else: - use default from registry -``` - -### 2. Model Configuration Centralized - -#### `ai/settings.py` - MODEL_CONFIG -- **Purpose**: Centralized model configurations for all AI functions -- **Configurations**: - ```python - MODEL_CONFIG = { - "auto_cluster": { - "model": "gpt-4o-mini", - "max_tokens": 3000, - "temperature": 0.7, - "response_format": {"type": "json_object"}, - }, - "generate_ideas": { - "model": "gpt-4.1", - "max_tokens": 4000, - "temperature": 0.7, - "response_format": {"type": "json_object"}, - }, - "generate_content": { - "model": "gpt-4.1", - "max_tokens": 8000, - "temperature": 0.7, - "response_format": None, # Text output - }, - "generate_images": { - "model": "dall-e-3", - "size": "1024x1024", - "provider": "openai", - }, - } - ``` - -#### Helper Functions -- `get_model_config(function_name)` - Get full config -- `get_model(function_name)` - Get model name -- `get_max_tokens(function_name)` - Get max tokens -- `get_temperature(function_name)` - Get temperature - -### 3. Updated All AI Functions - -#### `functions/auto_cluster.py` -- ✅ Uses `PromptRegistry.get_prompt()` -- ✅ Uses `get_model_config()` for model settings -- ✅ Removed direct `get_prompt_value()` calls - -#### `functions/generate_ideas.py` -- ✅ Uses `PromptRegistry.get_prompt()` with context -- ✅ Uses `get_model_config()` for model settings -- ✅ Clean prompt building with context variables - -#### `functions/generate_content.py` -- ✅ Uses `PromptRegistry.get_prompt()` with task support -- ✅ Uses `get_model_config()` for model settings -- ✅ Supports task-level prompt overrides - -#### `functions/generate_images.py` -- ✅ Uses `PromptRegistry.get_prompt()` for extraction -- ✅ Uses `PromptRegistry.get_image_prompt_template()` -- ✅ Uses `PromptRegistry.get_negative_prompt()` -- ✅ Uses `get_model_config()` for model settings - -### 4. Updated Engine - -#### `engine.py` -- ✅ Uses `get_model_config()` instead of `fn.get_model()` -- ✅ Passes model config to `run_ai_request()` -- ✅ Unified model selection across all functions - -### 5. Standardized Response Format - -All functions now return consistent format: -```python -{ - "success": True/False, - "output": "HTML or image_url or data", - "raw": raw_response_json, # Optional - "meta": { - "word_count": 1536, # For content - "keywords": [...], # For clusters - "model_used": "gpt-4.1", - "tokens": 250, - "cost": 0.000123 - }, - "error": None or error_message -} -``` - -## 📋 File Changes Summary - -| File | Changes | Status | -|------|---------|--------| -| `prompts.py` | Created PromptRegistry class | ✅ Complete | -| `settings.py` | Created MODEL_CONFIG and helpers | ✅ Complete | -| `functions/auto_cluster.py` | Updated to use registry and settings | ✅ Complete | -| `functions/generate_ideas.py` | Updated to use registry and settings | ✅ Complete | -| `functions/generate_content.py` | Updated to use registry and settings | ✅ Complete | -| `functions/generate_images.py` | Updated to use registry and settings | ✅ Complete | -| `engine.py` | Updated to use model config | ✅ Complete | -| `__init__.py` | Exported new modules | ✅ Complete | - -## 🔄 Migration Path - -### Old Code (Deprecated) -```python -from igny8_core.modules.system.utils import get_prompt_value, get_default_prompt -prompt_template = get_prompt_value(account, 'clustering') -prompt = prompt_template.replace('[IGNY8_KEYWORDS]', keywords_text) -``` - -### New Code (Recommended) -```python -from igny8_core.ai.prompts import PromptRegistry -from igny8_core.ai.settings import get_model_config - -# Get prompt from registry -prompt = PromptRegistry.get_prompt( - function_name='auto_cluster', - account=account, - context={'KEYWORDS': keywords_text} -) - -# Get model config -model_config = get_model_config('auto_cluster') -``` - -## ✅ Verification Checklist - -- [x] PromptRegistry created with hierarchical resolution -- [x] MODEL_CONFIG created with all function configs -- [x] All functions updated to use registry -- [x] All functions updated to use model config -- [x] Engine updated to use model config -- [x] Response format standardized -- [x] No direct prompt utility calls in functions -- [x] Task-level overrides supported -- [x] DB prompts supported -- [x] Default fallbacks working - -## 🎯 Benefits Achieved - -1. **Centralized Prompts**: All prompts in one registry -2. **Hierarchical Resolution**: Task → DB → Default -3. **Model Unification**: All models configured in one place -4. **Easy Customization**: Tenant admins can override prompts -5. **Consistent Execution**: All functions use same pattern -6. **Traceability**: Prompt source clearly identifiable -7. **Minimal Functions**: Functions are clean and focused - -## 📝 Prompt Source Traceability - -Each prompt execution logs its source: -- `[PROMPT] Using task-level prompt override for generate_content` -- `[PROMPT] Using DB prompt for generate_ideas (account 123)` -- `[PROMPT] Using default prompt for auto_cluster` - -## 🚀 Final Structure - -``` -/ai/ -├── functions/ -│ ├── auto_cluster.py ← Uses registry + settings -│ ├── generate_ideas.py ← Uses registry + settings -│ ├── generate_content.py ← Uses registry + settings -│ └── generate_images.py ← Uses registry + settings -├── prompts.py ← Prompt Registry ✅ -├── settings.py ← Model Configs ✅ -├── ai_core.py ← Unified execution ✅ -├── engine.py ← Uses settings ✅ -└── tracker.py ← Console logging ✅ -``` - -## ✅ Expected Outcomes Achieved - -- ✅ All AI executions use common format -- ✅ Prompt customization is dynamic and override-able -- ✅ No duplication across AI functions -- ✅ Every AI task has: - - ✅ Clean inputs - - ✅ Unified execution - - ✅ Standard outputs - - ✅ Clear error tracking - - ✅ Prompt traceability - diff --git a/docs/Architecture/01-ARCHITECTURE.md b/docs/Architecture/01-ARCHITECTURE.md deleted file mode 100644 index 6d69f353..00000000 --- a/docs/Architecture/01-ARCHITECTURE.md +++ /dev/null @@ -1,749 +0,0 @@ -# IGNY8 System Architecture - -**Version:** 1.0 -**Last Updated:** 2025-01-XX -**Purpose:** Complete system architecture documentation covering design patterns, principles, tech stack, and structural organization. - ---- - -## Table of Contents - -1. [System Overview](#system-overview) -2. [Tech Stack](#tech-stack) -3. [Core Architecture Principles](#core-architecture-principles) -4. [Project Structure](#project-structure) -5. [Key Architectural Patterns](#key-architectural-patterns) -6. [Multi-Tenancy Architecture](#multi-tenancy-architecture) -7. [Module Organization](#module-organization) -8. [API Architecture](#api-architecture) -9. [Frontend Architecture](#frontend-architecture) -10. [Backend Architecture](#backend-architecture) -11. [Database Architecture](#database-architecture) -12. [Security Architecture](#security-architecture) -13. [Deployment Architecture](#deployment-architecture) - ---- - -## System Overview - -**IGNY8** is a full-stack SaaS platform for SEO keyword management and AI-driven content generation. The system migrated from a WordPress plugin architecture to a modern Django + React architecture, providing a scalable, multi-account platform for content planning and generation. - -### Core Capabilities - -- **Multi-Account SaaS Platform**: Complete account isolation with site/sector hierarchy -- **SEO Keyword Management**: Import, organize, and cluster keywords -- **AI-Powered Content Planning**: Automated keyword clustering and content idea generation -- **AI Content Generation**: Automated blog post and article generation -- **Image Generation**: AI-powered image generation for content -- **WordPress Integration**: Publish content directly to WordPress sites -- **Subscription Management**: Plan-based limits and billing integration - ---- - -## Tech Stack - -### Backend - -- **Framework**: Django 5.2+ with Django REST Framework (DRF) -- **Database**: PostgreSQL -- **Task Queue**: Celery with Redis broker -- **Authentication**: JWT (JSON Web Tokens) + Session-based auth -- **API**: RESTful API with DRF ViewSets -- **Caching**: Redis -- **File Storage**: Local filesystem (configurable for S3) - -### Frontend - -- **Framework**: React 19 with TypeScript -- **Build Tool**: Vite -- **Styling**: Tailwind CSS -- **State Management**: Zustand -- **Routing**: React Router v6 -- **HTTP Client**: Fetch API with custom wrapper -- **UI Components**: Custom component library - -### Infrastructure - -- **Containerization**: Docker & Docker Compose -- **Reverse Proxy**: Caddy (HTTPS on port 443) -- **Process Management**: Supervisor (for Celery workers) -- **Monitoring**: Portainer (optional) - -### Development Tools - -- **Backend**: Python 3.11+, pip, virtualenv -- **Frontend**: Node.js 18+, npm/yarn -- **Version Control**: Git -- **Code Quality**: ESLint, Prettier (frontend), Black, Flake8 (backend) - ---- - -## Core Architecture Principles - -### 1. Configuration-Driven Everything - -**Principle**: Zero HTML/JSX duplication - All UI rendered from configuration. - -- **Tables, filters, forms** all driven by config files -- **Single source of truth** - Change config, UI updates everywhere -- **Page-local config** for page-specific settings -- **Shared snippets** for reusable column/filter/action definitions - -**Implementation**: -- Frontend: Config files in `/config/pages/` and `/config/snippets/` -- Backend: DRF serializers and ViewSet actions - -### 2. Multi-Tenancy Foundation - -**Principle**: Complete account isolation with automatic filtering. - -- All models inherit `AccountBaseModel` with automatic account isolation -- All ViewSets inherit `AccountModelViewSet` with automatic account filtering -- Middleware injects account context from JWT on every request -- Site > Sector hierarchy for content organization - -**Implementation**: -- Base models: `AccountBaseModel`, `SiteSectorBaseModel` -- Base ViewSets: `AccountModelViewSet`, `SiteSectorModelViewSet` -- Middleware: `AccountContextMiddleware` sets `request.account` - -### 3. Template System (4 Universal Templates) - -**Principle**: Reusable templates for all page types. - -- **DashboardTemplate**: Module home pages (KPIs, workflow steps, charts) -- **TablePageTemplate**: CRUD table pages (Keywords, Clusters, Tasks, etc.) -- **FormPageTemplate**: Settings/form pages (Settings, Integration, etc.) -- **SystemPageTemplate**: System/admin pages (Logs, Status, Monitoring) - -**Implementation**: -- Frontend: `/templates/` directory with 4 template components -- Config-driven: Templates accept config objects for customization - -### 4. Unified AI Processor - -**Principle**: Single interface for all AI operations. - -- Single `AIProcessor` class handles all AI operations -- Manual and automated workflows use same functions -- Action-based routing: 'clustering', 'ideas', 'content_generation', 'image_generation' -- Account-specific API keys and model configuration - -**Implementation**: -- Backend: `AIProcessor` class in `/utils/ai_processor.py` -- Integration: Loads API keys from `IntegrationSettings` model - -### 5. Module-Based Organization - -**Principle**: Clear module boundaries with shared utilities. - -- Each module = Django app (`/modules/{module}/`) -- Clear module boundaries with shared utilities -- Module router pattern for subpage routing -- Consistent structure across modules - -**Implementation**: -- Backend: `/modules/planner/`, `/modules/writer/`, `/modules/system/`, `/modules/billing/` -- Frontend: `/pages/Planner/`, `/pages/Writer/`, `/pages/Settings/`, etc. - ---- - -## Project Structure - -``` -igny8/ -├── backend/ # Django backend -│ └── igny8_core/ # Django project -│ ├── auth/ # Multi-tenancy, User, Account, Plan models -│ │ ├── models.py # Account, User, Plan, Site, Sector, Industry models -│ │ ├── views.py # Account, User, Site, Sector ViewSets -│ │ ├── serializers.py # Account, User, Plan serializers -│ │ └── urls.py # Auth module URLs -│ ├── modules/ # Feature modules -│ │ ├── planner/ # Keywords, Clusters, Ideas -│ │ │ ├── models.py # Keywords, Clusters, ContentIdeas models -│ │ │ ├── views.py # KeywordViewSet, ClusterViewSet, ContentIdeasViewSet -│ │ │ ├── tasks.py # Celery tasks for AI operations -│ │ │ ├── serializers.py # Model serializers -│ │ │ └── urls.py # Planner module URLs -│ │ ├── writer/ # Tasks, Content, Images -│ │ │ ├── models.py # Tasks, Content, Images models -│ │ │ ├── views.py # TasksViewSet -│ │ │ ├── tasks.py # Celery tasks for content/image generation -│ │ │ └── urls.py # Writer module URLs -│ │ ├── system/ # Settings, Prompts, Integration -│ │ │ ├── models.py # AIPrompt, IntegrationSettings, AuthorProfile, Strategy -│ │ │ ├── views.py # AIPromptViewSet, AuthorProfileViewSet -│ │ │ ├── integration_views.py # IntegrationSettingsViewSet, task_progress -│ │ │ ├── utils.py # Default prompts, prompt loading -│ │ │ └── urls.py # System module URLs -│ │ └── billing/ # Credits, Transactions, Usage -│ │ ├── models.py # CreditTransaction, UsageLog models -│ │ ├── views.py # Billing ViewSets -│ │ └── services.py # CreditService -│ ├── api/ # API base classes -│ │ └── base.py # AccountModelViewSet, SiteSectorModelViewSet -│ ├── utils/ # Shared utilities -│ │ ├── ai_processor.py # Unified AI interface -│ │ └── content_normalizer.py # Content processing utilities -│ ├── middleware/ # Custom middleware -│ │ ├── account.py # AccountContextMiddleware (sets request.account) -│ │ └── resource_tracker.py # ResourceTrackerMiddleware (API metrics) -│ ├── settings.py # Django settings -│ ├── urls.py # Root URL configuration -│ └── celery.py # Celery configuration -│ -├── frontend/ # React frontend -│ └── src/ -│ ├── pages/ # Page components -│ │ ├── Planner/ # KeywordsPage, ClustersPage, IdeasPage, Dashboard -│ │ ├── Writer/ # TasksPage, DraftsPage, PublishedPage, Dashboard -│ │ ├── Settings/ # General, Integration, Status, ImportExport -│ │ ├── Billing/ # Credits, Transactions, Usage -│ │ └── AuthPages/ # SignIn, SignUp -│ ├── templates/ # 4 master templates -│ │ ├── DashboardTemplate.tsx -│ │ ├── TablePageTemplate.tsx -│ │ ├── FormPageTemplate.tsx -│ │ └── SystemPageTemplate.tsx -│ ├── components/ # UI components -│ │ ├── layout/ # AppLayout, Sidebar, Header, Breadcrumbs -│ │ ├── table/ # DataTable, Filters, Actions, Pagination -│ │ ├── ui/ # Button, Card, Modal, Toast, etc. -│ │ └── auth/ # ProtectedRoute, Auth components -│ ├── config/ # Configuration files -│ │ ├── pages/ # Page-specific configs -│ │ │ └── keywords.config.tsx -│ │ ├── snippets/ # Shared column/filter/action definitions -│ │ │ ├── columns.snippets.ts -│ │ │ ├── filters.snippets.ts -│ │ │ └── actions.snippets.ts -│ │ └── routes.config.ts # Route configuration -│ ├── store/ # Zustand stores -│ │ ├── authStore.ts # Authentication state -│ │ ├── plannerStore.ts # Planner module state -│ │ ├── siteStore.ts # Site selection state -│ │ └── aiRequestLogsStore.ts # AI request/response logs -│ ├── services/ # API clients -│ │ └── api.ts # fetchAPI, API functions -│ ├── hooks/ # Custom React hooks -│ │ ├── useProgressModal.ts # Progress modal for long-running tasks -│ │ └── useAuth.ts # Authentication hook -│ ├── layout/ # Layout components -│ │ └── AppLayout.tsx # Main app layout wrapper -│ ├── App.tsx # Root component with routing -│ └── main.tsx # Entry point -│ -└── docs/ # Documentation - └── ActiveDocs/ # Active documentation - ├── 01-ARCHITECTURE.md # This file - ├── 02-FRONTEND.md # Frontend documentation - ├── 03-BACKEND.md # Backend documentation - ├── 04-AI-FUNCTIONS.md # AI functions documentation - └── 05-ACCOUNT-USER-PLAN.md # Account/User/Plan documentation -``` - ---- - -## Key Architectural Patterns - -### Configuration System (Page-Local Config + Shared Snippets) - -**Rule**: Config = Page-Local, Snippets = Shared - -**Structure**: -``` -/pages/Planner/KeywordsPage.tsx -├── Imports snippets from /config/snippets/ -├── Defines page-local tableConfig, filterConfig, actionsConfig -└── Passes config to TablePageTemplate - -/config/snippets/ -├── columns.snippets.ts # statusColumn, titleColumn, etc. -├── filters.snippets.ts # statusFilter, dateRangeFilter, etc. -├── actions.snippets.ts # commonActions, bulkActions, etc. -``` - -**Benefits**: -- Reusable components across pages -- Page-specific customization -- Single source of truth for shared definitions - -### Base ViewSet Pattern - -**Backend Pattern**: All ViewSets inherit from base classes for consistent behavior. - -**Base Classes**: -- `AccountModelViewSet`: Automatic account filtering -- `SiteSectorModelViewSet`: Account + site/sector filtering + access control - -**Benefits**: -- Consistent access control -- Automatic account isolation -- Reduced code duplication - -### Celery Task Pattern - -**Pattern**: Long-running operations use Celery tasks with progress tracking. - -**Structure**: -1. API endpoint queues Celery task -2. Task updates progress via `update_state()` -3. Frontend polls `task_progress` endpoint -4. Progress displayed in modal - -**Benefits**: -- Non-blocking API responses -- Real-time progress updates -- Scalable background processing - -### AI Function Pattern - -**Pattern**: All AI functions follow consistent structure. - -**Structure**: -1. API Endpoint: Validates input, queues Celery task -2. Celery Task: Wraps core function with progress tracking -3. Core Function: Business logic, calls AIProcessor -4. AIProcessor: Makes API calls, returns structured data -5. Core Function: Saves results to database - -**Benefits**: -- Consistent error handling -- Progress tracking -- Reusable AI interface - ---- - -## Multi-Tenancy Architecture - -### Account Isolation - -**Principle**: All data is isolated by account. - -**Implementation**: -- All models inherit `AccountBaseModel` (has `account` ForeignKey) -- All ViewSets inherit `AccountModelViewSet` (filters by `request.account`) -- Middleware sets `request.account` from JWT token - -**Access Control**: -- Admin/Developer users: Bypass account filtering (see all accounts) -- System account users: Bypass account filtering (see all accounts) -- Regular users: Only see data from their account - -### Site/Sector Hierarchy - -**Structure**: -``` -Account (1) ──< (N) Site -Site (1) ──< (1-5) Sector -Sector (1) ──< (N) Keywords, Clusters, ContentIdeas, Tasks -``` - -**Implementation**: -- Models inherit `SiteSectorBaseModel` (has `site` and `sector` ForeignKeys) -- ViewSets inherit `SiteSectorModelViewSet` (filters by accessible sites) -- User access control via `User.get_accessible_sites()` - -**Site Access Control**: -- System account users: All active sites -- Developers: All active sites -- Owners/Admins: All sites in their account -- Editors/Viewers: Only sites granted via `SiteUserAccess` - ---- - -## Module Organization - -### Planner Module - -**Purpose**: Keyword management and content planning. - -**Models**: -- `Keywords`: Individual keywords with volume, difficulty, intent -- `Clusters`: Keyword clusters (groups of related keywords) -- `ContentIdeas`: Content ideas generated from clusters - -**ViewSets**: -- `KeywordViewSet`: CRUD + `auto_cluster` action -- `ClusterViewSet`: CRUD + `auto_generate_ideas` action -- `ContentIdeasViewSet`: CRUD operations - -**Tasks**: -- `auto_cluster_keywords_task`: AI-powered keyword clustering -- `auto_generate_ideas_task`: AI-powered content idea generation - -### Writer Module - -**Purpose**: Content generation and management. - -**Models**: -- `Tasks`: Content generation tasks -- `Content`: Generated content (HTML) -- `Images`: Generated images for tasks - -**ViewSets**: -- `TasksViewSet`: CRUD + `auto_generate_content`, `auto_generate_images` actions - -**Tasks**: -- `auto_generate_content_task`: AI-powered content generation -- `auto_generate_images_task`: AI-powered image generation - -### System Module - -**Purpose**: System settings, prompts, and integrations. - -**Models**: -- `AIPrompt`: AI prompt templates (clustering, ideas, content, images) -- `IntegrationSettings`: API keys and configuration (OpenAI, Runware, etc.) -- `AuthorProfile`: Writing style profiles -- `Strategy`: Content strategies per sector - -**ViewSets**: -- `AIPromptViewSet`: CRUD for prompts -- `IntegrationSettingsViewSet`: CRUD + `test_openai`, `test_runware`, `generate_image`, `task_progress` actions -- `AuthorProfileViewSet`: CRUD for author profiles -- `StrategyViewSet`: CRUD for strategies - -### Billing Module - -**Purpose**: Credits, transactions, and usage tracking. - -**Models**: -- `CreditTransaction`: Credit purchase/usage transactions -- `UsageLog`: Daily/monthly usage tracking - -**ViewSets**: -- `CreditTransactionViewSet`: CRUD for transactions -- `UsageLogViewSet`: Read-only usage logs - -**Services**: -- `CreditService`: Credit calculation and deduction logic - ---- - -## API Architecture - -### RESTful API Design - -**Base URL**: `/api/v1/` - -**Endpoint Structure**: -- `/api/v1/planner/keywords/` - Keywords CRUD -- `/api/v1/planner/keywords/auto_cluster/` - Auto-cluster action -- `/api/v1/planner/clusters/` - Clusters CRUD -- `/api/v1/planner/clusters/auto_generate_ideas/` - Auto-generate ideas action -- `/api/v1/writer/tasks/` - Tasks CRUD -- `/api/v1/writer/tasks/auto_generate_content/` - Auto-generate content action -- `/api/v1/system/settings/task_progress/{task_id}/` - Task progress polling - -### Authentication - -**Methods**: -- JWT (JSON Web Tokens) - Primary method -- Session-based auth - Fallback for admin - -**Flow**: -1. User signs in → Backend returns JWT token -2. Frontend stores token in localStorage -3. Frontend includes token in `Authorization: Bearer {token}` header -4. Backend middleware validates token and sets `request.user` and `request.account` - -### Response Format - -**Success Response**: -```json -{ - "success": true, - "data": { ... }, - "message": "Optional message" -} -``` - -**Error Response**: -```json -{ - "success": false, - "message": "Error message", - "errors": { ... } -} -``` - -### Pagination - -**Format**: Page-based pagination - -**Response**: -```json -{ - "count": 100, - "next": "http://api.example.com/api/v1/resource/?page=2", - "previous": null, - "results": [ ... ] -} -``` - ---- - -## Frontend Architecture - -### Component Hierarchy - -``` -App -└── AppLayout - ├── Sidebar (navigation) - ├── Header (user menu, notifications) - └── Main Content - └── Page Component - └── Template (DashboardTemplate, TablePageTemplate, etc.) - └── Components (DataTable, Filters, etc.) -``` - -### State Management - -**Zustand Stores**: -- `authStore`: Authentication state (user, token, account) -- `plannerStore`: Planner module state -- `siteStore`: Selected site/sector -- `aiRequestLogsStore`: AI request/response logs -- `pageSizeStore`: Table page size preference - -**Local State**: React `useState` for component-specific state - -### Routing - -**Structure**: React Router v6 with nested routes - -**Routes**: -- `/` - Home/Dashboard -- `/planner` - Planner Dashboard -- `/planner/keywords` - Keywords page -- `/planner/clusters` - Clusters page -- `/planner/ideas` - Ideas page -- `/writer` - Writer Dashboard -- `/writer/tasks` - Tasks page -- `/settings` - Settings pages - -**Protected Routes**: All routes except `/signin` and `/signup` require authentication - ---- - -## Backend Architecture - -### Model Inheritance Hierarchy - -``` -models.Model -└── AccountBaseModel (adds account ForeignKey) - └── SiteSectorBaseModel (adds site, sector ForeignKeys) - └── Keywords, Clusters, ContentIdeas, Tasks, etc. -``` - -### ViewSet Inheritance Hierarchy - -``` -viewsets.ModelViewSet -└── AccountModelViewSet (adds account filtering) - └── SiteSectorModelViewSet (adds site/sector filtering) - └── KeywordViewSet, ClusterViewSet, TasksViewSet, etc. -``` - -### Middleware Stack - -1. **SecurityMiddleware**: Django security middleware -2. **SessionMiddleware**: Session management -3. **AuthenticationMiddleware**: User authentication -4. **AccountContextMiddleware**: Sets `request.account` from JWT -5. **ResourceTrackerMiddleware**: Tracks API request metrics - -### Celery Task Architecture - -**Broker**: Redis - -**Workers**: Separate Celery worker processes - -**Task Structure**: -```python -@shared_task(bind=True) -def my_task(self, ...): - # Update progress - self.update_state(state='PROGRESS', meta={...}) - # Do work - result = do_work() - # Return result - return result -``` - -**Progress Tracking**: -- Frontend polls `/api/v1/system/settings/task_progress/{task_id}/` -- Backend returns task state and meta information -- Progress displayed in modal - ---- - -## Database Architecture - -### Core Tables - -- `igny8_accounts`: Account information -- `igny8_users`: User accounts -- `igny8_plans`: Subscription plans -- `igny8_subscriptions`: Active subscriptions -- `igny8_sites`: Sites within accounts -- `igny8_sectors`: Sectors within sites -- `igny8_industries`: Global industry templates -- `igny8_industry_sectors`: Industry sector templates - -### Planner Tables - -- `igny8_keywords`: Keywords -- `igny8_clusters`: Keyword clusters -- `igny8_content_ideas`: Content ideas - -### Writer Tables - -- `igny8_tasks`: Content generation tasks -- `igny8_content`: Generated content -- `igny8_images`: Generated images - -### System Tables - -- `igny8_ai_prompts`: AI prompt templates -- `igny8_integration_settings`: API keys and configuration -- `igny8_author_profiles`: Writing style profiles -- `igny8_strategies`: Content strategies - -### Billing Tables - -- `igny8_credit_transactions`: Credit transactions -- `igny8_usage_logs`: Usage tracking - -### Indexes - -**Account Isolation**: All tables have indexes on `account` - -**Site/Sector Filtering**: Tables with site/sector have composite indexes on `(account, site, sector)` - -**Performance**: Indexes on frequently queried fields (status, created_at, etc.) - ---- - -## Security Architecture - -### Authentication - -**JWT Tokens**: -- Signed with secret key -- Contains user ID and account ID -- Expires after configured time -- Stored in localStorage (frontend) - -**Session Auth**: -- Fallback for admin interface -- Django session framework - -### Authorization - -**Role-Based Access Control (RBAC)**: -- `developer`: Full system access -- `owner`: Full account access -- `admin`: Account admin access -- `editor`: Content editing access -- `viewer`: Read-only access - -**Access Control**: -- Account-level: Automatic filtering by `request.account` -- Site-level: Filtering by `user.get_accessible_sites()` -- Action-level: Permission checks in ViewSet actions - -### Data Isolation - -**Account Isolation**: -- All queries filtered by account -- Admin/Developer override for system accounts - -**Site Access Control**: -- Users can only access granted sites -- Admin/Developer override for all sites - -### API Security - -**CORS**: Configured for frontend domain - -**CSRF**: Enabled for session-based auth - -**Rate Limiting**: (Future implementation) - -**Input Validation**: DRF serializers validate all input - ---- - -## Deployment Architecture - -### Docker Compose Setup - -**Services**: -- `backend`: Django application (port 8010/8011) -- `frontend`: React application (port 5173/8021) -- `db`: PostgreSQL database -- `redis`: Redis for Celery broker and caching -- `caddy`: Reverse proxy (HTTPS on port 443) -- `celery-worker`: Celery worker process -- `celery-beat`: Celery beat scheduler (optional) - -### Environment Configuration - -**Backend**: -- `DJANGO_SETTINGS_MODULE`: Django settings module -- `DATABASE_URL`: PostgreSQL connection string -- `REDIS_URL`: Redis connection string -- `SECRET_KEY`: Django secret key -- `OPENAI_API_KEY`: OpenAI API key (fallback) - -**Frontend**: -- `VITE_API_URL`: Backend API URL -- `VITE_APP_NAME`: Application name - -### Scaling Considerations - -**Horizontal Scaling**: -- Multiple Celery workers -- Multiple backend instances (load balanced) -- Multiple frontend instances (static files) - -**Vertical Scaling**: -- Database connection pooling -- Redis connection pooling -- Celery worker concurrency - -### Monitoring - -**Application Monitoring**: -- ResourceTrackerMiddleware tracks API request metrics -- Celery task monitoring via Flower (optional) - -**Infrastructure Monitoring**: -- Portainer for container monitoring -- Database monitoring via PostgreSQL logs -- Redis monitoring via Redis CLI - ---- - -## Summary - -The IGNY8 architecture is built on: - -1. **Configuration-Driven Design**: Zero duplication, single source of truth -2. **Multi-Tenancy Foundation**: Complete account isolation with site/sector hierarchy -3. **Template System**: 4 universal templates for all page types -4. **Unified AI Interface**: Single AIProcessor for all AI operations -5. **Module-Based Organization**: Clear boundaries with shared utilities -6. **RESTful API**: Consistent API design with DRF -7. **Modern Frontend**: React + TypeScript with Zustand state management -8. **Scalable Backend**: Django + Celery for async processing -9. **Security First**: JWT auth, RBAC, data isolation -10. **Docker Deployment**: Containerized for easy deployment and scaling - -This architecture ensures scalability, maintainability, and extensibility while maintaining a clean separation of concerns across modules. - diff --git a/docs/GIT-AUTO-SYNC-ARCHITECTURE.md b/docs/GIT-AUTO-SYNC-ARCHITECTURE.md deleted file mode 100644 index e41a1a04..00000000 --- a/docs/GIT-AUTO-SYNC-ARCHITECTURE.md +++ /dev/null @@ -1,225 +0,0 @@ -# Git Auto-Sync Architecture - Complete Picture - -## Overview -This document explains how the automatic git synchronization works between Gitea (bare repository) and the VPS working copy. - -## Architecture Components - -### 1. **Bare Repository (Gitea Server)** -- **Host Path**: `/data/app/gitea/git/repositories/salman/igny8.git` -- **Container Path**: `/data/git/repositories/salman/igny8.git` -- **Type**: Bare repository (no working tree) -- **Purpose**: Stores all commits, branches, and git history -- **Access**: Served by Gitea at `https://git.igny8.com/salman/igny8.git` - -### 2. **Working Copy (VPS Deployment)** -- **Host Path**: `/data/app/igny8` -- **Container Path**: `/deploy/igny8` (mounted from host) -- **Type**: Full git repository with working tree -- **Purpose**: Live application code that runs on VPS -- **Remotes**: - - `origin`: `https://git.igny8.com/salman/igny8.git` (HTTPS remote) - - `deploy`: `/data/git/repositories/salman/igny8.git` (local bare repo path) - -### 3. **Docker Volume Mount** -```yaml -# From docker-compose.yml -volumes: - - ./gitea:/data # Gitea data directory - - /data/app/igny8:/deploy/igny8:rw # Mount working copy into container -``` - -This mount makes the VPS working copy accessible inside the Gitea container at `/deploy/igny8`. - -### 4. **Post-Receive Hook** -- **Location**: `/data/app/gitea/git/repositories/salman/igny8.git/hooks/post-receive` -- **Trigger**: Automatically runs after every `git push` to Gitea -- **Execution Context**: Runs inside Gitea container, in the bare repository directory - -## Complete Flow: Push to Auto-Update - -### Step 1: Developer Pushes to Gitea -```bash -git push origin main -# or -git push https://git.igny8.com/salman/igny8.git main -``` - -### Step 2: Gitea Receives Push -- Push arrives at Gitea server -- Gitea validates and stores commits in bare repository -- Bare repo is updated: `/data/app/gitea/git/repositories/salman/igny8.git` - -### Step 3: Post-Receive Hook Executes -The hook runs automatically with this context: -- **Current Directory**: `/data/git/repositories/salman/igny8.git` (bare repo) -- **Environment**: Inside Gitea container -- **Access**: Can access both bare repo and mounted working copy - -### Step 4: Hook Logic Breakdown - -```bash -# 1. Define paths -DEPLOY_DIR="/deploy/igny8" # Working copy (mounted from host) -SOURCE_REPO="$(pwd)" # Bare repo: /data/git/repositories/salman/igny8.git - -# 2. Check if working copy is a git repository -if [ -d "$DEPLOY_DIR/.git" ]; then - # Working copy exists and is a full git repo - - # 3. Set up git command with explicit paths - GIT_CMD="git --git-dir=$DEPLOY_DIR/.git --work-tree=$DEPLOY_DIR" - - # 4. Add 'deploy' remote if it doesn't exist - # This points to the bare repo (no network needed, direct file access) - if ! $GIT_CMD remote get-url deploy >/dev/null 2>&1; then - $GIT_CMD remote add deploy "$SOURCE_REPO" - fi - - # 5. Fetch latest commits from bare repo - # Fetches directly from file system, no HTTPS/SSH needed - $GIT_CMD fetch deploy main - - # 6. Reset working tree to latest commit - # This updates all files in /data/app/igny8 to match latest commit - $GIT_CMD reset --hard remotes/deploy/main - - # 7. Update origin/main tracking branch - # This is CRITICAL: tells git that local HEAD matches origin/main - # Without this, git status shows "ahead" even though files are synced - $GIT_CMD update-ref refs/remotes/origin/main HEAD - -else - # First time setup: working copy doesn't exist yet - # Checkout files from bare repo to create working copy - export GIT_DIR="$SOURCE_REPO" - export GIT_WORK_TREE="$DEPLOY_DIR" - git checkout -f main -fi -``` - -### Step 5: Result -- **Files Updated**: All files in `/data/app/igny8` now match latest commit -- **Git Status**: Shows "up to date with origin/main" (no phantom commits) -- **Application**: If using volume mounts, containers see changes immediately - -## Why This Design? - -### Problem We Solved -1. **Phantom Commits**: Previously, hook updated files but not git metadata, causing git to think local was "ahead" -2. **Manual Sync Required**: Had to manually `git pull` after every push -3. **Sync Issues**: Working tree and git metadata were out of sync - -### Solution -1. **Direct File System Access**: Hook uses `deploy` remote pointing to bare repo path (no network overhead) -2. **Complete Sync**: Updates both files AND git metadata (tracking branches) -3. **Automatic**: Runs on every push, no manual intervention needed - -## Key Git Concepts Used - -### 1. Bare Repository -- Repository without working tree -- Only contains `.git` contents (objects, refs, etc.) -- Used by servers to store code without checking out files - -### 2. Working Tree -- Directory with actual files you can edit -- Has `.git` directory with repository metadata -- This is what developers work with - -### 3. Remote Tracking Branches -- `refs/remotes/origin/main`: Git's record of what `origin/main` was last time we fetched -- When we update this to match HEAD, git knows we're in sync -- Without updating this, git compares HEAD to stale tracking branch → shows "ahead" - -### 4. Git Reset --hard -- Moves HEAD to specified commit -- Updates working tree to match that commit -- Discards any local changes (force update) - -## File System Layout - -``` -Host System: -├── /data/app/gitea/ -│ └── git/repositories/salman/igny8.git/ (bare repo) -│ ├── objects/ (all commits, trees, blobs) -│ ├── refs/ (branches, tags) -│ └── hooks/ -│ └── post-receive (auto-sync hook) -│ -└── /data/app/igny8/ (working copy) - ├── .git/ (git metadata) - │ ├── config (has 'origin' and 'deploy' remotes) - │ └── refs/ - │ └── remotes/ - │ ├── origin/main (tracking branch) - │ └── deploy/main (tracking branch) - ├── backend/ (actual application files) - ├── frontend/ - └── ... - -Inside Gitea Container: -├── /data/git/repositories/salman/igny8.git/ (same as host bare repo) -└── /deploy/igny8/ (mounted from /data/app/igny8) - └── (same contents as host /data/app/igny8) -``` - -## Checking Status - -### On Host VPS -```bash -cd /data/app/igny8 -git status # Should show "up to date with origin/main" -git log --oneline -5 # See recent commits -git remote -v # See remotes (origin + deploy) -``` - -### Hook Logs -```bash -docker exec gitea cat /data/gitea/log/hooks.log -``` - -### Manual Sync (if needed) -```bash -cd /data/app/igny8 -git pull origin main # Pull from HTTPS remote -# OR -git fetch deploy main # Fetch from local bare repo -git reset --hard remotes/deploy/main -``` - -## Troubleshooting - -### Issue: "Your branch is ahead of origin/main" -**Cause**: Hook didn't update `refs/remotes/origin/main` tracking branch -**Fix**: Hook should run `git update-ref refs/remotes/origin/main HEAD` (already in hook) - -### Issue: Files not updating after push -**Check**: -1. Hook logs: `docker exec gitea cat /data/gitea/log/hooks.log` -2. Hook executable: `ls -la /data/app/gitea/git/repositories/salman/igny8.git/hooks/post-receive` -3. Mount exists: `docker exec gitea ls -la /deploy/igny8` - -### Issue: Hook not running -**Check**: -1. Gitea logs: `docker logs gitea --tail 50` -2. Hook file exists and is executable -3. Push actually succeeded (check Gitea web UI) - -## Summary - -**The Complete Flow:** -1. Developer pushes → Gitea bare repo updated -2. Post-receive hook triggers automatically -3. Hook fetches from bare repo (via `deploy` remote) -4. Hook resets working copy to latest commit -5. Hook updates tracking branch metadata -6. VPS working copy is now in sync -7. Application containers see updated files (via volume mounts) - -**Key Innovation:** -- Uses local file system path (`deploy` remote) instead of HTTPS -- Updates both files AND git metadata -- Fully automatic, no manual steps required - diff --git a/docs/Architecture/MASTER-ARCHITECTURE.md b/docs/MASTER-ARCHITECTURE.md similarity index 100% rename from docs/Architecture/MASTER-ARCHITECTURE.md rename to docs/MASTER-ARCHITECTURE.md diff --git a/AI_FILES_ANALYSIS.md b/docs/ai-docs/AI_FILES_ANALYSIS.md similarity index 100% rename from AI_FILES_ANALYSIS.md rename to docs/ai-docs/AI_FILES_ANALYSIS.md diff --git a/AI_FUNCTIONS_AUDIT_REPORT.md b/docs/ai-docs/AI_FUNCTIONS_AUDIT_REPORT.md similarity index 100% rename from AI_FUNCTIONS_AUDIT_REPORT.md rename to docs/ai-docs/AI_FUNCTIONS_AUDIT_REPORT.md diff --git a/AI_MASTER_ARCHITECTURE.md b/docs/ai-docs/AI_MASTER_ARCHITECTURE.md similarity index 100% rename from AI_MASTER_ARCHITECTURE.md rename to docs/ai-docs/AI_MASTER_ARCHITECTURE.md diff --git a/igny8-ai-seo-wp-plugin/CHANGELOG_live.md b/igny8-ai-seo-wp-plugin/CHANGELOG_live.md new file mode 100644 index 00000000..6e5b427f --- /dev/null +++ b/igny8-ai-seo-wp-plugin/CHANGELOG_live.md @@ -0,0 +1,59 @@ +## [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 new file mode 100644 index 00000000..fc8f21c1 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/ai/_README.php @@ -0,0 +1,14 @@ + ['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 new file mode 100644 index 00000000..e6937e20 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/ai/modules-ai.php @@ -0,0 +1,1809 @@ + 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 new file mode 100644 index 00000000..b215d326 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/ai/openai-api.php @@ -0,0 +1,1729 @@ +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 new file mode 100644 index 00000000..a26ad494 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/ai/prompts-library.php @@ -0,0 +1,310 @@ + 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 new file mode 100644 index 00000000..77241e39 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/ai/runware-api.php @@ -0,0 +1,191 @@ + '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.036, // 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 new file mode 100644 index 00000000..45c8fa2a --- /dev/null +++ b/igny8-ai-seo-wp-plugin/ai/writer/images/image-generation.php @@ -0,0 +1,534 @@ + 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 new file mode 100644 index 00000000..666107d3 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/css/core-backup.css @@ -0,0 +1,3039 @@ +/* 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 new file mode 100644 index 00000000..666107d3 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/css/core.css @@ -0,0 +1,3039 @@ +/* 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 new file mode 100644 index 00000000..1c1e3d3a --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/css/image-injection.css @@ -0,0 +1,51 @@ +/** + * 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 new file mode 100644 index 00000000..aab49bea --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/js/core.js @@ -0,0 +1,4961 @@ +/** + * 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 new file mode 100644 index 00000000..d3dae4ce --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/js/image-queue-processor.js @@ -0,0 +1,436 @@ +/** + * 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 new file mode 100644 index 00000000..f669a0fd --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/shortcodes/_README.php @@ -0,0 +1,14 @@ + ''], $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 new file mode 100644 index 00000000..5dbfffc3 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/templates/igny8_clusters_template.csv @@ -0,0 +1,4 @@ +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 new file mode 100644 index 00000000..a165a451 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/templates/igny8_ideas_template.csv @@ -0,0 +1,4 @@ +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 new file mode 100644 index 00000000..7a5b750d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/assets/templates/igny8_keywords_template.csv @@ -0,0 +1,4 @@ +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 new file mode 100644 index 00000000..313ed5a2 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/_README.php @@ -0,0 +1,14 @@ + 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 new file mode 100644 index 00000000..c7777219 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/import-modal-tpl.php @@ -0,0 +1,67 @@ + + +
          +
          +
          +

          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 new file mode 100644 index 00000000..2515500e --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/kpi-tpl.php @@ -0,0 +1,140 @@ + $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 new file mode 100644 index 00000000..955c500d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/pagination-tpl.php @@ -0,0 +1,136 @@ + 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 new file mode 100644 index 00000000..2d5f9eda --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/table-tpl.php @@ -0,0 +1,870 @@ + [ + '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 new file mode 100644 index 00000000..a3bde6d5 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/admin/global-helpers.php @@ -0,0 +1,1084 @@ +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 new file mode 100644 index 00000000..882bed6a --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/admin/init.php @@ -0,0 +1,135 @@ + $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 new file mode 100644 index 00000000..ddf8018f --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/admin/menu.php @@ -0,0 +1,356 @@ +'; + $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 new file mode 100644 index 00000000..85136547 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/admin/meta-boxes.php @@ -0,0 +1,387 @@ +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 new file mode 100644 index 00000000..2d940f2e --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/admin/module-manager-class.php @@ -0,0 +1,181 @@ +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 new file mode 100644 index 00000000..90aef380 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-handlers.php @@ -0,0 +1,1610 @@ + 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 new file mode 100644 index 00000000..a8294f44 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/cron/igny8-cron-master-dispatcher.php @@ -0,0 +1,384 @@ +"; + 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 new file mode 100644 index 00000000..d939ea47 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/db/db-migration.php @@ -0,0 +1,253 @@ +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 new file mode 100644 index 00000000..059f9122 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/db/db.php @@ -0,0 +1,970 @@ +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 new file mode 100644 index 00000000..1ebd709c --- /dev/null +++ b/igny8-ai-seo-wp-plugin/core/global-layout.php @@ -0,0 +1,463 @@ + +
          + + + + + +
          + + +
          + +
          +
          +
          + +
          +
          +
          + + +
          +
          +

          '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 new file mode 100644 index 00000000..af1bc34e --- /dev/null +++ b/igny8-ai-seo-wp-plugin/debug/_README.php @@ -0,0 +1,14 @@ + +
          +
          +

          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 new file mode 100644 index 00000000..df162345 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/docs/COMPLETE_WORKFLOWS_DOCUMENTATION.md @@ -0,0 +1,723 @@ +# 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 new file mode 100644 index 00000000..30ad91eb --- /dev/null +++ b/igny8-ai-seo-wp-plugin/docs/FILE_TREE.txt @@ -0,0 +1,122 @@ +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 new file mode 100644 index 00000000..0fe85ba9 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/docs/IGNY8_PAGES_TABLE.md @@ -0,0 +1,162 @@ +# 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 new file mode 100644 index 00000000..c0fc9414 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/docs/IGNY8_SNAPSHOT_V5.2.0.md @@ -0,0 +1,523 @@ +# 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 new file mode 100644 index 0000000000000000000000000000000000000000..0bf54a181a6e5114ccbb0b72ad7bb1c0884b5ad5 GIT binary patch literal 130736 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 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 new file mode 100644 index 00000000..1a12e54b --- /dev/null +++ b/igny8-ai-seo-wp-plugin/docs/TROUBLESHOOTING_Converting_to_blocks_and_image_shortcode_injection.md @@ -0,0 +1,308 @@ +# 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 new file mode 100644 index 00000000..a17d71c2 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/docs/_README.php @@ -0,0 +1,14 @@ +]+>', "`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 new file mode 100644 index 00000000..dbeca0c4 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/flows/sync-ajax.php @@ -0,0 +1,485 @@ +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 new file mode 100644 index 00000000..7d46db03 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/flows/sync-functions.php @@ -0,0 +1,673 @@ + 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 new file mode 100644 index 00000000..7e2650c1 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/flows/sync-hooks.php @@ -0,0 +1,99 @@ +"; + 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 new file mode 100644 index 00000000..e0862f1f --- /dev/null +++ b/igny8-ai-seo-wp-plugin/igny8.php @@ -0,0 +1,246 @@ + '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 new file mode 100644 index 00000000..650ed56c --- /dev/null +++ b/igny8-ai-seo-wp-plugin/install.php @@ -0,0 +1,43 @@ + +
          +

          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 new file mode 100644 index 00000000..b0551bbb --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/_README.php @@ -0,0 +1,14 @@ + + +
          +
          + 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 new file mode 100644 index 00000000..5e0caa41 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/export-modal-tpl.php @@ -0,0 +1,77 @@ + + +
          +
          +
          +

          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 new file mode 100644 index 00000000..3d53a88d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/filters-tpl.php @@ -0,0 +1,136 @@ + + * + * + * @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 new file mode 100644 index 00000000..372aee15 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/components/forms-tpl.php @@ -0,0 +1,176 @@ +
          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 new file mode 100644 index 00000000..445857da --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/config/_README.php @@ -0,0 +1,14 @@ + [ + '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 new file mode 100644 index 00000000..2e70863b --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/config/forms-config.php @@ -0,0 +1,638 @@ + [ + '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 new file mode 100644 index 00000000..6dc0f46c --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/config/import-export-config.php @@ -0,0 +1,150 @@ + [ + '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 new file mode 100644 index 00000000..55324462 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/config/kpi-config.php @@ -0,0 +1,581 @@ + [ + '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 new file mode 100644 index 00000000..3803fd87 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/config/tables-config.php @@ -0,0 +1,989 @@ + [ + '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 new file mode 100644 index 00000000..494fe4f1 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/help/docs.php @@ -0,0 +1,841 @@ + + + +
          +
          +
          +

          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 new file mode 100644 index 00000000..ac4a891b --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/help/function-testing.php @@ -0,0 +1,122 @@ + +
          +

          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 new file mode 100644 index 00000000..5abdd24f --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/help/help.php @@ -0,0 +1,834 @@ + + +
          + + +
          +
          +
          +

          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 new file mode 100644 index 00000000..d9d98cf3 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/help/system-testing.php @@ -0,0 +1,329 @@ + +
          +

          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 new file mode 100644 index 00000000..0650dd8e --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/home.php @@ -0,0 +1,231 @@ + +
          + +
          +
          + +
          +

          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 new file mode 100644 index 00000000..38a15de4 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/planner/clusters.php @@ -0,0 +1,84 @@ + +
          +
          + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..ad9e4aea --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/planner/ideas.php @@ -0,0 +1,84 @@ + +
          +
          + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..3fc67974 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/planner/keywords.php @@ -0,0 +1,84 @@ + +
          +
          + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..e8d9068d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/planner/planner.php @@ -0,0 +1,653 @@ +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 new file mode 100644 index 00000000..41958afa --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/settings/general-settings.php @@ -0,0 +1,676 @@ +

          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 new file mode 100644 index 00000000..62b2cdc8 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/settings/import-export.php @@ -0,0 +1,267 @@ + '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 new file mode 100644 index 00000000..03729271 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/settings/integration.php @@ -0,0 +1,744 @@ + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..b932c5cf --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/settings/schedules.php @@ -0,0 +1,297 @@ + $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 new file mode 100644 index 00000000..63b3d9e0 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/settings/status.php @@ -0,0 +1,353 @@ + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..9f7baa87 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/thinker/image-testing.php @@ -0,0 +1,917 @@ + 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 new file mode 100644 index 00000000..b76dd280 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/thinker/profile.php @@ -0,0 +1,344 @@ + +
          +
          + + + +
          +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..a659907d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/thinker/strategies.php @@ -0,0 +1,408 @@ + +
          +
          + + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..6208938d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/writer/published.php @@ -0,0 +1,90 @@ + +
          +
          + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..efeb103d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/writer/tasks.php @@ -0,0 +1,90 @@ + +
          +
          + + +
          +
          +
          +
          +

          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 new file mode 100644 index 00000000..9126ccc5 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/modules/writer/writer.php @@ -0,0 +1,660 @@ +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 new file mode 100644 index 00000000..1222b7c0 --- /dev/null +++ b/igny8-ai-seo-wp-plugin/shortcodes/ai-shortcodes.php @@ -0,0 +1,147 @@ + $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 new file mode 100644 index 00000000..72b86c2d --- /dev/null +++ b/igny8-ai-seo-wp-plugin/uninstall.php @@ -0,0 +1,208 @@ +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();