diff --git a/backend/igny8_core/business/content/services/validation_service.py b/backend/igny8_core/business/content/services/validation_service.py index ccce86e1..ee7f4597 100644 --- a/backend/igny8_core/business/content/services/validation_service.py +++ b/backend/igny8_core/business/content/services/validation_service.py @@ -26,17 +26,7 @@ class ContentValidationService: """ errors = [] - # Stage 3: Enforce "no cluster, no task" rule when feature flag enabled - from django.conf import settings - if getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False): - if not task.cluster: - errors.append({ - 'field': 'cluster', - 'code': 'missing_cluster', - 'message': 'Task must be associated with a cluster before content generation', - }) - - # Stage 3: Validate entity_type is set + # Validate entity_type is set if not task.content_type: errors.append({ 'field': 'content_type', diff --git a/backend/igny8_core/business/planning/services/clustering_service.py b/backend/igny8_core/business/planning/services/clustering_service.py index bf6f77c3..2e03a05a 100644 --- a/backend/igny8_core/business/planning/services/clustering_service.py +++ b/backend/igny8_core/business/planning/services/clustering_service.py @@ -52,26 +52,12 @@ class ClusteringService: # Delegate to AI task from igny8_core.ai.tasks import run_ai_task - from django.conf import settings payload = { 'ids': keyword_ids, 'sector_id': sector_id } - # Stage 1: When USE_SITE_BUILDER_REFACTOR is enabled, payload can include - # taxonomy hints and dimension metadata for enhanced clustering. - # TODO (Stage 2/3): Enhance clustering to collect and use: - # - Taxonomy hints from SiteBlueprintTaxonomy - # - Dimension metadata (context_type, dimension_meta) for clusters - # - Attribute values from Keywords.attribute_values - if getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False): - logger.info( - f"Clustering with refactor enabled: {len(keyword_ids)} keywords, " - f"sector_id={sector_id}, account_id={account.id}" - ) - # Future: Add taxonomy hints and dimension metadata to payload - try: if hasattr(run_ai_task, 'delay'): # Celery available - queue async diff --git a/backend/igny8_core/modules/planner/serializers.py b/backend/igny8_core/modules/planner/serializers.py index e62b42b3..89893c99 100644 --- a/backend/igny8_core/modules/planner/serializers.py +++ b/backend/igny8_core/modules/planner/serializers.py @@ -1,5 +1,4 @@ from rest_framework import serializers -from django.conf import settings from .models import Keywords, Clusters, ContentIdeas from igny8_core.auth.models import SeedKeyword @@ -69,12 +68,6 @@ class KeywordSerializer(serializers.ModelSerializer): ] read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'keyword', 'volume', 'difficulty', 'country'] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Only include Stage 1 fields when feature flag is enabled - if getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False): - self.fields['attribute_values'] = serializers.JSONField(read_only=True) - def validate(self, attrs): """Validate that either seed_keyword_id OR custom keyword fields are provided""" # For create operations, need either seed_keyword_id OR custom keyword diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 4b9474e4..f76701b4 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -22,7 +22,6 @@ DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' # Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=True to enable unified exception handler # Set IGNY8_DEBUG_THROTTLE=True to bypass rate limiting in development IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', str(DEBUG)).lower() == 'true' -USE_SITE_BUILDER_REFACTOR = os.getenv('USE_SITE_BUILDER_REFACTOR', 'false').lower() == 'true' ALLOWED_HOSTS = [ '*', # Allow all hosts for flexibility diff --git a/docs/fixes/phase2-module-activation.md b/docs/fixes/phase2-module-activation.md new file mode 100644 index 00000000..8ba14448 --- /dev/null +++ b/docs/fixes/phase2-module-activation.md @@ -0,0 +1,152 @@ +# Phase 2 Module Activation Guide + +> Reference document for activating disabled modules (Linker, Optimizer, SiteBuilder) + +## Current Status (as of December 2025) + +| Module | Status | Backend Flag | Migration | +|--------|--------|--------------|-----------| +| **SiteBuilder** | ❌ DEPRECATED | `site_builder_enabled` | Disabled via 0011 | +| **Linker** | ⏸️ Phase 2 | `linker_enabled` | Disabled via 0011 | +| **Optimizer** | ⏸️ Phase 2 | `optimizer_enabled` | Disabled via 0011 | + +--- + +## How Module Disabling Works + +### 1. Database Flag (GlobalIntegrationSettings) +```python +# backend/igny8_core/modules/system/global_settings_models.py +site_builder_enabled = models.BooleanField(default=False) +linker_enabled = models.BooleanField(default=False) +optimizer_enabled = models.BooleanField(default=False) +``` + +### 2. Migration Set Defaults +```python +# backend/igny8_core/modules/system/migrations/0011_disable_phase2_modules.py +# Sets all three modules to disabled for existing records +``` + +### 3. API Returns Settings +```python +# backend/igny8_core/modules/system/settings_views.py +# GET /api/module-settings/ returns enabled/disabled status +``` + +### 4. Frontend Checks Settings +```typescript +// frontend/src/store/moduleStore.ts +// useModuleStore.isModuleEnabled('linker') → checks API response +``` + +### 5. Sidebar Hides Menu Items +```tsx +// frontend/src/layout/AppSidebar.tsx +if (isModuleEnabled('linker')) { + // Add menu item +} +``` + +--- + +## Activation Steps for Phase 2 + +### Option A: Via Django Admin (Recommended) + +1. Log into Django Admin (`/admin/`) +2. Navigate to **System → Global Integration Settings** +3. Edit the singleton record +4. Set `linker_enabled` or `optimizer_enabled` to `True` +5. Save + +### Option B: Via Database + +```sql +UPDATE system_globalintegrationsettings +SET linker_enabled = TRUE +WHERE id = 1; +``` + +### Option C: Via Management Command (TBD) + +```bash +python manage.py enable_module linker +python manage.py enable_module optimizer +``` + +--- + +## Pre-Activation Checklist + +Before enabling a Phase 2 module: + +### Linker Module +- [ ] Verify `modules/linker/views.py` ViewSet is functional +- [ ] Verify `pages/Linker/` frontend pages exist +- [ ] Test API endpoints manually +- [ ] Add route protection for `/linker/*` paths +- [ ] Update documentation status + +### Optimizer Module +- [ ] Verify `modules/optimizer/views.py` ViewSet is functional +- [ ] Verify `business/optimization/` services work +- [ ] Verify `ai/functions/optimize.py` AI function +- [ ] Verify `pages/Optimizer/` frontend pages exist +- [ ] Test API endpoints manually +- [ ] Add route protection for `/optimizer/*` paths +- [ ] Update documentation status + +--- + +## Route Protection (TODO for Phase 2) + +Currently, direct URL access (e.g., `/linker`) still works even when module is disabled. + +### Recommended Implementation: + +```tsx +// frontend/src/components/common/ModuleGuard.tsx +export function ModuleGuard({ module, children }: { module: string; children: React.ReactNode }) { + const { isModuleEnabled } = useModuleStore(); + + if (!isModuleEnabled(module)) { + return ; + } + + return <>{children}; +} + +// In routes: + + + +} /> +``` + +--- + +## SiteBuilder (DEPRECATED) + +**Do NOT activate SiteBuilder.** This module is deprecated and code is being removed. + +### Removed Items (Task 5.1) +- ✅ `frontend/src/__tests__/sites/` - Test directory deleted +- ✅ `USE_SITE_BUILDER_REFACTOR` - Feature flag removed from settings.py +- ✅ Feature flag checks - Removed from clustering_service.py, validation_service.py, serializers.py +- ✅ Tasks.tsx - Removed SiteBuilder filter logic + +### Remaining References (Documentation Only) +- Migration comments (safe to keep) +- CHANGELOG.md entries (historical) +- IGNY8-APP.md (documents deprecated status) +- Database field (kept for backward compatibility) + +--- + +## Related Documentation + +- [LINKER.md](../10-MODULES/LINKER.md) - Full Linker module docs +- [OPTIMIZER.md](../10-MODULES/OPTIMIZER.md) - Full Optimizer module docs +- [SYSTEM-SETTINGS.md](../10-MODULES/SYSTEM-SETTINGS.md) - Settings model reference diff --git a/frontend/src/__tests__/sites/BulkGeneration.test.tsx b/frontend/src/__tests__/sites/BulkGeneration.test.tsx deleted file mode 100644 index 38e37072..00000000 --- a/frontend/src/__tests__/sites/BulkGeneration.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Tests for Bulk Generation UI - * Phase 5: Sites Renderer & Bulk Generation - */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { builderApi } from '../../../services/siteBuilder.api'; - -// Mock API -vi.mock('../../../services/siteBuilder.api'); - -describe('Bulk Generation', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('generates all pages when button clicked', async () => { - // Test: Bulk page generation works - const mockGenerate = vi.fn().mockResolvedValue({ - success: true, - pages_queued: 5, - task_ids: [1, 2, 3, 4, 5], - }); - - (builderApi as any).generateAllPages = mockGenerate; - - // This would be tested in the actual component - expect(mockGenerate).toBeDefined(); - }); - - it('tracks progress during generation', async () => { - // Test: Progress tracking works - const mockProgress = { - pages_queued: 5, - task_ids: [1, 2, 3, 4, 5], - celery_task_id: 'task-123', - }; - - expect(mockProgress.pages_queued).toBe(5); - expect(mockProgress.task_ids).toHaveLength(5); - }); - - it('creates tasks for selected pages only', async () => { - // Test: Selected pages can be generated - const selectedPageIds = [1, 3, 5]; - const mockGenerate = vi.fn().mockResolvedValue({ - success: true, - pages_queued: 3, - task_ids: selectedPageIds, - }); - - expect(selectedPageIds).toHaveLength(3); - }); - - it('force regenerates when option selected', async () => { - // Test: Force regenerate works - const mockGenerate = vi.fn().mockResolvedValue({ - success: true, - pages_queued: 5, - task_ids: [1, 2, 3, 4, 5], - }); - - await mockGenerate(1, { force: true }); - expect(mockGenerate).toHaveBeenCalledWith(1, { force: true }); - }); -}); - diff --git a/frontend/src/__tests__/sites/ComponentLibrary.test.tsx b/frontend/src/__tests__/sites/ComponentLibrary.test.tsx deleted file mode 100644 index cc9c659e..00000000 --- a/frontend/src/__tests__/sites/ComponentLibrary.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Tests for Component Library - * Phase 7: UI Components & Prompt Management - */ -import { describe, it, expect } from 'vitest'; -import { render } from '@testing-library/react'; -import { HeroBlock } from '../../components/shared/blocks/HeroBlock'; -import { FeatureGridBlock } from '../../components/shared/blocks/FeatureGridBlock'; -import { StatsPanel } from '../../components/shared/blocks/StatsPanel'; - -describe('Component Library', () => { - it('all components render correctly', () => { - // Test: All components render correctly - const { container: hero } = render( - - ); - expect(hero).toBeDefined(); - - const { container: features } = render( - - ); - expect(features).toBeDefined(); - - const { container: stats } = render( - - ); - expect(stats).toBeDefined(); - }); - - it('component library is complete', () => { - // Test: Component library is complete - const components = [ - 'HeroBlock', - 'FeatureGridBlock', - 'StatsPanel', - 'ServicesBlock', - 'ProductsBlock', - 'TestimonialsBlock', - 'ContactFormBlock', - 'CTABlock', - 'ImageGalleryBlock', - 'VideoBlock', - 'TextBlock', - 'QuoteBlock', - ]; - - expect(components.length).toBeGreaterThanOrEqual(12); - }); - - it('no duplicate components exist', () => { - // Test: No duplicate components - const components = [ - 'HeroBlock', - 'FeatureGridBlock', - 'StatsPanel', - 'HeroBlock', // Duplicate - ]; - - const unique = [...new Set(components)]; - expect(unique.length).toBeLessThan(components.length); - }); - - it('components are accessible', () => { - // Test: All components are accessible - const { container } = render( - - ); - - const heading = container.querySelector('h2'); - expect(heading).toBeDefined(); - expect(heading?.textContent).toBe('Test'); - }); - - it('components are responsive', () => { - // Test: All components are responsive - // Responsiveness is tested via CSS, not JS - // This test verifies responsive classes exist - const responsiveClasses = ['sm:', 'md:', 'lg:', 'xl:']; - expect(responsiveClasses.length).toBeGreaterThan(0); - }); -}); - diff --git a/frontend/src/__tests__/sites/Integration.test.tsx b/frontend/src/__tests__/sites/Integration.test.tsx deleted file mode 100644 index 8beddd26..00000000 --- a/frontend/src/__tests__/sites/Integration.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Tests for Site Integration UI - * Phase 6: Site Integration & Multi-Destination Publishing - */ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import PlatformSelector from '../../components/integration/PlatformSelector'; -import IntegrationStatus from '../../components/integration/IntegrationStatus'; - -describe('Site Integration', () => { - it('platform selector allows selecting platform', () => { - // Test: Site integrations work correctly - const handleChange = vi.fn(); - const { container } = render( - - ); - - const select = container.querySelector('select'); - expect(select).toBeDefined(); - }); - - it('integration status displays sync status', () => { - // Test: Site integrations work correctly - const { container } = render( - - ); - - expect(container).toBeDefined(); - }); - - it('publishing settings save correctly', async () => { - // Test: Publishing settings work - const mockSave = vi.fn().mockResolvedValue({ success: true }); - - await mockSave({ - defaultDestinations: ['sites', 'wordpress'], - autoPublishEnabled: true, - }); - - expect(mockSave).toHaveBeenCalled(); - }); - - it('multi-destination publishing works', async () => { - // Test: Multi-destination publishing works - const destinations = ['sites', 'wordpress', 'shopify']; - const mockPublish = vi.fn().mockResolvedValue({ - success: true, - results: destinations.map(d => ({ destination: d, success: true })), - }); - - const result = await mockPublish(1, destinations); - expect(result.success).toBe(true); - expect(result.results).toHaveLength(3); - }); -}); - diff --git a/frontend/src/__tests__/sites/LayoutSystem.test.tsx b/frontend/src/__tests__/sites/LayoutSystem.test.tsx deleted file mode 100644 index aa526433..00000000 --- a/frontend/src/__tests__/sites/LayoutSystem.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Tests for Layout System - * Phase 7: UI Components & Prompt Management - */ -import { describe, it, expect } from 'vitest'; -import { render } from '@testing-library/react'; -import LayoutSelector from '../../components/sites/LayoutSelector'; -import LayoutPreview from '../../components/sites/LayoutPreview'; - -describe('Layout System', () => { - it('layout selector displays all layouts', () => { - // Test: Layout system works - const layouts = [ - { id: 'default', name: 'Default', category: 'marketing' }, - { id: 'minimal', name: 'Minimal', category: 'marketing' }, - { id: 'magazine', name: 'Magazine', category: 'blog' }, - { id: 'ecommerce', name: 'Ecommerce', category: 'ecommerce' }, - { id: 'portfolio', name: 'Portfolio', category: 'portfolio' }, - { id: 'blog', name: 'Blog', category: 'blog' }, - { id: 'corporate', name: 'Corporate', category: 'corporate' }, - ]; - - expect(layouts).toHaveLength(7); - }); - - it('layout preview renders correctly', () => { - // Test: Layout system works - const { container } = render( - - ); - - expect(container).toBeDefined(); - }); - - it('template system works', () => { - // Test: Template system works - const templates = [ - { id: 'marketing', name: 'Marketing Template' }, - { id: 'landing', name: 'Landing Template' }, - { id: 'blog', name: 'Blog Template' }, - ]; - - expect(templates).toHaveLength(3); - }); - - it('cms styling system works', () => { - // Test: CMS styling system works - const stylePresets = ['modern', 'classic', 'minimal', 'bold', 'elegant', 'tech']; - const colorSchemes = ['blue', 'purple', 'green', 'dark']; - const typographyPresets = ['modern', 'classic', 'editorial', 'minimal', 'tech']; - - expect(stylePresets.length).toBeGreaterThan(0); - expect(colorSchemes.length).toBeGreaterThan(0); - expect(typographyPresets.length).toBeGreaterThan(0); - }); -}); - diff --git a/frontend/src/__tests__/sites/PromptManagement.test.tsx b/frontend/src/__tests__/sites/PromptManagement.test.tsx deleted file mode 100644 index 80340f4a..00000000 --- a/frontend/src/__tests__/sites/PromptManagement.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Tests for Prompt Management UI - * Phase 7: UI Components & Prompt Management - */ -import { describe, it, expect, vi } from 'vitest'; - -describe('Prompt Management', () => { - it('prompt management UI loads prompts', async () => { - // Test: Prompt management UI works - const mockPrompts = [ - { prompt_type: 'site_structure_generation', prompt_value: 'Test prompt' }, - ]; - - expect(mockPrompts).toHaveLength(1); - expect(mockPrompts[0].prompt_type).toBe('site_structure_generation'); - }); - - it('site builder section displays in prompts page', () => { - // Test: Prompt management UI works - const promptTypes = [ - 'clustering', - 'ideas', - 'content_generation', - 'site_structure_generation', - ]; - - const siteBuilderPrompts = promptTypes.filter(t => t === 'site_structure_generation'); - expect(siteBuilderPrompts).toHaveLength(1); - }); - - it('prompt editor saves site structure generation prompt', async () => { - // Test: Prompt management UI works - const mockSave = vi.fn().mockResolvedValue({ success: true }); - - await mockSave({ - prompt_type: 'site_structure_generation', - prompt_value: 'Updated prompt', - }); - - expect(mockSave).toHaveBeenCalled(); - }); -}); - diff --git a/frontend/src/__tests__/sites/SiteManagement.test.tsx b/frontend/src/__tests__/sites/SiteManagement.test.tsx deleted file mode 100644 index b38bc36e..00000000 --- a/frontend/src/__tests__/sites/SiteManagement.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Tests for Site Management UI - * Phase 6: Site Integration & Multi-Destination Publishing - */ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; - -describe('Site Management', () => { - it('site management dashboard displays sites', () => { - // Test: Site management UI works (core features) - const mockSites = [ - { id: 1, name: 'Site 1', status: 'active' }, - { id: 2, name: 'Site 2', status: 'active' }, - ]; - - expect(mockSites).toHaveLength(2); - expect(mockSites[0].name).toBe('Site 1'); - }); - - it('site content editor loads pages', async () => { - // Test: Site management UI works (core features) - const mockPages = [ - { id: 1, title: 'Home', slug: 'home' }, - { id: 2, title: 'About', slug: 'about' }, - ]; - - expect(mockPages).toHaveLength(2); - }); - - it('page manager allows reordering', () => { - // Test: Site management UI works (core features) - const pages = [ - { id: 1, order: 1 }, - { id: 2, order: 2 }, - { id: 3, order: 3 }, - ]; - - // Simulate drag-drop reorder - const reordered = [pages[2], pages[0], pages[1]]; - expect(reordered[0].id).toBe(3); - }); - - it('task integration shows site builder tasks', () => { - // Test: Task integration works - const mockTasks = [ - { id: 1, title: '[Site Builder] Home Page', status: 'queued' }, - { id: 2, title: '[Site Builder] About Page', status: 'queued' }, - ]; - - const siteBuilderTasks = mockTasks.filter(t => t.title.startsWith('[Site Builder]')); - expect(siteBuilderTasks).toHaveLength(2); - }); -}); - diff --git a/frontend/src/__tests__/sites/SiteManagementAdvanced.test.tsx b/frontend/src/__tests__/sites/SiteManagementAdvanced.test.tsx deleted file mode 100644 index 9e0fcda4..00000000 --- a/frontend/src/__tests__/sites/SiteManagementAdvanced.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Tests for Advanced Site Management - * Phase 7: UI Components & Prompt Management - */ -import { describe, it, expect, vi } from 'vitest'; - -describe('Advanced Site Management', () => { - it('site management works end-to-end', async () => { - // Test: Site management works end-to-end (advanced features) - const workflow = [ - 'load_sites', - 'select_site', - 'view_pages', - 'edit_content', - 'save_changes', - 'preview_site', - ]; - - expect(workflow).toHaveLength(6); - }); - - it('site preview loads deployment URL', async () => { - // Test: Site management works end-to-end (advanced features) - const mockDeployment = { - id: 1, - deployment_url: 'https://test-site.igny8.com', - status: 'deployed', - }; - - expect(mockDeployment.deployment_url).toBeDefined(); - expect(mockDeployment.status).toBe('deployed'); - }); - - it('site settings saves SEO metadata', async () => { - // Test: Site management works end-to-end (advanced features) - const mockSave = vi.fn().mockResolvedValue({ success: true }); - - await mockSave({ - seo_metadata: { - meta_title: 'Test Site', - meta_description: 'Test Description', - og_title: 'Test OG Title', - }, - }); - - expect(mockSave).toHaveBeenCalled(); - }); -}); - diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 26fa74e0..42e5a2b1 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -108,19 +108,13 @@ export const createTasksPageConfig = ( toggleContentKey: 'description', toggleContentLabel: 'Idea & Content Outline', render: (value: string, row: Task) => { - const isSiteBuilder = value?.startsWith('[Site Builder]'); - const displayTitle = isSiteBuilder && value ? value.replace('[Site Builder] ', '') : (value || 'Untitled'); + const displayTitle = value || 'Untitled'; return (
{displayTitle} - {isSiteBuilder && ( - - Site Builder - - )}
); }, diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index b0f0ea85..a0ec9697 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -160,15 +160,8 @@ export default function Tasks() { try { const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at'; - // Build search term - combine user search with Site Builder filter if needed - let finalSearchTerm = searchTerm; - if (sourceFilter === 'site_builder') { - // If user has a search term, combine it with Site Builder prefix - finalSearchTerm = searchTerm ? `[Site Builder] ${searchTerm}` : '[Site Builder]'; - } - const filters: TasksFilters = { - ...(finalSearchTerm && { search: finalSearchTerm }), + ...(searchTerm && { search: searchTerm }), ...(statusFilter && { status: statusFilter }), ...(clusterFilter && { cluster_id: clusterFilter }), ...(structureFilter && { content_structure: structureFilter }),