diff --git a/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md b/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md new file mode 100644 index 00000000..d0c3e2aa --- /dev/null +++ b/KEYWORDS-CLUSTERS-IDEAS-COMPLETE-MAPPING.md @@ -0,0 +1,263 @@ +# Keywords, Clusters & Ideas - Complete Field Mapping + +**Date:** December 3, 2025 + +--- + +## πŸ“Š KEYWORDS - Complete Field Reference + +### Backend Model Fields +**File:** `backend/igny8_core/business/planning/models.py` + +| Field Name | Type | Default | Required | Choices | Notes | +|------------|------|---------|----------|---------|-------| +| `id` | Integer PK | Auto | βœ… | - | Primary key | +| `seed_keyword` | ForeignKey | - | βœ… | - | Links to SeedKeyword (PROTECT on delete) | +| `volume_override` | Integer | NULL | ❌ | - | Site-specific override (uses seed_keyword.volume if NULL) | +| `difficulty_override` | Integer | NULL | ❌ | - | Site-specific override (uses seed_keyword.difficulty if NULL) | +| `attribute_values` | JSONField | [] | ❌ | - | Optional metadata (product specs, modifiers) | +| `cluster` | ForeignKey | NULL | ❌ | - | Parent cluster (SET_NULL on delete) | +| `status` | CharField(50) | `pending` | βœ… | `pending`, `active`, `archived` | Keyword status | +| `site` | ForeignKey | - | βœ… | - | Owner site (inherited) | +| `sector` | ForeignKey | - | βœ… | - | Owner sector (inherited) | +| `account` | ForeignKey | - | βœ… | - | Owner account (inherited) | +| `created_at` | DateTime | Auto | βœ… | - | Auto-generated | +| `updated_at` | DateTime | Auto | βœ… | - | Auto-updated | + +### Frontend Table Columns +**File:** `frontend/src/config/pages/keywords.config.tsx` + +| Column Key | Label | Visible Default | Sortable | Render Notes | +|----------|--------|-----------------|----------|--------------| +| `keyword` | Keyword | βœ… Yes | βœ… Yes | From seed_keyword.keyword (links to Keywords page) | +| `sector_name` | Sector | Conditional* | βœ… Yes | Badge (blue) - shown when viewing all sectors | +| `volume` | Volume | βœ… Yes | βœ… Yes | Formatted as number (e.g., 1,250) | +| `cluster_name` | Cluster | βœ… Yes | βœ… Yes | Parent cluster name or "-" | +| `difficulty` | Difficulty | βœ… Yes | βœ… Yes | Badge (1-5): 1-2=green, 3=amber, 4-5=red | +| `intent` | Intent | βœ… Yes | βœ… Yes | Badge colors: Transactional/Commercial=green, Navigational=amber | +| `status` | Status | βœ… Yes | βœ… Yes | Badge: pending=amber, active=green, archived=red | +| `created_at` | Created | βœ… Yes | βœ… Yes | Relative date (e.g., "2 hours ago") | +| (Hidden by default) | | | | | +| `updated_at` | Updated | ❌ No | βœ… Yes | Relative date | + +### Frontend Filter Dropdown +**File:** `frontend/src/config/pages/keywords.config.tsx` (Lines 310-360) + +| Filter Key | Label | Type | Options | Dynamic | +|-----------|-------|------|---------|---------| +| `search` | Search | Text | N/A | - | +| `status` | Status | Select | `pending`, `active`, `archived` | ❌ No | +| `intent` | Intent | Select | `informational`, `navigational`, `transactional`, `commercial` | ❌ No | +| `difficulty` | Difficulty | Select | `1-5` with labels | ❌ No | +| `cluster` | Cluster | Select | Dynamic from database | βœ… Yes (loads clusters) | +| `volume` | Volume Range | Custom | Min/Max number inputs | ❌ No (range picker) | + +### Frontend Create/Edit Form +**File:** `frontend/src/config/pages/keywords.config.tsx` (Lines 560-586) + +| Field Key | Label | Type | Required | Default | Options | +|-----------|-------|------|----------|---------|---------| +| `seed_keyword_id` | Seed Keyword | Select | βœ… Yes | - | Dynamic from availableSeedKeywords | +| `volume_override` | Volume Override | Number | ❌ No | NULL | Numeric input (optional override) | +| `difficulty_override` | Difficulty Override | Number | ❌ No | NULL | Select 1-5 | +| `cluster_id` | Cluster | Select | ❌ No | NULL | Dynamic from clusters array | +| `status` | Status | Select | βœ… Yes | `pending` | `pending`, `active`, `archived` | + +--- + +## πŸ“Š CLUSTERS - Complete Field Reference + +### Backend Model Fields +**File:** `backend/igny8_core/business/planning/models.py` + +| Field Name | Type | Default | Required | Choices | Notes | +|------------|------|---------|----------|---------|-------| +| `id` | Integer PK | Auto | βœ… | - | Primary key | +| `name` | CharField(255) | - | βœ… | - | Unique cluster name (unique=True) | +| `description` | TextField | NULL | ❌ | - | Optional cluster description | +| `keywords_count` | Integer | 0 | βœ… | - | Cached count of linked keywords | +| `ideas_count` | Integer | 0 | βœ… | - | Cached count of linked ideas | +| `volume` | Integer | 0 | βœ… | - | Cached total volume from keywords | +| `mapped_pages` | Integer | 0 | βœ… | - | Number of mapped pages | +| `status` | CharField(50) | `new` | βœ… | `new`, `idea`, `mapped` | Cluster status | +| `site` | ForeignKey | - | βœ… | - | Owner site (inherited) | +| `sector` | ForeignKey | - | βœ… | - | Owner sector (inherited) | +| `account` | ForeignKey | - | βœ… | - | Owner account (inherited) | +| `created_at` | DateTime | Auto | βœ… | - | Auto-generated | +| `updated_at` | DateTime | Auto | βœ… | - | Auto-updated | + +### Frontend Table Columns +**File:** `frontend/src/config/pages/clusters.config.tsx` + +| Column Key | Label | Visible Default | Sortable | Render Notes | +|----------|--------|-----------------|----------|--------------| +| `name` | Cluster Name | βœ… Yes | βœ… Yes | Link to cluster detail page | +| `sector_name` | Sector | Conditional* | βœ… Yes | Badge (blue) - shown when viewing all sectors | +| `keywords_count` | Keywords | βœ… Yes | βœ… Yes | Formatted as number (e.g., 45) | +| `ideas_count` | Ideas | βœ… Yes | βœ… Yes | Formatted as number (e.g., 12) | +| `volume` | Volume | βœ… Yes | βœ… Yes | Formatted as number (e.g., 5,280) | +| `difficulty` | Difficulty | βœ… Yes | βœ… Yes | Badge (1-5): 1-2=green, 3=amber, 4-5=red | +| `content_count` | Content | βœ… Yes | βœ… Yes | Formatted as number | +| `status` | Status | βœ… Yes | βœ… Yes | Badge: new=amber, idea=blue, mapped=green | +| `created_at` | Created | βœ… Yes | βœ… Yes | Relative date | +| (Hidden by default) | | | | | +| `description` | Description | ❌ No | ❌ No | Text truncated to 250px | +| `mapped_pages` | Mapped Pages | ❌ No | βœ… Yes | Formatted number | +| `updated_at` | Updated | ❌ No | βœ… Yes | Relative date | + +### Frontend Filter Dropdown +**File:** `frontend/src/config/pages/clusters.config.tsx` (Lines 240-290) + +| Filter Key | Label | Type | Options | Dynamic | +|-----------|-------|------|---------|---------| +| `search` | Search | Text | N/A | - | +| `status` | Status | Select | `new`, `idea`, `mapped` | ❌ No | +| `difficulty` | Difficulty | Select | `1-5` with labels | ❌ No | +| `volume` | Volume Range | Custom | Min/Max number inputs | ❌ No (range picker) | + +### Frontend Create/Edit Form +**File:** `frontend/src/config/pages/clusters.config.tsx` (Lines 405-418) + +| Field Key | Label | Type | Required | Default | Options | +|-----------|-------|------|----------|---------|---------| +| `name` | Cluster Name | Text | βœ… Yes | - | Text input (placeholder: "Enter cluster name") | +| `description` | Description | Textarea | ❌ No | NULL | Textarea (placeholder: "Enter cluster description") | +| `status` | Status | Select | βœ… Yes | `new` | `new`, `idea`, `mapped` | + +--- + +## πŸ“Š CONTENT IDEAS - Complete Field Reference + +### Backend Model Fields +**File:** `backend/igny8_core/business/planning/models.py` + +| Field Name | Type | Default | Required | Choices | Notes | +|------------|------|---------|----------|---------|-------| +| `id` | Integer PK | Auto | βœ… | - | Primary key | +| `idea_title` | CharField(255) | - | βœ… | - | Content idea title | +| `description` | TextField | NULL | ❌ | - | Content outline/description | +| `target_keywords` | CharField(500) | '' | ❌ | - | Comma-separated keywords (legacy) | +| `keyword_objects` | M2M(Keywords) | - | ❌ | - | Individual keywords linked to idea | +| `keyword_cluster` | ForeignKey(Clusters) | NULL | ❌ | - | Parent cluster (SET_NULL on delete) | +| `status` | CharField(50) | `new` | βœ… | `new`, `scheduled`, `completed`, `published` | Idea workflow status | +| `estimated_word_count` | Integer | 1000 | βœ… | - | Target article length | +| `content_type` | CharField(50) | `post` | βœ… | `post`, `page`, `product`, `taxonomy` | Content type | +| `content_structure` | CharField(50) | `article` | βœ… | See structures below | Content format/structure | +| `site` | ForeignKey | - | βœ… | - | Owner site (inherited) | +| `sector` | ForeignKey | - | βœ… | - | Owner sector (inherited) | +| `account` | ForeignKey | - | βœ… | - | Owner account (inherited) | +| `created_at` | DateTime | Auto | βœ… | - | Auto-generated | +| `updated_at` | DateTime | Auto | βœ… | - | Auto-updated | + +**Content Structure Choices (based on content_type):** +- **Post:** `article`, `guide`, `comparison`, `review`, `listicle` +- **Page:** `landing_page`, `business_page`, `service_page`, `general`, `cluster_hub` +- **Product:** `product_page` +- **Taxonomy:** `category_archive`, `tag_archive`, `attribute_archive` + +### Frontend Table Columns +**File:** `frontend/src/config/pages/ideas.config.tsx` + +| Column Key | Label | Visible Default | Sortable | Render Notes | +|----------|--------|-----------------|----------|--------------| +| `idea_title` | Title | βœ… Yes | βœ… Yes | Expandable (shows description) | +| `sector_name` | Sector | Conditional* | βœ… Yes | Badge (blue) - shown when viewing all sectors | +| `content_structure` | Structure | βœ… Yes | βœ… Yes | Badge (purple): article, guide, guide, etc. | +| `content_type` | Type | βœ… Yes | βœ… Yes | Badge (blue): post, page, product, taxonomy | +| `target_keywords` | Target Keywords | βœ… Yes | ❌ No | Text truncated to 250px | +| `keyword_cluster_name` | Cluster | βœ… Yes | βœ… Yes | Parent cluster name or "-" | +| `status` | Status | βœ… Yes | βœ… Yes | Badge: new=amber, scheduled=blue, completed=blue, published=green | +| `estimated_word_count` | Words | βœ… Yes | βœ… Yes | Formatted as number (e.g., 1,500) | +| `created_at` | Created | βœ… Yes | βœ… Yes | Relative date | +| (Hidden by default) | | | | | +| `updated_at` | Updated | ❌ No | βœ… Yes | Relative date | + +### Frontend Filter Dropdown +**File:** `frontend/src/config/pages/ideas.config.tsx` (Lines 218-270) + +| Filter Key | Label | Type | Options | Dynamic | +|-----------|-------|------|---------|---------| +| `search` | Search | Text | N/A | - | +| `status` | Status | Select | `new`, `scheduled`, `completed`, `published` | ❌ No | +| `content_structure` | Structure | Select | article, guide, comparison, etc. (all 13 options) | ❌ No | +| `content_type` | Type | Select | `post`, `page`, `product`, `taxonomy` | ❌ No | +| `cluster` | Cluster | Select | Dynamic from database | βœ… Yes (loads clusters) | + +### Frontend Create/Edit Form +**File:** `frontend/src/config/pages/ideas.config.tsx` (Lines 372-417) + +| Field Key | Label | Type | Required | Default | Options | +|-----------|-------|------|----------|---------|---------| +| `idea_title` | Title | Text | βœ… Yes | - | Text input (placeholder: "Enter idea title") | +| `description` | Description | Textarea | ❌ No | NULL | Textarea (placeholder: "Enter content outline") | +| `content_type` | Content Type | Select | βœ… Yes | `post` | `post`, `page`, `product`, `taxonomy` | +| `content_structure` | Content Structure | Select | βœ… Yes | `article` | 13 structure options (depends on content_type) | +| `target_keywords` | Target Keywords | Text | ❌ No | NULL | Text input (comma-separated) | +| `keyword_cluster_id` | Cluster | Select | ❌ No | NULL | Dynamic from clusters array | +| `status` | Status | Select | βœ… Yes | `new` | `new`, `scheduled`, `completed`, `published` | +| `estimated_word_count` | Word Count | Number | ❌ No | 1000 | Numeric input | + +--- + +## πŸ“ SUMMARY COMPARISON + +### Status Fields +| Module | Backend Default | Backend Choices | Frontend Form Default | Frontend Form Choices | +|--------|-----------------|-----------------|----------------------|----------------------| +| **Keywords** | `pending` | pending, active, archived | `pending` | pending, active, archived | +| **Clusters** | `new` | new, idea, mapped | `new` | new, idea, mapped | +| **Ideas** | `new` | new, scheduled, completed, published | `new` | new, scheduled, completed, published | + +### Required Fields (Must be filled) +| Module | Required Fields | +|--------|-----------------| +| **Keywords** | seed_keyword_id, status, site, sector, account | +| **Clusters** | name, status, site, sector, account | +| **Ideas** | idea_title, status, content_type, content_structure, estimated_word_count, site, sector, account | + +### Optional Fields +| Module | Optional Fields | +|--------|-----------------| +| **Keywords** | volume_override, difficulty_override, attribute_values, cluster_id | +| **Clusters** | description | +| **Ideas** | description, target_keywords, keyword_objects, keyword_cluster_id | + +### Dynamic Dropdowns (Loaded from DB) +| Module | Filter | Form | +|--------|--------|------| +| **Keywords** | cluster (dropdown) | seed_keyword_id (all available), cluster_id (all clusters for sector) | +| **Clusters** | - | - | +| **Ideas** | cluster (dropdown) | keyword_cluster_id (all clusters for sector) | + +### Visible-by-Default Table Columns +| Module | Count | Primary Columns | +|--------|-------|-----------------| +| **Keywords** | 9 | keyword, volume, cluster, difficulty, intent, status, created_at | +| **Clusters** | 11 | name, keywords_count, ideas_count, volume, difficulty, content_count, status, created_at | +| **Ideas** | 10 | idea_title, content_structure, content_type, target_keywords, cluster, status, word_count, created_at | + +--- + +## πŸ”‘ Key Differences + +### Keywords +- **Uses:** SeedKeyword (global pool) - one keyword per site/sector +- **Overrideable:** volume, difficulty (site-specific) +- **Links to:** Clusters (via cluster FK) +- **Status:** pending (awaiting cluster), active (clustered), archived + +### Clusters +- **Type:** Pure topic clusters (semantic groupings) +- **Auto-updated:** keywords_count, ideas_count, volume (cached from keywords) +- **Status:** new (no ideas), idea (has ideas), mapped (has content) +- **No overrides:** All values are cached/calculated + +### Ideas +- **Type:** Content concepts ready for production +- **Links:** Cluster (required for workflow), Keywords (optional M2M) +- **Customizable:** content_type, content_structure, word_count +- **Status:** new β†’ scheduled (queued to writer) β†’ completed (content generated) β†’ published + +--- + +**END OF COMPLETE FIELD MAPPING** diff --git a/STATUS-IMPLEMENTATION-TABLES.md b/STATUS-IMPLEMENTATION-TABLES.md new file mode 100644 index 00000000..7ab9514b --- /dev/null +++ b/STATUS-IMPLEMENTATION-TABLES.md @@ -0,0 +1,335 @@ +# Status Implementation Tables - Complete Reference + +**Date:** December 3, 2025 + +--- + +## 🎯 KEYWORDS MODULE + +### Backend Model +**File:** `/backend/igny8_core/business/planning/models.py` + +| Field | Type | Choices | Default | Required | Description | +|-------|------|---------|---------|----------|-------------| +| `status` | CharField(50) | `new`, `mapped` | `new` | βœ… Yes | Workflow status | +| `disabled` | BooleanField | - | `False` | βœ… Yes | Filter control (exclude from processes) | + +--- + +### Frontend Table Column +**File:** `/frontend/src/config/pages/keywords.config.tsx` (Lines ~230-248) + +| Status Value | Badge Color | Badge Label | Display When | +|--------------|-------------|-------------|--------------| +| `new` | Amber/Yellow | New | Keyword not yet clustered | +| `mapped` | Green | Mapped | Keyword assigned to cluster | +| *(disabled=true)* | Red/Gray | Disabled | User manually disabled (optional display) | + +--- + +### Frontend Filter Dropdown +**File:** `/frontend/src/config/pages/keywords.config.tsx` (Lines ~310-318) + +| Filter Type | Options | Default | Description | +|-------------|---------|---------|-------------| +| Status Select | `new`, `mapped` | All | Workflow status filter | +| Disabled Checkbox | Show/Hide disabled | Hide disabled | Filter control | + +--- + +### Frontend Form Fields +**File:** `/frontend/src/config/pages/keywords.config.tsx` (Lines ~560-570) + +| Field | Type | Options | Default | Required | Editable | +|-------|------|---------|---------|----------|----------| +| Status | Select | `new`, `mapped` | `new` | βœ… Yes | βœ… Yes | +| Disabled | Checkbox | true/false | `false` | ❌ No | βœ… Yes | + +--- + +## 🎯 CLUSTERS MODULE + +### Backend Model +**File:** `/backend/igny8_core/business/planning/models.py` + +| Field | Type | Choices | Default | Required | Description | +|-------|------|---------|---------|----------|-------------| +| `status` | CharField(50) | `new`, `mapped` | `new` | βœ… Yes | Workflow status | +| `disabled` | BooleanField | - | `False` | βœ… Yes | Filter control (exclude from processes) | + +--- + +### Frontend Table Column +**File:** `/frontend/src/config/pages/clusters.config.tsx` (Lines ~190-200) + +| Status Value | Badge Color | Badge Label | Display When | +|--------------|-------------|-------------|--------------| +| `new` | Amber/Yellow | New | Cluster created, no ideas generated yet | +| `mapped` | Green | Mapped | Ideas generated from cluster | +| *(disabled=true)* | Red/Gray | Disabled | User manually disabled (optional display) | + +--- + +### Frontend Filter Dropdown +**File:** `/frontend/src/config/pages/clusters.config.tsx` (Lines ~240-253) + +| Filter Type | Options | Default | Description | +|-------------|---------|---------|-------------| +| Status Select | `new`, `mapped` | All | Workflow status filter | +| Disabled Checkbox | Show/Hide disabled | Hide disabled | Filter control | + +--- + +### Frontend Form Fields +**File:** `/frontend/src/config/pages/clusters.config.tsx` (Lines ~405-418) + +| Field | Type | Options | Default | Required | Editable | +|-------|------|---------|---------|----------|----------| +| Status | Select | `new`, `mapped` | `new` | βœ… Yes | βœ… Yes | +| Disabled | Checkbox | true/false | `false` | ❌ No | βœ… Yes | + +--- + +## 🎯 IDEAS MODULE + +### Backend Model +**File:** `/backend/igny8_core/business/planning/models.py` + +| Field | Type | Choices | Default | Required | Description | +|-------|------|---------|---------|----------|-------------| +| `status` | CharField(50) | `new`, `queued`, `completed` | `new` | βœ… Yes | Workflow status | +| `disabled` | BooleanField | - | `False` | βœ… Yes | Filter control (exclude from processes) | + +--- + +### Frontend Table Column +**File:** `/frontend/src/config/pages/ideas.config.tsx` (Lines ~170-185) + +| Status Value | Badge Color | Badge Label | Display When | +|--------------|-------------|-------------|--------------| +| `new` | Amber/Yellow | New | Idea generated, not queued yet | +| `queued` | Blue | Queued | Task created in Writer module | +| `completed` | Green | Completed | Content generated (Task completed) | +| *(disabled=true)* | Red/Gray | Disabled | User manually disabled (optional display) | + +--- + +### Frontend Filter Dropdown +**File:** `/frontend/src/config/pages/ideas.config.tsx` (Lines ~218-228) + +| Filter Type | Options | Default | Description | +|-------------|---------|---------|-------------| +| Status Select | `new`, `queued`, `completed` | All | Workflow status filter | +| Disabled Checkbox | Show/Hide disabled | Hide disabled | Filter control | + +--- + +### Frontend Form Fields +**File:** `/frontend/src/config/pages/ideas.config.tsx` (Lines ~372-385) + +| Field | Type | Options | Default | Required | Editable | +|-------|------|---------|---------|----------|----------| +| Status | Select | `new`, `queued`, `completed` | `new` | βœ… Yes | βœ… Yes | +| Disabled | Checkbox | true/false | `false` | ❌ No | βœ… Yes | + +--- + +--- + +# πŸ”„ STATUS TRANSITION TABLES + +## KEYWORDS Status Transitions + +| Current Status | Trigger/Action | Next Status | Auto/Manual | Updated By | +|----------------|----------------|-------------|-------------|------------| +| `new` | AI auto_cluster runs | `mapped` | πŸ€– Auto | `auto_cluster.py` Line 297 | +| `new` | User manually assigns to cluster | `mapped` | πŸ‘€ Manual | User form edit | +| `new` | User toggles disabled | *(stays new)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| `mapped` | User toggles disabled | *(stays mapped)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| *(any)* | User re-enables | *(stays same)* + `disabled=false` | πŸ‘€ Manual | User form edit | + +**Workflow Path:** +``` +new ──[auto_cluster AI]──> mapped + β”‚ + └──[user toggle disabled]──> (status unchanged, disabled flag set) +``` + +--- + +## CLUSTERS Status Transitions + +| Current Status | Trigger/Action | Next Status | Auto/Manual | Updated By | +|----------------|----------------|-------------|-------------|------------| +| `new` | AI generate_ideas runs | `mapped` | πŸ€– Auto | `generate_ideas.py` (new code) | +| `new` | User manually creates ideas | `mapped` | πŸ‘€ Manual | User workflow | +| `new` | User toggles disabled | *(stays new)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| `mapped` | User toggles disabled | *(stays mapped)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| *(any)* | User re-enables | *(stays same)* + `disabled=false` | πŸ‘€ Manual | User form edit | + +**Workflow Path:** +``` +new ──[generate_ideas AI]──> mapped + β”‚ + └──[user toggle disabled]──> (status unchanged, disabled flag set) +``` + +--- + +## IDEAS Status Transitions + +| Current Status | Trigger/Action | Next Status | Auto/Manual | Updated By | +|----------------|----------------|-------------|-------------|------------| +| `new` | User bulk-queues ideas to writer | `queued` | πŸ‘€ Manual | `views.py` Line 1084 | +| `queued` | Writer AI generates content | `completed` | πŸ€– Auto | `generate_content.py` Line 318 (syncs from Task) | +| `queued` | Task status becomes 'completed' | `completed` | πŸ€– Auto | Auto-sync from Task.status | +| `new` | User toggles disabled | *(stays new)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| `queued` | User toggles disabled | *(stays queued)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| `completed` | User toggles disabled | *(stays completed)* + `disabled=true` | πŸ‘€ Manual | User form edit | +| *(any)* | User re-enables | *(stays same)* + `disabled=false` | πŸ‘€ Manual | User form edit | + +**Workflow Path:** +``` +new ──[user bulk_queue]──> queued ──[generate_content AI]──> completed + β”‚ β”‚ β”‚ + └──[user toggle disabled]────┴───────────[user toggle]β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + (status unchanged, disabled flag set) +``` + +--- + +--- + +# πŸ“Š COMBINED STATUS OVERVIEW + +## All Modules - Status Values + +| Module | Workflow Statuses | Filter Status | Total Unique Values | +|--------|-------------------|---------------|---------------------| +| **Keywords** | `new`, `mapped` | `disabled` (boolean) | 2 status + 1 flag | +| **Clusters** | `new`, `mapped` | `disabled` (boolean) | 2 status + 1 flag | +| **Ideas** | `new`, `queued`, `completed` | `disabled` (boolean) | 3 status + 1 flag | + +--- + +## Process Inclusion Matrix + +| Status | Auto-Cluster AI | Generate-Ideas AI | Bulk-Queue | Generate-Content AI | Dashboard Metrics | +|--------|-----------------|-------------------|------------|---------------------|-------------------| +| Keywords: `new` | βœ… Included | ❌ N/A | ❌ N/A | ❌ N/A | βœ… Counted | +| Keywords: `mapped` | ❌ Already clustered | βœ… Used for ideas | ❌ N/A | ❌ N/A | βœ… Counted | +| Keywords: `disabled=true` | ❌ Excluded | ❌ Excluded | ❌ N/A | ❌ N/A | ❌ Excluded | +| Clusters: `new` | ❌ N/A | βœ… Included | ❌ N/A | ❌ N/A | βœ… Counted | +| Clusters: `mapped` | ❌ N/A | ❌ Already has ideas | βœ… Ideas can queue | ❌ N/A | βœ… Counted | +| Clusters: `disabled=true` | ❌ N/A | ❌ Excluded | ❌ Excluded | ❌ N/A | ❌ Excluded | +| Ideas: `new` | ❌ N/A | ❌ N/A | βœ… Included | ❌ N/A | βœ… Counted | +| Ideas: `queued` | ❌ N/A | ❌ N/A | ❌ Already queued | βœ… Included | βœ… Counted | +| Ideas: `completed` | ❌ N/A | ❌ N/A | ❌ Already done | ❌ Already done | βœ… Counted | +| Ideas: `disabled=true` | ❌ N/A | ❌ N/A | ❌ Excluded | ❌ Excluded | ❌ Excluded | + +--- + +--- + +# πŸ”„ DATA MIGRATION TRANSITION TABLE + +## Keywords Migration + +| Old Status | New Status | New Disabled Flag | Logic | +|------------|------------|-------------------|-------| +| `pending` | `new` | `false` | Direct mapping | +| `active` | `mapped` | `false` | Keyword was clustered | +| `archived` | `mapped` | `true` | Preserve data, mark as disabled | + +**SQL Preview:** +```sql +-- Keywords migration +UPDATE igny8_keywords +SET status = 'new', disabled = false +WHERE status = 'pending'; + +UPDATE igny8_keywords +SET status = 'mapped', disabled = false +WHERE status = 'active'; + +UPDATE igny8_keywords +SET status = 'mapped', disabled = true +WHERE status = 'archived'; +``` + +--- + +## Clusters Migration + +| Old Status | New Status | New Disabled Flag | Logic | +|------------|------------|-------------------|-------| +| `active` (with ideas_count > 0) | `mapped` | `false` | Cluster has ideas | +| `active` (with ideas_count = 0) | `new` | `false` | Cluster has no ideas yet | +| *(no archived status exists)* | - | - | - | + +**SQL Preview:** +```sql +-- Clusters migration +UPDATE igny8_clusters +SET status = 'mapped', disabled = false +WHERE ideas_count > 0; + +UPDATE igny8_clusters +SET status = 'new', disabled = false +WHERE ideas_count = 0; +``` + +--- + +## Ideas Migration + +| Old Status | New Status | New Disabled Flag | Logic | +|------------|------------|-------------------|-------| +| `new` | `new` | `false` | No change | +| `scheduled` | `queued` | `false` | Rename to match writer workflow | +| `published` | `completed` | `false` | Publishing is separate deployment | +| *(no archived status)* | - | - | - | + +**SQL Preview:** +```sql +-- Ideas migration +UPDATE igny8_content_ideas +SET status = 'new', disabled = false +WHERE status = 'new'; + +UPDATE igny8_content_ideas +SET status = 'queued', disabled = false +WHERE status = 'scheduled'; + +UPDATE igny8_content_ideas +SET status = 'completed', disabled = false +WHERE status = 'published'; +``` + +--- + +--- + +# πŸ“‹ SUMMARY COMPARISON TABLE + +## Before vs After + +| Module | Current Status Values | New Status Values | Current Filter | New Filter | +|--------|----------------------|-------------------|----------------|------------| +| **Keywords** | `pending`, `active`, `archived` | `new`, `mapped` | Status dropdown | Status dropdown + disabled checkbox | +| **Clusters** | `active` (hardcoded, no choices) | `new`, `mapped` | Status dropdown | Status dropdown + disabled checkbox | +| **Ideas** | `new`, `scheduled`, `published` | `new`, `queued`, `completed` | Status dropdown | Status dropdown + disabled checkbox | + +--- + +## Status Count Changes + +| Module | Before | After | Change | +|--------|--------|-------|--------| +| **Keywords** | 3 status values | 2 workflow + 1 boolean flag | -1 status (simplified) | +| **Clusters** | 1 hardcoded value | 2 workflow + 1 boolean flag | +1 status (proper choices) | +| **Ideas** | 3 status values | 3 workflow + 1 boolean flag | Same count (renamed) | + +--- + +**END OF IMPLEMENTATION TABLES** diff --git a/STATUS-UNIFIED-STRUCTURE-PLAN-v2.md b/STATUS-UNIFIED-STRUCTURE-PLAN-v2.md new file mode 100644 index 00000000..0cb60ef4 --- /dev/null +++ b/STATUS-UNIFIED-STRUCTURE-PLAN-v2.md @@ -0,0 +1,541 @@ +# Unified Status Structure Plan - Keywords, Clusters, Ideas (CORRECTED) + +**Date:** December 3, 2025 +**Objective:** Implement unified, workflow-driven status values across all three modules + +--- + +## 🎯 Key Concept: Status vs Workflow + +### βœ… WORKFLOW STATUS (Auto-updated by AI/System) +These statuses represent the workflow progression: +- Keywords: `new` β†’ `mapped` +- Clusters: `new` β†’ `mapped` +- Ideas: `new` β†’ `queued` β†’ `completed` + +### πŸ”˜ OPTIONAL MANUAL STATUS (User control only) +`disabled` = **NOT a workflow step**, but a **filter status** +- Does NOT affect workflow +- Manually set by user only +- Excludes items from all automated processes +- Excludes from dashboard metrics/suggestions +- Can be toggled on/off anytime + +**Important:** `disabled` is a **data attribute**, not a workflow state. Items with `disabled=true` are simply ignored by all processes. + +--- + +## πŸ“‹ Module Status Definitions + +### KEYWORDS Module +**Workflow Statuses:** `new` β†’ `mapped` +**Optional Filter Status:** `disabled` (manual user control) + +| Status | Type | Meaning | When Set | Process Behavior | +|--------|------|---------|----------|-----------------| +| **new** | Workflow | Keyword attached to site, awaiting cluster assignment | User adds keyword via import/UI | Included in auto-cluster AI suggestions | +| **mapped** | Workflow | Keyword assigned to a cluster | AI auto_cluster completes or manual cluster assignment | Can be used for idea generation; included in cluster-based workflows | +| **disabled** | Filter | Manually excluded from processes | User manually sets | Excluded from auto-cluster, idea generation, dashboard (optional) | + +**Workflow Transition:** +``` +new ──[AI auto_cluster]──> mapped + β”‚ + └──[User toggle]──> disabled (any time, no workflow impact) +``` + +--- + +### CLUSTERS Module +**Workflow Statuses:** `new` β†’ `mapped` +**Optional Filter Status:** `disabled` (manual user control) + +| Status | Type | Meaning | When Set | Process Behavior | +|--------|------|---------|----------|-----------------| +| **new** | Workflow | Cluster created; ready for idea generation | AI clustering creates OR user manually creates | Included in auto-generate-ideas suggestions | +| **mapped** | Workflow | Ideas generated from this cluster | AI generate_ideas completes | Ideas are ready; can be queued to writer | +| **disabled** | Filter | Manually excluded from processes | User manually sets | Excluded from idea generation, suggestion lists, dashboard (optional) | + +**Workflow Transition:** +``` +new ──[AI generate_ideas]──> mapped + β”‚ + └──[User toggle]──> disabled (any time, no workflow impact) +``` + +--- + +### IDEAS Module +**Workflow Statuses:** `new` β†’ `queued` β†’ `completed` +**Optional Filter Status:** `disabled` (manual user control) + +| Status | Type | Meaning | When Set | Process Behavior | +|--------|------|---------|----------|-----------------| +| **new** | Workflow | Idea generated by AI; awaiting queue to writer | AI generate_ideas creates | Included in bulk-queue suggestions | +| **queued** | Workflow | Queued to writer; Task record created in Writer module | User bulk-queues ideas to writer | Task is assigned; waiting for content generation | +| **completed** | Workflow | Content generated from task (tracked via Task.status='completed') | generate_content AI completes; Content record created | Final automated state; content ready for publishing/deployment | +| **disabled** | Filter | Manually excluded from processes | User manually sets | Excluded from queue suggestions, bulk operations, dashboard (optional) | + +**Workflow Transition:** +``` +new ──[User bulk_queue]──> queued ──[Writer AI: generate_content]──> completed + β”‚ + └──[User toggle]──> disabled (any time, no workflow impact) +``` + +**Note on `completed`:** +- When a Task's status becomes `completed`, the related Idea automatically becomes `completed` +- No separate `published` status needed; publishing is a separate content deployment action +- One idea = one content piece through task tracking + +--- + +## πŸ”„ Process Exclusion Rules + +### When `disabled=true` (filter status): +Items are **excluded from**: +- βœ— AI process suggestions (auto-cluster, generate-ideas) +- βœ— Bulk operation selections (queue to writer, generate content) +- βœ— Dashboard workflow metrics (unless explicitly shown) +- βœ— Progress calculations (% mapped, % queued, etc.) + +### When workflow status applies: +- βœ… Included in relevant processes +- βœ… Included in dashboard metrics +- βœ… Included in AI suggestions +- βœ… Can be bulk-operated on + +--- + +## πŸ“Š Status Update Flow + +``` +═══ KEYWORDS WORKFLOW ═══ +1. User imports SeedKeywords + └─> Keywords created with status='new' + +2. Auto-cluster AI runs + └─> Keywords assigned to clusters + └─> Keywords status changes to 'mapped' + +3. [Optional] User manually disables keyword + └─> Keywords.disabled = true + └─> Excluded from all processes + + +═══ CLUSTERS WORKFLOW ═══ +1. Auto-cluster AI creates/updates clusters + └─> Clusters created with status='new' + +2. AI generate-ideas runs on 'new' clusters + └─> Ideas created for cluster + └─> Clusters status changes to 'mapped' + +3. [Optional] User manually disables cluster + └─> Clusters.disabled = true + └─> Excluded from all processes + + +═══ IDEAS WORKFLOW ═══ +1. AI generate-ideas creates ideas for clusters + └─> Ideas created with status='new' + +2. User bulk-queues ideas to writer + └─> Task created in Writer module + └─> Ideas status changes to 'queued' + +3. Writer AI (generate-content) creates content + └─> Content record created + └─> Task.status = 'completed' + └─> Ideas.status = 'completed' [Auto-sync from Task] + +4. [Optional] User manually disables idea + └─> Ideas.disabled = true + └─> Excluded from all processes +``` + +--- + +## πŸ”§ Implementation Changes + +### Backend Model Changes + +#### 1. `/backend/igny8_core/business/planning/models.py` + +**Keywords Model:** +```python +# REMOVE old STATUS_CHOICES +# OLD: +STATUS_CHOICES = [ + ('active', 'Active'), + ('pending', 'Pending'), + ('archived', 'Archived'), +] + +# NEW: +STATUS_CHOICES = [ + ('new', 'New'), + ('mapped', 'Mapped'), +] +status = models.CharField( + max_length=50, + choices=STATUS_CHOICES, + default='new' +) + +# ADD new filter field: +disabled = models.BooleanField(default=False, help_text="Exclude from processes") +``` + +**Clusters Model:** +```python +# ADD STATUS_CHOICES (currently hardcoded as default='active', no choices) +# CURRENT: status = models.CharField(max_length=50, default='active') + +# NEW: +STATUS_CHOICES = [ + ('new', 'New'), + ('mapped', 'Mapped'), +] +status = models.CharField( + max_length=50, + choices=STATUS_CHOICES, + default='new' +) + +# ADD new filter field: +disabled = models.BooleanField(default=False, help_text="Exclude from processes") +``` + +**ContentIdeas Model:** +```python +# UPDATE STATUS_CHOICES +# REMOVE 'scheduled', 'published' +STATUS_CHOICES = [ + ('new', 'New'), + ('queued', 'Queued'), + ('completed', 'Completed'), +] +status = models.CharField( + max_length=50, + choices=STATUS_CHOICES, + default='new' +) + +# ADD new filter field: +disabled = models.BooleanField(default=False, help_text="Exclude from processes") +``` + +--- + +### Backend AI Function Updates + +#### 2. `/backend/igny8_core/ai/functions/auto_cluster.py` + +**Location: Line ~297 in save_output()** +```python +# CURRENT: +status='active' + +# NEW: +status='mapped' # When keywords are assigned to cluster +``` + +--- + +#### 3. `/backend/igny8_core/ai/functions/generate_ideas.py` + +**Location: After ideas created in save_output()** +```python +# NEW: Add after creating ideas +# Update cluster status from 'new' to 'mapped' after ideas generated +for cluster in clusters_used: + if cluster.status == 'new': + cluster.status = 'mapped' + cluster.save() +``` + +--- + +#### 4. `/backend/igny8_core/ai/functions/generate_content.py` + +**Location: Line ~318 in save_output()** +```python +# CURRENT: +task.status = 'completed' +task.save() + +# NEW: Auto-sync idea status from task +task.status = 'completed' +task.save() + +# NEW: Update related idea to 'completed' +if hasattr(task, 'idea') and task.idea: + idea = task.idea + idea.status = 'completed' + idea.save() +``` + +--- + +### Backend API Updates + +#### 5. `/backend/igny8_core/modules/planner/views.py` + +**Queue ideas to writer (Line ~1084):** +```python +# CURRENT: +idea.status = 'scheduled' + +# NEW: +idea.status = 'queued' +``` + +**CSV import defaults (Line ~563):** +```python +# CURRENT: +status=row.get('status', 'pending') or 'pending' + +# NEW: +status=row.get('status', 'new') or 'new' +``` + +**Filter out disabled items in list views:** +```python +# NEW: When returning lists, exclude disabled=true (optional, configurable) +queryset = queryset.filter(disabled=False) # Or show both with filter option +``` + +--- + +### Frontend Configuration Updates + +#### 6. `/frontend/src/config/pages/keywords.config.tsx` + +**Table Column - Status Badge (Lines ~230-248):** +```typescript +// CURRENT: +new: 'amber', active: 'green', archived: 'red' + +// NEW: +new: 'amber', mapped: 'green' + +// ADD: Display 'disabled' badge if disabled=true +``` + +**Filter Dropdown (Lines ~310-318):** +```typescript +// CURRENT: +options: ['pending', 'active', 'archived'] + +// NEW: +options: ['new', 'mapped'], +additionalFilters: [ + { key: 'disabled', label: 'Show Disabled', type: 'checkbox', default: false } +] +``` + +**Form Field Default (Lines ~560-570):** +```typescript +// CURRENT: +default: 'pending' + +// NEW: +default: 'new' + +// ADD: disabled checkbox in form +``` + +--- + +#### 7. `/frontend/src/config/pages/clusters.config.tsx` + +**Table Column - Status Badge (Lines ~190-200):** +```typescript +// CURRENT: +(missing or wrong values) + +// NEW: +new: 'amber', mapped: 'green' + +// ADD: Display 'disabled' badge if disabled=true +``` + +**Filter Dropdown (Lines ~240-253):** +```typescript +// CURRENT: +options: ['new', 'idea', 'mapped'] + +// NEW: +options: ['new', 'mapped'], +additionalFilters: [ + { key: 'disabled', label: 'Show Disabled', type: 'checkbox', default: false } +] +``` + +**Form Field (Lines ~405-418):** +```typescript +// ADD: disabled checkbox in form +// Keep status default as 'new' +``` + +--- + +#### 8. `/frontend/src/config/pages/ideas.config.tsx` + +**Table Column - Status Badge (Lines ~170-185):** +```typescript +// CURRENT: +new: 'amber', scheduled: 'blue', completed: 'blue', published: 'green' + +// NEW: +new: 'amber', queued: 'blue', completed: 'green' + +// ADD: Display 'disabled' badge if disabled=true +``` + +**Filter Dropdown (Lines ~218-228):** +```typescript +// CURRENT: +options: ['new', 'scheduled', 'completed', 'published'] + +// NEW: +options: ['new', 'queued', 'completed'], +additionalFilters: [ + { key: 'disabled', label: 'Show Disabled', type: 'checkbox', default: false } +] +``` + +**Form Field (Lines ~372-385):** +```typescript +// REMOVE: 'published' option +// ADD: disabled checkbox in form +// Keep status default as 'new' +``` + +--- + +### Frontend Dashboard + +#### 9. `/frontend/src/pages/Planner/Dashboard.tsx` + +**Status Metrics:** +```typescript +// Update calculations: +// - Keywords: new vs mapped +// - Clusters: new vs mapped +// - Ideas: new vs queued vs completed + +// Optional: Add disabled count metrics +``` + +--- + +## πŸ“ Change Summary + +### Total Files: 9 + +| File | Changes | Type | +|------|---------|------| +| **models.py** | Add STATUS_CHOICES for Clusters; Update Keywords/Ideas; Add `disabled` field to all 3 | Backend | +| **auto_cluster.py** | Line 297: `status='active'` β†’ `status='mapped'` | Backend AI | +| **generate_ideas.py** | After ideas created: Set cluster `status='mapped'` | Backend AI | +| **generate_content.py** | Line 318: Also sync `idea.status='completed'` from task | Backend AI | +| **views.py** | Queue ideas (line 1084): `'scheduled'` β†’ `'queued'`; CSV import: `'pending'` β†’ `'new'` | Backend API | +| **keywords.config.tsx** | Update badge/filter/form for new/mapped; Add disabled checkbox | Frontend Config | +| **clusters.config.tsx** | Update badge/filter/form for new/mapped; Add disabled checkbox | Frontend Config | +| **ideas.config.tsx** | Update badge/filter/form for new/queued/completed; Add disabled checkbox | Frontend Config | +| **Dashboard.tsx** | Update metrics calculations for new status values | Frontend Page | + +--- + +## βœ… Workflow Validations + +### Keywords +``` +βœ“ new β†’ mapped (via auto_cluster) +βœ“ Can toggle disabled at any time (no workflow impact) +βœ“ Disabled items excluded from auto-cluster suggestions +``` + +### Clusters +``` +βœ“ new β†’ mapped (via generate_ideas) +βœ“ Can toggle disabled at any time (no workflow impact) +βœ“ Disabled items excluded from idea generation +``` + +### Ideas +``` +βœ“ new β†’ queued (via bulk_queue_to_writer) +βœ“ queued β†’ completed (via generate_content, tracked via Task.status) +βœ“ Can toggle disabled at any time (no workflow impact) +βœ“ Disabled items excluded from queue suggestions +``` + +--- + +## πŸ—‘οΈ Data Migration Strategy + +### Django Migration File +**File:** `backend/igny8_core/business/planning/migrations/NNNN_unified_status_fields.py` + +```python +# Forward migration: +# 1. Add disabled=BooleanField(default=False) to all 3 models +# 2. Add STATUS_CHOICES to Clusters model +# 3. Update Keywords.status data: +# - pending β†’ new +# - active β†’ mapped +# - archived β†’ mapped + set disabled=True +# 4. Update Clusters.status data: +# - active (with ideas_count > 0) β†’ mapped +# - active (with ideas_count = 0) β†’ new +# 5. Update ContentIdeas.status data: +# - scheduled β†’ queued +# - published β†’ completed +# - new β†’ new (no change) + +# Reverse migration: +# 1. Remove disabled field from all 3 models +# 2. Restore old STATUS_CHOICES +# 3. Reverse data transformations: +# - Keywords: newβ†’pending, mappedβ†’active, disabled=Trueβ†’archived +# - Clusters: Remove STATUS_CHOICES, set all to 'active' +# - Ideas: queuedβ†’scheduled, completedβ†’published +``` + +--- + +## πŸ§ͺ Testing Checklist + +- [ ] Keywords: status='new' by default +- [ ] Keywords: auto_cluster sets status='mapped' +- [ ] Clusters: status='new' by default +- [ ] Clusters: generate_ideas sets status='mapped' +- [ ] Ideas: status='new' by default +- [ ] Ideas: bulk_queue_to_writer sets status='queued' +- [ ] Ideas: generate_content sets status='completed' (via task sync) +- [ ] All modules: disabled=true excludes from processes +- [ ] All modules: disabled checkbox toggles correctly +- [ ] Dashboard: metrics use new status values +- [ ] Filters: disabled checkbox hides disabled items by default +- [ ] Migration: old data transforms correctly + +--- + +## 🎯 Key Differences from Previous Plan + +| Previous | Now Corrected | +|----------|---------------| +| `disabled` was workflow step | `disabled` is filter status only | +| 3 workflows statuses per module | 2 workflow statuses per module | +| Published status for Ideas | Completed is final; publish is separate action | +| Ideas tracked separately | Ideas auto-sync from Task status | +| Unclear disabled behavior | Clear: disabled excluded from all processes | + +--- + +**Status:** βœ… Plan Complete & Corrected +**Ready for:** Implementation Phase 1 (Backend Models) + diff --git a/STATUS-UNIFIED-STRUCTURE-PLAN.md b/STATUS-UNIFIED-STRUCTURE-PLAN.md new file mode 100644 index 00000000..9a1572c1 --- /dev/null +++ b/STATUS-UNIFIED-STRUCTURE-PLAN.md @@ -0,0 +1,526 @@ +# Unified Status Structure Plan - Keywords, Clusters, Ideas + +**Date:** December 3, 2025 +**Objective:** Implement unified, workflow-driven status values across all three modules + +--- + +## 🎯 Proposed Unified Status Structure + +### Module-Specific Status Values + +#### KEYWORDS Module +**Current:** `pending`, `active`, `archived` +**Proposed:** `new`, `mapped`, `disabled` + +| Status | Meaning | When Used | Next Step | +|--------|---------|-----------|-----------| +| **new** | Keyword just attached to site, not yet assigned to cluster | User adds keyword | AI auto-cluster runs β†’ `mapped` | +| **mapped** | Keyword is assigned to a cluster | AI clustering completes OR manual assignment | Can generate ideas from cluster | +| **disabled** | Keyword is archived/inactive | User manually disables | No workflow activity | + +**Workflow:** +``` +new β†’ mapped β†’ (can be used for ideas) + ↓ +disabled (any time) +``` + +--- + +#### CLUSTERS Module +**Current:** `active`, `pending`, `archived` +**Proposed:** `new`, `mapped`, `disabled` + +| Status | Meaning | When Used | Next Step | +|--------|---------|-----------|-----------| +| **new** | Cluster created but no content ideas generated yet | AI clustering creates cluster OR manual creation | AI generate_ideas runs β†’ `mapped` | +| **mapped** | Cluster has ideas generated from it | AI generate_ideas completes | Ideas ready to queue to writer | +| **disabled** | Cluster is archived/inactive | User manually disables | No workflow activity | + +**Workflow:** +``` +new β†’ mapped β†’ (ideas queue to writer) + ↓ +disabled (any time) +``` + +--- + +#### IDEAS Module +**Current:** `new`, `scheduled`, `completed`, `published` +**Proposed:** `new`, `queued`, `completed`, `published` + +| Status | Meaning | When Used | Next Step | +|--------|---------|-----------|-----------| +| **new** | Idea generated by AI but not queued to writer yet | AI generate_ideas creates idea | User queues to writer β†’ `queued` | +| **queued** | Idea queued to writer, task created in Writer module | Bulk queue to writer API called | Content generated by writer β†’ `completed` | +| **completed** | Content generated from idea (Task completed with content) | generate_content AI function creates Content record | Manual publish β†’ `published` | +| **published** | Content published to site | User publishes OR auto-publish | Final state | + +**Workflow:** +``` +new β†’ queued β†’ completed β†’ published +``` + +--- + +## πŸ“Š Status Update Flow Diagram + +``` +KEYWORDS WORKFLOW: +1. User adds SeedKeyword β†’ Keywords status='new' +2. AI auto_cluster runs β†’ Keywords status='mapped' (assigned to cluster) +3. User can disable β†’ Keywords status='disabled' + +CLUSTERS WORKFLOW: +1. AI creates cluster β†’ Clusters status='new' +2. AI generate_ideas runs β†’ Clusters status='mapped' (has ideas) +3. User can disable β†’ Clusters status='disabled' + +IDEAS WORKFLOW: +1. AI creates idea β†’ Ideas status='new' +2. User bulk queues ideas β†’ Ideas status='queued' (task created in Writer) +3. Writer AI generates content β†’ Ideas status='completed' (Content record created) +4. User publishes content β†’ Ideas status='published' +``` + +--- + +## πŸ”§ Change Locations & Impact + +### Backend Model Files to Update + +#### 1. `/backend/igny8_core/business/planning/models.py` + +**Keywords Model (Lines 42-46):** +```python +# CURRENT: +STATUS_CHOICES = [ + ('active', 'Active'), + ('pending', 'Pending'), + ('archived', 'Archived'), +] + +# PROPOSED: +STATUS_CHOICES = [ + ('new', 'New'), + ('mapped', 'Mapped'), + ('disabled', 'Disabled'), +] + +# DEFAULT: Change from 'pending' β†’ 'new' +``` + +**Clusters Model (Line 12):** +```python +# CURRENT: +status = models.CharField(max_length=50, default='active') + +# PROPOSED: +status = models.CharField(max_length=50, default='new', + choices=[('new', 'New'), ('mapped', 'Mapped'), ('disabled', 'Disabled')]) +``` + +**ContentIdeas Model (Lines 150-157):** +```python +# CURRENT: +STATUS_CHOICES = [ + ('new', 'New'), + ('scheduled', 'Scheduled'), + ('published', 'Published'), +] + +# PROPOSED: +STATUS_CHOICES = [ + ('new', 'New'), + ('queued', 'Queued'), + ('completed', 'Completed'), + ('published', 'Published'), +] + +# DEFAULT: Keep 'new' +``` + +--- + +### Backend AI Function Files to Update + +#### 2. `/backend/igny8_core/ai/functions/auto_cluster.py` (Line 297) + +**When keywords are assigned to clusters:** +```python +# CURRENT: +status='active' + +# PROPOSED: +status='mapped' +``` + +**Location:** In `save_output()` method when updating Keywords + +--- + +#### 3. `/backend/igny8_core/ai/functions/generate_ideas.py` (Line 224) + +**When clusters change status after ideas generated:** +```python +# CURRENT: No cluster status update + +# PROPOSED: After creating ideas, update cluster status: +# In save_output() method, after creating ideas: +cluster.status = 'mapped' +cluster.save() +``` + +**Additional:** Ideas default stays `status='new'` + +--- + +#### 4. `/backend/igny8_core/ai/functions/generate_content.py` (Line 318) + +**When content is generated from a task:** +```python +# CURRENT: +task.status = 'completed' +task.save() + +# PROPOSED: Also update the related Idea status: +task.status = 'completed' +task.save() + +# NEW: Update idea to completed +if hasattr(task, 'idea') and task.idea: + idea = task.idea + idea.status = 'completed' + idea.save() +``` + +**Location:** In `save_output()` method after content is created + +--- + +### Backend API Endpoint Files to Update + +#### 5. `/backend/igny8_core/modules/planner/views.py` + +**Line 1029 - Queue ideas to writer filter:** +```python +# CURRENT: +queueable_ideas = all_ideas.filter(status='new') + +# NO CHANGE NEEDED (stays 'new') +``` + +**Line 1084 - Update idea status when queued:** +```python +# CURRENT: +idea.status = 'scheduled' +idea.save() + +# PROPOSED: +idea.status = 'queued' +idea.save() +``` + +**Line 563 - Bulk create keywords from CSV:** +```python +# CURRENT: +status=row.get('status', 'pending') or 'pending' + +# PROPOSED: +status=row.get('status', 'new') or 'new' +``` + +--- + +### Frontend Configuration Files to Update + +#### 6. `/frontend/src/config/pages/keywords.config.tsx` + +**Table Column Status Badge (Lines 230-248):** +```typescript +// CURRENT: +pending: 'amber', active: 'green', archived: 'red' + +// PROPOSED: +new: 'amber', mapped: 'green', disabled: 'red' +``` + +**Filter Dropdown (Lines 310-318):** +```typescript +// CURRENT: +options: ['pending', 'active', 'archived'] + +// PROPOSED: +options: ['new', 'mapped', 'disabled'] +``` + +**Form Field Default (Lines 560-570):** +```typescript +// CURRENT: +default: 'pending' + +// PROPOSED: +default: 'new' +``` + +--- + +#### 7. `/frontend/src/config/pages/clusters.config.tsx` + +**Table Column Status Badge (Lines 190-200):** +```typescript +// CURRENT: +active: 'green', archived: 'red' (inconsistent from backend) + +// PROPOSED: +new: 'amber', mapped: 'green', disabled: 'red' +``` + +**Filter Dropdown (Lines 240-253):** +```typescript +// CURRENT: +options: ['active', 'archived'] (missing 'new') + +// PROPOSED: +options: ['new', 'mapped', 'disabled'] +``` + +**Form Field Default (Lines 405-418):** +```typescript +// CURRENT: +default: 'new' + +// PROPOSED: +default: 'new' (no change, already correct) +``` + +--- + +#### 8. `/frontend/src/config/pages/ideas.config.tsx` + +**Table Column Status Badge (Lines 170-185):** +```typescript +// CURRENT: +new: 'amber', scheduled: 'blue', completed: 'blue', published: 'green' + +// PROPOSED: +new: 'amber', queued: 'blue', completed: 'blue', published: 'green' +``` + +**Filter Dropdown (Lines 218-228):** +```typescript +// CURRENT: +options: ['new', 'scheduled', 'completed', 'published'] +// NOTE: missing 'completed' in some places + +// PROPOSED: +options: ['new', 'queued', 'completed', 'published'] +``` + +**Form Field Default (Lines 372-385):** +```typescript +// CURRENT: +default: 'new' + +// PROPOSED: +default: 'new' (no change) +``` + +--- + +### Frontend Dashboard & State Files + +#### 9. `/frontend/src/pages/Planner/Dashboard.tsx` + +**Status Metrics (Lines 121-155):** +```typescript +// Updates status counting logic +// From: keywordsByStatus, clustersByStatus, ideaByStatus +// To: Use new status values + +// Example refactor: +// OLD: keywordsByStatus['active'] vs keywordsByStatus['pending'] +// NEW: keywordsByStatus['mapped'] vs keywordsByStatus['new'] +``` + +**No API call changes needed** - API returns correct values + +--- + +#### 10. `/frontend/src/services/api.ts` + +**No changes needed** - Only returns what backend provides + +--- + +### Frontend Component Files + +#### 11. `/frontend/src/pages/Planner/Keywords.tsx` + +**Status Filter Logic (if hardcoded):** +- Search for status comparisons +- Update from old values β†’ new values + +#### 12. `/frontend/src/pages/Planner/Clusters.tsx` + +**Status Filter Logic (if hardcoded):** +- Search for status comparisons +- Update from old values β†’ new values + +#### 13. `/frontend/src/pages/Planner/Ideas.tsx` + +**Status Filter Logic (if hardcoded):** +- Update from `scheduled` β†’ `queued` +- Keep `new`, `completed`, `published` + +--- + +### Database Migration Files + +#### 14. New Django Migration Required + +**File to create:** `/backend/igny8_core/business/planning/migrations/NNNN_update_status_choices.py` + +```python +# Migration tasks: +# 1. Update Keywords STATUS_CHOICES in model +# 2. Data migration: pending β†’ new, active β†’ mapped, archived β†’ disabled +# 3. Update Clusters status field with choices +# 4. Data migration: active β†’ new (or map by count of ideas) +# 5. Update ContentIdeas STATUS_CHOICES +# 6. Data migration: scheduled β†’ queued +``` + +**Changes needed:** +- Keywords: pendingβ†’new, activeβ†’mapped, archivedβ†’disabled +- Clusters: activeβ†’new or mapped (based on ideas_count), archivedβ†’disabled +- ContentIdeas: scheduledβ†’queued + +--- + +## πŸ“ Location Summary + +### Total Files to Update: 13 + +| Category | Count | Files | +|----------|-------|-------| +| Backend Models | 1 | models.py | +| Backend AI Functions | 3 | auto_cluster.py, generate_ideas.py, generate_content.py | +| Backend API Views | 1 | views.py | +| Frontend Configs | 3 | keywords.config.tsx, clusters.config.tsx, ideas.config.tsx | +| Frontend Pages | 3 | Dashboard.tsx, Keywords.tsx, Clusters.tsx, Ideas.tsx | +| Database Migrations | 1 | New migration file | +| **TOTAL** | **13** | | + +--- + +## πŸ”„ Dependency Order + +### Phase 1: Backend Models & Data Migration (BLOCKER) +1. Update `models.py` STATUS_CHOICES for all 3 models +2. Create Django migration with data transformation +3. Run migration on database +4. Test: Backend API returns new status values + +### Phase 2: AI Functions (Status Update Logic) +5. Update `auto_cluster.py` line 297: keywords status β†’ 'mapped' +6. Add to `generate_ideas.py`: clusters status β†’ 'mapped' after ideas created +7. Update `generate_content.py` line 318: ideas status β†’ 'completed' after content created +8. Test: AI workflows set correct status values + +### Phase 3: Backend API (Response Values) +9. Update `views.py` line 563: CSV import defaults to 'new' +10. Update `views.py` line 1084: queue to writer uses 'queued' +11. Test: API responses have new status values + +### Phase 4: Frontend Configuration (Display Logic) +12. Update `keywords.config.tsx` (3 locations: badge, filter, form) +13. Update `clusters.config.tsx` (3 locations: badge, filter, form) +14. Update `ideas.config.tsx` (3 locations: badge, filter, form) +15. Test: Filters and forms display new status values + +### Phase 5: Frontend Pages (State Management) +16. Update `Dashboard.tsx` status metrics calculations +17. Update `Keywords.tsx` status filters (if hardcoded) +18. Update `Clusters.tsx` status filters (if hardcoded) +19. Update `Ideas.tsx` status filters and comparisons +20. Test: Dashboard shows correct counts, filters work + +--- + +## πŸ§ͺ Testing Checklist + +### Backend Tests +- [ ] Models accept new status values only (no old values allowed) +- [ ] Migration transforms data correctly +- [ ] Auto-cluster: Keywords change from new β†’ mapped +- [ ] Generate ideas: Clusters change from new β†’ mapped +- [ ] Generate content: Ideas change from queued β†’ completed +- [ ] API returns new status values + +### Frontend Tests +- [ ] Keywords: Table shows new/mapped/disabled badges with correct colors +- [ ] Keywords: Filter dropdown shows new/mapped/disabled options +- [ ] Keywords: Create form defaults to 'new' +- [ ] Clusters: Table shows new/mapped/disabled badges +- [ ] Clusters: Filter shows new/mapped/disabled options +- [ ] Clusters: Create form defaults to 'new' +- [ ] Ideas: Table shows new/queued/completed/published badges +- [ ] Ideas: Filter shows all 4 status options +- [ ] Ideas: Queue to writer changes status to 'queued' +- [ ] Dashboard: Metrics count correctly with new status values +- [ ] Dashboard: Workflow percentage bars calculate correctly + +### Integration Tests +- [ ] Full workflow: keyword β†’ cluster β†’ idea β†’ queued β†’ completed β†’ published +- [ ] Status transitions are unidirectional (newβ†’mapped, queuedβ†’completed, etc.) +- [ ] Disabled items don't appear in workflow suggestions +- [ ] Bulk operations update status correctly + +--- + +## 🎯 Benefits of Unified Structure + +1. **Consistency:** All modules follow same pattern (new β†’ mapped β†’ disabled) +2. **Clarity:** Status names reflect actual workflow state, not just data state +3. **Scalability:** Easy to add new statuses without confusion +4. **Maintainability:** Single place to understand entire status flow +5. **User Understanding:** Users immediately understand workflow progression +6. **AI Integration:** AI functions have clear status update points + +--- + +## ⚠️ Breaking Changes Alert + +### For API Consumers +- Old status values `pending`, `active`, `archived`, `scheduled` will no longer be accepted +- API will return only `new`, `mapped`, `disabled`, `queued`, `completed`, `published` +- Clients must update filtering logic + +### For Database +- Existing records will be migrated with data transformation +- No data loss, but status values will change +- Recommended: Backup database before running migration + +### For Frontend +- Old hardcoded status comparisons must be updated +- Filter options in dropdowns must match new values +- Status badges must use new color mappings + +--- + +## πŸ“‹ Rollback Plan + +If issues arise: +1. Reverse Django migration (restores old status values and data) +2. Revert backend code to previous version +3. Revert frontend configs to previous version +4. Test API and UI return to old behavior + +--- + +**Status:** Plan Complete - Ready for Implementation +**Next Step:** Execute Phase 1 (Backend Models & Migration) + diff --git a/backend/igny8_core/ai/functions/generate_content.py b/backend/igny8_core/ai/functions/generate_content.py index 57f3ceca..17c8ef76 100644 --- a/backend/igny8_core/ai/functions/generate_content.py +++ b/backend/igny8_core/ai/functions/generate_content.py @@ -318,6 +318,12 @@ class GenerateContentFunction(BaseAIFunction): task.status = 'completed' task.save(update_fields=['status', 'updated_at']) + # NEW: Auto-sync idea status from task status + if hasattr(task, 'idea') and task.idea: + task.idea.status = 'completed' + task.idea.save(update_fields=['status', 'updated_at']) + logger.info(f"Updated related idea ID {task.idea.id} to completed") + return { 'count': 1, 'content_id': content_record.id, diff --git a/backend/igny8_core/ai/functions/generate_ideas.py b/backend/igny8_core/ai/functions/generate_ideas.py index 2054d602..00898721 100644 --- a/backend/igny8_core/ai/functions/generate_ideas.py +++ b/backend/igny8_core/ai/functions/generate_ideas.py @@ -227,6 +227,11 @@ class GenerateIdeasFunction(BaseAIFunction): sector=cluster.sector, ) ideas_created += 1 + + # Update cluster status to 'mapped' after ideas are generated + if cluster and cluster.status == 'new': + cluster.status = 'mapped' + cluster.save() return { 'count': ideas_created, diff --git a/backend/igny8_core/business/automation/__init__.py b/backend/igny8_core/business/automation/__init__.py deleted file mode 100644 index 7d541fe5..00000000 --- a/backend/igny8_core/business/automation/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Automation business logic - AutomationRule, ScheduledTask models and services -""" - diff --git a/backend/igny8_core/business/automation/migrations/__init__.py b/backend/igny8_core/business/automation/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/igny8_core/business/automation/models.py b/backend/igny8_core/business/automation/models.py deleted file mode 100644 index e4121520..00000000 --- a/backend/igny8_core/business/automation/models.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Automation Models -Phase 2: Automation System -""" -from django.db import models -from django.core.validators import MinValueValidator -from igny8_core.auth.models import SiteSectorBaseModel, AccountBaseModel -import json - - -class AutomationRule(SiteSectorBaseModel): - """ - Automation Rule model for defining automated workflows. - - Rules can be triggered by: - - schedule: Time-based triggers (cron-like) - - event: Event-based triggers (content created, keyword added, etc.) - - manual: Manual execution only - """ - - TRIGGER_CHOICES = [ - ('schedule', 'Schedule'), - ('event', 'Event'), - ('manual', 'Manual'), - ] - - STATUS_CHOICES = [ - ('active', 'Active'), - ('inactive', 'Inactive'), - ('paused', 'Paused'), - ] - - name = models.CharField(max_length=255, help_text="Rule name") - description = models.TextField(blank=True, null=True, help_text="Rule description") - - # Trigger configuration - trigger = models.CharField(max_length=50, choices=TRIGGER_CHOICES, default='manual') - - # Schedule configuration (for schedule triggers) - # Stored as cron-like string: "0 0 * * *" (daily at midnight) - schedule = models.CharField( - max_length=100, - blank=True, - null=True, - help_text="Cron-like schedule string (e.g., '0 0 * * *' for daily at midnight)" - ) - - # Conditions (JSON field) - # Format: [{"field": "content.status", "operator": "equals", "value": "draft"}, ...] - conditions = models.JSONField( - default=list, - help_text="List of conditions that must be met for rule to execute" - ) - - # Actions (JSON field) - # Format: [{"type": "generate_content", "params": {...}}, ...] - actions = models.JSONField( - default=list, - help_text="List of actions to execute when rule triggers" - ) - - # Status - is_active = models.BooleanField(default=True, help_text="Whether rule is active") - status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='active') - - # Execution tracking - last_executed_at = models.DateTimeField(null=True, blank=True) - execution_count = models.IntegerField(default=0, validators=[MinValueValidator(0)]) - - # Metadata - metadata = models.JSONField(default=dict, help_text="Additional metadata") - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - app_label = 'automation' - db_table = 'igny8_automation_rules' - ordering = ['-created_at'] - verbose_name = 'Automation Rule' - verbose_name_plural = 'Automation Rules' - indexes = [ - models.Index(fields=['trigger', 'is_active']), - models.Index(fields=['status']), - models.Index(fields=['site', 'sector']), - models.Index(fields=['trigger', 'is_active', 'status']), - ] - - def __str__(self): - return f"{self.name} ({self.get_trigger_display()})" - - -class ScheduledTask(AccountBaseModel): - """ - Scheduled Task model for tracking scheduled automation rule executions. - """ - - STATUS_CHOICES = [ - ('pending', 'Pending'), - ('running', 'Running'), - ('completed', 'Completed'), - ('failed', 'Failed'), - ('cancelled', 'Cancelled'), - ] - - automation_rule = models.ForeignKey( - AutomationRule, - on_delete=models.CASCADE, - related_name='scheduled_tasks', - help_text="The automation rule this task belongs to" - ) - - scheduled_at = models.DateTimeField(help_text="When the task is scheduled to run") - executed_at = models.DateTimeField(null=True, blank=True, help_text="When the task was actually executed") - - status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='pending') - - # Execution results - result = models.JSONField(default=dict, help_text="Execution result data") - error_message = models.TextField(blank=True, null=True, help_text="Error message if execution failed") - - # Metadata - metadata = models.JSONField(default=dict, help_text="Additional metadata") - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - app_label = 'automation' - db_table = 'igny8_scheduled_tasks' - ordering = ['-scheduled_at'] - verbose_name = 'Scheduled Task' - verbose_name_plural = 'Scheduled Tasks' - indexes = [ - models.Index(fields=['automation_rule', 'status']), - models.Index(fields=['scheduled_at', 'status']), - models.Index(fields=['account', 'status']), - models.Index(fields=['status', 'scheduled_at']), - ] - - def __str__(self): - return f"Scheduled task for {self.automation_rule.name} at {self.scheduled_at}" - diff --git a/backend/igny8_core/business/automation/services/__init__.py b/backend/igny8_core/business/automation/services/__init__.py deleted file mode 100644 index d461a941..00000000 --- a/backend/igny8_core/business/automation/services/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Automation services -""" - diff --git a/backend/igny8_core/business/automation/services/action_executor.py b/backend/igny8_core/business/automation/services/action_executor.py deleted file mode 100644 index 5b057c9d..00000000 --- a/backend/igny8_core/business/automation/services/action_executor.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Action Executor -Executes rule actions -""" -import logging -from igny8_core.business.planning.services.clustering_service import ClusteringService -from igny8_core.business.planning.services.ideas_service import IdeasService -from igny8_core.business.content.services.content_generation_service import ContentGenerationService - -logger = logging.getLogger(__name__) - - -class ActionExecutor: - """Executes rule actions""" - - def __init__(self): - self.clustering_service = ClusteringService() - self.ideas_service = IdeasService() - self.content_service = ContentGenerationService() - - def execute(self, action, context, rule): - """ - Execute a single action. - - Args: - action: Action dict with 'type' and 'params' - context: Context dict - rule: AutomationRule instance - - Returns: - dict: Action execution result - """ - action_type = action.get('type') - params = action.get('params', {}) - - if action_type == 'cluster_keywords': - return self._execute_cluster_keywords(params, rule) - elif action_type == 'generate_ideas': - return self._execute_generate_ideas(params, rule) - elif action_type == 'generate_content': - return self._execute_generate_content(params, rule) - else: - logger.warning(f"Unknown action type: {action_type}") - return { - 'success': False, - 'error': f'Unknown action type: {action_type}' - } - - def _execute_cluster_keywords(self, params, rule): - """Execute cluster keywords action""" - keyword_ids = params.get('keyword_ids', []) - sector_id = params.get('sector_id') or (rule.sector.id if rule.sector else None) - - try: - result = self.clustering_service.cluster_keywords( - keyword_ids=keyword_ids, - account=rule.account, - sector_id=sector_id - ) - return result - except Exception as e: - logger.error(f"Error clustering keywords: {str(e)}", exc_info=True) - return { - 'success': False, - 'error': str(e) - } - - def _execute_generate_ideas(self, params, rule): - """Execute generate ideas action""" - cluster_ids = params.get('cluster_ids', []) - - try: - result = self.ideas_service.generate_ideas( - cluster_ids=cluster_ids, - account=rule.account - ) - return result - except Exception as e: - logger.error(f"Error generating ideas: {str(e)}", exc_info=True) - return { - 'success': False, - 'error': str(e) - } - - def _execute_generate_content(self, params, rule): - """Execute generate content action""" - task_ids = params.get('task_ids', []) - - try: - result = self.content_service.generate_content( - task_ids=task_ids, - account=rule.account - ) - return result - except Exception as e: - logger.error(f"Error generating content: {str(e)}", exc_info=True) - return { - 'success': False, - 'error': str(e) - } - diff --git a/backend/igny8_core/business/automation/services/automation_service.py b/backend/igny8_core/business/automation/services/automation_service.py deleted file mode 100644 index 5a91576f..00000000 --- a/backend/igny8_core/business/automation/services/automation_service.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Automation Service -Main service for executing automation rules -""" -import logging -from django.utils import timezone -from igny8_core.business.automation.models import AutomationRule, ScheduledTask -from igny8_core.business.automation.services.rule_engine import RuleEngine -from igny8_core.business.billing.services.credit_service import CreditService -from igny8_core.business.billing.exceptions import InsufficientCreditsError - -logger = logging.getLogger(__name__) - - -class AutomationService: - """Service for executing automation rules""" - - def __init__(self): - self.rule_engine = RuleEngine() - self.credit_service = CreditService() - - def execute_rule(self, rule, context=None): - """ - Execute an automation rule. - - Args: - rule: AutomationRule instance - context: Optional context dict for condition evaluation - - Returns: - dict: Execution result with status and data - """ - if not rule.is_active or rule.status != 'active': - return { - 'status': 'skipped', - 'reason': 'Rule is inactive', - 'rule_id': rule.id - } - - # Check credits (estimate based on actions) - estimated_credits = self._estimate_credits(rule) - try: - self.credit_service.check_credits_legacy(rule.account, estimated_credits) - except InsufficientCreditsError as e: - logger.warning(f"Rule {rule.id} skipped: {str(e)}") - return { - 'status': 'skipped', - 'reason': f'Insufficient credits: {str(e)}', - 'rule_id': rule.id - } - - # Execute via rule engine - try: - result = self.rule_engine.execute(rule, context or {}) - - # Update rule tracking - rule.last_executed_at = timezone.now() - rule.execution_count += 1 - rule.save(update_fields=['last_executed_at', 'execution_count']) - - return { - 'status': 'completed', - 'rule_id': rule.id, - 'result': result - } - except Exception as e: - logger.error(f"Error executing rule {rule.id}: {str(e)}", exc_info=True) - return { - 'status': 'failed', - 'reason': str(e), - 'rule_id': rule.id - } - - def _estimate_credits(self, rule): - """Estimate credits needed for rule execution""" - # Simple estimation based on action types - estimated = 0 - for action in rule.actions: - action_type = action.get('type', '') - if 'cluster' in action_type: - estimated += 10 - elif 'idea' in action_type: - estimated += 15 - elif 'content' in action_type: - estimated += 50 # Conservative estimate - else: - estimated += 5 # Default - return max(estimated, 10) # Minimum 10 credits - - def execute_scheduled_rules(self): - """ - Execute all scheduled rules that are due. - Called by Celery Beat task. - - Returns: - dict: Summary of executions - """ - from django.utils import timezone - now = timezone.now() - - # Get active scheduled rules - rules = AutomationRule.objects.filter( - trigger='schedule', - is_active=True, - status='active' - ) - - executed = 0 - skipped = 0 - failed = 0 - - for rule in rules: - # Check if rule should execute based on schedule - if self._should_execute_schedule(rule, now): - result = self.execute_rule(rule) - if result['status'] == 'completed': - executed += 1 - elif result['status'] == 'skipped': - skipped += 1 - else: - failed += 1 - - return { - 'executed': executed, - 'skipped': skipped, - 'failed': failed, - 'total': len(rules) - } - - def _should_execute_schedule(self, rule, now): - """ - Check if a scheduled rule should execute now. - Simple implementation - can be enhanced with proper cron parsing. - """ - if not rule.schedule: - return False - - # For now, simple check - can be enhanced with cron parser - # This is a placeholder - proper implementation would parse cron string - return True # Simplified for now - diff --git a/backend/igny8_core/business/automation/services/condition_evaluator.py b/backend/igny8_core/business/automation/services/condition_evaluator.py deleted file mode 100644 index dce9bd99..00000000 --- a/backend/igny8_core/business/automation/services/condition_evaluator.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Condition Evaluator -Evaluates rule conditions -""" -import logging - -logger = logging.getLogger(__name__) - - -class ConditionEvaluator: - """Evaluates rule conditions""" - - OPERATORS = { - 'equals': lambda a, b: a == b, - 'not_equals': lambda a, b: a != b, - 'greater_than': lambda a, b: a > b, - 'greater_than_or_equal': lambda a, b: a >= b, - 'less_than': lambda a, b: a < b, - 'less_than_or_equal': lambda a, b: a <= b, - 'in': lambda a, b: a in b, - 'contains': lambda a, b: b in a if isinstance(a, str) else a in b, - 'is_empty': lambda a, b: not a or (isinstance(a, str) and not a.strip()), - 'is_not_empty': lambda a, b: a and (not isinstance(a, str) or a.strip()), - } - - def evaluate(self, conditions, context): - """ - Evaluate a list of conditions. - - Args: - conditions: List of condition dicts - context: Context dict for field resolution - - Returns: - bool: True if all conditions are met - """ - if not conditions: - return True - - for condition in conditions: - if not self._evaluate_condition(condition, context): - return False - - return True - - def _evaluate_condition(self, condition, context): - """ - Evaluate a single condition. - - Condition format: - { - "field": "content.status", - "operator": "equals", - "value": "draft" - } - """ - field_path = condition.get('field') - operator = condition.get('operator', 'equals') - expected_value = condition.get('value') - - if not field_path: - logger.warning("Condition missing 'field'") - return False - - # Resolve field value from context - actual_value = self._resolve_field(field_path, context) - - # Get operator function - op_func = self.OPERATORS.get(operator) - if not op_func: - logger.warning(f"Unknown operator: {operator}") - return False - - # Evaluate - try: - return op_func(actual_value, expected_value) - except Exception as e: - logger.error(f"Error evaluating condition: {str(e)}", exc_info=True) - return False - - def _resolve_field(self, field_path, context): - """ - Resolve a field path from context. - - Examples: - - "content.status" -> context['content']['status'] - - "count" -> context['count'] - """ - parts = field_path.split('.') - value = context - - for part in parts: - if isinstance(value, dict): - value = value.get(part) - elif hasattr(value, part): - value = getattr(value, part) - else: - return None - - if value is None: - return None - - return value - diff --git a/backend/igny8_core/business/automation/services/rule_engine.py b/backend/igny8_core/business/automation/services/rule_engine.py deleted file mode 100644 index 724b4103..00000000 --- a/backend/igny8_core/business/automation/services/rule_engine.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Rule Engine -Orchestrates rule execution -""" -import logging -from igny8_core.business.automation.services.condition_evaluator import ConditionEvaluator -from igny8_core.business.automation.services.action_executor import ActionExecutor - -logger = logging.getLogger(__name__) - - -class RuleEngine: - """Orchestrates rule execution""" - - def __init__(self): - self.condition_evaluator = ConditionEvaluator() - self.action_executor = ActionExecutor() - - def execute(self, rule, context): - """ - Execute a rule by evaluating conditions and executing actions. - - Args: - rule: AutomationRule instance - context: Context dict for evaluation - - Returns: - dict: Execution results - """ - # Evaluate conditions - if rule.conditions: - conditions_met = self.condition_evaluator.evaluate(rule.conditions, context) - if not conditions_met: - return { - 'success': False, - 'reason': 'Conditions not met' - } - - # Execute actions - action_results = [] - for action in rule.actions: - try: - result = self.action_executor.execute(action, context, rule) - action_results.append({ - 'action': action, - 'success': True, - 'result': result - }) - except Exception as e: - logger.error(f"Action execution failed: {str(e)}", exc_info=True) - action_results.append({ - 'action': action, - 'success': False, - 'error': str(e) - }) - - return { - 'success': True, - 'actions': action_results - } - diff --git a/backend/igny8_core/business/automation/tasks.py b/backend/igny8_core/business/automation/tasks.py deleted file mode 100644 index 2b9b97c3..00000000 --- a/backend/igny8_core/business/automation/tasks.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Automation Celery Tasks -""" -from celery import shared_task -import logging -from igny8_core.business.automation.services.automation_service import AutomationService - -logger = logging.getLogger(__name__) - - -@shared_task(name='igny8_core.business.automation.tasks.execute_scheduled_automation_rules') -def execute_scheduled_automation_rules(): - """ - Execute all scheduled automation rules. - Called by Celery Beat. - """ - try: - service = AutomationService() - result = service.execute_scheduled_rules() - logger.info(f"Executed scheduled automation rules: {result}") - return result - except Exception as e: - logger.error(f"Error executing scheduled automation rules: {str(e)}", exc_info=True) - return { - 'success': False, - 'error': str(e) - } - diff --git a/backend/igny8_core/business/planning/models.py b/backend/igny8_core/business/planning/models.py index 8a9830ca..8da40bee 100644 --- a/backend/igny8_core/business/planning/models.py +++ b/backend/igny8_core/business/planning/models.py @@ -5,12 +5,18 @@ from igny8_core.auth.models import SiteSectorBaseModel, SeedKeyword class Clusters(SiteSectorBaseModel): """Clusters model for keyword grouping - pure topic clusters""" + STATUS_CHOICES = [ + ('new', 'New'), + ('mapped', 'Mapped'), + ] + name = models.CharField(max_length=255, unique=True, db_index=True) description = models.TextField(blank=True, null=True) keywords_count = models.IntegerField(default=0) volume = models.IntegerField(default=0) mapped_pages = models.IntegerField(default=0) - status = models.CharField(max_length=50, default='active') + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new') + disabled = models.BooleanField(default=False, help_text="Exclude from processes") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -37,9 +43,8 @@ class Keywords(SiteSectorBaseModel): """ STATUS_CHOICES = [ - ('active', 'Active'), - ('pending', 'Pending'), - ('archived', 'Archived'), + ('new', 'New'), + ('mapped', 'Mapped'), ] # Required: Link to global SeedKeyword @@ -75,7 +80,8 @@ class Keywords(SiteSectorBaseModel): related_name='keywords', limit_choices_to={'sector': models.F('sector')} # Cluster must be in same sector ) - status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='pending') + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new') + disabled = models.BooleanField(default=False, help_text="Exclude from processes") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -142,8 +148,8 @@ class ContentIdeas(SiteSectorBaseModel): STATUS_CHOICES = [ ('new', 'New'), - ('scheduled', 'Scheduled'), - ('published', 'Published'), + ('queued', 'Queued'), + ('completed', 'Completed'), ] CONTENT_TYPE_CHOICES = [ @@ -193,6 +199,7 @@ class ContentIdeas(SiteSectorBaseModel): ) # REMOVED: taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint functionality) status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new') + disabled = models.BooleanField(default=False, help_text="Exclude from processes") estimated_word_count = models.IntegerField(default=1000) content_type = models.CharField( max_length=50, diff --git a/backend/igny8_core/celery.py b/backend/igny8_core/celery.py index e864c785..4716ac40 100644 --- a/backend/igny8_core/celery.py +++ b/backend/igny8_core/celery.py @@ -25,10 +25,6 @@ app.conf.beat_schedule = { 'task': 'igny8_core.modules.billing.tasks.replenish_monthly_credits', 'schedule': crontab(hour=0, minute=0, day_of_month=1), # First day of month at midnight }, - 'execute-scheduled-automation-rules': { - 'task': 'igny8_core.business.automation.tasks.execute_scheduled_automation_rules', - 'schedule': crontab(minute='*/5'), # Every 5 minutes - }, # WordPress Publishing Tasks 'process-pending-wordpress-publications': { 'task': 'igny8_core.tasks.wordpress_publishing.process_pending_wordpress_publications', diff --git a/backend/igny8_core/modules/automation/__init__.py b/backend/igny8_core/modules/automation/__init__.py deleted file mode 100644 index 4844e08a..00000000 --- a/backend/igny8_core/modules/automation/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Automation Module - API Layer -Business logic is in business/automation/ -""" - diff --git a/backend/igny8_core/modules/automation/apps.py b/backend/igny8_core/modules/automation/apps.py deleted file mode 100644 index 6e6a07cf..00000000 --- a/backend/igny8_core/modules/automation/apps.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Automation App Configuration -""" -from django.apps import AppConfig - - -class AutomationConfig(AppConfig): - """Configuration for automation module""" - default_auto_field = 'django.db.models.BigAutoField' - name = 'igny8_core.modules.automation' - label = 'automation' - verbose_name = 'Automation' - diff --git a/backend/igny8_core/modules/automation/migrations/0001_initial.py b/backend/igny8_core/modules/automation/migrations/0001_initial.py deleted file mode 100644 index 855279ae..00000000 --- a/backend/igny8_core/modules/automation/migrations/0001_initial.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-20 23:27 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='AutomationRule', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='Rule name', max_length=255)), - ('description', models.TextField(blank=True, help_text='Rule description', null=True)), - ('trigger', models.CharField(choices=[('schedule', 'Schedule'), ('event', 'Event'), ('manual', 'Manual')], default='manual', max_length=50)), - ('schedule', models.CharField(blank=True, help_text="Cron-like schedule string (e.g., '0 0 * * *' for daily at midnight)", max_length=100, null=True)), - ('conditions', models.JSONField(default=list, help_text='List of conditions that must be met for rule to execute')), - ('actions', models.JSONField(default=list, help_text='List of actions to execute when rule triggers')), - ('is_active', models.BooleanField(default=True, help_text='Whether rule is active')), - ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('paused', 'Paused')], default='active', max_length=50)), - ('last_executed_at', models.DateTimeField(blank=True, null=True)), - ('execution_count', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])), - ('metadata', models.JSONField(default=dict, help_text='Additional metadata')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'Automation Rule', - 'verbose_name_plural': 'Automation Rules', - 'db_table': 'igny8_automation_rules', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='ScheduledTask', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('scheduled_at', models.DateTimeField(help_text='When the task is scheduled to run')), - ('executed_at', models.DateTimeField(blank=True, help_text='When the task was actually executed', null=True)), - ('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=50)), - ('result', models.JSONField(default=dict, help_text='Execution result data')), - ('error_message', models.TextField(blank=True, help_text='Error message if execution failed', null=True)), - ('metadata', models.JSONField(default=dict, help_text='Additional metadata')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'Scheduled Task', - 'verbose_name_plural': 'Scheduled Tasks', - 'db_table': 'igny8_scheduled_tasks', - 'ordering': ['-scheduled_at'], - }, - ), - ] diff --git a/backend/igny8_core/modules/automation/migrations/0002_initial.py b/backend/igny8_core/modules/automation/migrations/0002_initial.py deleted file mode 100644 index ef568aa9..00000000 --- a/backend/igny8_core/modules/automation/migrations/0002_initial.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-20 23:27 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('automation', '0001_initial'), - ('igny8_core_auth', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='automationrule', - name='account', - field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'), - ), - migrations.AddField( - model_name='automationrule', - name='sector', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'), - ), - migrations.AddField( - model_name='automationrule', - name='site', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'), - ), - migrations.AddField( - model_name='scheduledtask', - name='account', - field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'), - ), - migrations.AddField( - model_name='scheduledtask', - name='automation_rule', - field=models.ForeignKey(help_text='The automation rule this task belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_tasks', to='automation.automationrule'), - ), - migrations.AddIndex( - model_name='automationrule', - index=models.Index(fields=['trigger', 'is_active'], name='igny8_autom_trigger_32979f_idx'), - ), - migrations.AddIndex( - model_name='automationrule', - index=models.Index(fields=['status'], name='igny8_autom_status_827c0d_idx'), - ), - migrations.AddIndex( - model_name='automationrule', - index=models.Index(fields=['site', 'sector'], name='igny8_autom_site_id_d0a51d_idx'), - ), - migrations.AddIndex( - model_name='automationrule', - index=models.Index(fields=['trigger', 'is_active', 'status'], name='igny8_autom_trigger_f3f3e2_idx'), - ), - migrations.AddIndex( - model_name='scheduledtask', - index=models.Index(fields=['automation_rule', 'status'], name='igny8_sched_automat_da6c85_idx'), - ), - migrations.AddIndex( - model_name='scheduledtask', - index=models.Index(fields=['scheduled_at', 'status'], name='igny8_sched_schedul_1e3342_idx'), - ), - migrations.AddIndex( - model_name='scheduledtask', - index=models.Index(fields=['account', 'status'], name='igny8_sched_tenant__7244a8_idx'), - ), - migrations.AddIndex( - model_name='scheduledtask', - index=models.Index(fields=['status', 'scheduled_at'], name='igny8_sched_status_21f32f_idx'), - ), - ] diff --git a/backend/igny8_core/modules/automation/migrations/__init__.py b/backend/igny8_core/modules/automation/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/igny8_core/modules/automation/models.py b/backend/igny8_core/modules/automation/models.py deleted file mode 100644 index d1540065..00000000 --- a/backend/igny8_core/modules/automation/models.py +++ /dev/null @@ -1,5 +0,0 @@ -# Backward compatibility alias - models moved to business/automation/ -from igny8_core.business.automation.models import AutomationRule, ScheduledTask - -__all__ = ['AutomationRule', 'ScheduledTask'] - diff --git a/backend/igny8_core/modules/automation/serializers.py b/backend/igny8_core/modules/automation/serializers.py deleted file mode 100644 index 915691af..00000000 --- a/backend/igny8_core/modules/automation/serializers.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Serializers for Automation Models -""" -from rest_framework import serializers -from igny8_core.business.automation.models import AutomationRule, ScheduledTask - - -class AutomationRuleSerializer(serializers.ModelSerializer): - """Serializer for AutomationRule model""" - - class Meta: - model = AutomationRule - fields = [ - 'id', 'name', 'description', 'trigger', 'schedule', - 'conditions', 'actions', 'is_active', 'status', - 'last_executed_at', 'execution_count', - 'metadata', 'created_at', 'updated_at', - 'account', 'site', 'sector' - ] - read_only_fields = ['id', 'created_at', 'updated_at', 'last_executed_at', 'execution_count'] - - -class ScheduledTaskSerializer(serializers.ModelSerializer): - """Serializer for ScheduledTask model""" - automation_rule_name = serializers.CharField(source='automation_rule.name', read_only=True) - - class Meta: - model = ScheduledTask - fields = [ - 'id', 'automation_rule', 'automation_rule_name', - 'scheduled_at', 'executed_at', 'status', - 'result', 'error_message', 'metadata', - 'created_at', 'updated_at', 'account' - ] - read_only_fields = ['id', 'created_at', 'updated_at', 'executed_at'] - diff --git a/backend/igny8_core/modules/automation/urls.py b/backend/igny8_core/modules/automation/urls.py deleted file mode 100644 index fc4babe8..00000000 --- a/backend/igny8_core/modules/automation/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -URL patterns for automation module. -""" -from django.urls import path, include -from rest_framework.routers import DefaultRouter -from .views import AutomationRuleViewSet, ScheduledTaskViewSet - -router = DefaultRouter() -router.register(r'rules', AutomationRuleViewSet, basename='automation-rule') -router.register(r'scheduled-tasks', ScheduledTaskViewSet, basename='scheduled-task') - -urlpatterns = [ - path('', include(router.urls)), -] - diff --git a/backend/igny8_core/modules/automation/views.py b/backend/igny8_core/modules/automation/views.py deleted file mode 100644 index 6ec04661..00000000 --- a/backend/igny8_core/modules/automation/views.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -ViewSets for Automation Models -Unified API Standard v1.0 compliant -""" -from rest_framework import viewsets, status -from rest_framework.decorators import action -from rest_framework.response import Response -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters -from drf_spectacular.utils import extend_schema, extend_schema_view -from igny8_core.api.base import SiteSectorModelViewSet, AccountModelViewSet -from igny8_core.api.pagination import CustomPageNumberPagination -from igny8_core.api.response import success_response, error_response -from igny8_core.api.throttles import DebugScopedRateThrottle -from igny8_core.api.permissions import IsAuthenticatedAndActive, IsViewerOrAbove -from igny8_core.business.automation.models import AutomationRule, ScheduledTask -from igny8_core.business.automation.services.automation_service import AutomationService -from .serializers import AutomationRuleSerializer, ScheduledTaskSerializer - - -@extend_schema_view( - list=extend_schema(tags=['Automation']), - create=extend_schema(tags=['Automation']), - retrieve=extend_schema(tags=['Automation']), - update=extend_schema(tags=['Automation']), - partial_update=extend_schema(tags=['Automation']), - destroy=extend_schema(tags=['Automation']), -) -class AutomationRuleViewSet(SiteSectorModelViewSet): - """ - ViewSet for managing automation rules - Unified API Standard v1.0 compliant - """ - queryset = AutomationRule.objects.all() - serializer_class = AutomationRuleSerializer - permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove] - pagination_class = CustomPageNumberPagination - throttle_scope = 'automation' - throttle_classes = [DebugScopedRateThrottle] - - filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] - search_fields = ['name', 'description'] - ordering_fields = ['name', 'created_at', 'last_executed_at', 'execution_count'] - ordering = ['-created_at'] - filterset_fields = ['trigger', 'is_active', 'status'] - - @action(detail=True, methods=['post'], url_path='execute', url_name='execute') - def execute(self, request, pk=None): - """Manually execute an automation rule""" - rule = self.get_object() - service = AutomationService() - - try: - result = service.execute_rule(rule, context=request.data.get('context', {})) - return success_response( - data=result, - message='Rule executed successfully', - request=request - ) - except Exception as e: - return error_response( - error=str(e), - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - request=request - ) - - -@extend_schema_view( - list=extend_schema(tags=['Automation']), - create=extend_schema(tags=['Automation']), - retrieve=extend_schema(tags=['Automation']), - update=extend_schema(tags=['Automation']), - partial_update=extend_schema(tags=['Automation']), - destroy=extend_schema(tags=['Automation']), -) -class ScheduledTaskViewSet(AccountModelViewSet): - """ - ViewSet for managing scheduled tasks - Unified API Standard v1.0 compliant - """ - queryset = ScheduledTask.objects.select_related('automation_rule') - serializer_class = ScheduledTaskSerializer - permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove] - pagination_class = CustomPageNumberPagination - throttle_scope = 'automation' - throttle_classes = [DebugScopedRateThrottle] - - filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] - ordering_fields = ['scheduled_at', 'executed_at', 'status', 'created_at'] - ordering = ['-scheduled_at'] - filterset_fields = ['automation_rule', 'status'] - diff --git a/backend/igny8_core/modules/planner/migrations/0006_unified_status_refactor.py b/backend/igny8_core/modules/planner/migrations/0006_unified_status_refactor.py new file mode 100644 index 00000000..fb83a8f8 --- /dev/null +++ b/backend/igny8_core/modules/planner/migrations/0006_unified_status_refactor.py @@ -0,0 +1,114 @@ +# Generated migration for unified status refactor + +from django.db import migrations, models + + +def migrate_status_data(apps, schema_editor): + """Transform existing status data to new values""" + Keywords = apps.get_model('planner', 'Keywords') + Clusters = apps.get_model('planner', 'Clusters') + ContentIdeas = apps.get_model('planner', 'ContentIdeas') + + # Keywords: pendingβ†’new, activeβ†’mapped, archivedβ†’mapped+disabled=true + Keywords.objects.filter(status='pending').update(status='new') + Keywords.objects.filter(status='active').update(status='mapped') + # Handle archived: set to mapped and mark as disabled + archived_keywords = Keywords.objects.filter(status='archived') + for kw in archived_keywords: + kw.status = 'mapped' + kw.disabled = True + kw.save() + + # Clusters: active (with ideas)β†’mapped, active (no ideas)β†’new + # Check if cluster has any related ideas using the reverse relationship + for cluster in Clusters.objects.all(): + if cluster.ideas.exists(): + cluster.status = 'mapped' + else: + cluster.status = 'new' + cluster.save() + + # ContentIdeas: scheduledβ†’queued, publishedβ†’completed, new stays new + ContentIdeas.objects.filter(status='scheduled').update(status='queued') + ContentIdeas.objects.filter(status='published').update(status='completed') + + +def reverse_status_data(apps, schema_editor): + """Reverse migration: restore old status values""" + Keywords = apps.get_model('planner', 'Keywords') + Clusters = apps.get_model('planner', 'Clusters') + ContentIdeas = apps.get_model('planner', 'ContentIdeas') + + # Keywords: newβ†’pending, mappedβ†’active (or archived if disabled) + Keywords.objects.filter(status='new').update(status='pending') + Keywords.objects.filter(status='mapped', disabled=False).update(status='active') + Keywords.objects.filter(status='mapped', disabled=True).update(status='archived', disabled=False) + + # Clusters: all back to 'active' + Clusters.objects.all().update(status='active') + + # ContentIdeas: queuedβ†’scheduled, completedβ†’published + ContentIdeas.objects.filter(status='queued').update(status='scheduled') + ContentIdeas.objects.filter(status='completed').update(status='published') + + +class Migration(migrations.Migration): + + dependencies = [ + ('planner', '0005_field_rename_implementation'), + ] + + operations = [ + # Step 1: Add disabled field to all models (with default=False) + migrations.AddField( + model_name='clusters', + name='disabled', + field=models.BooleanField(default=False, help_text='Exclude from processes'), + ), + migrations.AddField( + model_name='keywords', + name='disabled', + field=models.BooleanField(default=False, help_text='Exclude from processes'), + ), + migrations.AddField( + model_name='contentideas', + name='disabled', + field=models.BooleanField(default=False, help_text='Exclude from processes'), + ), + + # Step 2: Alter Keywords status field choices + migrations.AlterField( + model_name='keywords', + name='status', + field=models.CharField( + choices=[('new', 'New'), ('mapped', 'Mapped')], + default='new', + max_length=50 + ), + ), + + # Step 3: Alter Clusters status field (add choices, change default) + migrations.AlterField( + model_name='clusters', + name='status', + field=models.CharField( + choices=[('new', 'New'), ('mapped', 'Mapped')], + default='new', + max_length=50 + ), + ), + + # Step 4: Alter ContentIdeas status field choices + migrations.AlterField( + model_name='contentideas', + name='status', + field=models.CharField( + choices=[('new', 'New'), ('queued', 'Queued'), ('completed', 'Completed')], + default='new', + max_length=50 + ), + ), + + # Step 5: Data migration - transform existing records + migrations.RunPython(migrate_status_data, reverse_status_data), + ] diff --git a/backend/igny8_core/modules/planner/views.py b/backend/igny8_core/modules/planner/views.py index 6a1b5394..54f6f145 100644 --- a/backend/igny8_core/modules/planner/views.py +++ b/backend/igny8_core/modules/planner/views.py @@ -560,7 +560,7 @@ class KeywordViewSet(SiteSectorModelViewSet): volume=int(row.get('volume', 0) or 0), difficulty=int(row.get('difficulty', 0) or 0), intent=row.get('intent', 'informational') or 'informational', - status=row.get('status', 'pending') or 'pending', + status=row.get('status', 'new') or 'new', site=site, sector=sector, account=account @@ -1080,8 +1080,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet): created_tasks.append(task.id) - # Update idea status - idea.status = 'scheduled' + # Update idea status to queued + idea.status = 'queued' idea.save() except Exception as e: errors.append({ diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 87dfa141..53ab939d 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -52,7 +52,7 @@ INSTALLED_APPS = [ 'igny8_core.modules.writer.apps.WriterConfig', 'igny8_core.modules.system.apps.SystemConfig', 'igny8_core.modules.billing.apps.BillingConfig', - 'igny8_core.modules.automation.apps.AutomationConfig', + # 'igny8_core.modules.automation.apps.AutomationConfig', # Removed - automation module disabled # 'igny8_core.business.site_building.apps.SiteBuildingConfig', # REMOVED: SiteBuilder/Blueprint deprecated 'igny8_core.business.optimization.apps.OptimizationConfig', 'igny8_core.business.publishing.apps.PublishingConfig', diff --git a/backend/igny8_core/urls.py b/backend/igny8_core/urls.py index f03cdadc..a6074841 100644 --- a/backend/igny8_core/urls.py +++ b/backend/igny8_core/urls.py @@ -42,7 +42,7 @@ urlpatterns = [ # Site Builder module removed - legacy blueprint functionality deprecated path('api/v1/system/', include('igny8_core.modules.system.urls')), path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints - path('api/v1/automation/', include('igny8_core.modules.automation.urls')), # Automation endpoints + # path('api/v1/automation/', include('igny8_core.modules.automation.urls')), # Automation endpoints - REMOVED path('api/v1/linker/', include('igny8_core.modules.linker.urls')), # Linker endpoints path('api/v1/optimizer/', include('igny8_core.modules.optimizer.urls')), # Optimizer endpoints path('api/v1/publisher/', include('igny8_core.modules.publisher.urls')), # Publisher endpoints diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 07a93f60..d6706a60 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -64,11 +64,6 @@ const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries")); // Setup Pages - Lazy loaded const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSectorsKeywords")); -// Other Pages - Lazy loaded -const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard")); -const AutomationRules = lazy(() => import("./pages/Automation/Rules")); -const AutomationTasks = lazy(() => import("./pages/Automation/Tasks")); - // Settings - Lazy loaded const GeneralSettings = lazy(() => import("./pages/Settings/General")); const Users = lazy(() => import("./pages/Settings/Users")); @@ -364,23 +359,6 @@ export default function App() { {/* Legacy redirect */} } /> - {/* Automation Module - Redirect dashboard to rules */} - } /> - - - - - - } /> - - - - - - } /> - {/* Settings */} diff --git a/frontend/src/api/automation.api.ts b/frontend/src/api/automation.api.ts deleted file mode 100644 index ea4e627f..00000000 --- a/frontend/src/api/automation.api.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { fetchAPI } from '../services/api'; - -/** - * Automation API Client - * Functions for automation rules and scheduled tasks - */ - -export interface AutomationRule { - id: number; - name: string; - description?: string; - trigger: 'schedule' | 'event' | 'manual'; - schedule?: string; // Cron-like string - conditions: Array<{ - field: string; - operator: string; - value: any; - }>; - actions: Array<{ - type: string; - params: Record; - }>; - is_active: boolean; - status: 'active' | 'inactive' | 'paused'; - execution_count: number; - last_executed_at?: string; - created_at: string; - updated_at: string; -} - -export interface ScheduledTask { - id: number; - rule_id?: number; - rule_name?: string; - task_type: string; - status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; - scheduled_at: string; - started_at?: string; - completed_at?: string; - result?: Record; - error?: string; - retry_count: number; - created_at: string; -} - -export interface AutomationRuleCreateData { - name: string; - description?: string; - trigger: 'schedule' | 'event' | 'manual'; - schedule?: string; - conditions?: Array<{ - field: string; - operator: string; - value: any; - }>; - actions: Array<{ - type: string; - params: Record; - }>; - is_active?: boolean; -} - -export interface AutomationRuleUpdateData extends Partial {} - -export const automationApi = { - /** - * List automation rules - */ - listRules: async (filters?: { - search?: string; - trigger?: string; - is_active?: boolean; - status?: string; - ordering?: string; - page?: number; - page_size?: number; - }) => { - const params = new URLSearchParams(); - if (filters?.search) params.append('search', filters.search); - if (filters?.trigger) params.append('trigger', filters.trigger); - if (filters?.is_active !== undefined) params.append('is_active', String(filters.is_active)); - if (filters?.status) params.append('status', filters.status); - if (filters?.ordering) params.append('ordering', filters.ordering); - if (filters?.page) params.append('page', String(filters.page)); - if (filters?.page_size) params.append('page_size', String(filters.page_size)); - - const query = params.toString(); - return await fetchAPI(`/v1/automation/rules/${query ? `?${query}` : ''}`); - }, - - /** - * Get a single automation rule - */ - getRule: async (id: number) => { - return await fetchAPI(`/v1/automation/rules/${id}/`) as AutomationRule; - }, - - /** - * Create a new automation rule - */ - createRule: async (data: AutomationRuleCreateData) => { - return await fetchAPI('/v1/automation/rules/', { - method: 'POST', - body: JSON.stringify(data), - }) as AutomationRule; - }, - - /** - * Update an automation rule - */ - updateRule: async (id: number, data: AutomationRuleUpdateData) => { - return await fetchAPI(`/v1/automation/rules/${id}/`, { - method: 'PATCH', - body: JSON.stringify(data), - }) as AutomationRule; - }, - - /** - * Delete an automation rule - */ - deleteRule: async (id: number) => { - return await fetchAPI(`/v1/automation/rules/${id}/`, { - method: 'DELETE', - }); - }, - - /** - * Execute an automation rule manually - */ - executeRule: async (id: number, context?: Record) => { - return await fetchAPI(`/v1/automation/rules/${id}/execute/`, { - method: 'POST', - body: JSON.stringify({ context: context || {} }), - }); - }, - - /** - * List scheduled tasks - */ - listTasks: async (filters?: { - rule_id?: number; - status?: string; - task_type?: string; - ordering?: string; - page?: number; - page_size?: number; - }) => { - const params = new URLSearchParams(); - if (filters?.rule_id) params.append('rule_id', String(filters.rule_id)); - if (filters?.status) params.append('status', filters.status); - if (filters?.task_type) params.append('task_type', filters.task_type); - if (filters?.ordering) params.append('ordering', filters.ordering); - if (filters?.page) params.append('page', String(filters.page)); - if (filters?.page_size) params.append('page_size', String(filters.page_size)); - - const query = params.toString(); - return await fetchAPI(`/v1/automation/scheduled-tasks/${query ? `?${query}` : ''}`); - }, - - /** - * Get a single scheduled task - */ - getTask: async (id: number) => { - return await fetchAPI(`/v1/automation/scheduled-tasks/${id}/`) as ScheduledTask; - }, - - /** - * Retry a failed scheduled task - */ - retryTask: async (id: number) => { - return await fetchAPI(`/v1/automation/scheduled-tasks/${id}/retry/`, { - method: 'POST', - }); - }, -}; - diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index f5231b1e..ab9312a8 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -192,7 +192,7 @@ export const createClustersPageConfig = ( render: (value: string) => { const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-'; return ( - + {properCase} ); @@ -248,8 +248,8 @@ export const createClustersPageConfig = ( type: 'select', options: [ { value: '', label: 'All Status' }, - { value: 'active', label: 'Active' }, - { value: 'archived', label: 'Archived' }, + { value: 'new', label: 'New' }, + { value: 'mapped', label: 'Mapped' }, ], }, { @@ -409,12 +409,12 @@ export const createClustersPageConfig = ( key: 'status', label: 'Status', type: 'select', - value: handlers.formData.status || 'active', + value: handlers.formData.status || 'new', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, status: value }), options: [ - { value: 'active', label: 'Active' }, - { value: 'archived', label: 'Archived' }, + { value: 'new', label: 'New' }, + { value: 'mapped', label: 'Mapped' }, ], }, ], diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index 6837ad28..bf7612a2 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -173,8 +173,8 @@ export const createIdeasPageConfig = ( render: (value: string) => { const statusColors: Record = { 'new': 'amber', - 'scheduled': 'info', - 'published': 'success', + 'queued': 'info', + 'completed': 'success', }; const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-'; return ( @@ -222,8 +222,8 @@ export const createIdeasPageConfig = ( options: [ { value: '', label: 'All Status' }, { value: 'new', label: 'New' }, - { value: 'scheduled', label: 'Scheduled' }, - { value: 'published', label: 'Published' }, + { value: 'queued', label: 'Queued' }, + { value: 'completed', label: 'Completed' }, ], }, { @@ -380,8 +380,8 @@ export const createIdeasPageConfig = ( handlers.setFormData({ ...handlers.formData, status: value }), options: [ { value: 'new', label: 'New' }, - { value: 'scheduled', label: 'Scheduled' }, - { value: 'published', label: 'Published' }, + { value: 'queued', label: 'Queued' }, + { value: 'completed', label: 'Completed' }, ], }, ], @@ -399,13 +399,13 @@ export const createIdeasPageConfig = ( calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length, }, { - label: 'Scheduled', + label: 'Queued', value: 0, accentColor: 'blue' as const, - calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'scheduled').length, + calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'queued').length, }, { - label: 'Published', + label: 'Completed', value: 0, accentColor: 'green' as const, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'published').length, diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index ca9abfcf..26a5043d 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -232,9 +232,9 @@ export const createKeywordsPageConfig = ( return ( handlers.setFormData({ ...handlers.formData, status: value }), options: [ - { value: 'pending', label: 'Pending' }, - { value: 'active', label: 'Active' }, - { value: 'archived', label: 'Archived' }, + { value: 'new', label: 'New' }, + { value: 'mapped', label: 'Mapped' }, ], }, ], diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index b0d3ced8..0571a468 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -95,15 +95,6 @@ const AppSidebar: React.FC = () => { }, ]; - // Add Automation if enabled (single item, no dropdown) - if (moduleEnabled('automation')) { - setupItems.push({ - icon: , - name: "Automation", - path: "/automation/rules", // Default to rules, submenus shown as in-page navigation - }); - } - // Add Thinker if enabled (single item, no dropdown) if (moduleEnabled('thinker')) { setupItems.push({ diff --git a/frontend/src/pages/Automation/Dashboard.tsx b/frontend/src/pages/Automation/Dashboard.tsx deleted file mode 100644 index c469aaf7..00000000 --- a/frontend/src/pages/Automation/Dashboard.tsx +++ /dev/null @@ -1,480 +0,0 @@ -import { useEffect, useState, lazy, Suspense } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import PageMeta from "../../components/common/PageMeta"; -import ComponentCard from "../../components/common/ComponentCard"; -import { ProgressBar } from "../../components/ui/progress"; -import { ApexOptions } from "apexcharts"; -import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard"; -import PageHeader from "../../components/common/PageHeader"; - -const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default }))); - -import { - BoltIcon, - ClockIcon, - CheckCircleIcon, - ArrowRightIcon, - CalendarIcon, - ListIcon, - GroupIcon, - FileTextIcon, - ArrowUpIcon, - ArrowDownIcon, - PaperPlaneIcon, - CloseIcon, - FileIcon, -} from "../../icons"; -import { useSiteStore } from "../../store/siteStore"; -import { useSectorStore } from "../../store/sectorStore"; - -interface AutomationStats { - activeWorkflows: number; - scheduledTasks: number; - completedToday: number; - successRate: number; - automationCoverage: { - keywords: boolean; - clustering: boolean; - ideas: boolean; - tasks: boolean; - content: boolean; - images: boolean; - publishing: boolean; - }; - recentActivity: Array<{ - id: number; - type: string; - status: string; - timestamp: Date; - itemsProcessed: number; - }>; -} - -export default function AutomationDashboard() { - const navigate = useNavigate(); - const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); - - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(new Date()); - - // Mock data for now - will be replaced with real API calls - useEffect(() => { - const fetchData = async () => { - setLoading(true); - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 500)); - - setStats({ - activeWorkflows: 3, - scheduledTasks: 12, - completedToday: 47, - successRate: 94.5, - automationCoverage: { - keywords: true, - clustering: true, - ideas: true, - tasks: false, - content: true, - images: true, - publishing: false, - }, - recentActivity: [ - { - id: 1, - type: "Content Generation", - status: "completed", - timestamp: new Date(Date.now() - 15 * 60 * 1000), - itemsProcessed: 5, - }, - { - id: 2, - type: "Image Generation", - status: "completed", - timestamp: new Date(Date.now() - 45 * 60 * 1000), - itemsProcessed: 8, - }, - { - id: 3, - type: "Keyword Clustering", - status: "completed", - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), - itemsProcessed: 12, - }, - ], - }); - - setLastUpdated(new Date()); - setLoading(false); - }; - - fetchData(); - }, [activeSite, activeSector]); - - const automationWorkflows = [ - { - id: 1, - name: "Full Pipeline Automation", - description: "Keywords β†’ Clusters β†’ Ideas β†’ Tasks β†’ Content β†’ Images β†’ Publish", - status: "active", - schedule: "Every 6 hours", - lastRun: "2 hours ago", - nextRun: "4 hours", - coverage: 85, - icon: PaperPlaneIcon, - color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]", - }, - { - id: 2, - name: "Writer Workflow", - description: "Tasks β†’ Content β†’ Images β†’ Publishing", - status: "active", - schedule: "Every 3 hours", - lastRun: "1 hour ago", - nextRun: "2 hours", - coverage: 92, - icon: FileTextIcon, - color: "from-[var(--color-success)] to-[var(--color-success-dark)]", - }, - { - id: 3, - name: "Planner Workflow", - description: "Keywords β†’ Clusters β†’ Ideas", - status: "active", - schedule: "Every 6 hours", - lastRun: "3 hours ago", - nextRun: "3 hours", - coverage: 78, - icon: ListIcon, - color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]", - }, - ]; - - const automationSteps = [ - { - step: "Keywords", - enabled: true, - description: "Auto-add keywords from opportunities", - path: "/planner/keyword-opportunities", - icon: ListIcon, - }, - { - step: "Clustering", - enabled: true, - description: "Auto-cluster keywords into groups", - path: "/planner/clusters", - icon: GroupIcon, - }, - { - step: "Ideas", - enabled: true, - description: "Auto-generate content ideas from clusters", - path: "/planner/ideas", - icon: BoltIcon, - }, - { - step: "Tasks", - enabled: false, - description: "Auto-create tasks from ideas", - path: "/writer/tasks", - icon: CheckCircleIcon, - }, - { - step: "Content", - enabled: true, - description: "Auto-generate content from tasks", - path: "/writer/content", - icon: FileTextIcon, - }, - { - step: "Images", - enabled: true, - description: "Auto-generate images for content", - path: "/writer/images", - icon: FileIcon, - }, - { - step: "Publishing", - enabled: false, - description: "Auto-publish content to WordPress", - path: "/writer/published", - icon: PaperPlaneIcon, - }, - ]; - - const chartOptions: ApexOptions = { - chart: { - type: "line", - height: 300, - toolbar: { show: false }, - zoom: { enabled: false }, - }, - stroke: { - curve: "smooth", - width: 3, - }, - xaxis: { - categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], - labels: { style: { colors: "#6b7280" } }, - }, - yaxis: { - labels: { style: { colors: "#6b7280" } }, - }, - legend: { - position: "top", - labels: { colors: "#6b7280" }, - }, - colors: ["var(--color-primary)", "var(--color-success)", "var(--color-purple)"], - grid: { - borderColor: "#e5e7eb", - }, - }; - - const chartSeries = [ - { - name: "Automated", - data: [12, 19, 15, 25, 22, 18, 24], - }, - { - name: "Manual", - data: [5, 8, 6, 10, 9, 7, 11], - }, - { - name: "Failed", - data: [1, 2, 1, 2, 1, 2, 1], - }, - ]; - - return ( - <> - - - -
- {/* Key Metrics */} -
- } - trend={0} - accentColor="purple" - /> - } - trend={0} - accentColor="blue" - /> - } - trend={0} - accentColor="green" - /> - } - trend={0} - accentColor="orange" - /> -
- - {/* Automation Workflows */} - -
- {automationWorkflows.map((workflow) => { - const Icon = workflow.icon; - return ( -
-
-
- -
- - {workflow.status} - -
-

{workflow.name}

-

{workflow.description}

-
-
- Schedule: - {workflow.schedule} -
-
- Last Run: - {workflow.lastRun} -
-
- Next Run: - {workflow.nextRun} -
-
-
-
- Coverage - {workflow.coverage}% -
- -
-
- - -
-
- ); - })} -
-
- - {/* Automation Steps Configuration */} - -
- {automationSteps.map((step, index) => { - const Icon = step.icon; - const isEnabled = stats?.automationCoverage[step.step.toLowerCase() as keyof typeof stats.automationCoverage] || false; - return ( - -
-
- -
-
- {isEnabled && } -
-
-

{step.step}

-

{step.description}

-
- Configure - -
- - ); - })} -
-
- - {/* Activity Chart */} -
- - Loading chart...
}> - - - - - {/* Recent Activity */} - -
- {stats?.recentActivity.map((activity) => ( -
-
- -
-
-
-

{activity.type}

- - {activity.status} - -
-
- {activity.itemsProcessed} items processed - β€’ - {new Date(activity.timestamp).toLocaleTimeString()} -
-
-
- ))} -
-
-
- - {/* Quick Actions */} - -
- - - - - -
-
- - - ); -} - diff --git a/frontend/src/pages/Automation/Rules.tsx b/frontend/src/pages/Automation/Rules.tsx deleted file mode 100644 index 35cc79ac..00000000 --- a/frontend/src/pages/Automation/Rules.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { useState, useEffect, useCallback, useMemo } from 'react'; -import { useNavigate } from 'react-router'; -import PageMeta from '../../components/common/PageMeta'; -import PageHeader from '../../components/common/PageHeader'; -import ComponentCard from '../../components/common/ComponentCard'; -import { automationApi, AutomationRule } from '../../api/automation.api'; -import { useToast } from '../../components/ui/toast/ToastContainer'; -import { BoltIcon, PlusIcon, TrashBinIcon, PencilIcon, PaperPlaneIcon, CloseIcon, TaskIcon, ClockIcon } from '../../icons'; -import { useSiteStore } from '../../store/siteStore'; -import { useSectorStore } from '../../store/sectorStore'; -import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; - -export default function AutomationRules() { - const navigate = useNavigate(); - const toast = useToast(); - const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); - - const [rules, setRules] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedRule, setSelectedRule] = useState(null); - const [isWizardOpen, setIsWizardOpen] = useState(false); - const [isEditMode, setIsEditMode] = useState(false); - - const loadRules = useCallback(async () => { - try { - setLoading(true); - const response = await automationApi.listRules({ - page_size: 100, - }); - setRules(response.results || []); - } catch (error: any) { - console.error('Error loading rules:', error); - toast.error(`Failed to load rules: ${error.message}`); - } finally { - setLoading(false); - } - }, [toast]); - - useEffect(() => { - loadRules(); - }, [loadRules]); - - const handleCreate = () => { - setSelectedRule(null); - setIsEditMode(false); - setIsWizardOpen(true); - }; - - const handleEdit = (rule: AutomationRule) => { - setSelectedRule(rule); - setIsEditMode(true); - setIsWizardOpen(true); - }; - - const handleDelete = async (id: number) => { - if (!confirm('Are you sure you want to delete this rule?')) return; - - try { - await automationApi.deleteRule(id); - toast.success('Rule deleted successfully'); - loadRules(); - } catch (error: any) { - toast.error(`Failed to delete rule: ${error.message}`); - } - }; - - const handleToggleActive = async (rule: AutomationRule) => { - try { - await automationApi.updateRule(rule.id, { - is_active: !rule.is_active, - }); - toast.success(`Rule ${rule.is_active ? 'deactivated' : 'activated'}`); - loadRules(); - } catch (error: any) { - toast.error(`Failed to update rule: ${error.message}`); - } - }; - - const handleExecute = async (id: number) => { - try { - await automationApi.executeRule(id); - toast.success('Rule executed successfully'); - loadRules(); - } catch (error: any) { - toast.error(`Failed to execute rule: ${error.message}`); - } - }; - - const getStatusBadge = (rule: AutomationRule) => { - if (!rule.is_active) { - return Inactive; - } - if (rule.status === 'paused') { - return Paused; - } - return Active; - }; - - const getTriggerBadge = (trigger: string) => { - const colors = { - schedule: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300', - event: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300', - manual: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', - }; - return ( - - {trigger.charAt(0).toUpperCase() + trigger.slice(1)} - - ); - }; - - // Automation navigation tabs - const automationTabs = [ - { label: 'Rules', path: '/automation/rules', icon: }, - { label: 'Tasks', path: '/automation/tasks', icon: }, - ]; - - return ( - <> - -
- , - color: 'purple', - }} - navigation={} - /> - -
-

- Create and manage automation rules to automate your workflows -

- -
- - {loading ? ( -
-
Loading rules...
-
- ) : rules.length === 0 ? ( - -
-

- You haven't created any automation rules yet. -

- -
-
- ) : ( -
- {rules.map((rule) => ( - -
-
- {getStatusBadge(rule)} - {getTriggerBadge(rule.trigger)} -
- - {rule.schedule && ( -
- Schedule: {rule.schedule} -
- )} - -
- Actions: {rule.actions.length} -
- - {rule.execution_count > 0 && ( -
- Executions: {rule.execution_count} - {rule.last_executed_at && ( - - (Last: {new Date(rule.last_executed_at).toLocaleDateString()}) - - )} -
- )} - -
- - {rule.trigger === 'manual' && ( - - )} - - -
-
-
- ))} -
- )} -
- - {/* Rule Creation/Edit Wizard Modal - TODO: Implement full wizard */} - {isWizardOpen && ( -
-
-

- {isEditMode ? 'Edit Rule' : 'Create Rule'} -

-

- Rule wizard coming soon. For now, use the API directly or create rules programmatically. -

-
- -
-
-
- )} - - ); -} - diff --git a/frontend/src/pages/Automation/Tasks.tsx b/frontend/src/pages/Automation/Tasks.tsx deleted file mode 100644 index ae6cded5..00000000 --- a/frontend/src/pages/Automation/Tasks.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import PageMeta from '../../components/common/PageMeta'; -import PageHeader from '../../components/common/PageHeader'; -import ComponentCard from '../../components/common/ComponentCard'; -import { automationApi, ScheduledTask } from '../../api/automation.api'; -import { useToast } from '../../components/ui/toast/ToastContainer'; -import { ClockIcon, CheckCircleIcon, XCircleIcon, ArrowRightIcon, BoltIcon } from '../../icons'; -import { useSiteStore } from '../../store/siteStore'; -import { useSectorStore } from '../../store/sectorStore'; -import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; - -export default function AutomationTasks() { - const toast = useToast(); - const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); - - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); - const [statusFilter, setStatusFilter] = useState('all'); - const [ruleFilter, setRuleFilter] = useState(null); - const [rules, setRules] = useState>([]); - - const loadTasks = useCallback(async () => { - try { - setLoading(true); - const filters: any = { - page_size: 100, - ordering: '-scheduled_at', - }; - if (statusFilter !== 'all') { - filters.status = statusFilter; - } - if (ruleFilter) { - filters.rule_id = ruleFilter; - } - const response = await automationApi.listTasks(filters); - setTasks(response.results || []); - } catch (error: any) { - console.error('Error loading tasks:', error); - toast.error(`Failed to load tasks: ${error.message}`); - } finally { - setLoading(false); - } - }, [statusFilter, ruleFilter, toast]); - - const loadRules = useCallback(async () => { - try { - const response = await automationApi.listRules({ page_size: 100 }); - setRules((response.results || []).map(r => ({ id: r.id, name: r.name }))); - } catch (error: any) { - console.error('Error loading rules:', error); - } - }, []); - - useEffect(() => { - loadRules(); - }, [loadRules]); - - useEffect(() => { - loadTasks(); - }, [loadTasks]); - - const handleRetry = async (id: number) => { - try { - await automationApi.retryTask(id); - toast.success('Task retry initiated'); - loadTasks(); - } catch (error: any) { - toast.error(`Failed to retry task: ${error.message}`); - } - }; - - const getStatusBadge = (status: string) => { - const badges = { - pending: { bg: 'bg-yellow-100', text: 'text-yellow-700', darkBg: 'dark:bg-yellow-900', darkText: 'dark:text-yellow-300', label: 'Pending' }, - running: { bg: 'bg-blue-100', text: 'text-blue-700', darkBg: 'dark:bg-blue-900', darkText: 'dark:text-blue-300', label: 'Running' }, - completed: { bg: 'bg-green-100', text: 'text-green-700', darkBg: 'dark:bg-green-900', darkText: 'dark:text-green-300', label: 'Completed' }, - failed: { bg: 'bg-red-100', text: 'text-red-700', darkBg: 'dark:bg-red-900', darkText: 'dark:text-red-300', label: 'Failed' }, - cancelled: { bg: 'bg-gray-100', text: 'text-gray-700', darkBg: 'dark:bg-gray-700', darkText: 'dark:text-gray-300', label: 'Cancelled' }, - }; - const badge = badges[status as keyof typeof badges] || badges.pending; - return ( - - {badge.label} - - ); - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString(); - }; - - const filteredTasks = tasks.filter(task => { - if (statusFilter !== 'all' && task.status !== statusFilter) return false; - if (ruleFilter && task.rule_id !== ruleFilter) return false; - return true; - }); - - // Automation navigation tabs - const automationTabs = [ - { label: 'Rules', path: '/automation/rules', icon: }, - { label: 'Tasks', path: '/automation/tasks', icon: }, - ]; - - return ( - <> - -
- , - color: 'blue', - }} - navigation={} - /> - -
-
- - -
- -
- - -
-
- - {loading ? ( -
-
Loading tasks...
-
- ) : filteredTasks.length === 0 ? ( - -
-

- {tasks.length === 0 - ? 'No scheduled tasks have been created yet.' - : 'No tasks match the current filters.'} -

-
-
- ) : ( -
- {filteredTasks.map((task) => ( - -
-
- {getStatusBadge(task.status)} - {task.retry_count > 0 && ( - - Retries: {task.retry_count} - - )} -
- -
-
- Scheduled: -
- {formatDate(task.scheduled_at)} -
-
- {task.started_at && ( -
- Started: -
- {formatDate(task.started_at)} -
-
- )} - {task.completed_at && ( -
- Completed: -
- {formatDate(task.completed_at)} -
-
- )} -
- - {task.error && ( -
-
- -
- Error: -

{task.error}

-
-
-
- )} - - {task.result && task.status === 'completed' && ( -
-
- -
- Result: -
-                            {JSON.stringify(task.result, null, 2)}
-                          
-
-
-
- )} - - {task.status === 'failed' && ( -
- -
- )} -
-
- ))} -
- )} -
- - ); -} - diff --git a/frontend/src/pages/Planner/Dashboard.tsx b/frontend/src/pages/Planner/Dashboard.tsx index 8ae5ea2f..42500ccb 100644 --- a/frontend/src/pages/Planner/Dashboard.tsx +++ b/frontend/src/pages/Planner/Dashboard.tsx @@ -485,7 +485,7 @@ export default function PlannerDashboard() { } accentColor="orange" trend={0}