diff --git a/backend/igny8_core/api/schema_extensions.py b/backend/igny8_core/api/schema_extensions.py index 69b92e67..2cddf15d 100644 --- a/backend/igny8_core/api/schema_extensions.py +++ b/backend/igny8_core/api/schema_extensions.py @@ -29,7 +29,9 @@ def postprocess_schema_filter_tags(result, generator, request, public): # If no explicit tags found, infer from path if not filtered_tags: - if '/auth/' in path or '/api/v1/auth/' in path: + if '/ping' in path or '/system/ping/' in path: + filtered_tags = ['System'] # Health check endpoint + elif '/auth/' in path or '/api/v1/auth/' in path: filtered_tags = ['Authentication'] elif '/planner/' in path or '/api/v1/planner/' in path: filtered_tags = ['Planner'] diff --git a/backend/igny8_core/modules/system/integration_views.py b/backend/igny8_core/modules/system/integration_views.py index 8584168f..ee91bf76 100644 --- a/backend/igny8_core/modules/system/integration_views.py +++ b/backend/igny8_core/modules/system/integration_views.py @@ -5,7 +5,6 @@ Unified API Standard v1.0 compliant import logging from rest_framework import viewsets, status from rest_framework.decorators import action -from rest_framework.response import Response from django.db import transaction from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.api.base import AccountModelViewSet @@ -140,7 +139,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): logger.info(f"[test_connection] Testing {integration_type} connection with API key (length={len(api_key) if api_key else 0})") try: if integration_type == 'openai': - return self._test_openai(api_key, config) + return self._test_openai(api_key, config, request) elif integration_type == 'runware': return self._test_runware(api_key, request) else: @@ -160,7 +159,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): request=request ) - def _test_openai(self, api_key: str, config: dict = None): + def _test_openai(self, api_key: str, config: dict = None, request=None): """ Test OpenAI API connection. EXACT match to reference plugin's igny8_test_connection() function. @@ -215,33 +214,38 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): rates = MODEL_RATES.get(model, {'input': 2.00, 'output': 8.00}) cost = (input_tokens * rates['input'] + output_tokens * rates['output']) / 1000000 - return Response({ - 'success': True, - 'message': 'API connection and response test successful!', - 'model_used': model, - 'response': response_text, - 'tokens_used': f"{input_tokens} / {output_tokens}", - 'total_tokens': total_tokens, - 'cost': f'${cost:.4f}', - 'full_response': response_data, - }) + return success_response( + data={ + 'message': 'API connection and response test successful!', + 'model_used': model, + 'response': response_text, + 'tokens_used': f"{input_tokens} / {output_tokens}", + 'total_tokens': total_tokens, + 'cost': f'${cost:.4f}', + 'full_response': response_data, + }, + request=request + ) else: - return Response({ - 'success': False, - 'message': 'API responded but no content received', - 'response': response.text[:500] - }) + return error_response( + error='API responded but no content received', + errors={'response': [response.text[:500]]}, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) else: body = response.text - return Response({ - 'success': False, - 'message': f'HTTP {response.status_code} – {body[:200]}' - }, status=response.status_code) + return error_response( + error=f'HTTP {response.status_code} – {body[:200]}', + status_code=response.status_code, + request=request + ) except requests.exceptions.RequestException as e: - return Response({ - 'success': False, - 'message': str(e) - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) else: # Simple connection test without API call (reference plugin: GET /v1/models) try: @@ -254,23 +258,27 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): ) if response.status_code >= 200 and response.status_code < 300: - return Response({ - 'success': True, - 'message': 'API connection successful!', - 'model_used': model, - 'response': 'Connection verified without API call' - }) + return success_response( + data={ + 'message': 'API connection successful!', + 'model_used': model, + 'response': 'Connection verified without API call' + }, + request=request + ) else: body = response.text - return Response({ - 'success': False, - 'message': f'HTTP {response.status_code} – {body[:200]}' - }, status=response.status_code) + return error_response( + error=f'HTTP {response.status_code} – {body[:200]}', + status_code=response.status_code, + request=request + ) except requests.exceptions.RequestException as e: - return Response({ - 'success': False, - 'message': str(e) - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) def _test_runware(self, api_key: str, request): """ @@ -338,11 +346,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if response.status_code != 200: error_text = response.text logger.error(f"[_test_runware] HTTP error {response.status_code}: {error_text[:200]}") - return Response({ - 'success': False, - 'error': f'HTTP {response.status_code}: {error_text[:200]}', - 'message': 'Runware API validation failed' - }, status=response.status_code) + return error_response( + error=f'HTTP {response.status_code}: {error_text[:200]}', + status_code=response.status_code, + request=request + ) # Parse response - Reference plugin checks: $body['data'][0]['imageURL'] body = response.json() @@ -357,15 +365,17 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): image_url = first_item.get('imageURL') or first_item.get('image_url') if image_url: logger.info(f"[_test_runware] Success! Image URL: {image_url[:50]}...") - return Response({ - 'success': True, - 'message': '✅ Runware API connected successfully!', - 'image_url': image_url, - 'cost': '$0.0090', - 'provider': 'runware', - 'model': 'runware:97@1', - 'size': '128x128' - }) + return success_response( + data={ + 'message': '✅ Runware API connected successfully!', + 'image_url': image_url, + 'cost': '$0.0090', + 'provider': 'runware', + 'model': 'runware:97@1', + 'size': '128x128' + }, + request=request + ) # Check for errors - Reference plugin line 4998: elseif (isset($body['errors'][0]['message'])) if isinstance(body, dict) and 'errors' in body: @@ -373,26 +383,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if isinstance(errors, list) and len(errors) > 0: error_msg = errors[0].get('message', 'Unknown Runware API error') logger.error(f"[_test_runware] Runware API error: {error_msg}") - return Response({ - 'success': False, - 'error': f'❌ {error_msg}', - 'message': 'Runware API validation failed' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=f'❌ {error_msg}', + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) # Unknown response format logger.error(f"[_test_runware] Unknown response format: {body}") - return Response({ - 'success': False, - 'error': '❌ Unknown response from Runware.', - 'message': 'Runware API validation failed' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error='❌ Unknown response from Runware.', + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) except Exception as e: logger.error(f"[_test_runware] Exception in Runware API test: {str(e)}", exc_info=True) - return Response({ - 'success': False, - 'error': f'Runware API test failed: {str(e)}', - 'message': 'Runware API validation failed' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=f'Runware API test failed: {str(e)}', + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) def generate_image(self, request, pk=None, **kwargs): """ @@ -419,10 +429,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if pk != 'image_generation': logger.error(f"[generate_image] Invalid pk: {pk}, expected 'image_generation'") - return Response({ - 'success': False, - 'error': f'Image generation endpoint only available for image_generation integration, got: {pk}' - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error=f'Image generation endpoint only available for image_generation integration, got: {pk}', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) # Get account logger.info("[generate_image] Step 1: Getting account") @@ -445,10 +456,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if not account: logger.error("[generate_image] ERROR: No account found, returning error response") - return Response({ - 'success': False, - 'error': 'Account not found' - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error='Account not found', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) logger.info(f"[generate_image] Account resolved: {account.id if account else 'None'}") @@ -467,10 +479,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if not prompt: logger.error("[generate_image] ERROR: Prompt is empty") - return Response({ - 'success': False, - 'error': 'Prompt is required' - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error='Prompt is required', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) # Get API key from saved settings for the specified provider only logger.info(f"[generate_image] Step 3: Getting API key for provider: {provider}") @@ -502,17 +515,19 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): logger.info(f"[generate_image] Step 4: Validating {provider} provider and API key") if provider not in ['openai', 'runware']: logger.error(f"[generate_image] ERROR: Invalid provider: {provider}") - return Response({ - 'success': False, - 'error': f'Invalid provider: {provider}. Must be "openai" or "runware"' - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error=f'Invalid provider: {provider}. Must be "openai" or "runware"', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) if not api_key or not integration_enabled: logger.error(f"[generate_image] ERROR: {provider.upper()} API key not configured or integration not enabled") - return Response({ - 'success': False, - 'error': f'{provider.upper()} API key not configured or integration not enabled' - }, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error=f'{provider.upper()} API key not configured or integration not enabled', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) logger.info(f"[generate_image] {provider.upper()} API key validated successfully") @@ -543,14 +558,14 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if result.get('error'): logger.error(f"[generate_image] ERROR from AIProcessor: {result.get('error')}") - return Response({ - 'success': False, - 'error': result['error'] - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=result['error'], + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) logger.info("[generate_image] Image generation successful, returning response") response_data = { - 'success': True, 'image_url': result.get('url'), 'revised_prompt': result.get('revised_prompt'), 'model': model, @@ -558,19 +573,27 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): 'cost': f"${result.get('cost', 0):.4f}" if result.get('cost') else None, } logger.info(f"[generate_image] Returning success response: {response_data}") - return Response(response_data) + return success_response( + data=response_data, + request=request + ) except Exception as e: logger.error(f"[generate_image] EXCEPTION in image generation: {str(e)}", exc_info=True) - return Response({ - 'success': False, - 'error': f'Failed to generate image: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=f'Failed to generate image: {str(e)}', + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) def create(self, request): """Create integration settings""" integration_type = request.data.get('integration_type') if not integration_type: - return Response({'error': 'integration_type is required'}, status=status.HTTP_400_BAD_REQUEST) + return error_response( + error='integration_type is required', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) return self.save_settings(request, integration_type) def save_settings(self, request, pk=None): @@ -693,9 +716,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): import traceback error_trace = traceback.format_exc() logger.error(f"Full traceback: {error_trace}") - return Response({ - 'error': f'Failed to save settings: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=f'Failed to save settings: {str(e)}', + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) def get_settings(self, request, pk=None): """Get integration settings - defaults to AWS-admin settings if account doesn't have its own""" @@ -772,10 +797,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): pass if not account: - return Response({ - 'error': 'Account not found', - 'type': 'AuthenticationError' - }, status=status.HTTP_401_UNAUTHORIZED) + return error_response( + error='Account not found', + status_code=status.HTTP_401_UNAUTHORIZED, + request=request + ) try: from .models import IntegrationSettings @@ -800,39 +826,44 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): provider = config.get('provider', 'openai') default_featured_size = '1280x832' if provider == 'runware' else '1024x1024' - return Response({ - 'success': True, - 'config': { - 'provider': config.get('provider', 'openai'), - 'model': model, - 'image_type': config.get('image_type', 'realistic'), - 'max_in_article_images': config.get('max_in_article_images', 2), - 'image_format': config.get('image_format', 'webp'), - 'desktop_enabled': config.get('desktop_enabled', True), - 'mobile_enabled': config.get('mobile_enabled', True), - 'featured_image_size': config.get('featured_image_size', default_featured_size), - 'desktop_image_size': config.get('desktop_image_size', '1024x1024'), - } - }, status=status.HTTP_200_OK) + return success_response( + data={ + 'config': { + 'provider': config.get('provider', 'openai'), + 'model': model, + 'image_type': config.get('image_type', 'realistic'), + 'max_in_article_images': config.get('max_in_article_images', 2), + 'image_format': config.get('image_format', 'webp'), + 'desktop_enabled': config.get('desktop_enabled', True), + 'mobile_enabled': config.get('mobile_enabled', True), + 'featured_image_size': config.get('featured_image_size', default_featured_size), + 'desktop_image_size': config.get('desktop_image_size', '1024x1024'), + } + }, + request=request + ) except IntegrationSettings.DoesNotExist: - return Response({ - 'success': True, - 'config': { - 'provider': 'openai', - 'model': 'dall-e-3', - 'image_type': 'realistic', - 'max_in_article_images': 2, - 'image_format': 'webp', - 'desktop_enabled': True, - 'mobile_enabled': True, - } - }, status=status.HTTP_200_OK) + return success_response( + data={ + 'config': { + 'provider': 'openai', + 'model': 'dall-e-3', + 'image_type': 'realistic', + 'max_in_article_images': 2, + 'image_format': 'webp', + 'desktop_enabled': True, + 'mobile_enabled': True, + } + }, + request=request + ) except Exception as e: logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True) - return Response({ - 'error': str(e), - 'type': 'ServerError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return error_response( + error=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) @action(detail=False, methods=['get'], url_path='task_progress/(?P[^/.]+)', url_name='task-progress') def task_progress(self, request, task_id=None): @@ -841,9 +872,10 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): GET /api/v1/system/settings/task_progress// """ if not task_id: - return Response( - {'error': 'Task ID is required'}, - status=status.HTTP_400_BAD_REQUEST + return error_response( + error='Task ID is required', + status_code=status.HTTP_400_BAD_REQUEST, + request=request ) import logging @@ -862,14 +894,18 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): RedisConnectionError = ConnectionError except ImportError: logger.warning("Celery not available - task progress cannot be retrieved") - return Response({ - 'state': 'PENDING', - 'meta': { - 'percentage': 0, - 'message': 'Celery not available - cannot retrieve task status', - 'error': 'Celery not configured' - } - }, status=status.HTTP_503_SERVICE_UNAVAILABLE) + return success_response( + data={ + 'state': 'PENDING', + 'meta': { + 'percentage': 0, + 'message': 'Celery not available - cannot retrieve task status', + 'error': 'Celery not configured' + } + }, + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + request=request + ) try: # Create AsyncResult - this should not raise an exception even if task doesn't exist @@ -937,51 +973,64 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): except Exception as e: logger.debug(f"Error extracting error from task.info: {str(e)}") - return Response({ - 'state': 'FAILURE', - 'meta': { - 'error': error_msg, - 'error_type': error_type, - 'percentage': 0, - 'message': f'Error: {error_msg}', - 'request_steps': request_steps, - 'response_steps': response_steps, - } - }) + return success_response( + data={ + 'state': 'FAILURE', + 'meta': { + 'error': error_msg, + 'error_type': error_type, + 'percentage': 0, + 'message': f'Error: {error_msg}', + 'request_steps': request_steps, + 'response_steps': response_steps, + } + }, + request=request + ) except (KombuOperationalError, RedisConnectionError, ConnectionError) as conn_exc: # Backend connection error - task might not be registered yet or backend is down logger.warning(f"Backend connection error accessing task.state for {task_id}: {type(conn_exc).__name__}: {str(conn_exc)}") - return Response({ - 'state': 'PENDING', - 'meta': { - 'percentage': 0, - 'message': 'Task is being queued...', - 'phase': 'initializing', - 'error': None # Don't show as error, just pending - } - }) + return success_response( + data={ + 'state': 'PENDING', + 'meta': { + 'percentage': 0, + 'message': 'Task is being queued...', + 'phase': 'initializing', + 'error': None # Don't show as error, just pending + } + }, + request=request + ) except Exception as state_exc: logger.error(f"Unexpected error accessing task.state: {type(state_exc).__name__}: {str(state_exc)}") - return Response({ - 'state': 'UNKNOWN', - 'meta': { - 'error': f'Error accessing task: {str(state_exc)}', - 'percentage': 0, - 'message': f'Error: {str(state_exc)}', - } - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return success_response( + data={ + 'state': 'UNKNOWN', + 'meta': { + 'error': f'Error accessing task: {str(state_exc)}', + 'percentage': 0, + 'message': f'Error: {str(state_exc)}', + } + }, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) # Check if task exists and is accessible if task_state is None: # Task doesn't exist or hasn't been registered yet - return Response({ - 'state': 'PENDING', - 'meta': { - 'percentage': 0, - 'message': 'Task not found or not yet registered', - 'phase': 'initializing', - } - }) + return success_response( + data={ + 'state': 'PENDING', + 'meta': { + 'percentage': 0, + 'message': 'Task not found or not yet registered', + 'phase': 'initializing', + } + }, + request=request + ) # Safely get task info/result # Try to get error from multiple sources @@ -1114,10 +1163,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): # Include image_queue if available (for image generation) if 'image_queue' in meta: response_meta['image_queue'] = meta['image_queue'] - return Response({ - 'state': task_state, - 'meta': response_meta - }) + return success_response( + data={ + 'state': task_state, + 'meta': response_meta + }, + request=request + ) elif task_state == 'SUCCESS': result = task_result or {} meta = result if isinstance(result, dict) else {} @@ -1133,10 +1185,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): response_meta['request_steps'] = meta['request_steps'] if 'response_steps' in meta: response_meta['response_steps'] = meta['response_steps'] - return Response({ - 'state': task_state, - 'meta': response_meta - }) + return success_response( + data={ + 'state': task_state, + 'meta': response_meta + }, + request=request + ) elif task_state == 'FAILURE': # Try to get error from task.info meta first (this is where run_ai_task sets it) if not error_message and isinstance(task_info, dict): @@ -1211,42 +1266,55 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): error_type = meta['error_type'] response_meta['error_type'] = error_type - return Response({ - 'state': task_state, - 'meta': response_meta - }) + return success_response( + data={ + 'state': task_state, + 'meta': response_meta + }, + request=request + ) else: # PENDING, STARTED, or other states - return Response({ - 'state': task_state, - 'meta': { - 'percentage': 0, - 'message': 'Task is starting...', - 'phase': 'initializing', - } - }) + return success_response( + data={ + 'state': task_state, + 'meta': { + 'percentage': 0, + 'message': 'Task is starting...', + 'phase': 'initializing', + } + }, + request=request + ) except (KombuOperationalError, RedisConnectionError, ConnectionError) as conn_error: # Backend connection error - task might not be registered yet or backend is down logger.warning(f"Backend connection error for task {task_id}: {type(conn_error).__name__}: {str(conn_error)}") - return Response({ - 'state': 'PENDING', - 'meta': { - 'percentage': 0, - 'message': 'Task is being queued...', - 'phase': 'initializing', - 'error': None # Don't show as error, just pending - } - }) + return success_response( + data={ + 'state': 'PENDING', + 'meta': { + 'percentage': 0, + 'message': 'Task is being queued...', + 'phase': 'initializing', + 'error': None # Don't show as error, just pending + } + }, + request=request + ) except Exception as task_error: logger.error(f"Error accessing Celery task {task_id}: {type(task_error).__name__}: {str(task_error)}", exc_info=True) - return Response({ - 'state': 'UNKNOWN', - 'meta': { - 'percentage': 0, - 'message': f'Error accessing task: {str(task_error)}', - 'error': str(task_error) - } - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return success_response( + data={ + 'state': 'UNKNOWN', + 'meta': { + 'percentage': 0, + 'message': f'Error accessing task: {str(task_error)}', + 'error': str(task_error) + } + }, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request + ) except Exception as e: # Check if it's a connection-related error - treat as PENDING instead of error @@ -1263,19 +1331,22 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if is_connection_error: logger.warning(f"Connection error getting task progress for {task_id}: {error_type}: {str(e)}") - return Response({ - 'state': 'PENDING', - 'meta': { - 'percentage': 0, - 'message': 'Task is being queued...', - 'phase': 'initializing', - 'error': None - } - }) + return success_response( + data={ + 'state': 'PENDING', + 'meta': { + 'percentage': 0, + 'message': 'Task is being queued...', + 'phase': 'initializing', + 'error': None + } + }, + request=request + ) else: logger.error(f"Error getting task progress for {task_id}: {error_type}: {str(e)}", exc_info=True) - return Response( - { + return success_response( + data={ 'state': 'ERROR', 'meta': { 'error': f'Error getting task status: {str(e)}', @@ -1283,6 +1354,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): 'message': f'Error: {str(e)}' } }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + request=request ) diff --git a/backend/igny8_core/modules/system/urls.py b/backend/igny8_core/modules/system/urls.py index da274511..5e19b49c 100644 --- a/backend/igny8_core/modules/system/urls.py +++ b/backend/igny8_core/modules/system/urls.py @@ -3,7 +3,7 @@ URL patterns for system module. """ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, system_status, get_request_metrics, gitea_webhook +from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, system_status, get_request_metrics, gitea_webhook, ping from .integration_views import IntegrationSettingsViewSet from .settings_views import ( SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet, @@ -51,6 +51,8 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({ urlpatterns = [ path('', include(router.urls)), + # Public health check endpoint (API Standard v1.0 requirement) + path('ping/', ping, name='system-ping'), # System status endpoint path('status/', system_status, name='system-status'), # Request metrics endpoint diff --git a/backend/igny8_core/modules/system/views.py b/backend/igny8_core/modules/system/views.py index 898d87fc..b23c0b26 100644 --- a/backend/igny8_core/modules/system/views.py +++ b/backend/igny8_core/modules/system/views.py @@ -269,6 +269,24 @@ class StrategyViewSet(AccountModelViewSet): filterset_fields = ['is_active', 'sector'] +@api_view(['GET']) +@permission_classes([AllowAny]) # Public endpoint +@extend_schema( + tags=['System'], + summary='Health Check', + description='Simple health check endpoint to verify API is responding' +) +def ping(request): + """ + Simple health check endpoint + Returns unified format: {success: true, data: {status: 'ok'}} + """ + return success_response( + data={'status': 'ok'}, + request=request + ) + + @api_view(['GET']) @permission_classes([AllowAny]) # Public endpoint for monitoring def system_status(request): diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 9b0bc051..4c20acb8 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -268,8 +268,12 @@ SPECTACULAR_SETTINGS = { ## Authentication All endpoints require JWT Bearer token authentication except: + - `GET /api/v1/system/ping/` - Health check endpoint - `POST /api/v1/auth/login/` - User login - `POST /api/v1/auth/register/` - User registration + - `GET /api/v1/auth/plans/` - List subscription plans + - `GET /api/v1/auth/industries/` - List industries + - `GET /api/v1/system/status/` - System status Include token in Authorization header: ``` diff --git a/frontend/src/components/common/ImageGenerationCard.tsx b/frontend/src/components/common/ImageGenerationCard.tsx index f9c4cca1..82b0c748 100644 --- a/frontend/src/components/common/ImageGenerationCard.tsx +++ b/frontend/src/components/common/ImageGenerationCard.tsx @@ -138,6 +138,8 @@ export default function ImageGenerationCard({ console.log('[ImageGenerationCard] Making request to image generation endpoint'); console.log('[ImageGenerationCard] Request body:', requestBody); + // fetchAPI extracts data from unified format {success: true, data: {...}} + // So data is the extracted response payload const data = await fetchAPI('/v1/system/settings/integrations/image_generation/generate/', { method: 'POST', body: JSON.stringify(requestBody), @@ -145,8 +147,10 @@ export default function ImageGenerationCard({ console.log('[ImageGenerationCard] Response data:', data); - if (!data.success) { - throw new Error(data.error || 'Failed to generate image'); + // fetchAPI extracts data from unified format, so data is the response payload + // If fetchAPI didn't throw, the request was successful + if (!data || typeof data !== 'object') { + throw new Error('Invalid response format'); } const imageData = { diff --git a/frontend/src/components/common/ValidationCard.tsx b/frontend/src/components/common/ValidationCard.tsx index e03072e5..2848f623 100644 --- a/frontend/src/components/common/ValidationCard.tsx +++ b/frontend/src/components/common/ValidationCard.tsx @@ -81,38 +81,30 @@ export default function ValidationCard({ }; } - // Test endpoint returns Response({success: True, ...}) directly (not unified format) - // So fetchAPI may or may not extract it - handle both cases + // Test endpoint now returns unified format {success: true, data: {...}} + // fetchAPI extracts the data field, so data is the inner object const data = await fetchAPI(`/v1/system/settings/integrations/${integrationId}/test/`, { method: 'POST', body: JSON.stringify(requestBody), }); - // Check if data has success field (direct Response format) or is extracted data - if (data && typeof data === 'object' && ('success' in data ? data.success : true)) { - // If data has success field, use it; otherwise assume success (extracted data) - const isSuccess = data.success !== false; - if (isSuccess) { - setTestResult({ - success: true, - message: data.message || 'API connection successful!', - model_used: data.model_used || data.model, - response: data.response, - tokens_used: data.tokens_used, - total_tokens: data.total_tokens, - cost: data.cost, - full_response: data.full_response || { - image_url: data.image_url, - provider: data.provider, - size: data.size, - }, - }); - } else { - setTestResult({ - success: false, - message: data.error || data.message || 'API connection failed', - }); - } + // fetchAPI extracts data from unified format, so data is the response payload + if (data && typeof data === 'object') { + // Success response - data contains message, model_used, response, etc. + setTestResult({ + success: true, + message: data.message || 'API connection successful!', + model_used: data.model_used || data.model, + response: data.response, + tokens_used: data.tokens_used, + total_tokens: data.total_tokens, + cost: data.cost, + full_response: data.full_response || { + image_url: data.image_url, + provider: data.provider, + size: data.size, + }, + }); } else { setTestResult({ success: false, diff --git a/frontend/src/pages/Settings/Integration.tsx b/frontend/src/pages/Settings/Integration.tsx index 908c0167..7244ab67 100644 --- a/frontend/src/pages/Settings/Integration.tsx +++ b/frontend/src/pages/Settings/Integration.tsx @@ -334,8 +334,7 @@ export default function Integration() { try { // fetchAPI extracts data from unified format {success: true, data: {...}} - // But test endpoint may return {success: true, ...} directly (not wrapped) - // So data could be either the extracted data object or the full response + // So data is the extracted response payload const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, { method: 'POST', body: JSON.stringify({ @@ -344,48 +343,23 @@ export default function Integration() { }), }); - // Handle both unified format (extracted) and direct format - // If data has success field, it's the direct response (not extracted) - // If data doesn't have success but has other fields, it's extracted data (successful) + // fetchAPI extracts data from unified format, so data is the response payload if (data && typeof data === 'object') { - if (data.success === true || data.success === false) { - // Direct response format (not extracted by fetchAPI) - if (data.success) { - toast.success(data.message || 'API connection test successful!'); - if (data.response) { - toast.info(`Response: ${data.response}`); - } - if (data.tokens_used) { - toast.info(`Tokens used: ${data.tokens_used}`); - } - - // Update validation status to success - if (selectedIntegration) { - setValidationStatuses(prev => ({ - ...prev, - [selectedIntegration]: 'success', - })); - } - } else { - throw new Error(data.error || data.message || 'Connection test failed'); - } - } else { - // Extracted data format (successful response) - toast.success('API connection test successful!'); - if (data.response) { - toast.info(`Response: ${data.response}`); - } - if (data.tokens_used) { - toast.info(`Tokens used: ${data.tokens_used}`); - } - - // Update validation status to success - if (selectedIntegration) { - setValidationStatuses(prev => ({ - ...prev, - [selectedIntegration]: 'success', - })); - } + // Success response - data contains message, response, tokens_used, etc. + toast.success(data.message || 'API connection test successful!'); + if (data.response) { + toast.info(`Response: ${data.response}`); + } + if (data.tokens_used) { + toast.info(`Tokens used: ${data.tokens_used}`); + } + + // Update validation status to success + if (selectedIntegration) { + setValidationStatuses(prev => ({ + ...prev, + [selectedIntegration]: 'success', + })); } } else { throw new Error('Invalid response format'); diff --git a/unified-api/API-STANDARD-v1.0.md b/unified-api/API-STANDARD-v1.0.md index 324e8fca..de307d45 100644 --- a/unified-api/API-STANDARD-v1.0.md +++ b/unified-api/API-STANDARD-v1.0.md @@ -144,7 +144,7 @@ Authorization: Bearer - `GET /api/v1/auth/plans/` - `GET /api/v1/auth/industries/` - `GET /api/v1/system/status/` -- `GET /api/ping/` (health check) +- `GET /api/v1/system/ping/` (health check) **All other endpoints require JWT authentication.**