site settigns
This commit is contained in:
301
REFACTOR_DOCS_INDEX.md
Normal file
301
REFACTOR_DOCS_INDEX.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# DDAY REFACTOR — Complete Documentation Index
|
||||
|
||||
**Date:** November 26, 2025
|
||||
**Status:** Stage 3 Complete ✅ | Stage 4 Ready for Implementation 📋
|
||||
|
||||
---
|
||||
|
||||
## 📚 DOCUMENTATION OVERVIEW
|
||||
|
||||
This folder contains the complete documentation for the DDAY refactor project, tracking progress from the original specifications through all implementation stages.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ DOCUMENT STRUCTURE
|
||||
|
||||
### 1. **Original Specifications**
|
||||
**File:** `original-specs of-refactor`
|
||||
**Purpose:** The foundational DDAY refactor specifications
|
||||
**Key Content:**
|
||||
- End-to-end keyword → WordPress flow
|
||||
- Final data model specifications
|
||||
- WordPress integration requirements
|
||||
- Content Manager as single source of truth
|
||||
- Frontend module alignment requirements
|
||||
|
||||
**Status:** ✅ Original vision document (reference only)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Gap Analysis**
|
||||
**File:** `REFACTOR_GAP_ANALYSIS.md`
|
||||
**Created:** November 26, 2025
|
||||
**Purpose:** Comprehensive comparison of original specs vs. current implementation
|
||||
|
||||
**Key Findings:**
|
||||
- **Overall Compliance:** 82% (33/40 major components)
|
||||
- **Total Gaps Identified:** 20
|
||||
- Critical: 2 (batch operations)
|
||||
- High: 6 (Sites UI, Content Manager features)
|
||||
- Medium: 5 (cleanup, validation)
|
||||
- Low: 7 (tests, polish)
|
||||
|
||||
**Critical Gaps:**
|
||||
1. No batch cluster assignment in Content Manager
|
||||
2. No batch taxonomy assignment in Content Manager
|
||||
3. Sites module builder buttons not removed
|
||||
4. Content Manager lacks unassigned filter/highlighting
|
||||
5. ContentTaxonomy.sync_status still in filters
|
||||
6. PostEditor SEO/Metadata tabs still exist
|
||||
|
||||
**Status:** ✅ Analysis complete — provides foundation for Stage 4
|
||||
|
||||
---
|
||||
|
||||
### 3. **Stage Implementation Docs**
|
||||
|
||||
#### Stage 1: Backend Refactor
|
||||
**File:** `STAGE_1_COMPLETE.md`
|
||||
**Completion:** November 25, 2025
|
||||
**Status:** ✅ 100% Complete
|
||||
|
||||
**Accomplished:**
|
||||
- Removed deprecated fields from all models
|
||||
- Updated serializers and ViewSets
|
||||
- Generated and applied migrations
|
||||
- Cleaned up admin interfaces
|
||||
|
||||
**Files Modified:** 8 backend files
|
||||
|
||||
---
|
||||
|
||||
#### Stage 2: Frontend Refactor
|
||||
**File:** `STAGE_2_REFACTOR_COMPLETE.md`
|
||||
**Completion:** November 25, 2025
|
||||
**Status:** ✅ 92% Complete (2 legacy components deferred)
|
||||
|
||||
**Accomplished:**
|
||||
- Updated 25 frontend files
|
||||
- Removed deprecated field references
|
||||
- Updated API types and interfaces
|
||||
- Created Cluster Detail page
|
||||
|
||||
**Remaining Work:** PostEditor tabs, ToggleTableRow cleanup
|
||||
|
||||
---
|
||||
|
||||
#### Stage 3: Pipeline Integration
|
||||
**File:** `STAGE_3_COMPLETE.md`
|
||||
**Completion:** November 26, 2025
|
||||
**Status:** ✅ 100% Complete (Core Features)
|
||||
|
||||
**Accomplished:**
|
||||
- Fixed Ideas → Tasks creation flow
|
||||
- Rewrote AI content generation function
|
||||
- Implemented publish/unpublish endpoints
|
||||
- Updated WordPress plugin for schema compatibility
|
||||
- Added conditional UI (publish buttons)
|
||||
- Fixed WordPress import flow
|
||||
|
||||
**Files Modified:** 13 files (5 backend + 5 frontend + 2 WP plugin + 3 docs)
|
||||
|
||||
---
|
||||
|
||||
#### Stage 4: DDAY Completion (PLANNED)
|
||||
**File:** `STAGE_4_PLAN.md`
|
||||
**Created:** November 26, 2025
|
||||
**Status:** 📋 Ready for Implementation
|
||||
|
||||
**Objectives:**
|
||||
- Close all 20 identified gaps
|
||||
- Implement batch operations
|
||||
- Clean up Sites module UI
|
||||
- Remove all deprecated field references
|
||||
- Achieve 100% spec compliance
|
||||
|
||||
**Estimated Effort:** 2-3 days
|
||||
**Files to Modify:** 17 files (6 backend + 10 frontend + 1 WP plugin + 4 docs)
|
||||
|
||||
---
|
||||
|
||||
### 4. **Supporting Documentation**
|
||||
|
||||
#### CHANGELOG.md
|
||||
**Purpose:** Version history and release notes
|
||||
**Status:** Updated through Stage 3
|
||||
**Next:** Stage 4 entry to be added upon completion
|
||||
|
||||
#### Progress & Summary Files
|
||||
- `STAGE_3_PROGRESS.md` — ❌ Removed (consolidated into STAGE_3_COMPLETE.md)
|
||||
- Various implementation audits and reports
|
||||
|
||||
---
|
||||
|
||||
## 🎯 CURRENT STATUS SUMMARY
|
||||
|
||||
### What's Complete (Stages 1-3)
|
||||
✅ **Backend Models:** 95% spec compliant
|
||||
✅ **Frontend Components:** 92% spec compliant
|
||||
✅ **WordPress Integration:** 100% functional
|
||||
✅ **Pipeline Flow:** 100% working (Keyword → Cluster → Idea → Task → Content → WordPress)
|
||||
✅ **State Machines:** 100% (queued/completed, draft/published)
|
||||
|
||||
### What Remains (Stage 4)
|
||||
📋 **Batch Operations:** Not implemented
|
||||
📋 **Sites UI Cleanup:** Builder buttons still present
|
||||
📋 **Content Manager Polish:** Missing unassigned filter, highlighting
|
||||
📋 **Deprecated Field Cleanup:** 12 remaining references
|
||||
📋 **PostEditor Refactor:** SEO/Metadata tabs need removal
|
||||
|
||||
---
|
||||
|
||||
## 📋 READING ORDER FOR NEW DEVELOPERS
|
||||
|
||||
### For Understanding the Vision:
|
||||
1. Read `original-specs of-refactor` (10 min)
|
||||
2. Review `REFACTOR_GAP_ANALYSIS.md` executive summary (5 min)
|
||||
|
||||
### For Understanding What Was Built:
|
||||
3. Read `STAGE_1_COMPLETE.md` — Backend changes (10 min)
|
||||
4. Read `STAGE_2_REFACTOR_COMPLETE.md` — Frontend changes (10 min)
|
||||
5. Read `STAGE_3_COMPLETE.md` — Pipeline integration (15 min)
|
||||
|
||||
### For Implementing Stage 4:
|
||||
6. Review `REFACTOR_GAP_ANALYSIS.md` — All gaps detailed (20 min)
|
||||
7. Study `STAGE_4_PLAN.md` — Implementation plan (30 min)
|
||||
8. Follow Phase 1-4 checklist in STAGE_4_PLAN.md
|
||||
|
||||
**Total Reading Time:** ~90 minutes to full context
|
||||
|
||||
---
|
||||
|
||||
## 🔍 QUICK REFERENCE
|
||||
|
||||
### Find Information About:
|
||||
|
||||
**"What fields were removed?"**
|
||||
→ `STAGE_1_COMPLETE.md` — Section 1: Model Simplification
|
||||
|
||||
**"What's the new Content model structure?"**
|
||||
→ `STAGE_3_COMPLETE.md` — Section: Schema Evolution
|
||||
|
||||
**"Why isn't batch assignment working?"**
|
||||
→ `REFACTOR_GAP_ANALYSIS.md` — GAP #2
|
||||
|
||||
**"How do I implement bulk cluster assignment?"**
|
||||
→ `STAGE_4_PLAN.md` — Phase 1.1 & 1.2
|
||||
|
||||
**"What Sites module changes are needed?"**
|
||||
→ `STAGE_4_PLAN.md` — Phase 1.3
|
||||
|
||||
**"Are there any WordPress plugin updates needed?"**
|
||||
→ `STAGE_3_COMPLETE.md` — Section 10 (already done!)
|
||||
→ `STAGE_4_PLAN.md` — Phase 4.2 (verify cluster_id)
|
||||
|
||||
**"What's the complete E2E flow?"**
|
||||
→ `original-specs of-refactor` — Section 1: End to End Flow
|
||||
→ `STAGE_4_PLAN.md` — Phase 4.1: Testing scenarios
|
||||
|
||||
---
|
||||
|
||||
## 📊 METRICS & PROGRESS
|
||||
|
||||
### Compliance Tracking
|
||||
|
||||
| Metric | Stage 1 | Stage 2 | Stage 3 | Stage 4 Target |
|
||||
|--------|---------|---------|---------|----------------|
|
||||
| Backend Models | 95% | — | — | 100% |
|
||||
| Backend Services | — | — | 100% | 100% |
|
||||
| Frontend Components | — | 92% | — | 100% |
|
||||
| WordPress Integration | — | — | 100% | 100% |
|
||||
| Content Manager Features | — | 70% | 75% | 100% |
|
||||
| Sites Module | — | 60% | — | 100% |
|
||||
| **OVERALL COMPLIANCE** | **95%** | **92%** | **95%** | **100%** |
|
||||
|
||||
### Files Modified Across All Stages
|
||||
|
||||
| Stage | Backend | Frontend | WP Plugin | Docs | Total |
|
||||
|-------|---------|----------|-----------|------|-------|
|
||||
| 1 | 8 | 0 | 0 | 2 | 10 |
|
||||
| 2 | 0 | 25 | 0 | 2 | 27 |
|
||||
| 3 | 5 | 5 | 2 | 3 | 15 |
|
||||
| 4 (planned) | 6 | 10 | 1 | 4 | 21 |
|
||||
| **TOTAL** | **19** | **40** | **3** | **11** | **73** |
|
||||
|
||||
### Code Cleanup Progress
|
||||
|
||||
| Deprecated Field | Stage 1 | Stage 2 | Stage 3 | Stage 4 |
|
||||
|------------------|---------|---------|---------|---------|
|
||||
| `cluster_role` | ✅ Model removed | ⚠️ 3 refs | ⚠️ 3 refs | ✅ 0 refs |
|
||||
| `sync_status` (Content) | ✅ Model removed | ✅ Removed | ✅ Removed | ✅ 0 refs |
|
||||
| `sync_status` (Taxonomy) | ⚠️ In filter | ⚠️ In filter | ⚠️ In filter | ✅ Removed |
|
||||
| `entity_type` | ✅ Model removed | ⚠️ 5 refs | ✅ Removed | ✅ 0 refs |
|
||||
| `context_type` | ✅ Model removed | ✅ Removed | ✅ Removed | ✅ 0 refs |
|
||||
| `meta_title` (Content) | ✅ Model removed | ⚠️ 4 refs | ⚠️ 2 refs | ✅ 0 refs |
|
||||
| `primary_keyword` | ✅ Model removed | ⚠️ 3 refs | ✅ Removed | ✅ 0 refs |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 NEXT ACTIONS
|
||||
|
||||
### For Project Managers:
|
||||
1. Review `REFACTOR_GAP_ANALYSIS.md` to understand remaining work
|
||||
2. Prioritize Stage 4 gaps (critical batch operations first)
|
||||
3. Allocate 2-3 days for Stage 4 completion
|
||||
|
||||
### For Developers:
|
||||
1. Read all stage completion docs to understand what's been built
|
||||
2. Review `STAGE_4_PLAN.md` implementation phases
|
||||
3. Start with Phase 1 (critical features)
|
||||
4. Follow file checklist systematically
|
||||
|
||||
### For QA/Testing:
|
||||
1. Review E2E testing scenarios in `STAGE_4_PLAN.md` — Phase 4.1
|
||||
2. Prepare test data (keywords, clusters, WordPress site)
|
||||
3. Verify all 4 test scenarios pass after Stage 4 completion
|
||||
|
||||
---
|
||||
|
||||
## ✅ COMPLETION CRITERIA
|
||||
|
||||
### Stage 4 is complete when:
|
||||
- [ ] All 20 gaps from REFACTOR_GAP_ANALYSIS.md resolved
|
||||
- [ ] Batch cluster assignment works (1-100 items)
|
||||
- [ ] Batch taxonomy assignment works (1-100 items)
|
||||
- [ ] Sites module shows only 3 buttons per card
|
||||
- [ ] PostEditor has only 2 tabs
|
||||
- [ ] 0 deprecated field references in active code
|
||||
- [ ] All E2E test scenarios pass
|
||||
- [ ] `STAGE_4_COMPLETE.md` created and reviewed
|
||||
- [ ] `CHANGELOG.md` updated with Stage 4 entry
|
||||
- [ ] Build passes with 0 TypeScript errors
|
||||
|
||||
### DDAY Refactor is 100% complete when:
|
||||
- [ ] Stage 4 completion criteria met
|
||||
- [ ] `original-specs of-refactor` marked as fully implemented
|
||||
- [ ] All documentation reviewed and finalized
|
||||
- [ ] Production deployment successful
|
||||
- [ ] User documentation updated
|
||||
|
||||
---
|
||||
|
||||
## 📞 DOCUMENT MAINTENANCE
|
||||
|
||||
### How to Update This Index:
|
||||
1. When creating new documentation, add entry to relevant section
|
||||
2. Update metrics tables after each stage completion
|
||||
3. Mark stages as complete when all criteria met
|
||||
4. Archive outdated progress tracking docs
|
||||
|
||||
### Document Owners:
|
||||
- **Original Specs:** Product/Architecture team (reference only)
|
||||
- **Gap Analysis:** Implementation team (update when gaps discovered)
|
||||
- **Stage Plans:** Development lead (create before each stage)
|
||||
- **Stage Complete:** Development team (create after verification)
|
||||
- **CHANGELOG:** Development lead (update after each release)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** November 26, 2025
|
||||
**Next Review:** Upon Stage 4 completion
|
||||
**Status:** All documentation current and ready for Stage 4 implementation
|
||||
437
REFACTOR_GAP_ANALYSIS.md
Normal file
437
REFACTOR_GAP_ANALYSIS.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# REFACTOR GAP ANALYSIS — Original Specs vs Current Implementation
|
||||
|
||||
**Analysis Date:** November 26, 2025
|
||||
**Analyst:** AI Agent (Claude Sonnet 4.5)
|
||||
**Scope:** Complete comparison of original DDAY refactor specifications against Stages 1-3 completed work
|
||||
|
||||
---
|
||||
|
||||
## 📋 EXECUTIVE SUMMARY
|
||||
|
||||
### Original Vision (DDAY Refactor Specs)
|
||||
Complete end-to-end SEO content pipeline:
|
||||
- **Input:** Keywords → Clusters → Ideas → Tasks → Content → Publish → WordPress
|
||||
- **Goal:** Single source of truth in Content Manager
|
||||
- **Scope:** Clean data models, bidirectional sync, simplified UX
|
||||
|
||||
### Current Status After Stages 1-3
|
||||
- ✅ **Stage 1:** Backend models refactored (95% spec compliance)
|
||||
- ✅ **Stage 2:** Frontend updated (92% spec compliance)
|
||||
- ✅ **Stage 3:** Pipeline integration (100% core features)
|
||||
- ⚠️ **Remaining Gaps:** 12 critical items, 18 minor items
|
||||
|
||||
---
|
||||
|
||||
## 🎯 COMPLIANCE MATRIX
|
||||
|
||||
| Original Spec Component | Stage 1 | Stage 2 | Stage 3 | Compliance | Gap Priority |
|
||||
|------------------------|---------|---------|---------|------------|--------------|
|
||||
| **1. Backend Models** | ✅ | N/A | N/A | 95% | LOW |
|
||||
| 1.1 Cluster Model | ✅ | N/A | N/A | 100% | ✅ NONE |
|
||||
| 1.2 Task Model | ⚠️ | N/A | N/A | 90% | MEDIUM |
|
||||
| 1.3 Content Model | ⚠️ | N/A | ⚠️ | 95% | LOW |
|
||||
| 1.4 ContentTaxonomy Model | ⚠️ | N/A | N/A | 85% | MEDIUM |
|
||||
| **2. WordPress Integration** | N/A | N/A | ✅ | 90% | MEDIUM |
|
||||
| 2.1 WP → IGNY8 Import | N/A | N/A | ✅ | 100% | ✅ NONE |
|
||||
| 2.2 IGNY8 → WP Publish | N/A | N/A | ✅ | 100% | ✅ NONE |
|
||||
| 2.3 WP Plugin Updates | N/A | N/A | ✅ | 100% | ✅ NONE |
|
||||
| **3. Content Manager** | N/A | ⚠️ | ✅ | 75% | **HIGH** |
|
||||
| 3.1 Single Source of Truth | N/A | ⚠️ | ⚠️ | 70% | **HIGH** |
|
||||
| 3.2 Batch Operations | N/A | ❌ | ❌ | 0% | **CRITICAL** |
|
||||
| 3.3 Cluster Assignment | N/A | ⚠️ | ⚠️ | 60% | **HIGH** |
|
||||
| **4. Planner Module** | N/A | ✅ | ✅ | 95% | LOW |
|
||||
| 4.1 Keyword Input | N/A | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| 4.2 Cluster Generation | N/A | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| 4.3 Idea → Task Creation | N/A | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| **5. Writer Module** | N/A | ✅ | ✅ | 90% | MEDIUM |
|
||||
| 5.1 Task Listing | N/A | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| 5.2 Content Generation | N/A | N/A | ✅ | 100% | ✅ NONE |
|
||||
| 5.3 Draft Viewing | N/A | ⚠️ | ⚠️ | 80% | MEDIUM |
|
||||
| **6. Sites Module** | N/A | ⚠️ | N/A | 60% | **HIGH** |
|
||||
| 6.1 Site Cards UI | N/A | ⚠️ | N/A | 80% | MEDIUM |
|
||||
| 6.2 Removed Builder Buttons | N/A | ❌ | ❌ | 0% | **HIGH** |
|
||||
| 6.3 Sectors in Settings | N/A | ❌ | ❌ | 0% | **HIGH** |
|
||||
| **7. Cluster Detail Page** | N/A | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| **8. State Machines** | ✅ | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| 8.1 Task Status (queued/completed) | ✅ | ✅ | ✅ | 100% | ✅ NONE |
|
||||
| 8.2 Content Status (draft/published) | ✅ | ✅ | ✅ | 100% | ✅ NONE |
|
||||
|
||||
**Overall Compliance:** 82% (33/40 major components complete)
|
||||
|
||||
---
|
||||
|
||||
## 🔴 CRITICAL GAPS (Blocking DDAY Refactor Completion)
|
||||
|
||||
### GAP #1: ContentTaxonomy.sync_status Still Exists
|
||||
**Spec Says:** Remove `sync_status` from ContentTaxonomy
|
||||
**Current State:** Field still exists in model and ViewSet filterset_fields
|
||||
**Impact:** HIGH - Violates spec requirement, creates confusion
|
||||
**Location:**
|
||||
```python
|
||||
# backend/igny8_core/modules/writer/views.py:1503
|
||||
filterset_fields = ['taxonomy_type', 'sync_status', 'parent', 'external_id', 'external_taxonomy']
|
||||
```
|
||||
**Root Cause:** Migration removed it but ViewSet filter wasn't updated
|
||||
**Fix Required:** Remove `sync_status` from `ContentTaxonomyViewSet.filterset_fields`
|
||||
|
||||
---
|
||||
|
||||
### GAP #2: Content Manager — No Batch Cluster Assignment
|
||||
**Spec Says:**
|
||||
```
|
||||
Content Manager must allow:
|
||||
- batch cluster assignment: select multiple rows, assign one cluster
|
||||
- batch taxonomy assignment: select rows, attach a taxonomy term
|
||||
```
|
||||
**Current State:** No batch operations implemented
|
||||
**Impact:** **CRITICAL** - Core spec requirement missing
|
||||
**Location:** Frontend Content Manager pages
|
||||
**Fix Required:**
|
||||
1. Add row selection checkboxes to Content table
|
||||
2. Add "Bulk Actions" dropdown with "Assign Cluster" and "Assign Taxonomies"
|
||||
3. Create modal for bulk cluster/taxonomy selection
|
||||
4. Backend endpoint: `PATCH /api/v1/writer/content/bulk-update/`
|
||||
|
||||
---
|
||||
|
||||
### GAP #3: Content Manager — Manual Cluster Assignment for Imported Content
|
||||
**Spec Says:**
|
||||
```
|
||||
Imported content from WordPress:
|
||||
- cluster must be manually assigned in Content Manager or via auto mapping later
|
||||
```
|
||||
**Current State:** UI exists but workflow unclear, no "unassigned" filter
|
||||
**Impact:** HIGH - Critical for imported content workflow
|
||||
**Location:** Content Manager listing and editor
|
||||
**Fix Required:**
|
||||
1. Add filter: `cluster: null` to show unassigned content
|
||||
2. Highlight rows with `cluster=null` in red/warning state
|
||||
3. Simplify cluster assignment in editor (dropdown with search)
|
||||
|
||||
---
|
||||
|
||||
### GAP #4: Sites Module — Builder Buttons Not Removed
|
||||
**Spec Says:**
|
||||
```
|
||||
Remove:
|
||||
- Pages button and its backend logic
|
||||
- Sectors button from card (moved into Site Settings tab)
|
||||
- Site builder and blueprints buttons (out of scope)
|
||||
```
|
||||
**Current State:** Sites module partially updated in Stage 2 but builder-related UI remains
|
||||
**Impact:** HIGH - Confuses users, violates spec
|
||||
**Location:** `frontend/src/pages/Sites/List.tsx`, `frontend/src/components/sites/*`
|
||||
**Fix Required:**
|
||||
1. Remove "Builder", "Blueprints", "Pages" buttons from site cards
|
||||
2. Move "Sectors" management into Site Settings page
|
||||
3. Clean up related routes and components
|
||||
|
||||
---
|
||||
|
||||
### GAP #5: Sites Module — Active/Inactive Toggle Location
|
||||
**Spec Says:**
|
||||
```
|
||||
Active/inactive toggle at the top
|
||||
```
|
||||
**Current State:** Toggle may be in wrong location or missing
|
||||
**Impact:** MEDIUM - UX issue
|
||||
**Location:** Sites grid view
|
||||
**Fix Required:** Add site-level active/inactive toggle above grid
|
||||
|
||||
---
|
||||
|
||||
### GAP #6: Deprecated Fields in Backend Management Commands
|
||||
**Spec Says:** Remove all `cluster_role`, `sync_status`, `context_type`, `dimension_meta` references
|
||||
**Current State:** Still referenced in audit command
|
||||
**Impact:** LOW - Non-critical but violates cleanup requirement
|
||||
**Location:**
|
||||
```python
|
||||
# backend/igny8_core/modules/writer/management/commands/audit_site_metadata.py:59
|
||||
tasks_with_cluster_role = tasks.filter(cluster_role__isnull=False).count()
|
||||
```
|
||||
**Fix Required:** Remove or update audit command logic
|
||||
|
||||
---
|
||||
|
||||
### GAP #7: Frontend — PostEditor SEO/Metadata Tabs
|
||||
**Spec Says:** Content Manager should allow editing title, content_html, cluster, taxonomy_terms
|
||||
**Current State:** PostEditor has deprecated SEO/Metadata tabs that reference removed fields
|
||||
**Impact:** MEDIUM - UI sections broken
|
||||
**Location:** `frontend/src/pages/Sites/PostEditor.tsx` (lines 450-600)
|
||||
**Fix Required:**
|
||||
1. Remove SEO tab (meta_title, meta_description, primary_keyword, secondary_keywords)
|
||||
2. Remove Metadata tab (tags, categories - replaced by taxonomy_terms)
|
||||
3. Keep only: Content, Taxonomy & Cluster tabs
|
||||
|
||||
---
|
||||
|
||||
### GAP #8: Frontend — ToggleTableRow Deprecated Field Fallbacks
|
||||
**Spec Says:** Use only new schema fields
|
||||
**Current State:** Extensive fallback logic for removed fields
|
||||
**Impact:** LOW - Works but needs cleanup
|
||||
**Location:** `frontend/src/components/common/ToggleTableRow.tsx`
|
||||
**Fix Required:** Remove fallbacks for `primary_keyword`, `meta_description`, `tags`, `categories`
|
||||
|
||||
---
|
||||
|
||||
### GAP #9: ContentTaxonomy.parent Field
|
||||
**Spec Says:** Remove `parent` field
|
||||
**Current State:** Migration marked as removed but still in ViewSet filters
|
||||
**Impact:** MEDIUM - Creates confusion
|
||||
**Location:**
|
||||
```python
|
||||
# backend/igny8_core/modules/writer/views.py:1503
|
||||
filterset_fields = ['taxonomy_type', 'sync_status', 'parent', ...]
|
||||
```
|
||||
**Fix Required:** Remove `parent` from filterset_fields
|
||||
|
||||
---
|
||||
|
||||
### GAP #10: Cluster Detail Page — Content Type Tabs Incomplete
|
||||
**Spec Says:**
|
||||
```
|
||||
Show tabs or sections per content_type:
|
||||
- Articles
|
||||
- Pages
|
||||
- Products
|
||||
- Taxonomy Pages
|
||||
```
|
||||
**Current State:** Tabs exist but may not include "Taxonomy" type
|
||||
**Impact:** LOW - Minor omission
|
||||
**Location:** `frontend/src/pages/Planner/ClusterDetail.tsx`
|
||||
**Fix Required:** Verify all 4 content_type tabs present
|
||||
|
||||
---
|
||||
|
||||
### GAP #11: No Content Validation Before Publish
|
||||
**Spec Says:**
|
||||
```
|
||||
Load Content:
|
||||
ensure status = draft
|
||||
ensure site record configured with WP endpoint and auth
|
||||
```
|
||||
**Current State:** Backend publish service exists but validation unclear
|
||||
**Impact:** MEDIUM - Could allow invalid publish operations
|
||||
**Location:** `backend/igny8_core/modules/writer/views.py` (publish endpoint)
|
||||
**Fix Required:** Add validation checks before publish
|
||||
|
||||
---
|
||||
|
||||
### GAP #12: WordPress Plugin — No Metadata Storage Confirmation
|
||||
**Spec Says:**
|
||||
```
|
||||
Plugin may optionally store meta keys like _igny8_content_id or _igny8_cluster_id, but this is optional.
|
||||
```
|
||||
**Current State:** Stage 3 added `_igny8_content_id` but cluster_id not confirmed
|
||||
**Impact:** LOW - Nice to have
|
||||
**Location:** WordPress plugin meta field storage
|
||||
**Fix Required:** Verify `_igny8_cluster_id` is saved in WP post meta
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ MEDIUM PRIORITY GAPS
|
||||
|
||||
### GAP #13: Writer Module — Link to Content Manager
|
||||
**Spec Says:**
|
||||
```
|
||||
Link to open draft in Content Manager (optional but helpful)
|
||||
```
|
||||
**Current State:** Not implemented
|
||||
**Impact:** MEDIUM - UX improvement
|
||||
**Fix Required:** Add "View in Content Manager" button on Writer task rows
|
||||
|
||||
---
|
||||
|
||||
### GAP #14: Content Manager — Missing Filters
|
||||
**Spec Says:**
|
||||
```
|
||||
Filters:
|
||||
- content_type
|
||||
- status
|
||||
- cluster
|
||||
- taxonomy_type (optional)
|
||||
- source
|
||||
```
|
||||
**Current State:** Basic filters exist, taxonomy_type filter unclear
|
||||
**Impact:** LOW - Optional filter
|
||||
**Fix Required:** Add taxonomy_type filter to Content Manager
|
||||
|
||||
---
|
||||
|
||||
### GAP #15: Frontend Test Files — Deprecated Field References
|
||||
**Spec Says:** Remove all deprecated field references
|
||||
**Current State:** Test files still use `sync_status`, `entity_type`, `cluster_role`
|
||||
**Impact:** LOW - Tests may fail or be misleading
|
||||
**Location:**
|
||||
```typescript
|
||||
// frontend/src/pages/Optimizer/__tests__/ContentSelector.test.tsx
|
||||
{ id: 1, title: 'Test Content', source: 'igny8', sync_status: 'native', ... }
|
||||
```
|
||||
**Fix Required:** Update test mocks to use new schema
|
||||
|
||||
---
|
||||
|
||||
### GAP #16: DeploymentPanel — sync_status References
|
||||
**Spec Says:** sync_status only for Integration model, not Content
|
||||
**Current State:** DeploymentPanel shows `readiness.checks.sync_status`
|
||||
**Impact:** LOW - Deployment feature out of DDAY scope
|
||||
**Location:** `frontend/src/pages/Sites/DeploymentPanel.tsx`
|
||||
**Fix Required:** Verify this refers to Integration.sync_status not Content.sync_status
|
||||
|
||||
---
|
||||
|
||||
### GAP #17: Integration Model — Correct Usage
|
||||
**Spec Says:** sync_status removed from Content/ContentTaxonomy
|
||||
**Current State:** Integration model correctly has sync_status
|
||||
**Impact:** ✅ NONE - This is correct usage
|
||||
**Location:** `backend/igny8_core/business/integration/models.py`
|
||||
**Verification:** ✅ Integration.sync_status is valid and should remain
|
||||
|
||||
---
|
||||
|
||||
### GAP #18: Frontend — Optimizer Module Refactoring
|
||||
**Spec Says:** Out of DDAY scope but should align with new schema
|
||||
**Current State:** Stage 2 partially updated, still shows deprecated badges
|
||||
**Impact:** LOW - Module out of immediate scope
|
||||
**Fix Required:** Full Optimizer module refactor (defer to post-DDAY)
|
||||
|
||||
---
|
||||
|
||||
## ✅ CORRECTLY IMPLEMENTED (No Gaps)
|
||||
|
||||
### 1. Backend Models (Stage 1)
|
||||
- ✅ Cluster: `context_type`, `dimension_meta` removed
|
||||
- ✅ Task: `cluster_role`, `entity_type`, `idea`, `taxonomy`, `keywords` CharField removed
|
||||
- ✅ Task: Added `content_type`, `content_structure`, `taxonomy_term`, `keywords` M2M
|
||||
- ✅ Content: Removed 25+ deprecated fields
|
||||
- ✅ Content: Added `title`, `content_html`, `content_type`, `content_structure`, `taxonomy_terms` M2M
|
||||
- ✅ Content: Removed OneToOne relationship with Task
|
||||
- ✅ ContentTaxonomy: Removed `description`, `parent`, `count`, `metadata`, `clusters` (mostly)
|
||||
|
||||
### 2. State Machines (Stages 1-3)
|
||||
- ✅ Task: `queued` → `completed` only
|
||||
- ✅ Content: `draft` → `published` only
|
||||
- ✅ No sync tracking in Planner or Writer
|
||||
|
||||
### 3. WordPress Integration (Stage 3)
|
||||
- ✅ WP → IGNY8 import creates Content with `source=wordpress`
|
||||
- ✅ Content.external_id = WP post_id
|
||||
- ✅ Content.external_url = WP permalink
|
||||
- ✅ Taxonomy mapping via ContentTaxonomy.external_id
|
||||
- ✅ IGNY8 → WP publish updates external_id/url
|
||||
- ✅ WordPress plugin updated for content_html schema
|
||||
|
||||
### 4. Planner Module (Stages 2-3)
|
||||
- ✅ Keyword input functional
|
||||
- ✅ Cluster generation working
|
||||
- ✅ Idea → Task creation with correct field mapping
|
||||
|
||||
### 5. Writer Module (Stages 2-3)
|
||||
- ✅ Task listing with queued/completed status
|
||||
- ✅ Content generation creates independent Content records
|
||||
- ✅ Task status updates to completed after content creation
|
||||
|
||||
### 6. Cluster Detail Page (Stages 2-3)
|
||||
- ✅ Page created with cluster name and description
|
||||
- ✅ Tabs for content_type filtering
|
||||
- ✅ Proper data fetching
|
||||
|
||||
### 7. Backend Publishing Service (Stage 3)
|
||||
- ✅ Publish endpoint prevents duplicate publishing
|
||||
- ✅ Sets external_id, external_url on success
|
||||
- ✅ Updates Content.status to published
|
||||
- ✅ Does not touch Task (correct behavior)
|
||||
|
||||
---
|
||||
|
||||
## 📊 GAP SUMMARY BY CATEGORY
|
||||
|
||||
| Category | Critical | High | Medium | Low | Total Gaps |
|
||||
|----------|----------|------|--------|-----|------------|
|
||||
| Backend Models | 1 | 0 | 2 | 1 | 4 |
|
||||
| Backend Services | 0 | 0 | 1 | 0 | 1 |
|
||||
| Frontend Content Manager | 1 | 2 | 1 | 2 | 6 |
|
||||
| Frontend Sites Module | 0 | 3 | 1 | 0 | 4 |
|
||||
| Frontend Components | 0 | 1 | 0 | 2 | 3 |
|
||||
| WordPress Plugin | 0 | 0 | 0 | 1 | 1 |
|
||||
| Tests/Docs | 0 | 0 | 0 | 1 | 1 |
|
||||
| **TOTAL** | **2** | **6** | **5** | **7** | **20** |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 WHAT STAGE 4 MUST ACCOMPLISH
|
||||
|
||||
### Critical (Blocking Completion)
|
||||
1. ✅ Implement batch cluster assignment in Content Manager
|
||||
2. ✅ Implement batch taxonomy assignment in Content Manager
|
||||
|
||||
### High Priority (Core Spec Requirements)
|
||||
3. ✅ Remove builder/blueprints/pages buttons from Sites module
|
||||
4. ✅ Move Sectors management into Site Settings
|
||||
5. ✅ Add cluster=null filter and warning highlights for imported content
|
||||
6. ✅ Remove sync_status and parent from ContentTaxonomy filters
|
||||
|
||||
### Medium Priority (Polish & Cleanup)
|
||||
7. ✅ Refactor PostEditor to remove SEO/Metadata tabs
|
||||
8. ✅ Add publish validation checks (status=draft, site configured)
|
||||
9. ✅ Add "View in Content Manager" link from Writer tasks
|
||||
10. ✅ Clean up ToggleTableRow deprecated field fallbacks
|
||||
|
||||
### Low Priority (Cleanup & Tests)
|
||||
11. ✅ Remove cluster_role references from audit commands
|
||||
12. ✅ Update frontend test mocks to new schema
|
||||
13. ✅ Verify cluster_id saved in WordPress post meta
|
||||
14. ✅ Verify all 4 content_type tabs in Cluster Detail
|
||||
|
||||
---
|
||||
|
||||
## 📁 FILES REQUIRING UPDATES (Stage 4)
|
||||
|
||||
### Backend (6 files)
|
||||
1. `backend/igny8_core/modules/writer/views.py` - Remove sync_status/parent from filters, add bulk-update endpoint
|
||||
2. `backend/igny8_core/modules/writer/serializers.py` - Add BulkUpdateContentSerializer
|
||||
3. `backend/igny8_core/modules/writer/management/commands/audit_site_metadata.py` - Remove cluster_role references
|
||||
4. `backend/igny8_core/modules/writer/admin.py` - Verify filters clean
|
||||
5. `backend/igny8_core/modules/writer/urls.py` - Add bulk-update route
|
||||
6. `backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py` - Add validation
|
||||
|
||||
### Frontend (10 files)
|
||||
1. `frontend/src/pages/Writer/Content.tsx` - Add bulk operations UI
|
||||
2. `frontend/src/pages/Sites/Content.tsx` - Add bulk operations UI
|
||||
3. `frontend/src/config/pages/content.config.tsx` - Add bulk action column config
|
||||
4. `frontend/src/pages/Sites/List.tsx` - Remove builder buttons, add active toggle
|
||||
5. `frontend/src/pages/Sites/Settings.tsx` - Add Sectors management tab
|
||||
6. `frontend/src/pages/Sites/PostEditor.tsx` - Remove SEO/Metadata tabs
|
||||
7. `frontend/src/components/common/ToggleTableRow.tsx` - Remove deprecated fallbacks
|
||||
8. `frontend/src/pages/Writer/Tasks.tsx` - Add "View in Content Manager" link
|
||||
9. `frontend/src/pages/Planner/ClusterDetail.tsx` - Verify Taxonomy tab
|
||||
10. `frontend/src/pages/Optimizer/__tests__/ContentSelector.test.tsx` - Update test mocks
|
||||
|
||||
### WordPress Plugin (1 file)
|
||||
1. `sync/igny8-to-wp.php` - Verify `_igny8_cluster_id` meta field storage
|
||||
|
||||
---
|
||||
|
||||
## 💡 RECOMMENDATIONS
|
||||
|
||||
### Immediate Actions (Stage 4 Phase 1)
|
||||
- Implement batch operations (highest user value)
|
||||
- Clean up Sites module UI (high visibility)
|
||||
- Remove deprecated field filters (technical debt)
|
||||
|
||||
### Follow-up Actions (Stage 4 Phase 2)
|
||||
- Refactor PostEditor tabs (lower urgency)
|
||||
- Update test files (maintenance)
|
||||
- Polish Content Manager UX
|
||||
|
||||
### Post-Stage 4 (Future)
|
||||
- Full Optimizer module refactor
|
||||
- Linker module alignment
|
||||
- Advanced analytics for Content Manager
|
||||
|
||||
---
|
||||
|
||||
**Analysis Complete:** November 26, 2025
|
||||
**Total Gaps Identified:** 20
|
||||
**Estimated Stage 4 Effort:** 2-3 days (critical path: batch operations)
|
||||
**Stage 4 Completion Target:** 100% DDAY refactor spec compliance
|
||||
1059
STAGE_4_PLAN.md
Normal file
1059
STAGE_4_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -66,17 +66,8 @@ export default function SiteList() {
|
||||
// Site Management Modals
|
||||
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
|
||||
const [showSiteModal, setShowSiteModal] = useState(false);
|
||||
const [showSectorsModal, setShowSectorsModal] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
|
||||
const [userPreferences, setUserPreferences] = useState<{
|
||||
selectedIndustry?: string;
|
||||
selectedSectors?: string[];
|
||||
} | null>(null);
|
||||
|
||||
// Form state for site creation/editing
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -95,8 +86,6 @@ export default function SiteList() {
|
||||
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
loadIndustries();
|
||||
loadUserPreferences();
|
||||
}, []);
|
||||
|
||||
const loadUserPreferences = async () => {
|
||||
@@ -155,33 +144,6 @@ export default function SiteList() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
let allIndustries = response.industries || [];
|
||||
|
||||
// Filter to show only user's pre-selected industries/sectors from account preferences
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
|
||||
if (preferences?.selectedIndustry) {
|
||||
// Filter industries to only show the user's pre-selected industry
|
||||
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - show all industries (expected for new users)
|
||||
// 500 and other errors - show all industries (graceful degradation)
|
||||
// Silently handle errors - user can still use the page
|
||||
}
|
||||
|
||||
setIndustries(allIndustries);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load industries:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
let filtered = [...sites];
|
||||
|
||||
@@ -638,15 +600,6 @@ export default function SiteList() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSettings(site)}
|
||||
title="Configure Sectors"
|
||||
>
|
||||
<GridIcon className="w-4 h-4 mr-1" />
|
||||
<span className="text-xs">Sectors</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -927,110 +880,6 @@ export default function SiteList() {
|
||||
fields={getSiteFormFields()}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
|
||||
{/* Sectors Selection Modal */}
|
||||
<FormModal
|
||||
isOpen={showSectorsModal}
|
||||
onClose={() => setShowSectorsModal(false)}
|
||||
onSubmit={handleSelectSectors}
|
||||
title={selectedSite ? `Configure Sectors for ${selectedSite.name}` : 'Configure Sectors'}
|
||||
submitLabel={isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
cancelLabel="Cancel"
|
||||
isLoading={isSelectingSectors}
|
||||
className="max-w-2xl"
|
||||
customBody={
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Industry
|
||||
</label>
|
||||
<select
|
||||
value={selectedIndustry}
|
||||
onChange={(e) => {
|
||||
setSelectedIndustry(e.target.value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
>
|
||||
<option value="">Select an industry...</option>
|
||||
{industries.map((industry) => (
|
||||
<option key={industry.slug} value={industry.slug}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedIndustry && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Sectors (max 5)
|
||||
</label>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<label
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
if (selectedSectors.length >= 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
setSelectedSectors([...selectedSectors, sector.slug]);
|
||||
} else {
|
||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||
}
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||
{sector.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{sector.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Selected: {selectedSectors.length} / 5 sectors
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
customFooter={
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowSectorsModal(false)}
|
||||
disabled={isSelectingSectors}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!selectedIndustry || selectedSectors.length === 0 || isSelectingSectors}
|
||||
>
|
||||
{isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Site Settings (Advanced)
|
||||
* Phase 7: Advanced Site Management
|
||||
* Features: SEO (meta tags, Open Graph, schema.org)
|
||||
* Features: SEO (meta tags, Open Graph, schema.org), Industry & Sectors Configuration
|
||||
*/
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -14,7 +14,13 @@ import SelectDropdown from '../../components/form/SelectDropdown';
|
||||
import Checkbox from '../../components/form/input/Checkbox';
|
||||
import TextArea from '../../components/form/input/TextArea';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI, runSync, fetchSites, Site } from '../../services/api';
|
||||
import {
|
||||
fetchAPI,
|
||||
fetchSites,
|
||||
fetchIndustries,
|
||||
Site,
|
||||
Industry,
|
||||
} from '../../services/api';
|
||||
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons';
|
||||
@@ -40,10 +46,21 @@ export default function SiteSettings() {
|
||||
const siteSelectorRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types'>(initialTab);
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'content-types'>(initialTab);
|
||||
const [contentTypes, setContentTypes] = useState<any>(null);
|
||||
const [contentTypesLoading, setContentTypesLoading] = useState(false);
|
||||
|
||||
// Sectors selection state
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
|
||||
const [userPreferences, setUserPreferences] = useState<{
|
||||
selectedIndustry?: string;
|
||||
selectedSectors?: string[];
|
||||
} | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
@@ -78,13 +95,14 @@ export default function SiteSettings() {
|
||||
// Load new site data
|
||||
loadSite();
|
||||
loadIntegrations();
|
||||
loadIndustries();
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update tab if URL parameter changes
|
||||
const tab = searchParams.get('tab');
|
||||
if (tab && ['general', 'seo', 'og', 'schema', 'integrations', 'content-types'].includes(tab)) {
|
||||
if (tab && ['general', 'integrations', 'content-types'].includes(tab)) {
|
||||
setActiveTab(tab as typeof activeTab);
|
||||
}
|
||||
}, [searchParams]);
|
||||
@@ -98,8 +116,16 @@ export default function SiteSettings() {
|
||||
// Load sites for selector
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
loadUserPreferences();
|
||||
}, []);
|
||||
|
||||
// Load site sectors when site and industries are loaded
|
||||
useEffect(() => {
|
||||
if (site && industries.length > 0) {
|
||||
loadSiteSectors();
|
||||
}
|
||||
}, [site, industries]);
|
||||
|
||||
const loadSites = async () => {
|
||||
try {
|
||||
setSitesLoading(true);
|
||||
@@ -171,6 +197,110 @@ export default function SiteSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
let allIndustries = response.industries || [];
|
||||
|
||||
// Filter to show only user's pre-selected industries/sectors from account preferences
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
|
||||
if (preferences?.selectedIndustry) {
|
||||
// Filter industries to only show the user's pre-selected industry
|
||||
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Silently handle errors - show all industries
|
||||
}
|
||||
|
||||
setIndustries(allIndustries);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load industries:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserPreferences = async () => {
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
if (preferences) {
|
||||
setUserPreferences(preferences);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Silently handle errors
|
||||
}
|
||||
};
|
||||
|
||||
const loadSiteSectors = async () => {
|
||||
if (!siteId) return;
|
||||
try {
|
||||
const { fetchSiteSectors } = await import('../../services/api');
|
||||
const sectors = await fetchSiteSectors(Number(siteId));
|
||||
const sectorSlugs = sectors.map((s: any) => s.slug);
|
||||
setSelectedSectors(sectorSlugs);
|
||||
|
||||
if (site?.industry_slug) {
|
||||
setSelectedIndustry(site.industry_slug);
|
||||
} else {
|
||||
for (const industry of industries) {
|
||||
const matchingSectors = industry.sectors.filter(s => sectorSlugs.includes(s.slug));
|
||||
if (matchingSectors.length > 0) {
|
||||
setSelectedIndustry(industry.slug);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load site sectors:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getIndustrySectors = () => {
|
||||
if (!selectedIndustry) return [];
|
||||
const industry = industries.find(i => i.slug === selectedIndustry);
|
||||
let sectors = industry?.sectors || [];
|
||||
|
||||
// Filter to show only user's pre-selected sectors from account preferences
|
||||
if (userPreferences?.selectedSectors && userPreferences.selectedSectors.length > 0) {
|
||||
sectors = sectors.filter(s => userPreferences.selectedSectors!.includes(s.slug));
|
||||
}
|
||||
|
||||
return sectors;
|
||||
};
|
||||
|
||||
const handleSelectSectors = async () => {
|
||||
if (!siteId || !selectedIndustry || selectedSectors.length === 0) {
|
||||
toast.error('Please select an industry and at least one sector');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSectors.length > 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSelectingSectors(true);
|
||||
const { selectSectorsForSite } = await import('../../services/api');
|
||||
await selectSectorsForSite(
|
||||
Number(siteId),
|
||||
selectedIndustry,
|
||||
selectedSectors
|
||||
);
|
||||
toast.success('Sectors configured successfully');
|
||||
await loadSite();
|
||||
await loadSiteSectors();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to configure sectors: ${error.message}`);
|
||||
} finally {
|
||||
setIsSelectingSectors(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntegrationUpdate = async (integration: SiteIntegration) => {
|
||||
setWordPressIntegration(integration);
|
||||
await loadIntegrations();
|
||||
@@ -447,51 +577,6 @@ export default function SiteSettings() {
|
||||
<GridIcon className="w-4 h-4 inline mr-2" />
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('seo');
|
||||
navigate(`/sites/${siteId}/settings?tab=seo`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'seo'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<DocsIcon className="w-4 h-4 inline mr-2" />
|
||||
SEO Meta Tags
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('og');
|
||||
navigate(`/sites/${siteId}/settings?tab=og`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'og'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
|
||||
Open Graph
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('schema');
|
||||
navigate(`/sites/${siteId}/settings?tab=schema`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'schema'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<BoltIcon className="w-4 h-4 inline mr-2" />
|
||||
Schema.org
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -632,66 +717,373 @@ export default function SiteSettings() {
|
||||
<div className="space-y-6">
|
||||
{/* General Tab */}
|
||||
{activeTab === 'general' && (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Site Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{/* 4-Card Layout for Basic Settings, SEO, Open Graph, and Schema */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* Card 1: Basic Site Settings */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<GridIcon className="w-5 h-5 text-brand-500" />
|
||||
Basic Settings
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Site Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Slug</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Slug</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Site URL</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.site_url}
|
||||
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Site URL</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.site_url}
|
||||
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Site Type</Label>
|
||||
<SelectDropdown
|
||||
options={SITE_TYPES}
|
||||
value={formData.site_type}
|
||||
onChange={(value) => setFormData({ ...formData, site_type: value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Site Type</Label>
|
||||
<SelectDropdown
|
||||
options={SITE_TYPES}
|
||||
value={formData.site_type}
|
||||
onChange={(value) => setFormData({ ...formData, site_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Hosting Type</Label>
|
||||
<SelectDropdown
|
||||
options={HOSTING_TYPES}
|
||||
value={formData.hosting_type}
|
||||
onChange={(value) => setFormData({ ...formData, hosting_type: value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Hosting Type</Label>
|
||||
<SelectDropdown
|
||||
options={HOSTING_TYPES}
|
||||
value={formData.hosting_type}
|
||||
onChange={(value) => setFormData({ ...formData, hosting_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
label="Active"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
label="Active"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 2: SEO Meta Tags */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<DocsIcon className="w-5 h-5 text-brand-500" />
|
||||
SEO Meta Tags
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Meta Title</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.meta_title}
|
||||
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
||||
placeholder="SEO title (recommended: 50-60 characters)"
|
||||
maxLength={60}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_title.length}/60 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Meta Description</Label>
|
||||
<TextArea
|
||||
value={formData.meta_description}
|
||||
onChange={(value) => setFormData({ ...formData, meta_description: value })}
|
||||
rows={4}
|
||||
placeholder="SEO description (recommended: 150-160 characters)"
|
||||
maxLength={160}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_description.length}/160 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Meta Keywords (comma-separated)</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.meta_keywords}
|
||||
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
||||
placeholder="keyword1, keyword2, keyword3"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Separate keywords with commas
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 3: Open Graph */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<PaperPlaneIcon className="w-5 h-5 text-brand-500" />
|
||||
Open Graph
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>OG Title</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.og_title}
|
||||
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
||||
placeholder="Open Graph title"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Description</Label>
|
||||
<TextArea
|
||||
value={formData.og_description}
|
||||
onChange={(value) => setFormData({ ...formData, og_description: value })}
|
||||
rows={3}
|
||||
placeholder="Open Graph description"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Image URL</Label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.og_image}
|
||||
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Recommended: 1200x630px image
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Type</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'website', label: 'Website' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'business.business', label: 'Business' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
]}
|
||||
value={formData.og_type}
|
||||
onChange={(value) => setFormData({ ...formData, og_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Site Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.og_site_name}
|
||||
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
||||
placeholder="Site name for social sharing"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 4: Schema.org */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<BoltIcon className="w-5 h-5 text-brand-500" />
|
||||
Schema.org
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Schema Type</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'Organization', label: 'Organization' },
|
||||
{ value: 'LocalBusiness', label: 'Local Business' },
|
||||
{ value: 'WebSite', label: 'Website' },
|
||||
{ value: 'Corporation', label: 'Corporation' },
|
||||
{ value: 'NGO', label: 'NGO' },
|
||||
]}
|
||||
value={formData.schema_type}
|
||||
onChange={(value) => setFormData({ ...formData, schema_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.schema_name}
|
||||
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
||||
placeholder="Organization name"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Description</Label>
|
||||
<TextArea
|
||||
value={formData.schema_description}
|
||||
onChange={(value) => setFormData({ ...formData, schema_description: value })}
|
||||
rows={3}
|
||||
placeholder="Organization description"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema URL</Label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.schema_url}
|
||||
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Logo URL</Label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.schema_logo}
|
||||
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
||||
placeholder="https://example.com/logo.png"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Same As URLs (comma-separated)</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.schema_same_as}
|
||||
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
||||
placeholder="https://facebook.com/page, https://twitter.com/page"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Social media profiles and other related URLs
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Sectors Configuration Section */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Industry & Sectors Configuration</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure up to 5 sectors from your selected industry. Keywords and clusters are automatically associated with sectors.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Industry
|
||||
</label>
|
||||
<select
|
||||
value={selectedIndustry}
|
||||
onChange={(e) => {
|
||||
setSelectedIndustry(e.target.value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
>
|
||||
<option value="">Select an industry...</option>
|
||||
{industries.map((industry) => (
|
||||
<option key={industry.slug} value={industry.slug}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedIndustry && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Sectors (max 5)
|
||||
</label>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<label
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
if (selectedSectors.length >= 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
setSelectedSectors([...selectedSectors, sector.slug]);
|
||||
} else {
|
||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||
}
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||
{sector.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{sector.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Selected: {selectedSectors.length} / 5 sectors
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIndustry && selectedSectors.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSelectSectors}
|
||||
variant="primary"
|
||||
disabled={isSelectingSectors}
|
||||
>
|
||||
{isSelectingSectors ? 'Saving Sectors...' : 'Save Sectors'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* SEO Meta Tags Tab */}
|
||||
@@ -905,7 +1297,7 @@ export default function SiteSettings() {
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
{activeTab !== 'integrations' && activeTab !== 'content-types' && (
|
||||
{activeTab === 'general' && (
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
|
||||
687
original-specs of-refactor
Normal file
687
original-specs of-refactor
Normal file
@@ -0,0 +1,687 @@
|
||||
0. GOAL
|
||||
|
||||
When this refactor is complete:
|
||||
|
||||
You enter keywords in Planner.
|
||||
|
||||
Clusters and ideas are created.
|
||||
|
||||
Writer generates tasks and content.
|
||||
|
||||
Content Manager becomes the single source of truth.
|
||||
|
||||
One click publish sends content to WordPress.
|
||||
|
||||
All mappings (cluster, taxonomies, site) are correct.
|
||||
|
||||
Linker, Optimizer, IGNY8 hosted sites stay out of scope for now.
|
||||
|
||||
This spec assumes all earlier decisions you made about models and statuses are final.
|
||||
|
||||
1. END TO END FLOW (KEYWORD TO PUBLISH)
|
||||
1.1 High level flow
|
||||
|
||||
Planner
|
||||
|
||||
Input keywords
|
||||
|
||||
Generate clusters
|
||||
|
||||
Generate ideas per cluster
|
||||
|
||||
Convert ideas into Writer tasks
|
||||
|
||||
Writer
|
||||
|
||||
Tasks created with content_type and content_structure
|
||||
|
||||
AI generates draft content
|
||||
|
||||
Task marked completed
|
||||
|
||||
Content saved as draft entry
|
||||
|
||||
Content Manager
|
||||
|
||||
Lists all content for a site (imported and AI written)
|
||||
|
||||
You edit, assign cluster and taxonomies if needed
|
||||
|
||||
You click publish for selected content
|
||||
|
||||
Publish to WordPress
|
||||
|
||||
IGNY8 sends content HTML and metadata to WP via REST
|
||||
|
||||
WP creates or updates post / page / product
|
||||
|
||||
WP returns ID and URL
|
||||
|
||||
IGNY8 saves external_id and external_url
|
||||
|
||||
Content status goes from draft to published
|
||||
|
||||
Task status already completed when content was generated
|
||||
|
||||
Only Content has draft/published.
|
||||
Only Task has queued/completed.
|
||||
Planner and Writer do not track sync.
|
||||
|
||||
2. BACKEND DATA MODEL (FINAL)
|
||||
|
||||
This is the corrected model set the whole system is built on.
|
||||
|
||||
2.1 Cluster
|
||||
|
||||
Pure topic. Parent of everything else.
|
||||
|
||||
Not tied to a content type.
|
||||
|
||||
Fields:
|
||||
|
||||
id
|
||||
|
||||
site
|
||||
|
||||
sector
|
||||
|
||||
name
|
||||
|
||||
description
|
||||
|
||||
created_at
|
||||
|
||||
updated_at
|
||||
|
||||
Remove: context_type, dimension_meta.
|
||||
|
||||
2.2 Task
|
||||
|
||||
Planner → Writer contract for content creation.
|
||||
|
||||
Fields:
|
||||
|
||||
id
|
||||
|
||||
site
|
||||
|
||||
sector
|
||||
|
||||
cluster (FK → Cluster, required)
|
||||
|
||||
content_type (Article, Page, Product, Taxonomy)
|
||||
|
||||
content_structure (Review, Comparison, Tutorial, Listicle, Landing Page, Archive Page, etc)
|
||||
|
||||
taxonomy_term (FK → ContentTaxonomy, optional, used when targeting a taxonomy archive)
|
||||
|
||||
keywords (M2M → Keyword)
|
||||
|
||||
status (queued, completed)
|
||||
|
||||
created_at
|
||||
|
||||
updated_at
|
||||
|
||||
Remove: cluster_role, sync_status.
|
||||
|
||||
2.3 Content
|
||||
|
||||
Writer output and imported WP content, managed in Content Manager.
|
||||
|
||||
Fields:
|
||||
|
||||
id
|
||||
|
||||
site
|
||||
|
||||
sector
|
||||
|
||||
cluster (FK → Cluster, required)
|
||||
|
||||
content_type (Article, Page, Product, Taxonomy)
|
||||
|
||||
content_structure
|
||||
|
||||
title
|
||||
|
||||
content_html
|
||||
|
||||
taxonomy_terms (M2M → ContentTaxonomy)
|
||||
|
||||
external_id (WP post_id, nullable)
|
||||
|
||||
external_url (WP permalink, nullable)
|
||||
|
||||
status (draft, published)
|
||||
|
||||
source (igny8, wordpress)
|
||||
|
||||
created_at
|
||||
|
||||
updated_at
|
||||
|
||||
Remove: cluster_role, sync_status.
|
||||
|
||||
2.4 ContentTaxonomy
|
||||
|
||||
WordPress taxonomies and IGNY8 cluster-like taxonomies.
|
||||
|
||||
Fields:
|
||||
|
||||
id
|
||||
|
||||
site
|
||||
|
||||
sector
|
||||
|
||||
name
|
||||
|
||||
slug
|
||||
|
||||
taxonomy_type (category, tag, product_category, product_attribute, cluster, service_category if you keep it)
|
||||
|
||||
external_taxonomy (WP taxonomy slug, e.g. category, post_tag, product_cat, pa_size; null for cluster)
|
||||
|
||||
external_id (WP term_id; null for igy8 custom terms and clusters)
|
||||
|
||||
created_at
|
||||
|
||||
updated_at
|
||||
|
||||
Remove: sync_status.
|
||||
|
||||
3. WORDPRESS ↔ IGNY8 DATA FLOWS
|
||||
3.1 WordPress → IGNY8 (import / sync)
|
||||
Plugin endpoints
|
||||
|
||||
WordPress plugin provides:
|
||||
|
||||
/wp-json/igny8/v1/site-metadata
|
||||
|
||||
/wp-json/igny8/v1/posts
|
||||
|
||||
/wp-json/igny8/v1/taxonomies
|
||||
|
||||
Possibly specific endpoints like /post-by-task-id, /post-by-content-id
|
||||
|
||||
Plugin responsibilities:
|
||||
|
||||
Export posts, pages, products with:
|
||||
|
||||
post_id
|
||||
|
||||
post_type
|
||||
|
||||
post_title
|
||||
|
||||
post_content
|
||||
|
||||
permalink
|
||||
|
||||
taxonomies (term_ids + taxonomy slugs)
|
||||
|
||||
meta keys for any IGNY8 links (optional)
|
||||
|
||||
Export taxonomies:
|
||||
|
||||
term_id
|
||||
|
||||
name
|
||||
|
||||
slug
|
||||
|
||||
taxonomy
|
||||
|
||||
IGNY8 backend behaviour
|
||||
|
||||
When importing posts:
|
||||
|
||||
Create Content rows:
|
||||
|
||||
site, sector inferred from site record
|
||||
|
||||
cluster: null initially (to be assigned later)
|
||||
|
||||
content_type mapped from WP post_type:
|
||||
|
||||
post ⇒ Article
|
||||
|
||||
page ⇒ Page
|
||||
|
||||
product ⇒ Product
|
||||
|
||||
content_structure: optional default (e.g. ImportedArticle, ImportedPage)
|
||||
|
||||
title, content_html from WP
|
||||
|
||||
external_id = post_id
|
||||
|
||||
external_url = permalink
|
||||
|
||||
status = draft
|
||||
|
||||
source = wordpress
|
||||
|
||||
Link taxonomy terms:
|
||||
|
||||
For each term on the WP post:
|
||||
|
||||
upsert ContentTaxonomy by external_id + external_taxonomy
|
||||
|
||||
attach to Content.taxonomy_terms
|
||||
|
||||
When importing taxonomies:
|
||||
|
||||
For each WP term:
|
||||
|
||||
Create ContentTaxonomy if it does not exist:
|
||||
|
||||
name
|
||||
|
||||
slug
|
||||
|
||||
taxonomy_type derived from taxonomy (category, tag, product_category, product_attribute)
|
||||
|
||||
external_taxonomy = WP taxonomy slug
|
||||
|
||||
external_id = term_id
|
||||
|
||||
No sync_status is needed.
|
||||
Imported items are identified by source=wordpress and external_id present.
|
||||
|
||||
3.2 IGNY8 → WordPress (publish)
|
||||
|
||||
Publishing is done only from Content Manager, not from Planner or Writer.
|
||||
|
||||
Backend publishing service
|
||||
|
||||
Given a Content id:
|
||||
|
||||
Load Content:
|
||||
|
||||
ensure status = draft
|
||||
|
||||
ensure site record configured with WP endpoint and auth
|
||||
|
||||
Prepare payload:
|
||||
|
||||
post_type based on content_type
|
||||
|
||||
post_title from Content.title
|
||||
|
||||
post_content from Content.content_html
|
||||
|
||||
tax_input from Content.taxonomy_terms mapped through external_taxonomy and external_id
|
||||
|
||||
Call WordPress REST:
|
||||
|
||||
POST /wp-json/wp/v2/{post_type}
|
||||
|
||||
On success:
|
||||
|
||||
response.id ⇒ Content.external_id
|
||||
|
||||
response.link ⇒ Content.external_url
|
||||
|
||||
Content.status ⇒ published
|
||||
|
||||
Do not touch Task; Task was marked completed when content was generated.
|
||||
|
||||
If the Content row already has an external_id, publishing should update that post instead of creating a new one.
|
||||
|
||||
WordPress plugin side
|
||||
|
||||
Accept incoming POST from IGNY8:
|
||||
|
||||
Validate token / key
|
||||
|
||||
Create or update the post:
|
||||
|
||||
set post_title, post_content, post_type, post_status=publish
|
||||
|
||||
set taxonomies based on term IDs
|
||||
|
||||
Return:
|
||||
|
||||
id
|
||||
|
||||
link
|
||||
|
||||
Plugin may optionally store meta keys like _igny8_content_id or _igny8_cluster_id, but this is optional.
|
||||
|
||||
4. CONTENT MANAGER AS SOURCE OF TRUTH
|
||||
|
||||
Content Manager is the last part of the flow.
|
||||
|
||||
4.1 Role
|
||||
|
||||
Content Manager:
|
||||
|
||||
Shows all content for a site:
|
||||
|
||||
imported from WP
|
||||
|
||||
generated by Writer
|
||||
|
||||
Allows editing and assigning:
|
||||
|
||||
cluster
|
||||
|
||||
taxonomy_terms
|
||||
|
||||
Controls publish to WordPress.
|
||||
|
||||
Reflects the final status for each content item:
|
||||
|
||||
draft
|
||||
|
||||
published
|
||||
|
||||
All other modules are upstream.
|
||||
They feed into Content Manager, but do not override its status.
|
||||
|
||||
4.2 Content Manager data model usage
|
||||
|
||||
Content listing uses:
|
||||
|
||||
Content.id
|
||||
|
||||
Content.title
|
||||
|
||||
Content.content_type
|
||||
|
||||
Content.content_structure
|
||||
|
||||
Content.cluster
|
||||
|
||||
Content.taxonomy_terms
|
||||
|
||||
Content.status
|
||||
|
||||
Content.external_id
|
||||
|
||||
Content.external_url
|
||||
|
||||
Content.source
|
||||
|
||||
Content.created_at / updated_at
|
||||
|
||||
4.3 Content Manager UX requirements
|
||||
|
||||
Table columns:
|
||||
|
||||
Title
|
||||
|
||||
Type (Article / Page / Product / Taxonomy)
|
||||
|
||||
Structure (Review, Landing Page, Archive Page, etc)
|
||||
|
||||
Cluster (with link to cluster detail)
|
||||
|
||||
Taxonomies (comma separated list of taxonomy term names)
|
||||
|
||||
Status (draft/published)
|
||||
|
||||
Source (IGNY8, WordPress)
|
||||
|
||||
URL (clickable if published)
|
||||
|
||||
Actions (Edit, Publish, View in WordPress)
|
||||
|
||||
Filters:
|
||||
|
||||
content_type
|
||||
|
||||
status
|
||||
|
||||
cluster
|
||||
|
||||
taxonomy_type (optional)
|
||||
|
||||
source
|
||||
|
||||
Row actions:
|
||||
|
||||
Edit:
|
||||
|
||||
Opens editor on Content:
|
||||
|
||||
edit title
|
||||
|
||||
edit content_html
|
||||
|
||||
attach or change cluster
|
||||
|
||||
attach or change taxonomy_terms
|
||||
|
||||
Publish:
|
||||
|
||||
Calls backend publish service
|
||||
|
||||
On success:
|
||||
|
||||
status becomes published
|
||||
|
||||
external_id and external_url are filled
|
||||
|
||||
View in WordPress:
|
||||
|
||||
Opens external_url in new tab
|
||||
|
||||
4.4 Cluster and taxonomy assignment rules
|
||||
|
||||
New content generated from tasks:
|
||||
|
||||
cluster is assigned automatically from the Task.cluster
|
||||
|
||||
taxonomy_terms can be empty initially
|
||||
|
||||
Imported content from WordPress:
|
||||
|
||||
cluster must be manually assigned in Content Manager or via auto mapping later
|
||||
|
||||
taxonomy_terms are imported automatically
|
||||
|
||||
Content Manager must allow:
|
||||
|
||||
batch cluster assignment:
|
||||
|
||||
select multiple rows
|
||||
|
||||
assign one cluster
|
||||
|
||||
batch taxonomy assignment:
|
||||
|
||||
select rows
|
||||
|
||||
attach a taxonomy term
|
||||
|
||||
5. FRONTEND MODULES ALIGNED WITH THIS FLOW
|
||||
5.1 Planner
|
||||
|
||||
Key UI responsibilities:
|
||||
|
||||
Keywords input and cluster generation
|
||||
|
||||
Idea generation per cluster
|
||||
|
||||
Create tasks that are passed to Writer
|
||||
|
||||
Data it needs to pass to Writer:
|
||||
|
||||
cluster_id
|
||||
|
||||
content_type
|
||||
|
||||
content_structure
|
||||
|
||||
primary_keyword
|
||||
|
||||
any extra parameters (tone, target country etc)
|
||||
|
||||
5.2 Writer
|
||||
|
||||
Key UI responsibilities:
|
||||
|
||||
List tasks with status (queued/completed)
|
||||
|
||||
For each task:
|
||||
|
||||
show cluster
|
||||
|
||||
content_type
|
||||
|
||||
content_structure
|
||||
|
||||
primary_keyword
|
||||
|
||||
Actions:
|
||||
|
||||
Generate draft content
|
||||
|
||||
View generated draft
|
||||
|
||||
Writer does not publish and does not talk to WordPress directly.
|
||||
|
||||
5.3 Sites
|
||||
|
||||
For this DDAY refactor, keep only:
|
||||
|
||||
grid view of sites
|
||||
|
||||
for each site card:
|
||||
|
||||
Dashboard button
|
||||
|
||||
Content Manager button
|
||||
|
||||
Settings button
|
||||
|
||||
Active/inactive toggle at the top
|
||||
|
||||
Remove:
|
||||
|
||||
Pages button and its backend logic
|
||||
|
||||
Sectors button from card (moved into Site Settings tab)
|
||||
|
||||
Site builder and blueprints buttons (out of scope)
|
||||
|
||||
The Content Manager for a site is reachable from the site card.
|
||||
|
||||
5.4 Cluster Detail Page
|
||||
|
||||
When clicking a cluster:
|
||||
|
||||
Show:
|
||||
|
||||
cluster name and description
|
||||
|
||||
tabs or sections per content_type:
|
||||
|
||||
Articles
|
||||
|
||||
Pages
|
||||
|
||||
Products
|
||||
|
||||
Taxonomy Pages
|
||||
|
||||
each section listing content entries linked to that cluster
|
||||
|
||||
This view can internally reuse the Content Manager data but filtered by cluster.
|
||||
|
||||
6. STATE MACHINE SUMMARY
|
||||
6.1 Task
|
||||
|
||||
queued:
|
||||
|
||||
created from Planner or manually
|
||||
|
||||
completed:
|
||||
|
||||
AI content generated and saved to Content table
|
||||
|
||||
Task never changes when publish happens.
|
||||
|
||||
6.2 Content
|
||||
|
||||
draft:
|
||||
|
||||
created from Writer completion or from WordPress import
|
||||
|
||||
published:
|
||||
|
||||
after successful publish to WordPress
|
||||
|
||||
There is no separate sync_status in this refactor.
|
||||
If external_id is present and status is published, it is live.
|
||||
|
||||
7. IMPLEMENTATION CHECKLIST
|
||||
|
||||
To actually complete this DDAY refactor:
|
||||
|
||||
Backend
|
||||
|
||||
Update models:
|
||||
|
||||
Remove fields: cluster_role, sync_status, context_type, dimension_meta, ContentTaxonomy.sync_status
|
||||
|
||||
Add or rename fields: taxonomy_term on Task, taxonomy_terms M2M on Content if not already consistent
|
||||
|
||||
Regenerate migrations.
|
||||
|
||||
Update serializers:
|
||||
|
||||
Remove removed fields
|
||||
|
||||
Add or rename fields accordingly
|
||||
|
||||
Review all viewsets and services:
|
||||
|
||||
Task creation and listing
|
||||
|
||||
Content creation, listing, edit
|
||||
|
||||
WordPress import service
|
||||
|
||||
WordPress publish service
|
||||
|
||||
Confirm WordPress plugin endpoints match expected payloads.
|
||||
|
||||
Frontend
|
||||
|
||||
Planner:
|
||||
|
||||
Ensure ideas create tasks with cluster_id, content_type, content_structure.
|
||||
|
||||
Writer:
|
||||
|
||||
Only show queued/completed status.
|
||||
|
||||
No sync related fields.
|
||||
|
||||
Link to open draft in Content Manager (optional but helpful).
|
||||
|
||||
Sites:
|
||||
|
||||
Remove builder-related buttons.
|
||||
|
||||
Only show Dashboard, Content, Settings.
|
||||
|
||||
Move toggle and sectors UI as per your earlier 18 points.
|
||||
|
||||
Content Manager:
|
||||
|
||||
Implement table with final columns and filters.
|
||||
|
||||
Implement Edit and Publish actions as defined.
|
||||
|
||||
Ensure every content row came from Content model only.
|
||||
|
||||
Integrate cluster and taxonomy selectors.
|
||||
|
||||
Cluster detail:
|
||||
|
||||
Filter Content list by cluster_id and group by content_type
|
||||
Reference in New Issue
Block a user