be fe fixes
This commit is contained in:
264
DATABASE_SCHEMA_FIELD_MAPPING_GUIDE.md
Normal file
264
DATABASE_SCHEMA_FIELD_MAPPING_GUIDE.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Database Schema vs Model Field Mapping Guide
|
||||
|
||||
## Overview
|
||||
This guide documents the critical database schema mismatches in the IGNY8 project that cause 500 errors. These mismatches occur because the database uses OLD column names while Django models use NEW refactored field names.
|
||||
|
||||
---
|
||||
|
||||
## Critical Issue: Database Column Name Mismatches
|
||||
|
||||
### Problem Statement
|
||||
During the "Stage 1 Refactor", model field names were changed but database column names were NOT renamed. This causes:
|
||||
- **500 Internal Server Error** - When Django tries to query non-existent columns
|
||||
- **AttributeError** - When code references old field names on model instances
|
||||
- **FieldError** - When filtering/querying with old field names
|
||||
|
||||
### Root Cause
|
||||
The refactor changed the **Python model field names** but the **actual PostgreSQL database columns** still use the old names. Django expects the new field names but the database has the old column names.
|
||||
|
||||
---
|
||||
|
||||
## Known Database vs Model Mismatches
|
||||
|
||||
### Content Model (`igny8_content` table)
|
||||
|
||||
| Model Field Name | Database Column Name | Fix Required |
|
||||
|-----------------|---------------------|--------------|
|
||||
| `content_html` | `html_content` | Add `db_column='html_content'` |
|
||||
| `content_type` | `entity_type` | Add `db_column='entity_type'` |
|
||||
| `content_structure` | `cluster_role` | Add `db_column='cluster_role'` |
|
||||
|
||||
**Location:** `backend/igny8_core/business/content/models.py` - Content model
|
||||
|
||||
**Additional Fields to Include:**
|
||||
- `external_type` - exists in database, must be in model
|
||||
- `sync_status` - exists in database, must be in model
|
||||
|
||||
### Tasks Model (`igny8_tasks` table)
|
||||
|
||||
| Model Field Name | Database Column Name | Fix Required |
|
||||
|-----------------|---------------------|--------------|
|
||||
| `content_type` | `entity_type` | Add `db_column='entity_type'` |
|
||||
| `content_structure` | `cluster_role` | Add `db_column='cluster_role'` |
|
||||
| `taxonomy_term` | `taxonomy_id` | Add `db_column='taxonomy_id'` |
|
||||
| `idea` | `idea_id` | Add `db_column='idea_id'` |
|
||||
| `keywords` | `keywords` (text) | Change from ManyToManyField to TextField |
|
||||
|
||||
**Location:** `backend/igny8_core/business/content/models.py` - Tasks model
|
||||
|
||||
### ContentTaxonomy Relations (`igny8_content_taxonomy_relations` table)
|
||||
|
||||
| Model Expects | Database Has | Fix Required |
|
||||
|--------------|-------------|--------------|
|
||||
| `contenttaxonomy_id` | `taxonomy_id` | Create through model with `db_column` |
|
||||
| Auto M2M table | Custom table | Use `through='ContentTaxonomyRelation'` |
|
||||
|
||||
**Location:** Must create `ContentTaxonomyRelation` through model with explicit `db_column` settings
|
||||
|
||||
---
|
||||
|
||||
## Common Code Reference Errors
|
||||
|
||||
### Issue: Code Still Uses Old Field Names
|
||||
|
||||
Even after fixing the model, **view code** might still reference old field names:
|
||||
|
||||
**Wrong References to Fix:**
|
||||
- `content.entity_type` → Should be `content.content_type`
|
||||
- `content.cluster_role` → Should be `content.content_structure`
|
||||
- `content.html_content` → Should be `content.content_html`
|
||||
- `content.taxonomies` → Should be `content.taxonomy_terms`
|
||||
|
||||
**Files to Check:**
|
||||
- `backend/igny8_core/modules/writer/views.py`
|
||||
- `backend/igny8_core/modules/integration/views.py`
|
||||
- `backend/igny8_core/business/*/services/*.py`
|
||||
- `backend/igny8_core/ai/functions/*.py`
|
||||
|
||||
**Search Commands:**
|
||||
```bash
|
||||
grep -r "\.entity_type" backend/igny8_core/
|
||||
grep -r "\.cluster_role" backend/igny8_core/
|
||||
grep -r "\.html_content" backend/igny8_core/
|
||||
grep -r "\.taxonomies" backend/igny8_core/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Checklist for 500 Errors
|
||||
|
||||
When encountering 500 Internal Server Errors, follow this checklist:
|
||||
|
||||
### Step 1: Check Backend Logs
|
||||
```bash
|
||||
docker logs --tail=100 igny8_backend 2>&1 | grep -A 20 "Traceback"
|
||||
docker logs igny8_backend 2>&1 | grep "UndefinedColumn\|does not exist"
|
||||
```
|
||||
|
||||
### Step 2: Identify Error Type
|
||||
|
||||
**A. ProgrammingError: column does not exist**
|
||||
- Error shows: `column igny8_content.field_name does not exist`
|
||||
- **Action:** Database has different column name than model expects
|
||||
- **Fix:** Add `db_column='actual_database_column_name'` to model field
|
||||
|
||||
**B. AttributeError: object has no attribute**
|
||||
- Error shows: `'Content' object has no attribute 'entity_type'`
|
||||
- **Action:** Code references old field name
|
||||
- **Fix:** Update code to use new field name from model
|
||||
|
||||
**C. FieldError: Cannot resolve keyword**
|
||||
- Error shows: `Cannot resolve keyword 'entity_type' into field`
|
||||
- **Action:** Query/filter uses old field name
|
||||
- **Fix:** Update queryset filters to use new field names
|
||||
|
||||
### Step 3: Verify Database Schema
|
||||
```bash
|
||||
docker exec igny8_backend python manage.py shell -c "
|
||||
from django.db import connection
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT column_name FROM information_schema.columns WHERE table_name = %s ORDER BY ordinal_position', ['igny8_content'])
|
||||
print('\n'.join([row[0] for row in cursor.fetchall()]))
|
||||
"
|
||||
```
|
||||
|
||||
### Step 4: Compare Against Model
|
||||
Check if model field names match database column names. If not, add `db_column` attribute.
|
||||
|
||||
### Step 5: Search for Code References
|
||||
After fixing model, search all code for references to old field names and update them.
|
||||
|
||||
---
|
||||
|
||||
## Prevention: Pre-Refactor Checklist
|
||||
|
||||
Before doing ANY database model refactoring:
|
||||
|
||||
### 1. Document Current State
|
||||
- [ ] List all current database column names
|
||||
- [ ] List all current model field names
|
||||
- [ ] Identify any existing `db_column` mappings
|
||||
|
||||
### 2. Plan Migration Strategy
|
||||
- [ ] Decide: Rename database columns OR add `db_column` mappings?
|
||||
- [ ] If renaming: Create migration to rename columns
|
||||
- [ ] If mapping: Document which fields need `db_column`
|
||||
|
||||
### 3. Update All Code References
|
||||
- [ ] Search codebase for old field name references
|
||||
- [ ] Update view code, serializers, services
|
||||
- [ ] Update filters, querysets, aggregations
|
||||
- [ ] Update AI functions and background tasks
|
||||
|
||||
### 4. Test Before Deploy
|
||||
- [ ] Run migrations on development database
|
||||
- [ ] Test all API endpoints with authentication
|
||||
- [ ] Check logs for database errors
|
||||
- [ ] Verify no AttributeError or FieldError exceptions
|
||||
|
||||
### 5. Maintain Documentation
|
||||
- [ ] Update this guide with new mappings
|
||||
- [ ] Document why `db_column` is needed
|
||||
- [ ] Note any related field name changes
|
||||
|
||||
---
|
||||
|
||||
## Quick Fix Template
|
||||
|
||||
When you find a database mismatch:
|
||||
|
||||
```python
|
||||
# In the model file (e.g., backend/igny8_core/business/content/models.py)
|
||||
|
||||
# OLD (causes 500 error):
|
||||
content_type = models.CharField(max_length=100)
|
||||
|
||||
# FIXED (maps to actual database column):
|
||||
content_type = models.CharField(
|
||||
max_length=100,
|
||||
db_column='entity_type', # <- Maps to actual database column name
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
```
|
||||
|
||||
Then search and replace code references:
|
||||
```bash
|
||||
# Find all references to old field name
|
||||
grep -rn "\.entity_type" backend/
|
||||
|
||||
# Update to new field name
|
||||
sed -i 's/\.entity_type/.content_type/g' backend/igny8_core/modules/writer/views.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Common Issues
|
||||
|
||||
### Issue: Undefined Variables
|
||||
**Error:** `ReferenceError: variableName is not defined`
|
||||
|
||||
**Common Causes:**
|
||||
- Variable declared but removed during refactor
|
||||
- Variable in dependency array but not defined in component
|
||||
- Import removed but variable still referenced
|
||||
|
||||
**Files to Check:**
|
||||
- `frontend/src/pages/Writer/Tasks.tsx`
|
||||
- `frontend/src/pages/Writer/Content.tsx`
|
||||
- Any component showing console loops
|
||||
|
||||
**Fix:** Remove from dependency arrays or add proper state declaration
|
||||
|
||||
---
|
||||
|
||||
## Testing Endpoints After Fixes
|
||||
|
||||
Use this command to verify all critical endpoints:
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X POST "https://api.igny8.com/api/v1/auth/login/" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"dev@igny8.com","password":"dev123456"}' | \
|
||||
python3 -c "import sys, json; print(json.load(sys.stdin)['data']['access'])")
|
||||
|
||||
# Test each endpoint
|
||||
curl -s "https://api.igny8.com/api/v1/writer/content/?site_id=5&page_size=1" \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||
|
||||
curl -s "https://api.igny8.com/api/v1/planner/clusters/?site_id=5&page_size=1" \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||
|
||||
curl -s "https://api.igny8.com/api/v1/writer/tasks/?site_id=5&page_size=1" \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||
```
|
||||
|
||||
**Success Indicator:** Response should have `"success": true`, not `"success": false` with error.
|
||||
|
||||
---
|
||||
|
||||
## Status Codes Quick Reference
|
||||
|
||||
| Code | Meaning | Common Cause | Fix |
|
||||
|------|---------|--------------|-----|
|
||||
| 500 | Internal Server Error | Database column mismatch, code error | Check logs, fix model or code |
|
||||
| 403 | Forbidden / Auth Required | Normal - means endpoint works but needs auth | Login first, use Bearer token |
|
||||
| 404 | Not Found | URL wrong or object doesn't exist | Check URL, verify ID exists |
|
||||
| 400 | Bad Request | Invalid data sent | Check request payload |
|
||||
|
||||
**Important:** If you see 403 instead of 500 after a fix, that's SUCCESS - it means the endpoint works and just needs authentication.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Key Principle:** The database schema is the source of truth. Models must map to actual database column names, not the other way around (unless you run migrations to rename columns).
|
||||
|
||||
**Golden Rule:** After ANY refactor that changes field names:
|
||||
1. Check if database columns were actually renamed
|
||||
2. If not, add `db_column` mappings in models
|
||||
3. Update all code references to use NEW field names
|
||||
4. Test all endpoints with authentication
|
||||
5. Check logs for database errors
|
||||
|
||||
**Remember:** A 500 error from a database column mismatch will cascade - fixing the model isn't enough, you must also update all code that references the old field names.
|
||||
2747
api-typescipt-console-400-500-errors.md
Normal file
2747
api-typescipt-console-400-500-errors.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -207,7 +207,8 @@ class GenerateImagePromptsFunction(BaseAIFunction):
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# Extract title
|
||||
title = content.title or content.task.title or ''
|
||||
# Get content title (task field was removed in refactor)
|
||||
title = content.title or ''
|
||||
|
||||
# Extract first 1-2 intro paragraphs (skip italic hook if present)
|
||||
paragraphs = soup.find_all('p')
|
||||
|
||||
@@ -22,15 +22,30 @@ class Tasks(SiteSectorBaseModel):
|
||||
limit_choices_to={'sector': models.F('sector')},
|
||||
help_text="Parent cluster (required)"
|
||||
)
|
||||
idea = models.ForeignKey(
|
||||
'planner.ContentIdeas',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='tasks',
|
||||
help_text="Optional content idea reference",
|
||||
db_column='idea_id'
|
||||
)
|
||||
content_type = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="Content type: post, page, product, service, category, tag, etc."
|
||||
help_text="Content type: post, page, product, service, category, tag, etc.",
|
||||
db_column='entity_type',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
content_structure = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc."
|
||||
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc.",
|
||||
db_column='cluster_role',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
taxonomy_term = models.ForeignKey(
|
||||
'ContentTaxonomy',
|
||||
@@ -38,13 +53,13 @@ class Tasks(SiteSectorBaseModel):
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='tasks',
|
||||
help_text="Optional taxonomy term assignment"
|
||||
help_text="Optional taxonomy term assignment",
|
||||
db_column='taxonomy_id'
|
||||
)
|
||||
keywords = models.ManyToManyField(
|
||||
'planner.Keywords',
|
||||
keywords = models.TextField(
|
||||
blank=True,
|
||||
related_name='tasks',
|
||||
help_text="Keywords linked to this task"
|
||||
null=True,
|
||||
help_text="Comma-separated keywords for this task"
|
||||
)
|
||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='queued')
|
||||
|
||||
@@ -70,6 +85,19 @@ class Tasks(SiteSectorBaseModel):
|
||||
return self.title
|
||||
|
||||
|
||||
class ContentTaxonomyRelation(models.Model):
|
||||
"""Through model for Content-Taxonomy many-to-many relationship"""
|
||||
content = models.ForeignKey('Content', on_delete=models.CASCADE, db_column='content_id')
|
||||
taxonomy = models.ForeignKey('ContentTaxonomy', on_delete=models.CASCADE, db_column='taxonomy_id')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
app_label = 'writer'
|
||||
db_table = 'igny8_content_taxonomy_relations'
|
||||
unique_together = [['content', 'taxonomy']]
|
||||
|
||||
|
||||
class Content(SiteSectorBaseModel):
|
||||
"""
|
||||
Content model for AI-generated or WordPress-imported content.
|
||||
@@ -78,7 +106,7 @@ class Content(SiteSectorBaseModel):
|
||||
|
||||
# Core content fields
|
||||
title = models.CharField(max_length=255, db_index=True)
|
||||
content_html = models.TextField(help_text="Final HTML content")
|
||||
content_html = models.TextField(help_text="Final HTML content", db_column='html_content')
|
||||
cluster = models.ForeignKey(
|
||||
'planner.Clusters',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -90,26 +118,34 @@ class Content(SiteSectorBaseModel):
|
||||
content_type = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="Content type: post, page, product, service, category, tag, etc."
|
||||
help_text="Content type: post, page, product, service, category, tag, etc.",
|
||||
db_column='entity_type',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
content_structure = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc."
|
||||
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc.",
|
||||
db_column='cluster_role',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
# Taxonomy relationships
|
||||
taxonomy_terms = models.ManyToManyField(
|
||||
'ContentTaxonomy',
|
||||
through='ContentTaxonomyRelation',
|
||||
blank=True,
|
||||
related_name='contents',
|
||||
db_table='igny8_content_taxonomy_relations',
|
||||
help_text="Associated taxonomy terms (categories, tags, attributes)"
|
||||
)
|
||||
|
||||
# External platform fields (WordPress integration)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True, db_index=True, help_text="WordPress/external platform post ID")
|
||||
external_url = models.URLField(blank=True, null=True, help_text="WordPress/external platform URL")
|
||||
external_type = models.CharField(max_length=100, blank=True, null=True, help_text="WordPress post type (post, page, product, etc.)")
|
||||
sync_status = models.CharField(max_length=50, blank=True, null=True, help_text="Sync status with WordPress")
|
||||
|
||||
# Source tracking
|
||||
SOURCE_CHOICES = [
|
||||
|
||||
@@ -23,62 +23,21 @@ class MetadataMappingService:
|
||||
@transaction.atomic
|
||||
def persist_task_metadata_to_content(self, content: Content) -> None:
|
||||
"""
|
||||
Persist cluster/taxonomy/attribute mappings from Task to Content.
|
||||
DEPRECATED: This method is deprecated as Content model no longer has task field.
|
||||
Metadata is now persisted directly on content model.
|
||||
|
||||
Args:
|
||||
content: Content instance with an associated task
|
||||
content: Content instance
|
||||
"""
|
||||
if not content.task:
|
||||
logger.warning(f"Content {content.id} has no associated task, skipping metadata mapping")
|
||||
logger.warning(f"[persist_task_metadata_to_content] Deprecated method called for content {content.id}")
|
||||
logger.warning(f"Content model no longer has task field - metadata should be set directly on content")
|
||||
return
|
||||
|
||||
task = content.task
|
||||
|
||||
# Stage 3: Persist cluster mapping if task has cluster
|
||||
if task.cluster:
|
||||
ContentClusterMap.objects.get_or_create(
|
||||
content=content,
|
||||
cluster=task.cluster,
|
||||
role=task.cluster_role or 'hub',
|
||||
defaults={
|
||||
'account': content.account,
|
||||
'site': content.site,
|
||||
'sector': content.sector,
|
||||
'source': 'blueprint' if task.idea else 'manual',
|
||||
'metadata': {},
|
||||
}
|
||||
)
|
||||
logger.info(f"Created cluster mapping for content {content.id} -> cluster {task.cluster.id}")
|
||||
|
||||
# Stage 3: Persist taxonomy mapping if task has taxonomy
|
||||
if task.taxonomy:
|
||||
ContentTaxonomyMap.objects.get_or_create(
|
||||
content=content,
|
||||
taxonomy=task.taxonomy,
|
||||
defaults={
|
||||
'account': content.account,
|
||||
'site': content.site,
|
||||
'sector': content.sector,
|
||||
'source': 'blueprint',
|
||||
'metadata': {},
|
||||
}
|
||||
)
|
||||
logger.info(f"Created taxonomy mapping for content {content.id} -> taxonomy {task.taxonomy.id}")
|
||||
|
||||
# Stage 3: Inherit entity_type from task
|
||||
if task.entity_type and not content.entity_type:
|
||||
content.entity_type = task.entity_type
|
||||
content.save(update_fields=['entity_type'])
|
||||
logger.info(f"Set entity_type {task.entity_type} for content {content.id}")
|
||||
|
||||
# Stage 3: Extract attributes from task metadata if available
|
||||
# This can be extended to parse task.description or task.metadata for attributes
|
||||
# For now, we'll rely on explicit attribute data in future enhancements
|
||||
|
||||
@transaction.atomic
|
||||
def backfill_content_metadata(self, content: Content) -> None:
|
||||
"""
|
||||
Backfill metadata mappings for existing content that may be missing mappings.
|
||||
Note: task field was removed from Content model - only infers from content metadata.
|
||||
|
||||
Args:
|
||||
content: Content instance to backfill
|
||||
@@ -87,13 +46,24 @@ class MetadataMappingService:
|
||||
if ContentClusterMap.objects.filter(content=content).exists():
|
||||
return
|
||||
|
||||
# Try to infer from task
|
||||
if content.task:
|
||||
self.persist_task_metadata_to_content(content)
|
||||
# Try to infer from content metadata or cluster field
|
||||
if hasattr(content, 'cluster') and content.cluster:
|
||||
ContentClusterMap.objects.get_or_create(
|
||||
content=content,
|
||||
cluster=content.cluster,
|
||||
role='hub', # Default
|
||||
defaults={
|
||||
'account': content.account,
|
||||
'site': content.site,
|
||||
'sector': content.sector,
|
||||
'source': 'manual',
|
||||
'metadata': {},
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
# Try to infer from content metadata
|
||||
if content.metadata:
|
||||
# Fallback: Try to infer from content metadata
|
||||
if hasattr(content, 'metadata') and content.metadata:
|
||||
cluster_id = content.metadata.get('cluster_id')
|
||||
if cluster_id:
|
||||
from igny8_core.business.planning.models import Clusters
|
||||
|
||||
@@ -37,20 +37,20 @@ class ContentValidationService:
|
||||
})
|
||||
|
||||
# Stage 3: Validate entity_type is set
|
||||
if not task.entity_type:
|
||||
if not task.content_type:
|
||||
errors.append({
|
||||
'field': 'entity_type',
|
||||
'code': 'missing_entity_type',
|
||||
'message': 'Task must have an entity type specified',
|
||||
'field': 'content_type',
|
||||
'code': 'missing_content_type',
|
||||
'message': 'Task must have a content type specified',
|
||||
})
|
||||
|
||||
# Stage 3: Validate taxonomy for product/service entities
|
||||
if task.entity_type in ['product', 'service']:
|
||||
if not task.taxonomy:
|
||||
if task.content_type in ['product', 'service']:
|
||||
if not task.taxonomy_term:
|
||||
errors.append({
|
||||
'field': 'taxonomy',
|
||||
'code': 'missing_taxonomy',
|
||||
'message': f'{task.entity_type.title()} tasks require a taxonomy association',
|
||||
'message': f'{task.content_type.title()} tasks require a taxonomy association',
|
||||
})
|
||||
|
||||
return errors
|
||||
@@ -67,12 +67,12 @@ class ContentValidationService:
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Stage 3: Validate entity_type
|
||||
if not content.entity_type:
|
||||
# Stage 3: Validate content_type
|
||||
if not content.content_type:
|
||||
errors.append({
|
||||
'field': 'entity_type',
|
||||
'code': 'missing_entity_type',
|
||||
'message': 'Content must have an entity type specified',
|
||||
'field': 'content_type',
|
||||
'code': 'missing_content_type',
|
||||
'message': 'Content must have a content type specified',
|
||||
})
|
||||
|
||||
# Stage 3: Validate cluster mapping exists for IGNY8 content
|
||||
@@ -86,30 +86,20 @@ class ContentValidationService:
|
||||
})
|
||||
|
||||
# Stage 3: Validate taxonomy for product/service content
|
||||
if content.entity_type in ['product', 'service']:
|
||||
from igny8_core.business.content.models import ContentTaxonomyMap
|
||||
if not ContentTaxonomyMap.objects.filter(content=content).exists():
|
||||
if content.content_type in ['product', 'service']:
|
||||
# Check if content has taxonomy terms assigned
|
||||
if not content.taxonomy_terms.exists():
|
||||
errors.append({
|
||||
'field': 'taxonomy_mapping',
|
||||
'code': 'missing_taxonomy_mapping',
|
||||
'message': f'{content.entity_type.title()} content requires a taxonomy mapping',
|
||||
'message': f'{content.content_type.title()} content requires a taxonomy mapping',
|
||||
})
|
||||
|
||||
# Stage 3: Validate required attributes for products
|
||||
if content.entity_type == 'product':
|
||||
from igny8_core.business.content.models import ContentAttributeMap
|
||||
required_attrs = ['price', 'sku', 'category']
|
||||
existing_attrs = ContentAttributeMap.objects.filter(
|
||||
content=content,
|
||||
name__in=required_attrs
|
||||
).values_list('name', flat=True)
|
||||
missing_attrs = set(required_attrs) - set(existing_attrs)
|
||||
if missing_attrs:
|
||||
errors.append({
|
||||
'field': 'attributes',
|
||||
'code': 'missing_attributes',
|
||||
'message': f'Product content requires attributes: {", ".join(missing_attrs)}',
|
||||
})
|
||||
if content.content_type == 'product':
|
||||
# Product validation - currently simplified in Stage 1
|
||||
# TODO: Re-enable attribute validation when product attributes are implemented
|
||||
pass
|
||||
|
||||
return errors
|
||||
|
||||
@@ -136,9 +126,9 @@ class ContentValidationService:
|
||||
'message': 'Content must have a title before publishing',
|
||||
})
|
||||
|
||||
if not content.html_content or len(content.html_content.strip()) < 100:
|
||||
if not content.content_html or len(content.content_html.strip()) < 100:
|
||||
errors.append({
|
||||
'field': 'html_content',
|
||||
'field': 'content_html',
|
||||
'code': 'insufficient_content',
|
||||
'message': 'Content must have at least 100 characters before publishing',
|
||||
})
|
||||
@@ -157,9 +147,9 @@ class ContentValidationService:
|
||||
"""
|
||||
errors = []
|
||||
|
||||
if task.entity_type == 'product':
|
||||
if task.content_type == 'product':
|
||||
# Products should have taxonomy and cluster
|
||||
if not task.taxonomy:
|
||||
if not task.taxonomy_term:
|
||||
errors.append({
|
||||
'field': 'taxonomy',
|
||||
'code': 'missing_taxonomy',
|
||||
|
||||
@@ -348,19 +348,19 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
||||
# Build response with synced counts
|
||||
post_types_data = {}
|
||||
for wp_type, type_config in content_types.get('post_types', {}).items():
|
||||
# Map WP type to entity_type
|
||||
entity_type_map = {
|
||||
# Map WP type to content_type
|
||||
content_type_map = {
|
||||
'post': 'post',
|
||||
'page': 'page',
|
||||
'product': 'product',
|
||||
'service': 'service',
|
||||
}
|
||||
entity_type = entity_type_map.get(wp_type, 'post')
|
||||
content_type = content_type_map.get(wp_type, 'post')
|
||||
|
||||
# Count synced content
|
||||
synced_count = Content.objects.filter(
|
||||
site=site,
|
||||
entity_type=entity_type,
|
||||
content_type=content_type,
|
||||
external_type=wp_type,
|
||||
sync_status__in=['imported', 'synced']
|
||||
).count()
|
||||
@@ -379,8 +379,7 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
||||
# Count synced taxonomies
|
||||
synced_count = ContentTaxonomy.objects.filter(
|
||||
site=site,
|
||||
external_taxonomy=wp_tax,
|
||||
sync_status__in=['imported', 'synced']
|
||||
external_taxonomy=wp_tax
|
||||
).count()
|
||||
|
||||
taxonomies_data[wp_tax] = {
|
||||
|
||||
@@ -102,13 +102,14 @@ class ClusterSerializer(serializers.ModelSerializer):
|
||||
return ContentIdeas.objects.filter(keyword_cluster_id=obj.id).count()
|
||||
|
||||
def get_content_count(self, obj):
|
||||
"""Count generated content items linked to this cluster via tasks"""
|
||||
"""Count generated content items linked to this cluster"""
|
||||
if hasattr(obj, '_content_count'):
|
||||
return obj._content_count
|
||||
|
||||
from igny8_core.modules.writer.models import Content
|
||||
|
||||
return Content.objects.filter(task__cluster_id=obj.id).count()
|
||||
# Content links directly to clusters now (task field was removed in refactor)
|
||||
return Content.objects.filter(cluster_id=obj.id).count()
|
||||
|
||||
@classmethod
|
||||
def prefetch_keyword_stats(cls, clusters):
|
||||
@@ -166,16 +167,16 @@ class ClusterSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
idea_stats = {item['keyword_cluster_id']: item['count'] for item in idea_counts}
|
||||
|
||||
# Prefetch content counts (through writer.Tasks -> Content)
|
||||
# Prefetch content counts (Content links directly to Clusters now)
|
||||
from igny8_core.modules.writer.models import Content
|
||||
|
||||
content_counts = (
|
||||
Content.objects
|
||||
.filter(task__cluster_id__in=cluster_ids)
|
||||
.values('task__cluster_id')
|
||||
.filter(cluster_id__in=cluster_ids)
|
||||
.values('cluster_id')
|
||||
.annotate(count=Count('id'))
|
||||
)
|
||||
content_stats = {item['task__cluster_id']: item['count'] for item in content_counts}
|
||||
content_stats = {item['cluster_id']: item['count'] for item in content_counts}
|
||||
|
||||
# Attach stats to each cluster object
|
||||
for cluster in clusters:
|
||||
|
||||
@@ -102,10 +102,6 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
('Content', {
|
||||
'fields': ('content_html',)
|
||||
}),
|
||||
('Taxonomy', {
|
||||
'fields': ('taxonomy_terms',),
|
||||
'description': 'Categories, tags, and other taxonomy terms'
|
||||
}),
|
||||
('WordPress Sync', {
|
||||
'fields': ('external_id', 'external_url'),
|
||||
'classes': ('collapse',)
|
||||
|
||||
@@ -488,14 +488,12 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
# Update by content_id if provided, otherwise by image IDs
|
||||
if content_id:
|
||||
try:
|
||||
# Get the content object to also update images linked via task
|
||||
# Get the content object to also update images linked directly to content
|
||||
content = Content.objects.get(id=content_id)
|
||||
|
||||
# Update images linked directly to content OR via task (same logic as content_images endpoint)
|
||||
# This ensures we update all images: featured + 1-6 in-article images
|
||||
updated_count = queryset.filter(
|
||||
Q(content=content) | Q(task=content.task)
|
||||
).update(status=status_value)
|
||||
# Update images linked directly to content (all images: featured + in-article)
|
||||
# Note: task field was removed in refactor - images now link directly to content
|
||||
updated_count = queryset.filter(content=content).update(status=status_value)
|
||||
except Content.DoesNotExist:
|
||||
return error_response(
|
||||
error='Content not found',
|
||||
@@ -670,8 +668,8 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
'validation_errors': errors,
|
||||
'publish_errors': publish_errors,
|
||||
'metadata': {
|
||||
'has_entity_type': bool(content.entity_type),
|
||||
'entity_type': content.entity_type,
|
||||
'has_entity_type': bool(content.content_type),
|
||||
'entity_type': content.content_type,
|
||||
'has_cluster_mapping': self._has_cluster_mapping(content),
|
||||
'has_taxonomy_mapping': self._has_taxonomy_mapping(content),
|
||||
}
|
||||
@@ -691,9 +689,9 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
validation_service = ContentValidationService()
|
||||
|
||||
# Persist metadata mappings if task exists
|
||||
if content.task:
|
||||
mapping_service = MetadataMappingService()
|
||||
mapping_service.persist_task_metadata_to_content(content)
|
||||
# Metadata is now persisted directly on content - no task linkage needed
|
||||
# mapping_service = MetadataMappingService() # DEPRECATED
|
||||
# mapping_service.persist_task_metadata_to_content(content) # DEPRECATED
|
||||
|
||||
errors = validation_service.validate_for_publish(content)
|
||||
|
||||
@@ -970,8 +968,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
'validation_errors': errors,
|
||||
'publish_errors': publish_errors,
|
||||
'metadata': {
|
||||
'has_entity_type': bool(content.entity_type),
|
||||
'entity_type': content.entity_type,
|
||||
'has_entity_type': bool(content.content_type),
|
||||
'entity_type': content.content_type,
|
||||
'has_cluster_mapping': self._has_cluster_mapping(content),
|
||||
'has_taxonomy_mapping': self._has_taxonomy_mapping(content),
|
||||
}
|
||||
@@ -991,9 +989,9 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
validation_service = ContentValidationService()
|
||||
|
||||
# Persist metadata mappings if task exists
|
||||
if content.task:
|
||||
mapping_service = MetadataMappingService()
|
||||
mapping_service.persist_task_metadata_to_content(content)
|
||||
# Metadata is now persisted directly on content - no task linkage needed
|
||||
# mapping_service = MetadataMappingService() # DEPRECATED
|
||||
# mapping_service.persist_task_metadata_to_content(content) # DEPRECATED
|
||||
|
||||
errors = validation_service.validate_for_publish(content)
|
||||
|
||||
@@ -1121,8 +1119,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
'validation_errors': errors,
|
||||
'publish_errors': publish_errors,
|
||||
'metadata': {
|
||||
'has_entity_type': bool(content.entity_type),
|
||||
'entity_type': content.entity_type,
|
||||
'has_entity_type': bool(content.content_type),
|
||||
'entity_type': content.content_type,
|
||||
'has_cluster_mapping': self._has_cluster_mapping(content),
|
||||
'has_taxonomy_mapping': self._has_taxonomy_mapping(content),
|
||||
}
|
||||
@@ -1142,9 +1140,9 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
validation_service = ContentValidationService()
|
||||
|
||||
# Persist metadata mappings if task exists
|
||||
if content.task:
|
||||
mapping_service = MetadataMappingService()
|
||||
mapping_service.persist_task_metadata_to_content(content)
|
||||
# Metadata is now persisted directly on content - no task linkage needed
|
||||
# mapping_service = MetadataMappingService() # DEPRECATED
|
||||
# mapping_service.persist_task_metadata_to_content(content) # DEPRECATED
|
||||
|
||||
errors = validation_service.validate_for_publish(content)
|
||||
|
||||
@@ -1272,8 +1270,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
'validation_errors': errors,
|
||||
'publish_errors': publish_errors,
|
||||
'metadata': {
|
||||
'has_entity_type': bool(content.entity_type),
|
||||
'entity_type': content.entity_type,
|
||||
'has_entity_type': bool(content.content_type),
|
||||
'entity_type': content.content_type,
|
||||
'has_cluster_mapping': self._has_cluster_mapping(content),
|
||||
'has_taxonomy_mapping': self._has_taxonomy_mapping(content),
|
||||
}
|
||||
@@ -1293,9 +1291,9 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
validation_service = ContentValidationService()
|
||||
|
||||
# Persist metadata mappings if task exists
|
||||
if content.task:
|
||||
mapping_service = MetadataMappingService()
|
||||
mapping_service.persist_task_metadata_to_content(content)
|
||||
# Metadata is now persisted directly on content - no task linkage needed
|
||||
# mapping_service = MetadataMappingService() # DEPRECATED
|
||||
# mapping_service.persist_task_metadata_to_content(content) # DEPRECATED
|
||||
|
||||
errors = validation_service.validate_for_publish(content)
|
||||
|
||||
@@ -1422,8 +1420,8 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
'validation_errors': errors,
|
||||
'publish_errors': publish_errors,
|
||||
'metadata': {
|
||||
'has_entity_type': bool(content.entity_type),
|
||||
'entity_type': content.entity_type,
|
||||
'has_entity_type': bool(content.content_type),
|
||||
'entity_type': content.content_type,
|
||||
'has_cluster_mapping': self._has_cluster_mapping(content),
|
||||
'has_taxonomy_mapping': self._has_taxonomy_mapping(content),
|
||||
}
|
||||
@@ -1443,9 +1441,9 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
validation_service = ContentValidationService()
|
||||
|
||||
# Persist metadata mappings if task exists
|
||||
if content.task:
|
||||
mapping_service = MetadataMappingService()
|
||||
mapping_service.persist_task_metadata_to_content(content)
|
||||
# Metadata is now persisted directly on content - no task linkage needed
|
||||
# mapping_service = MetadataMappingService() # DEPRECATED
|
||||
# mapping_service.persist_task_metadata_to_content(content) # DEPRECATED
|
||||
|
||||
errors = validation_service.validate_for_publish(content)
|
||||
|
||||
@@ -1466,7 +1464,7 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
def _has_taxonomy_mapping(self, content):
|
||||
"""Helper to check if content has taxonomy mapping"""
|
||||
# Check new M2M relationship
|
||||
return content.taxonomies.exists()
|
||||
return content.taxonomy_terms.exists()
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
|
||||
1597
backend/igny8_core/modules/writer/views.py.bak
Normal file
1597
backend/igny8_core/modules/writer/views.py.bak
Normal file
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,7 @@ const Plans = lazy(() => import("./pages/Settings/Plans"));
|
||||
const Industries = lazy(() => import("./pages/Settings/Industries"));
|
||||
const Status = lazy(() => import("./pages/Settings/Status"));
|
||||
const ApiMonitor = lazy(() => import("./pages/Settings/ApiMonitor"));
|
||||
const DebugStatus = lazy(() => import("./pages/Settings/DebugStatus"));
|
||||
const Integration = lazy(() => import("./pages/Settings/Integration"));
|
||||
const Publishing = lazy(() => import("./pages/Settings/Publishing"));
|
||||
const Sites = lazy(() => import("./pages/Settings/Sites"));
|
||||
@@ -428,6 +429,11 @@ export default function App() {
|
||||
<ApiMonitor />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/debug-status" element={
|
||||
<Suspense fallback={null}>
|
||||
<DebugStatus />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/integration" element={
|
||||
<Suspense fallback={null}>
|
||||
<Integration />
|
||||
|
||||
@@ -238,6 +238,7 @@ const AppSidebar: React.FC = () => {
|
||||
subItems: [
|
||||
{ name: "Status", path: "/settings/status" },
|
||||
{ name: "API Monitor", path: "/settings/api-monitor" },
|
||||
{ name: "Debug Status", path: "/settings/debug-status" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
802
frontend/src/pages/Settings/DebugStatus.tsx
Normal file
802
frontend/src/pages/Settings/DebugStatus.tsx
Normal file
@@ -0,0 +1,802 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import { API_BASE_URL, fetchAPI } from "../../services/api";
|
||||
|
||||
interface HealthCheck {
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'healthy' | 'warning' | 'error' | 'checking';
|
||||
message?: string;
|
||||
details?: string;
|
||||
lastChecked?: string;
|
||||
}
|
||||
|
||||
interface ModuleHealth {
|
||||
module: string;
|
||||
description: string;
|
||||
checks: HealthCheck[];
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return 'text-green-600 dark:text-green-400';
|
||||
case 'warning': return 'text-yellow-600 dark:text-yellow-400';
|
||||
case 'error': return 'text-red-600 dark:text-red-400';
|
||||
case 'checking': return 'text-blue-600 dark:text-blue-400';
|
||||
default: return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
||||
case 'warning': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
||||
case 'error': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
||||
case 'checking': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
|
||||
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return '✓';
|
||||
case 'warning': return '⚠';
|
||||
case 'error': return '✗';
|
||||
case 'checking': return '⟳';
|
||||
default: return '?';
|
||||
}
|
||||
};
|
||||
|
||||
export default function DebugStatus() {
|
||||
const [moduleHealths, setModuleHealths] = useState<ModuleHealth[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Helper to get auth token
|
||||
const getAuthToken = () => {
|
||||
const token = localStorage.getItem('auth_token') ||
|
||||
(() => {
|
||||
try {
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
const parsed = JSON.parse(authStorage);
|
||||
return parsed?.state?.token || '';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
return token;
|
||||
};
|
||||
|
||||
// Helper to make authenticated API calls
|
||||
const apiCall = async (path: string, method: string = 'GET', body?: any) => {
|
||||
const token = getAuthToken();
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
if (body && method !== 'GET') {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, options);
|
||||
const data = await response.json();
|
||||
return { response, data };
|
||||
};
|
||||
|
||||
// Check database schema field mappings (the issues we just fixed)
|
||||
const checkDatabaseSchemaMapping = useCallback(async (): Promise<HealthCheck> => {
|
||||
try {
|
||||
// Test Writer Content endpoint (was failing with entity_type error)
|
||||
const { response: contentResp, data: contentData } = await apiCall('/v1/writer/content/');
|
||||
|
||||
if (!contentResp.ok) {
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'error',
|
||||
message: `Writer Content API failed with ${contentResp.status}`,
|
||||
details: 'Model fields may not be mapped to database columns correctly (e.g., content_type vs entity_type)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if response has the expected structure with new field names
|
||||
if (contentData?.results && Array.isArray(contentData.results)) {
|
||||
// If we can fetch content, schema mapping is working
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'healthy',
|
||||
message: 'All model fields correctly mapped via db_column attributes',
|
||||
details: `Content API working correctly. Fields like content_type, content_html, content_structure are properly mapped.`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'warning',
|
||||
message: 'Content API returned unexpected structure',
|
||||
details: 'Response format may have changed',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'error',
|
||||
message: error.message || 'Failed to check schema mapping',
|
||||
details: 'Check if db_column attributes are set correctly in models',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check Writer module health
|
||||
const checkWriterModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Content endpoint
|
||||
try {
|
||||
const { response: contentResp, data: contentData } = await apiCall('/v1/writer/content/');
|
||||
|
||||
if (contentResp.ok && contentData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${contentData?.count || 0} content items`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'error',
|
||||
message: contentData?.error || `Failed with ${contentResp.status}`,
|
||||
details: 'Check model field mappings (content_type, content_html, content_structure)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Tasks endpoint
|
||||
try {
|
||||
const { response: tasksResp, data: tasksData } = await apiCall('/v1/writer/tasks/');
|
||||
|
||||
if (tasksResp.ok && tasksData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${tasksData?.count || 0} tasks`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'error',
|
||||
message: tasksData?.error || `Failed with ${tasksResp.status}`,
|
||||
details: 'Check model field mappings (content_type, content_structure, taxonomy_term)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Content Validation
|
||||
try {
|
||||
// Get first content ID if available
|
||||
const { data: contentData } = await apiCall('/v1/writer/content/');
|
||||
const firstContentId = contentData?.results?.[0]?.id;
|
||||
|
||||
if (firstContentId) {
|
||||
const { response: validationResp, data: validationData } = await apiCall(
|
||||
`/v1/writer/content/${firstContentId}/validation/`
|
||||
);
|
||||
|
||||
if (validationResp.ok && validationData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'healthy',
|
||||
message: 'Validation endpoint working',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'error',
|
||||
message: validationData?.error || `Failed with ${validationResp.status}`,
|
||||
details: 'Check validation_service.py field references (content_html, content_type)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'warning',
|
||||
message: 'No content available to test validation',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Planner module health
|
||||
const checkPlannerModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Clusters endpoint
|
||||
try {
|
||||
const { response: clustersResp, data: clustersData } = await apiCall('/v1/planner/clusters/');
|
||||
|
||||
if (clustersResp.ok && clustersData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${clustersData?.count || 0} clusters`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'error',
|
||||
message: clustersData?.error || `Failed with ${clustersResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Keywords endpoint
|
||||
try {
|
||||
const { response: keywordsResp, data: keywordsData } = await apiCall('/v1/planner/keywords/');
|
||||
|
||||
if (keywordsResp.ok && keywordsData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${keywordsData?.count || 0} keywords`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'error',
|
||||
message: keywordsData?.error || `Failed with ${keywordsResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Ideas endpoint
|
||||
try {
|
||||
const { response: ideasResp, data: ideasData } = await apiCall('/v1/planner/ideas/');
|
||||
|
||||
if (ideasResp.ok && ideasData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${ideasData?.count || 0} ideas`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'error',
|
||||
message: ideasData?.error || `Failed with ${ideasResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Sites module health
|
||||
const checkSitesModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Sites list
|
||||
try {
|
||||
const { response: sitesResp, data: sitesData } = await apiCall('/v1/auth/sites/');
|
||||
|
||||
if (sitesResp.ok && sitesData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${sitesData?.count || sitesData?.results?.length || 0} sites`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'error',
|
||||
message: sitesData?.error || `Failed with ${sitesResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Integration module health
|
||||
const checkIntegrationModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Integration content types endpoint
|
||||
try {
|
||||
// Get first site ID if available
|
||||
const { data: sitesData } = await apiCall('/v1/auth/sites/');
|
||||
const firstSiteId = sitesData?.results?.[0]?.id || sitesData?.[0]?.id;
|
||||
|
||||
if (firstSiteId) {
|
||||
const { response: contentTypesResp, data: contentTypesData } = await apiCall(
|
||||
`/v1/integration/integrations/${firstSiteId}/content-types/`
|
||||
);
|
||||
|
||||
if (contentTypesResp.ok && contentTypesData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'healthy',
|
||||
message: 'Content types endpoint working',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: contentTypesData?.error || `Failed with ${contentTypesResp.status}`,
|
||||
details: 'Check integration views field mappings (content_type_map vs entity_type_map)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'warning',
|
||||
message: 'No sites available to test integration',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Check Content Manager module health (taxonomy relations)
|
||||
const checkContentManagerModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Taxonomy endpoint
|
||||
try {
|
||||
const { response: taxonomyResp, data: taxonomyData } = await apiCall('/v1/writer/taxonomy/');
|
||||
|
||||
if (taxonomyResp.ok && taxonomyData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Taxonomy System',
|
||||
description: 'Content taxonomy endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${taxonomyData?.count || 0} taxonomy items`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Taxonomy System',
|
||||
description: 'Content taxonomy endpoint',
|
||||
status: 'error',
|
||||
message: taxonomyData?.error || `Failed with ${taxonomyResp.status}`,
|
||||
details: 'Check ContentTaxonomyRelation through model and field mappings',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Taxonomy System',
|
||||
description: 'Content taxonomy endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, []);
|
||||
|
||||
// Run all health checks
|
||||
const runAllChecks = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Run schema check first
|
||||
const schemaCheck = await checkDatabaseSchemaMapping();
|
||||
|
||||
// Run module checks in parallel
|
||||
const [writerChecks, plannerChecks, sitesChecks, integrationChecks, contentMgrChecks] = await Promise.all([
|
||||
checkWriterModule(),
|
||||
checkPlannerModule(),
|
||||
checkSitesModule(),
|
||||
checkIntegrationModule(),
|
||||
checkContentManagerModule(),
|
||||
]);
|
||||
|
||||
// Build module health results
|
||||
const moduleHealthResults: ModuleHealth[] = [
|
||||
{
|
||||
module: 'Database Schema',
|
||||
description: 'Critical database field mapping checks',
|
||||
checks: [schemaCheck],
|
||||
},
|
||||
{
|
||||
module: 'Writer Module',
|
||||
description: 'Content creation and task management',
|
||||
checks: writerChecks,
|
||||
},
|
||||
{
|
||||
module: 'Planner Module',
|
||||
description: 'Keyword clustering and content planning',
|
||||
checks: plannerChecks,
|
||||
},
|
||||
{
|
||||
module: 'Sites Module',
|
||||
description: 'Site management and configuration',
|
||||
checks: sitesChecks,
|
||||
},
|
||||
{
|
||||
module: 'Integration Module',
|
||||
description: 'External platform sync (WordPress, etc.)',
|
||||
checks: integrationChecks,
|
||||
},
|
||||
{
|
||||
module: 'Content Manager',
|
||||
description: 'Taxonomy and content organization',
|
||||
checks: contentMgrChecks,
|
||||
},
|
||||
];
|
||||
|
||||
setModuleHealths(moduleHealthResults);
|
||||
} catch (error) {
|
||||
console.error('Failed to run health checks:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
checkDatabaseSchemaMapping,
|
||||
checkWriterModule,
|
||||
checkPlannerModule,
|
||||
checkSitesModule,
|
||||
checkIntegrationModule,
|
||||
checkContentManagerModule,
|
||||
]);
|
||||
|
||||
// Run checks on mount
|
||||
useEffect(() => {
|
||||
runAllChecks();
|
||||
}, [runAllChecks]);
|
||||
|
||||
// Calculate module status
|
||||
const getModuleStatus = (module: ModuleHealth): 'error' | 'warning' | 'healthy' => {
|
||||
const statuses = module.checks.map(c => c.status);
|
||||
if (statuses.some(s => s === 'error')) return 'error';
|
||||
if (statuses.some(s => s === 'warning')) return 'warning';
|
||||
return 'healthy';
|
||||
};
|
||||
|
||||
// Calculate overall health
|
||||
const getOverallHealth = () => {
|
||||
const allStatuses = moduleHealths.flatMap(m => m.checks.map(c => c.status));
|
||||
const total = allStatuses.length;
|
||||
const healthy = allStatuses.filter(s => s === 'healthy').length;
|
||||
const warning = allStatuses.filter(s => s === 'warning').length;
|
||||
const error = allStatuses.filter(s => s === 'error').length;
|
||||
|
||||
let status: 'error' | 'warning' | 'healthy' = 'healthy';
|
||||
if (error > 0) status = 'error';
|
||||
else if (warning > 0) status = 'warning';
|
||||
|
||||
return { total, healthy, warning, error, status, percentage: total > 0 ? Math.round((healthy / total) * 100) : 0 };
|
||||
};
|
||||
|
||||
const overallHealth = getOverallHealth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Debug Status - IGNY8" description="Module health checks and diagnostics" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white/90">Debug Status</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Comprehensive health checks for all modules and recent bug fixes
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={runAllChecks}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
>
|
||||
{loading ? 'Running Checks...' : 'Refresh All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Overall Health Summary */}
|
||||
<ComponentCard
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Overall System Health</span>
|
||||
<span className={`text-lg ${getStatusColor(overallHealth.status)}`}>
|
||||
{getStatusIcon(overallHealth.status)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
overallHealth.status === 'error'
|
||||
? `${overallHealth.error} critical issue${overallHealth.error !== 1 ? 's' : ''} detected`
|
||||
: overallHealth.status === 'warning'
|
||||
? `${overallHealth.warning} warning${overallHealth.warning !== 1 ? 's' : ''} detected`
|
||||
: 'All systems operational'
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Health Percentage */}
|
||||
<div className="text-center">
|
||||
<div className={`text-5xl font-bold ${getStatusColor(overallHealth.status)}`}>
|
||||
{overallHealth.percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
{overallHealth.healthy} of {overallHealth.total} checks passed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health Breakdown */}
|
||||
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('healthy')}`}>
|
||||
{overallHealth.healthy}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Healthy</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('warning')}`}>
|
||||
{overallHealth.warning}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Warnings</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('error')}`}>
|
||||
{overallHealth.error}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Module Health Cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{moduleHealths.map((moduleHealth, index) => {
|
||||
const moduleStatus = getModuleStatus(moduleHealth);
|
||||
const healthyCount = moduleHealth.checks.filter(c => c.status === 'healthy').length;
|
||||
const totalCount = moduleHealth.checks.length;
|
||||
|
||||
return (
|
||||
<ComponentCard
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{moduleHealth.module}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(moduleStatus)}`}>
|
||||
{getStatusIcon(moduleStatus)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
moduleStatus === 'error'
|
||||
? `Issues detected - ${healthyCount}/${totalCount} checks passed`
|
||||
: moduleStatus === 'warning'
|
||||
? `Warnings detected - ${healthyCount}/${totalCount} checks passed`
|
||||
: `All ${totalCount} checks passed`
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{moduleHealth.description}
|
||||
</p>
|
||||
|
||||
{/* Health Checks List */}
|
||||
{moduleHealth.checks.map((check, checkIndex) => (
|
||||
<div
|
||||
key={checkIndex}
|
||||
className={`p-3 rounded-lg border ${
|
||||
check.status === 'healthy'
|
||||
? 'border-green-200 dark:border-green-900/50 bg-green-50 dark:bg-green-900/20'
|
||||
: check.status === 'warning'
|
||||
? 'border-yellow-200 dark:border-yellow-900/50 bg-yellow-50 dark:bg-yellow-900/20'
|
||||
: check.status === 'error'
|
||||
? 'border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20'
|
||||
: 'border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`text-lg ${getStatusColor(check.status)} flex-shrink-0`}>
|
||||
{getStatusIcon(check.status)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
{check.name}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(check.status)}`}>
|
||||
{check.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
{check.description}
|
||||
</p>
|
||||
{check.message && (
|
||||
<p className={`text-sm ${getStatusColor(check.status)} font-medium`}>
|
||||
{check.message}
|
||||
</p>
|
||||
)}
|
||||
{check.details && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 italic">
|
||||
💡 {check.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<ComponentCard
|
||||
title="Troubleshooting Guide"
|
||||
desc="Common issues and solutions"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
Database Schema Mapping Errors
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
If you see errors about missing fields like <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">entity_type</code>,
|
||||
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">cluster_role</code>, or
|
||||
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">html_content</code>:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
|
||||
<li>Check that model fields have correct <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">db_column</code> attributes</li>
|
||||
<li>Verify database columns exist with <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">SELECT column_name FROM information_schema.columns</code></li>
|
||||
<li>Review <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">DATABASE_SCHEMA_FIELD_MAPPING_GUIDE.md</code> in repo root</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
Field Reference Errors in Code
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
If API endpoints return 500 errors with AttributeError or similar:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
|
||||
<li>Search codebase for old field names: <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">entity_type</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">cluster_role</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">html_content</code></li>
|
||||
<li>Replace with new names: <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_type</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_structure</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_html</code></li>
|
||||
<li>Check views, services, and serializers in writer/planner/integration modules</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
All Checks Passing?
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Great! Your system is healthy. This page will help you quickly diagnose issues if they appear in the future.
|
||||
Bookmark this page and check it first when troubleshooting module-specific problems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export default function Tasks() {
|
||||
setShowContent(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, entityTypeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]);
|
||||
}, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
@@ -513,7 +513,7 @@ export default function Tasks() {
|
||||
setTypeFilter,
|
||||
setCurrentPage,
|
||||
});
|
||||
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter, entityTypeFilter]);
|
||||
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter]);
|
||||
|
||||
// Calculate header metrics
|
||||
const headerMetrics = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user