old automation cleanup adn status feilds of planner udpate

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-03 05:13:53 +00:00
parent 7df6e190fc
commit c9f082cb12
40 changed files with 1832 additions and 2134 deletions

View File

@@ -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**

View File

@@ -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**

View File

@@ -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)

View File

@@ -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)

View File

@@ -318,6 +318,12 @@ class GenerateContentFunction(BaseAIFunction):
task.status = 'completed' task.status = 'completed'
task.save(update_fields=['status', 'updated_at']) 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 { return {
'count': 1, 'count': 1,
'content_id': content_record.id, 'content_id': content_record.id,

View File

@@ -228,6 +228,11 @@ class GenerateIdeasFunction(BaseAIFunction):
) )
ideas_created += 1 ideas_created += 1
# Update cluster status to 'mapped' after ideas are generated
if cluster and cluster.status == 'new':
cluster.status = 'mapped'
cluster.save()
return { return {
'count': ideas_created, 'count': ideas_created,
'ideas_created': ideas_created 'ideas_created': ideas_created

View File

@@ -1,4 +0,0 @@
"""
Automation business logic - AutomationRule, ScheduledTask models and services
"""

View File

@@ -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}"

View File

@@ -1,4 +0,0 @@
"""
Automation services
"""

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -5,12 +5,18 @@ from igny8_core.auth.models import SiteSectorBaseModel, SeedKeyword
class Clusters(SiteSectorBaseModel): class Clusters(SiteSectorBaseModel):
"""Clusters model for keyword grouping - pure topic clusters""" """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) name = models.CharField(max_length=255, unique=True, db_index=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
keywords_count = models.IntegerField(default=0) keywords_count = models.IntegerField(default=0)
volume = models.IntegerField(default=0) volume = models.IntegerField(default=0)
mapped_pages = 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) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -37,9 +43,8 @@ class Keywords(SiteSectorBaseModel):
""" """
STATUS_CHOICES = [ STATUS_CHOICES = [
('active', 'Active'), ('new', 'New'),
('pending', 'Pending'), ('mapped', 'Mapped'),
('archived', 'Archived'),
] ]
# Required: Link to global SeedKeyword # Required: Link to global SeedKeyword
@@ -75,7 +80,8 @@ class Keywords(SiteSectorBaseModel):
related_name='keywords', related_name='keywords',
limit_choices_to={'sector': models.F('sector')} # Cluster must be in same sector 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) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -142,8 +148,8 @@ class ContentIdeas(SiteSectorBaseModel):
STATUS_CHOICES = [ STATUS_CHOICES = [
('new', 'New'), ('new', 'New'),
('scheduled', 'Scheduled'), ('queued', 'Queued'),
('published', 'Published'), ('completed', 'Completed'),
] ]
CONTENT_TYPE_CHOICES = [ CONTENT_TYPE_CHOICES = [
@@ -193,6 +199,7 @@ class ContentIdeas(SiteSectorBaseModel):
) )
# REMOVED: taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint functionality) # REMOVED: taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint functionality)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new') 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) estimated_word_count = models.IntegerField(default=1000)
content_type = models.CharField( content_type = models.CharField(
max_length=50, max_length=50,

View File

@@ -25,10 +25,6 @@ app.conf.beat_schedule = {
'task': 'igny8_core.modules.billing.tasks.replenish_monthly_credits', '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 '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 # WordPress Publishing Tasks
'process-pending-wordpress-publications': { 'process-pending-wordpress-publications': {
'task': 'igny8_core.tasks.wordpress_publishing.process_pending_wordpress_publications', 'task': 'igny8_core.tasks.wordpress_publishing.process_pending_wordpress_publications',

View File

@@ -1,5 +0,0 @@
"""
Automation Module - API Layer
Business logic is in business/automation/
"""

View File

@@ -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'

View File

@@ -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'],
},
),
]

View File

@@ -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'),
),
]

View File

@@ -1,5 +0,0 @@
# Backward compatibility alias - models moved to business/automation/
from igny8_core.business.automation.models import AutomationRule, ScheduledTask
__all__ = ['AutomationRule', 'ScheduledTask']

View File

@@ -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']

View File

@@ -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)),
]

View File

@@ -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']

View File

@@ -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),
]

View File

@@ -560,7 +560,7 @@ class KeywordViewSet(SiteSectorModelViewSet):
volume=int(row.get('volume', 0) or 0), volume=int(row.get('volume', 0) or 0),
difficulty=int(row.get('difficulty', 0) or 0), difficulty=int(row.get('difficulty', 0) or 0),
intent=row.get('intent', 'informational') or 'informational', intent=row.get('intent', 'informational') or 'informational',
status=row.get('status', 'pending') or 'pending', status=row.get('status', 'new') or 'new',
site=site, site=site,
sector=sector, sector=sector,
account=account account=account
@@ -1080,8 +1080,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
created_tasks.append(task.id) created_tasks.append(task.id)
# Update idea status # Update idea status to queued
idea.status = 'scheduled' idea.status = 'queued'
idea.save() idea.save()
except Exception as e: except Exception as e:
errors.append({ errors.append({

View File

@@ -52,7 +52,7 @@ INSTALLED_APPS = [
'igny8_core.modules.writer.apps.WriterConfig', 'igny8_core.modules.writer.apps.WriterConfig',
'igny8_core.modules.system.apps.SystemConfig', 'igny8_core.modules.system.apps.SystemConfig',
'igny8_core.modules.billing.apps.BillingConfig', '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.site_building.apps.SiteBuildingConfig', # REMOVED: SiteBuilder/Blueprint deprecated
'igny8_core.business.optimization.apps.OptimizationConfig', 'igny8_core.business.optimization.apps.OptimizationConfig',
'igny8_core.business.publishing.apps.PublishingConfig', 'igny8_core.business.publishing.apps.PublishingConfig',

View File

@@ -42,7 +42,7 @@ urlpatterns = [
# Site Builder module removed - legacy blueprint functionality deprecated # Site Builder module removed - legacy blueprint functionality deprecated
path('api/v1/system/', include('igny8_core.modules.system.urls')), 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/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/linker/', include('igny8_core.modules.linker.urls')), # Linker endpoints
path('api/v1/optimizer/', include('igny8_core.modules.optimizer.urls')), # Optimizer 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 path('api/v1/publisher/', include('igny8_core.modules.publisher.urls')), # Publisher endpoints

View File

@@ -64,11 +64,6 @@ const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
// Setup Pages - Lazy loaded // Setup Pages - Lazy loaded
const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSectorsKeywords")); 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 // Settings - Lazy loaded
const GeneralSettings = lazy(() => import("./pages/Settings/General")); const GeneralSettings = lazy(() => import("./pages/Settings/General"));
const Users = lazy(() => import("./pages/Settings/Users")); const Users = lazy(() => import("./pages/Settings/Users"));
@@ -364,23 +359,6 @@ export default function App() {
{/* Legacy redirect */} {/* Legacy redirect */}
<Route path="/setup/industries-sectors-keywords" element={<Navigate to="/setup/add-keywords" replace />} /> <Route path="/setup/industries-sectors-keywords" element={<Navigate to="/setup/add-keywords" replace />} />
{/* Automation Module - Redirect dashboard to rules */}
<Route path="/automation" element={<Navigate to="/automation/rules" replace />} />
<Route path="/automation/rules" element={
<Suspense fallback={null}>
<ModuleGuard module="automation">
<AutomationRules />
</ModuleGuard>
</Suspense>
} />
<Route path="/automation/tasks" element={
<Suspense fallback={null}>
<ModuleGuard module="automation">
<AutomationTasks />
</ModuleGuard>
</Suspense>
} />
{/* Settings */} {/* Settings */}
<Route path="/settings" element={ <Route path="/settings" element={
<Suspense fallback={null}> <Suspense fallback={null}>

View File

@@ -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<string, any>;
}>;
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<string, any>;
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<string, any>;
}>;
is_active?: boolean;
}
export interface AutomationRuleUpdateData extends Partial<AutomationRuleCreateData> {}
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<string, any>) => {
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',
});
},
};

View File

@@ -192,7 +192,7 @@ export const createClustersPageConfig = (
render: (value: string) => { render: (value: string) => {
const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-'; const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-';
return ( return (
<Badge color={value === 'active' ? 'success' : 'warning'} size="xs" variant="soft"> <Badge color={value === 'mapped' ? 'success' : 'amber'} size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span> <span className="text-[11px] font-normal">{properCase}</span>
</Badge> </Badge>
); );
@@ -248,8 +248,8 @@ export const createClustersPageConfig = (
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Status' }, { value: '', label: 'All Status' },
{ value: 'active', label: 'Active' }, { value: 'new', label: 'New' },
{ value: 'archived', label: 'Archived' }, { value: 'mapped', label: 'Mapped' },
], ],
}, },
{ {
@@ -409,12 +409,12 @@ export const createClustersPageConfig = (
key: 'status', key: 'status',
label: 'Status', label: 'Status',
type: 'select', type: 'select',
value: handlers.formData.status || 'active', value: handlers.formData.status || 'new',
onChange: (value: any) => onChange: (value: any) =>
handlers.setFormData({ ...handlers.formData, status: value }), handlers.setFormData({ ...handlers.formData, status: value }),
options: [ options: [
{ value: 'active', label: 'Active' }, { value: 'new', label: 'New' },
{ value: 'archived', label: 'Archived' }, { value: 'mapped', label: 'Mapped' },
], ],
}, },
], ],

View File

@@ -173,8 +173,8 @@ export const createIdeasPageConfig = (
render: (value: string) => { render: (value: string) => {
const statusColors: Record<string, 'success' | 'amber' | 'info'> = { const statusColors: Record<string, 'success' | 'amber' | 'info'> = {
'new': 'amber', 'new': 'amber',
'scheduled': 'info', 'queued': 'info',
'published': 'success', 'completed': 'success',
}; };
const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-'; const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-';
return ( return (
@@ -222,8 +222,8 @@ export const createIdeasPageConfig = (
options: [ options: [
{ value: '', label: 'All Status' }, { value: '', label: 'All Status' },
{ value: 'new', label: 'New' }, { value: 'new', label: 'New' },
{ value: 'scheduled', label: 'Scheduled' }, { value: 'queued', label: 'Queued' },
{ value: 'published', label: 'Published' }, { value: 'completed', label: 'Completed' },
], ],
}, },
{ {
@@ -380,8 +380,8 @@ export const createIdeasPageConfig = (
handlers.setFormData({ ...handlers.formData, status: value }), handlers.setFormData({ ...handlers.formData, status: value }),
options: [ options: [
{ value: 'new', label: 'New' }, { value: 'new', label: 'New' },
{ value: 'scheduled', label: 'Scheduled' }, { value: 'queued', label: 'Queued' },
{ value: 'published', label: 'Published' }, { value: 'completed', label: 'Completed' },
], ],
}, },
], ],
@@ -399,13 +399,13 @@ export const createIdeasPageConfig = (
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length,
}, },
{ {
label: 'Scheduled', label: 'Queued',
value: 0, value: 0,
accentColor: 'blue' as const, 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, value: 0,
accentColor: 'green' as const, accentColor: 'green' as const,
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'published').length, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'published').length,

View File

@@ -232,9 +232,9 @@ export const createKeywordsPageConfig = (
return ( return (
<Badge <Badge
color={ color={
value === 'active' value === 'mapped'
? 'success' ? 'success'
: value === 'pending' : value === 'new'
? 'amber' ? 'amber'
: 'error' : 'error'
} }
@@ -312,9 +312,8 @@ export const createKeywordsPageConfig = (
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Status' }, { value: '', label: 'All Status' },
{ value: 'active', label: 'Active' }, { value: 'new', label: 'New' },
{ value: 'pending', label: 'Pending' }, { value: 'mapped', label: 'Mapped' },
{ value: 'archived', label: 'Archived' },
], ],
}, },
{ {
@@ -560,13 +559,12 @@ export const createKeywordsPageConfig = (
key: 'status', key: 'status',
label: 'Status', label: 'Status',
type: 'select', type: 'select',
value: handlers.formData.status || 'pending', value: handlers.formData.status || 'new',
onChange: (value: any) => onChange: (value: any) =>
handlers.setFormData({ ...handlers.formData, status: value }), handlers.setFormData({ ...handlers.formData, status: value }),
options: [ options: [
{ value: 'pending', label: 'Pending' }, { value: 'new', label: 'New' },
{ value: 'active', label: 'Active' }, { value: 'mapped', label: 'Mapped' },
{ value: 'archived', label: 'Archived' },
], ],
}, },
], ],

View File

@@ -95,15 +95,6 @@ const AppSidebar: React.FC = () => {
}, },
]; ];
// Add Automation if enabled (single item, no dropdown)
if (moduleEnabled('automation')) {
setupItems.push({
icon: <BoltIcon />,
name: "Automation",
path: "/automation/rules", // Default to rules, submenus shown as in-page navigation
});
}
// Add Thinker if enabled (single item, no dropdown) // Add Thinker if enabled (single item, no dropdown)
if (moduleEnabled('thinker')) { if (moduleEnabled('thinker')) {
setupItems.push({ setupItems.push({

View File

@@ -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<AutomationStats | null>(null);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(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 (
<>
<PageMeta title="Automation Dashboard - IGNY8" description="Manage and monitor automation workflows" />
<PageHeader title="Automation Dashboard" />
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<EnhancedMetricCard
title="Active Workflows"
value={stats?.activeWorkflows || 0}
icon={<BoltIcon className="size-6" />}
trend={0}
accentColor="purple"
/>
<EnhancedMetricCard
title="Scheduled Tasks"
value={stats?.scheduledTasks || 0}
icon={<CalendarIcon className="size-6" />}
trend={0}
accentColor="blue"
/>
<EnhancedMetricCard
title="Completed Today"
value={stats?.completedToday || 0}
icon={<CheckCircleIcon className="size-6" />}
trend={0}
accentColor="green"
/>
<EnhancedMetricCard
title="Success Rate"
value={`${stats?.successRate || 0}%`}
icon={<PaperPlaneIcon className="size-6" />}
trend={0}
accentColor="orange"
/>
</div>
{/* Automation Workflows */}
<ComponentCard title="Automation Workflows" desc="Manage your automated content pipelines">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{automationWorkflows.map((workflow) => {
const Icon = workflow.icon;
return (
<div
key={workflow.id}
className="rounded-2xl border-2 border-slate-200 bg-white p-6 hover:shadow-lg transition-all"
>
<div className="flex items-start justify-between mb-4">
<div className={`inline-flex size-12 rounded-xl bg-gradient-to-br ${workflow.color} items-center justify-center text-white shadow-lg`}>
<Icon className="h-6 w-6" />
</div>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
workflow.status === "active"
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-700"
}`}>
{workflow.status}
</span>
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2">{workflow.name}</h3>
<p className="text-sm text-slate-600 mb-4">{workflow.description}</p>
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-xs text-slate-600">
<span>Schedule:</span>
<span className="font-semibold">{workflow.schedule}</span>
</div>
<div className="flex items-center justify-between text-xs text-slate-600">
<span>Last Run:</span>
<span>{workflow.lastRun}</span>
</div>
<div className="flex items-center justify-between text-xs text-slate-600">
<span>Next Run:</span>
<span className="font-semibold text-[var(--color-primary)]">{workflow.nextRun}</span>
</div>
</div>
<div className="mb-4">
<div className="flex items-center justify-between text-xs text-slate-600 mb-1">
<span>Coverage</span>
<span className="font-semibold">{workflow.coverage}%</span>
</div>
<ProgressBar value={workflow.coverage} className="h-2" />
</div>
<div className="flex gap-2">
<button className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-slate-100 text-slate-700 px-4 py-2 text-sm font-semibold hover:bg-slate-200 transition">
<CloseIcon className="h-4 w-4" />
Pause
</button>
<button className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white px-4 py-2 text-sm font-semibold hover:shadow-lg transition">
<PaperPlaneIcon className="h-4 w-4" />
Run Now
</button>
</div>
</div>
);
})}
</div>
</ComponentCard>
{/* Automation Steps Configuration */}
<ComponentCard title="Automation Steps" desc="Configure which steps are automated">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{automationSteps.map((step, index) => {
const Icon = step.icon;
const isEnabled = stats?.automationCoverage[step.step.toLowerCase() as keyof typeof stats.automationCoverage] || false;
return (
<Link
key={step.step}
to={step.path}
className="rounded-xl border-2 border-slate-200 bg-white p-5 hover:shadow-lg transition-all group"
>
<div className="flex items-center justify-between mb-3">
<div className={`inline-flex size-10 rounded-lg bg-gradient-to-br ${
isEnabled
? "from-[var(--color-success)] to-[var(--color-success-dark)]"
: "from-slate-300 to-slate-400"
} items-center justify-center text-white shadow-md`}>
<Icon className="h-5 w-5" />
</div>
<div className={`size-5 rounded-full border-2 flex items-center justify-center ${
isEnabled
? "border-[var(--color-success)] bg-[var(--color-success)]"
: "border-slate-300 bg-white"
}`}>
{isEnabled && <CheckCircleIcon className="h-3 w-3 text-white" />}
</div>
</div>
<h4 className="font-semibold text-slate-900 mb-1">{step.step}</h4>
<p className="text-xs text-slate-600">{step.description}</p>
<div className="mt-3 flex items-center gap-1 text-xs text-[var(--color-primary)] opacity-0 group-hover:opacity-100 transition">
<span>Configure</span>
<ArrowRightIcon className="h-3 w-3" />
</div>
</Link>
);
})}
</div>
</ComponentCard>
{/* Activity Chart */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComponentCard title="Automation Activity" desc="Last 7 days of automation activity">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart options={chartOptions} series={chartSeries} type="line" height={300} />
</Suspense>
</ComponentCard>
{/* Recent Activity */}
<ComponentCard title="Recent Activity" desc="Latest automation executions">
<div className="space-y-4">
{stats?.recentActivity.map((activity) => (
<div
key={activity.id}
className="flex items-center gap-4 p-4 rounded-lg border border-slate-200 bg-white"
>
<div className="size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
<BoltIcon className="h-5 w-5" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-slate-900">{activity.type}</h4>
<span className={`text-xs px-2 py-1 rounded-full ${
activity.status === "completed"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}>
{activity.status}
</span>
</div>
<div className="flex items-center gap-4 text-xs text-slate-600">
<span>{activity.itemsProcessed} items processed</span>
<span></span>
<span>{new Date(activity.timestamp).toLocaleTimeString()}</span>
</div>
</div>
</div>
))}
</div>
</ComponentCard>
</div>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Manually trigger automation workflows">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => navigate("/planner/keyword-opportunities")}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0693e3] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<ListIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Run Planner Workflow</h4>
<p className="text-sm text-slate-600">Keywords Clusters Ideas</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
</button>
<button
onClick={() => navigate("/writer/tasks")}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0bbf87] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
<FileTextIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Run Writer Workflow</h4>
<p className="text-sm text-slate-600">Tasks Content Images</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#0bbf87] transition" />
</button>
<button
onClick={() => navigate("/writer/published")}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#5d4ae3] hover:shadow-lg transition-all group"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
<PaperPlaneIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Run Full Pipeline</h4>
<p className="text-sm text-slate-600">Complete end-to-end automation</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#5d4ae3] transition" />
</button>
</div>
</ComponentCard>
</div>
</>
);
}

View File

@@ -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<AutomationRule[]>([]);
const [loading, setLoading] = useState(true);
const [selectedRule, setSelectedRule] = useState<AutomationRule | null>(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 <span className="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">Inactive</span>;
}
if (rule.status === 'paused') {
return <span className="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Paused</span>;
}
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">Active</span>;
};
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 (
<span className={`px-2 py-1 text-xs rounded-full ${colors[trigger as keyof typeof colors] || colors.manual}`}>
{trigger.charAt(0).toUpperCase() + trigger.slice(1)}
</span>
);
};
// Automation navigation tabs
const automationTabs = [
{ label: 'Rules', path: '/automation/rules', icon: <BoltIcon /> },
{ label: 'Tasks', path: '/automation/tasks', icon: <ClockIcon /> },
];
return (
<>
<PageMeta title="Automation Rules" />
<div className="space-y-6">
<PageHeader
title="Automation Rules"
lastUpdated={new Date()}
badge={{
icon: <BoltIcon />,
color: 'purple',
}}
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
/>
<div className="flex items-center justify-between">
<p className="text-gray-600 dark:text-gray-400">
Create and manage automation rules to automate your workflows
</p>
<button
onClick={handleCreate}
className="flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
<PlusIcon />
Create Rule
</button>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading rules...</div>
</div>
) : rules.length === 0 ? (
<ComponentCard title="No Rules" desc="Create your first automation rule to get started">
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400 mb-4">
You haven't created any automation rules yet.
</p>
<button
onClick={handleCreate}
className="px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
Create Your First Rule
</button>
</div>
</ComponentCard>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{rules.map((rule) => (
<ComponentCard
key={rule.id}
title={rule.name}
desc={rule.description || 'No description'}
>
<div className="space-y-4">
<div className="flex items-center justify-between">
{getStatusBadge(rule)}
{getTriggerBadge(rule.trigger)}
</div>
{rule.schedule && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Schedule:</strong> {rule.schedule}
</div>
)}
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Actions:</strong> {rule.actions.length}
</div>
{rule.execution_count > 0 && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Executions:</strong> {rule.execution_count}
{rule.last_executed_at && (
<span className="ml-2">
(Last: {new Date(rule.last_executed_at).toLocaleDateString()})
</span>
)}
</div>
)}
<div className="flex items-center gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => handleToggleActive(rule)}
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
title={rule.is_active ? 'Deactivate' : 'Activate'}
>
{rule.is_active ? <CloseIcon className="w-4 h-4" /> : <PaperPlaneIcon className="w-4 h-4" />}
</button>
{rule.trigger === 'manual' && (
<button
onClick={() => handleExecute(rule.id)}
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
title="Execute Now"
>
<PaperPlaneIcon className="w-4 h-4" />
</button>
)}
<button
onClick={() => handleEdit(rule)}
className="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
title="Edit"
>
<PencilIcon className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(rule.id)}
className="px-3 py-1.5 text-sm rounded-lg border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors flex items-center justify-center"
title="Delete"
>
<TrashBinIcon className="w-4 h-4" />
</button>
</div>
</div>
</ComponentCard>
))}
</div>
)}
</div>
{/* Rule Creation/Edit Wizard Modal - TODO: Implement full wizard */}
{isWizardOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
<h3 className="text-xl font-bold mb-4">
{isEditMode ? 'Edit Rule' : 'Create Rule'}
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Rule wizard coming soon. For now, use the API directly or create rules programmatically.
</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsWizardOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Close
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -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<ScheduledTask[]>([]);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<string>('all');
const [ruleFilter, setRuleFilter] = useState<number | null>(null);
const [rules, setRules] = useState<Array<{ id: number; name: string }>>([]);
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 (
<span className={`px-2 py-1 text-xs rounded-full ${badge.bg} ${badge.text} ${badge.darkBg} ${badge.darkText}`}>
{badge.label}
</span>
);
};
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: <BoltIcon /> },
{ label: 'Tasks', path: '/automation/tasks', icon: <ClockIcon /> },
];
return (
<>
<PageMeta title="Scheduled Tasks" />
<div className="space-y-6">
<PageHeader
title="Scheduled Tasks"
lastUpdated={new Date()}
badge={{
icon: <ClockIcon />,
color: 'blue',
}}
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
/>
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Filter by Status
</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800"
>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="running">Running</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Filter by Rule
</label>
<select
value={ruleFilter || ''}
onChange={(e) => setRuleFilter(e.target.value ? Number(e.target.value) : null)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800"
>
<option value="">All Rules</option>
{rules.map(rule => (
<option key={rule.id} value={rule.id}>{rule.name}</option>
))}
</select>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading tasks...</div>
</div>
) : filteredTasks.length === 0 ? (
<ComponentCard title="No Tasks" desc="No scheduled tasks found">
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400">
{tasks.length === 0
? 'No scheduled tasks have been created yet.'
: 'No tasks match the current filters.'}
</p>
</div>
</ComponentCard>
) : (
<div className="space-y-4">
{filteredTasks.map((task) => (
<ComponentCard
key={task.id}
title={`Task #${task.id} - ${task.task_type}`}
desc={task.rule_name ? `Rule: ${task.rule_name}` : 'Manual task'}
>
<div className="space-y-3">
<div className="flex items-center justify-between">
{getStatusBadge(task.status)}
{task.retry_count > 0 && (
<span className="text-sm text-gray-500 dark:text-gray-400">
Retries: {task.retry_count}
</span>
)}
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<strong className="text-gray-700 dark:text-gray-300">Scheduled:</strong>
<div className="text-gray-600 dark:text-gray-400">
{formatDate(task.scheduled_at)}
</div>
</div>
{task.started_at && (
<div>
<strong className="text-gray-700 dark:text-gray-300">Started:</strong>
<div className="text-gray-600 dark:text-gray-400">
{formatDate(task.started_at)}
</div>
</div>
)}
{task.completed_at && (
<div>
<strong className="text-gray-700 dark:text-gray-300">Completed:</strong>
<div className="text-gray-600 dark:text-gray-400">
{formatDate(task.completed_at)}
</div>
</div>
)}
</div>
{task.error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-start gap-2">
<XCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div>
<strong className="text-red-800 dark:text-red-300">Error:</strong>
<p className="text-red-700 dark:text-red-400 text-sm mt-1">{task.error}</p>
</div>
</div>
</div>
)}
{task.result && task.status === 'completed' && (
<div className="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex items-start gap-2">
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
<div>
<strong className="text-green-800 dark:text-green-300">Result:</strong>
<pre className="text-green-700 dark:text-green-400 text-xs mt-1 overflow-auto">
{JSON.stringify(task.result, null, 2)}
</pre>
</div>
</div>
</div>
)}
{task.status === 'failed' && (
<div className="flex justify-end pt-2 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => handleRetry(task.id)}
className="flex items-center gap-2 px-4 py-2 text-sm bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
<ArrowRightIcon className="w-4 h-4" />
Retry Task
</button>
</div>
)}
</div>
</ComponentCard>
))}
</div>
)}
</div>
</>
);
}

View File

@@ -485,7 +485,7 @@ export default function PlannerDashboard() {
<EnhancedMetricCard <EnhancedMetricCard
title="Ideas Generated" title="Ideas Generated"
value={stats.ideas.total} value={stats.ideas.total}
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} pending`} subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} not queued`}
icon={<BoltIcon className="size-6" />} icon={<BoltIcon className="size-6" />}
accentColor="orange" accentColor="orange"
trend={0} trend={0}