stage 4-2

This commit is contained in:
alorig
2025-11-20 04:00:51 +05:00
parent ec3ca2da5d
commit 584dce7b8e
13 changed files with 2424 additions and 15 deletions

View File

@@ -0,0 +1,445 @@
"""
Sync Health Service
Stage 4: Track sync health, mismatches, and logs
Provides health monitoring for site integrations.
"""
import logging
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from django.utils import timezone
from igny8_core.business.integration.models import SiteIntegration
logger = logging.getLogger(__name__)
class SyncHealthService:
"""
Service for tracking sync health and detecting mismatches.
"""
def get_sync_status(
self,
site_id: int,
integration_id: Optional[int] = None
) -> Dict[str, Any]:
"""
Get sync status for a site or specific integration.
Args:
site_id: Site ID
integration_id: Optional integration ID (if None, returns all integrations)
Returns:
dict: {
'site_id': int,
'integrations': [
{
'id': int,
'platform': str,
'status': str,
'last_sync_at': datetime,
'sync_enabled': bool,
'is_healthy': bool,
'error': str,
'mismatch_count': int
}
],
'overall_status': str, # 'healthy', 'warning', 'error'
'last_sync_at': datetime
}
"""
try:
integrations_query = SiteIntegration.objects.filter(
site_id=site_id,
is_active=True
)
if integration_id:
integrations_query = integrations_query.filter(id=integration_id)
integrations = []
overall_healthy = True
last_sync = None
for integration in integrations_query:
mismatch_count = self._count_mismatches(integration)
is_healthy = (
integration.sync_status == 'success' and
mismatch_count == 0 and
(not integration.sync_error or integration.sync_error == '')
)
if not is_healthy:
overall_healthy = False
if integration.last_sync_at:
if last_sync is None or integration.last_sync_at > last_sync:
last_sync = integration.last_sync_at
integrations.append({
'id': integration.id,
'platform': integration.platform,
'status': integration.sync_status,
'last_sync_at': integration.last_sync_at.isoformat() if integration.last_sync_at else None,
'sync_enabled': integration.sync_enabled,
'is_healthy': is_healthy,
'error': integration.sync_error,
'mismatch_count': mismatch_count
})
# Determine overall status
if overall_healthy:
overall_status = 'healthy'
elif any(i['status'] == 'failed' for i in integrations):
overall_status = 'error'
else:
overall_status = 'warning'
return {
'site_id': site_id,
'integrations': integrations,
'overall_status': overall_status,
'last_sync_at': last_sync.isoformat() if last_sync else None
}
except Exception as e:
logger.error(f"Error getting sync status: {e}", exc_info=True)
return {
'site_id': site_id,
'integrations': [],
'overall_status': 'error',
'last_sync_at': None,
'error': str(e)
}
def get_mismatches(
self,
site_id: int,
integration_id: Optional[int] = None
) -> Dict[str, Any]:
"""
Get detailed mismatch information.
Args:
site_id: Site ID
integration_id: Optional integration ID
Returns:
dict: {
'taxonomies': {
'missing_in_wordpress': List[Dict],
'missing_in_igny8': List[Dict],
'mismatched': List[Dict]
},
'products': {
'missing_in_wordpress': List[Dict],
'missing_in_igny8': List[Dict]
},
'posts': {
'missing_in_wordpress': List[Dict],
'missing_in_igny8': List[Dict]
}
}
"""
try:
integrations_query = SiteIntegration.objects.filter(
site_id=site_id,
is_active=True
)
if integration_id:
integrations_query = integrations_query.filter(id=integration_id)
all_mismatches = {
'taxonomies': {
'missing_in_wordpress': [],
'missing_in_igny8': [],
'mismatched': []
},
'products': {
'missing_in_wordpress': [],
'missing_in_igny8': []
},
'posts': {
'missing_in_wordpress': [],
'missing_in_igny8': []
}
}
for integration in integrations_query:
if integration.platform == 'wordpress':
mismatches = self._detect_wordpress_mismatches(integration)
# Merge mismatches
for key in all_mismatches:
if key in mismatches:
all_mismatches[key]['missing_in_wordpress'].extend(
mismatches[key].get('missing_in_wordpress', [])
)
all_mismatches[key]['missing_in_igny8'].extend(
mismatches[key].get('missing_in_igny8', [])
)
if 'mismatched' in mismatches[key]:
all_mismatches[key]['mismatched'].extend(
mismatches[key]['mismatched']
)
return all_mismatches
except Exception as e:
logger.error(f"Error getting mismatches: {e}", exc_info=True)
return {
'taxonomies': {'missing_in_wordpress': [], 'missing_in_igny8': [], 'mismatched': []},
'products': {'missing_in_wordpress': [], 'missing_in_igny8': []},
'posts': {'missing_in_wordpress': [], 'missing_in_igny8': []},
'error': str(e)
}
def get_sync_logs(
self,
site_id: int,
integration_id: Optional[int] = None,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
Get sync logs for a site or integration.
Args:
site_id: Site ID
integration_id: Optional integration ID
limit: Maximum number of logs to return
Returns:
List of log dictionaries
"""
try:
integrations_query = SiteIntegration.objects.filter(
site_id=site_id,
is_active=True
)
if integration_id:
integrations_query = integrations_query.filter(id=integration_id)
logs = []
for integration in integrations_query:
# Use SiteIntegration fields as log entries
if integration.last_sync_at:
logs.append({
'integration_id': integration.id,
'platform': integration.platform,
'timestamp': integration.last_sync_at.isoformat(),
'status': integration.sync_status,
'error': integration.sync_error,
'duration': None, # Not tracked in current model
'items_processed': None # Not tracked in current model
})
# Sort by timestamp descending
logs.sort(key=lambda x: x['timestamp'] or '', reverse=True)
return logs[:limit]
except Exception as e:
logger.error(f"Error getting sync logs: {e}", exc_info=True)
return []
def record_sync_run(
self,
integration_id: int,
result: Dict[str, Any]
) -> None:
"""
Record a sync run result.
Args:
integration_id: Integration ID
result: Sync result dict from ContentSyncService
"""
try:
integration = SiteIntegration.objects.get(id=integration_id)
if result.get('success'):
integration.sync_status = 'success'
integration.last_sync_at = timezone.now()
integration.sync_error = None
else:
integration.sync_status = 'failed'
integration.sync_error = result.get('error', 'Unknown error')
integration.save(update_fields=['sync_status', 'last_sync_at', 'sync_error', 'updated_at'])
logger.info(
f"[SyncHealthService] Recorded sync run for integration {integration_id}: "
f"status={integration.sync_status}, synced_count={result.get('synced_count', 0)}"
)
except SiteIntegration.DoesNotExist:
logger.warning(f"Integration {integration_id} not found for sync recording")
except Exception as e:
logger.error(f"Error recording sync run: {e}", exc_info=True)
def _count_mismatches(self, integration: SiteIntegration) -> int:
"""
Count total mismatches for an integration.
Args:
integration: SiteIntegration instance
Returns:
int: Total mismatch count
"""
try:
if integration.platform != 'wordpress':
return 0
mismatches = self._detect_wordpress_mismatches(integration)
count = 0
for category in mismatches.values():
count += len(category.get('missing_in_wordpress', []))
count += len(category.get('missing_in_igny8', []))
count += len(category.get('mismatched', []))
return count
except Exception as e:
logger.warning(f"Error counting mismatches: {e}")
return 0
def _detect_wordpress_mismatches(
self,
integration: SiteIntegration
) -> Dict[str, Any]:
"""
Detect mismatches between IGNY8 and WordPress.
Args:
integration: SiteIntegration instance
Returns:
dict: Mismatch details
"""
mismatches = {
'taxonomies': {
'missing_in_wordpress': [],
'missing_in_igny8': [],
'mismatched': []
},
'products': {
'missing_in_wordpress': [],
'missing_in_igny8': []
},
'posts': {
'missing_in_wordpress': [],
'missing_in_igny8': []
}
}
try:
from igny8_core.utils.wordpress import WordPressClient
from igny8_core.business.site_building.models import SiteBlueprint, SiteBlueprintTaxonomy
from igny8_core.business.content.models import Content
credentials = integration.get_credentials()
client = WordPressClient(
site_url=integration.config_json.get('site_url', ''),
username=credentials.get('username'),
app_password=credentials.get('app_password')
)
# Get site blueprint
blueprint = SiteBlueprint.objects.filter(
account=integration.account,
site=integration.site
).first()
if not blueprint:
return mismatches
# Check taxonomy mismatches
# Get IGNY8 taxonomies
igny8_taxonomies = SiteBlueprintTaxonomy.objects.filter(
site_blueprint=blueprint
)
# Get WordPress categories
wp_categories = client.get_categories(per_page=100)
wp_category_ids = {str(cat['id']): cat for cat in wp_categories}
# Get WordPress tags
wp_tags = client.get_tags(per_page=100)
wp_tag_ids = {str(tag['id']): tag for tag in wp_tags}
for taxonomy in igny8_taxonomies:
if taxonomy.external_reference:
# Check if still exists in WordPress
if taxonomy.taxonomy_type in ['blog_category', 'product_category']:
if taxonomy.external_reference not in wp_category_ids:
mismatches['taxonomies']['missing_in_wordpress'].append({
'id': taxonomy.id,
'name': taxonomy.name,
'type': taxonomy.taxonomy_type,
'external_reference': taxonomy.external_reference
})
elif taxonomy.taxonomy_type in ['blog_tag', 'product_tag']:
if taxonomy.external_reference not in wp_tag_ids:
mismatches['taxonomies']['missing_in_wordpress'].append({
'id': taxonomy.id,
'name': taxonomy.name,
'type': taxonomy.taxonomy_type,
'external_reference': taxonomy.external_reference
})
else:
# Taxonomy exists in IGNY8 but not synced to WordPress
mismatches['taxonomies']['missing_in_wordpress'].append({
'id': taxonomy.id,
'name': taxonomy.name,
'type': taxonomy.taxonomy_type
})
# Check for WordPress taxonomies not in IGNY8
for cat in wp_categories:
if not SiteBlueprintTaxonomy.objects.filter(
site_blueprint=blueprint,
external_reference=str(cat['id'])
).exists():
mismatches['taxonomies']['missing_in_igny8'].append({
'name': cat['name'],
'slug': cat['slug'],
'type': 'blog_category',
'external_reference': str(cat['id'])
})
for tag in wp_tags:
if not SiteBlueprintTaxonomy.objects.filter(
site_blueprint=blueprint,
external_reference=str(tag['id'])
).exists():
mismatches['taxonomies']['missing_in_igny8'].append({
'name': tag['name'],
'slug': tag['slug'],
'type': 'blog_tag',
'external_reference': str(tag['id'])
})
# Check content mismatches (basic check)
igny8_content = Content.objects.filter(
account=integration.account,
site=integration.site,
source='igny8',
status='publish'
)
for content in igny8_content[:50]: # Limit check
if content.metadata and content.metadata.get('wordpress_id'):
# Content should exist in WordPress (would need to check)
# For now, just note if metadata exists
pass
else:
# Content not synced to WordPress
mismatches['posts']['missing_in_wordpress'].append({
'id': content.id,
'title': content.title,
'type': content.content_type
})
except Exception as e:
logger.warning(f"Error detecting WordPress mismatches: {e}")
return mismatches

View File

@@ -26,6 +26,7 @@ class SyncService:
) -> Dict[str, Any]:
"""
Perform synchronization.
Stage 4: Enhanced to record sync runs for health tracking.
Args:
integration: SiteIntegration instance
@@ -74,13 +75,23 @@ class SyncService:
total_synced = to_result.get('synced_count', 0) + from_result.get('synced_count', 0)
return {
result = {
'success': to_result.get('success') and from_result.get('success'),
'synced_count': total_synced,
'to_external': to_result,
'from_external': from_result
}
# Stage 4: Record sync run for health tracking
try:
from igny8_core.business.integration.services.sync_health_service import SyncHealthService
sync_health_service = SyncHealthService()
sync_health_service.record_sync_run(integration.id, result)
except Exception as e:
logger.warning(f"Failed to record sync run: {e}")
return result
except Exception as e:
logger.error(
f"[SyncService] Error syncing integration {integration.id}: {str(e)}",
@@ -91,11 +102,21 @@ class SyncService:
integration.sync_error = str(e)
integration.save(update_fields=['sync_status', 'sync_error', 'updated_at'])
return {
error_result = {
'success': False,
'error': str(e),
'synced_count': 0
}
# Stage 4: Record failed sync run
try:
from igny8_core.business.integration.services.sync_health_service import SyncHealthService
sync_health_service = SyncHealthService()
sync_health_service.record_sync_run(integration.id, error_result)
except Exception as e:
logger.warning(f"Failed to record sync run: {e}")
return error_result
def _sync_to_external(
self,

View File

@@ -1,13 +1,14 @@
"""
Sites Renderer Adapter
Phase 5: Sites Renderer & Publishing
Stage 4: Enhanced with Stage 3 metadata (clusters, taxonomies, internal links)
Adapter for deploying sites to IGNY8 Sites renderer.
"""
import logging
import json
import os
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, List
from pathlib import Path
from datetime import datetime
@@ -106,6 +107,7 @@ class SitesRendererAdapter(BaseAdapter):
"""
Build site definition JSON from blueprint.
Merges actual Content from Writer into PageBlueprint blocks.
Stage 4: Enhanced with Stage 3 metadata (clusters, taxonomies, internal links).
Args:
site_blueprint: SiteBlueprint instance
@@ -113,13 +115,24 @@ class SitesRendererAdapter(BaseAdapter):
Returns:
dict: Site definition structure
"""
from igny8_core.business.content.models import Tasks, Content
from igny8_core.business.content.models import Tasks, Content, ContentClusterMap, ContentTaxonomyMap
# Get all pages
pages = []
content_id_to_page = {} # Map content IDs to pages for metadata lookup
for page in site_blueprint.pages.all().order_by('order'):
# Get blocks from blueprint (placeholders)
blocks = page.blocks_json or []
page_metadata = {
'entity_type': page.entity_type if hasattr(page, 'entity_type') else None,
'cluster_id': None,
'cluster_name': None,
'cluster_role': None,
'taxonomy_id': None,
'taxonomy_name': None,
'internal_links': []
}
# Try to find actual Content from Writer
# PageBlueprint -> Task (by title pattern) -> Content
@@ -155,6 +168,43 @@ class SitesRendererAdapter(BaseAdapter):
logger.info(
f"[SitesRendererAdapter] Converted HTML content to text block for page {page.slug}"
)
# Stage 4: Add Stage 3 metadata if content exists
if content:
content_id_to_page[content.id] = page.slug
# Get cluster mapping
cluster_map = ContentClusterMap.objects.filter(content=content).first()
if cluster_map and cluster_map.cluster:
page_metadata['cluster_id'] = cluster_map.cluster.id
page_metadata['cluster_name'] = cluster_map.cluster.name
page_metadata['cluster_role'] = cluster_map.role or task.cluster_role if task else None
# Get taxonomy mapping
taxonomy_map = ContentTaxonomyMap.objects.filter(content=content).first()
if taxonomy_map and taxonomy_map.taxonomy:
page_metadata['taxonomy_id'] = taxonomy_map.taxonomy.id
page_metadata['taxonomy_name'] = taxonomy_map.taxonomy.name
# Get internal links from content
if content.internal_links:
page_metadata['internal_links'] = content.internal_links
# Use content entity_type if available
if content.entity_type:
page_metadata['entity_type'] = content.entity_type
# Fallback to task metadata if content not found
if task and not page_metadata.get('cluster_id'):
if task.cluster:
page_metadata['cluster_id'] = task.cluster.id
page_metadata['cluster_name'] = task.cluster.name
page_metadata['cluster_role'] = task.cluster_role
if task.taxonomy:
page_metadata['taxonomy_id'] = task.taxonomy.id
page_metadata['taxonomy_name'] = task.taxonomy.name
if task.entity_type:
page_metadata['entity_type'] = task.entity_type
pages.append({
'id': page.id,
@@ -163,8 +213,15 @@ class SitesRendererAdapter(BaseAdapter):
'type': page.type,
'blocks': blocks,
'status': page.status,
'metadata': page_metadata, # Stage 4: Add metadata
})
# Stage 4: Build navigation with cluster grouping
navigation = self._build_navigation_with_metadata(site_blueprint, pages)
# Stage 4: Build taxonomy tree for breadcrumbs
taxonomy_tree = self._build_taxonomy_tree(site_blueprint)
# Build site definition
definition = {
'id': site_blueprint.id,
@@ -173,7 +230,8 @@ class SitesRendererAdapter(BaseAdapter):
'version': site_blueprint.version,
'layout': site_blueprint.structure_json.get('layout', 'default'),
'theme': site_blueprint.structure_json.get('theme', {}),
'navigation': site_blueprint.structure_json.get('navigation', []),
'navigation': navigation, # Stage 4: Enhanced navigation
'taxonomy_tree': taxonomy_tree, # Stage 4: Taxonomy tree for breadcrumbs
'pages': pages,
'config': site_blueprint.config_json,
'created_at': site_blueprint.created_at.isoformat(),
@@ -182,6 +240,116 @@ class SitesRendererAdapter(BaseAdapter):
return definition
def _build_navigation_with_metadata(
self,
site_blueprint: SiteBlueprint,
pages: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Build navigation structure with cluster grouping.
Stage 4: Groups pages by cluster for better navigation.
Args:
site_blueprint: SiteBlueprint instance
pages: List of page dictionaries
Returns:
List of navigation items
"""
# If explicit navigation exists in structure_json, use it
explicit_nav = site_blueprint.structure_json.get('navigation', [])
if explicit_nav:
return explicit_nav
# Otherwise, build navigation from pages grouped by cluster
navigation = []
# Group pages by cluster
cluster_groups = {}
ungrouped_pages = []
for page in pages:
if page.get('status') in ['published', 'ready']:
cluster_id = page.get('metadata', {}).get('cluster_id')
if cluster_id:
if cluster_id not in cluster_groups:
cluster_groups[cluster_id] = {
'cluster_id': cluster_id,
'cluster_name': page.get('metadata', {}).get('cluster_name', 'Unknown'),
'pages': []
}
cluster_groups[cluster_id]['pages'].append({
'slug': page['slug'],
'title': page['title'],
'type': page['type']
})
else:
ungrouped_pages.append({
'slug': page['slug'],
'title': page['title'],
'type': page['type']
})
# Add cluster groups to navigation
for cluster_group in cluster_groups.values():
navigation.append({
'type': 'cluster',
'name': cluster_group['cluster_name'],
'items': cluster_group['pages']
})
# Add ungrouped pages
if ungrouped_pages:
navigation.extend(ungrouped_pages)
return navigation if navigation else [
{'slug': page['slug'], 'title': page['title']}
for page in pages
if page.get('status') in ['published', 'ready']
]
def _build_taxonomy_tree(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Build taxonomy tree structure for breadcrumbs.
Stage 4: Creates hierarchical taxonomy structure.
Args:
site_blueprint: SiteBlueprint instance
Returns:
dict: Taxonomy tree structure
"""
taxonomies = site_blueprint.taxonomies.all()
tree = {
'categories': [],
'tags': [],
'product_categories': [],
'product_attributes': []
}
for taxonomy in taxonomies:
taxonomy_item = {
'id': taxonomy.id,
'name': taxonomy.name,
'slug': taxonomy.slug,
'type': taxonomy.taxonomy_type,
'description': taxonomy.description
}
if taxonomy.taxonomy_type in ['blog_category', 'product_category']:
category_key = 'product_categories' if 'product' in taxonomy.taxonomy_type else 'categories'
tree[category_key].append(taxonomy_item)
elif taxonomy.taxonomy_type in ['blog_tag', 'product_tag']:
tag_key = 'product_tags' if 'product' in taxonomy.taxonomy_type else 'tags'
if tag_key not in tree:
tree[tag_key] = []
tree[tag_key].append(taxonomy_item)
elif taxonomy.taxonomy_type == 'product_attribute':
tree['product_attributes'].append(taxonomy_item)
return tree
def _write_site_definition(
self,
site_blueprint: SiteBlueprint,

View File

@@ -0,0 +1,422 @@
"""
Deployment Readiness Service
Stage 4: Checks if site blueprint is ready for deployment
Validates cluster coverage, content validation, sync status, and taxonomy completeness.
"""
import logging
from typing import Dict, Any, List
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.content.services.validation_service import ContentValidationService
from igny8_core.business.integration.services.sync_health_service import SyncHealthService
logger = logging.getLogger(__name__)
class DeploymentReadinessService:
"""
Service for checking deployment readiness.
"""
def __init__(self):
self.validation_service = ContentValidationService()
self.sync_health_service = SyncHealthService()
def check_readiness(self, site_blueprint_id: int) -> Dict[str, Any]:
"""
Check if site blueprint is ready for deployment.
Args:
site_blueprint_id: SiteBlueprint ID
Returns:
dict: {
'ready': bool,
'checks': {
'cluster_coverage': bool,
'content_validation': bool,
'sync_status': bool,
'taxonomy_completeness': bool
},
'errors': List[str],
'warnings': List[str],
'details': {
'cluster_coverage': dict,
'content_validation': dict,
'sync_status': dict,
'taxonomy_completeness': dict
}
}
"""
try:
blueprint = SiteBlueprint.objects.get(id=site_blueprint_id)
except SiteBlueprint.DoesNotExist:
return {
'ready': False,
'checks': {},
'errors': [f'SiteBlueprint {site_blueprint_id} not found'],
'warnings': [],
'details': {}
}
checks = {}
errors = []
warnings = []
details = {}
# Check 1: Cluster Coverage
cluster_check = self._check_cluster_coverage(blueprint)
checks['cluster_coverage'] = cluster_check['ready']
details['cluster_coverage'] = cluster_check
if not cluster_check['ready']:
errors.extend(cluster_check.get('errors', []))
if cluster_check.get('warnings'):
warnings.extend(cluster_check['warnings'])
# Check 2: Content Validation
content_check = self._check_content_validation(blueprint)
checks['content_validation'] = content_check['ready']
details['content_validation'] = content_check
if not content_check['ready']:
errors.extend(content_check.get('errors', []))
if content_check.get('warnings'):
warnings.extend(content_check['warnings'])
# Check 3: Sync Status (if WordPress integration exists)
sync_check = self._check_sync_status(blueprint)
checks['sync_status'] = sync_check['ready']
details['sync_status'] = sync_check
if not sync_check['ready']:
warnings.extend(sync_check.get('warnings', []))
if sync_check.get('errors'):
errors.extend(sync_check['errors'])
# Check 4: Taxonomy Completeness
taxonomy_check = self._check_taxonomy_completeness(blueprint)
checks['taxonomy_completeness'] = taxonomy_check['ready']
details['taxonomy_completeness'] = taxonomy_check
if not taxonomy_check['ready']:
warnings.extend(taxonomy_check.get('warnings', []))
if taxonomy_check.get('errors'):
errors.extend(taxonomy_check['errors'])
# Overall readiness: all critical checks must pass
ready = (
checks.get('cluster_coverage', False) and
checks.get('content_validation', False)
)
return {
'ready': ready,
'checks': checks,
'errors': errors,
'warnings': warnings,
'details': details
}
def _check_cluster_coverage(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Check if all clusters have required coverage.
Returns:
dict: {
'ready': bool,
'total_clusters': int,
'covered_clusters': int,
'incomplete_clusters': List[Dict],
'errors': List[str],
'warnings': List[str]
}
"""
try:
cluster_links = blueprint.cluster_links.all()
total_clusters = cluster_links.count()
if total_clusters == 0:
return {
'ready': False,
'total_clusters': 0,
'covered_clusters': 0,
'incomplete_clusters': [],
'errors': ['No clusters attached to blueprint'],
'warnings': []
}
incomplete_clusters = []
covered_count = 0
for cluster_link in cluster_links:
if cluster_link.coverage_status == 'complete':
covered_count += 1
else:
incomplete_clusters.append({
'cluster_id': cluster_link.cluster_id,
'cluster_name': getattr(cluster_link.cluster, 'name', 'Unknown'),
'status': cluster_link.coverage_status,
'role': cluster_link.role
})
ready = covered_count == total_clusters
errors = []
warnings = []
if not ready:
if covered_count == 0:
errors.append('No clusters have complete coverage')
else:
warnings.append(
f'{total_clusters - covered_count} of {total_clusters} clusters need coverage'
)
return {
'ready': ready,
'total_clusters': total_clusters,
'covered_clusters': covered_count,
'incomplete_clusters': incomplete_clusters,
'errors': errors,
'warnings': warnings
}
except Exception as e:
logger.error(f"Error checking cluster coverage: {e}", exc_info=True)
return {
'ready': False,
'total_clusters': 0,
'covered_clusters': 0,
'incomplete_clusters': [],
'errors': [f'Error checking cluster coverage: {str(e)}'],
'warnings': []
}
def _check_content_validation(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Check if all published content passes validation.
Returns:
dict: {
'ready': bool,
'total_content': int,
'valid_content': int,
'invalid_content': List[Dict],
'errors': List[str],
'warnings': List[str]
}
"""
try:
from igny8_core.business.content.models import Content, Tasks
# Get all content associated with this blueprint
# Content is linked via Tasks -> PageBlueprint -> SiteBlueprint
page_ids = blueprint.pages.values_list('id', flat=True)
# Find tasks that match page blueprints
tasks = Tasks.objects.filter(
account=blueprint.account,
site=blueprint.site,
sector=blueprint.sector
)
# Filter tasks that might be related to this blueprint
# (This is a simplified check - in practice, tasks should have blueprint reference)
content_items = Content.objects.filter(
task__in=tasks,
status='publish',
source='igny8'
)
total_content = content_items.count()
if total_content == 0:
return {
'ready': True, # No content to validate is OK
'total_content': 0,
'valid_content': 0,
'invalid_content': [],
'errors': [],
'warnings': ['No published content found for validation']
}
invalid_content = []
valid_count = 0
for content in content_items:
errors = self.validation_service.validate_for_publish(content)
if errors:
invalid_content.append({
'content_id': content.id,
'title': content.title or 'Untitled',
'errors': errors
})
else:
valid_count += 1
ready = len(invalid_content) == 0
errors = []
warnings = []
if not ready:
errors.append(
f'{len(invalid_content)} of {total_content} content items have validation errors'
)
return {
'ready': ready,
'total_content': total_content,
'valid_content': valid_count,
'invalid_content': invalid_content,
'errors': errors,
'warnings': warnings
}
except Exception as e:
logger.error(f"Error checking content validation: {e}", exc_info=True)
return {
'ready': False,
'total_content': 0,
'valid_content': 0,
'invalid_content': [],
'errors': [f'Error checking content validation: {str(e)}'],
'warnings': []
}
def _check_sync_status(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Check sync status for WordPress integrations.
Returns:
dict: {
'ready': bool,
'has_integration': bool,
'sync_status': str,
'mismatch_count': int,
'errors': List[str],
'warnings': List[str]
}
"""
try:
from igny8_core.business.integration.models import SiteIntegration
integrations = SiteIntegration.objects.filter(
site=blueprint.site,
is_active=True,
platform='wordpress'
)
if not integrations.exists():
return {
'ready': True, # No WordPress integration is OK
'has_integration': False,
'sync_status': None,
'mismatch_count': 0,
'errors': [],
'warnings': []
}
# Get sync status from SyncHealthService
sync_status = self.sync_health_service.get_sync_status(blueprint.site.id)
overall_status = sync_status.get('overall_status', 'error')
is_healthy = overall_status == 'healthy'
# Count total mismatches
mismatch_count = sum(
i.get('mismatch_count', 0) for i in sync_status.get('integrations', [])
)
errors = []
warnings = []
if not is_healthy:
if overall_status == 'error':
errors.append('WordPress sync has errors')
else:
warnings.append('WordPress sync has warnings')
if mismatch_count > 0:
warnings.append(f'{mismatch_count} sync mismatches detected')
# Sync status doesn't block deployment, but should be warned
return {
'ready': True, # Sync issues are warnings, not blockers
'has_integration': True,
'sync_status': overall_status,
'mismatch_count': mismatch_count,
'errors': errors,
'warnings': warnings
}
except Exception as e:
logger.error(f"Error checking sync status: {e}", exc_info=True)
return {
'ready': True, # Don't block on sync check errors
'has_integration': False,
'sync_status': None,
'mismatch_count': 0,
'errors': [],
'warnings': [f'Could not check sync status: {str(e)}']
}
def _check_taxonomy_completeness(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Check if taxonomies are complete for the site type.
Returns:
dict: {
'ready': bool,
'total_taxonomies': int,
'required_taxonomies': List[str],
'missing_taxonomies': List[str],
'errors': List[str],
'warnings': List[str]
}
"""
try:
taxonomies = blueprint.taxonomies.all()
total_taxonomies = taxonomies.count()
# Determine required taxonomies based on site type
site_type = blueprint.site.site_type if hasattr(blueprint.site, 'site_type') else None
required_types = []
if site_type == 'blog':
required_types = ['blog_category', 'blog_tag']
elif site_type == 'ecommerce':
required_types = ['product_category', 'product_tag', 'product_attribute']
elif site_type == 'company':
required_types = ['service_category']
existing_types = set(taxonomies.values_list('taxonomy_type', flat=True))
missing_types = set(required_types) - existing_types
ready = len(missing_types) == 0
errors = []
warnings = []
if not ready:
warnings.append(
f'Missing required taxonomies for {site_type} site: {", ".join(missing_types)}'
)
if total_taxonomies == 0:
warnings.append('No taxonomies defined')
return {
'ready': ready,
'total_taxonomies': total_taxonomies,
'required_taxonomies': required_types,
'missing_taxonomies': list(missing_types),
'errors': errors,
'warnings': warnings
}
except Exception as e:
logger.error(f"Error checking taxonomy completeness: {e}", exc_info=True)
return {
'ready': True, # Don't block on taxonomy check errors
'total_taxonomies': 0,
'required_taxonomies': [],
'missing_taxonomies': [],
'errors': [],
'warnings': [f'Could not check taxonomy completeness: {str(e)}']
}