be fe fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-26 10:43:51 +00:00
parent 4fe68cc271
commit 1cbc347cdc
15 changed files with 5557 additions and 149 deletions

View 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.

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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] = {

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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" },
],
},
{

View 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>
</>
);
}

View File

@@ -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(() => {