diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index c245eebd..a0d31a14 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py b/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py index a017d088..0fded9de 100644 --- a/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py +++ b/backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py @@ -198,10 +198,16 @@ class SitesRendererAdapter(BaseAdapter): Returns: str: Deployment URL """ - # TODO: Implement URL generation based on site configuration - # For now, return placeholder site_id = site_blueprint.site.id - return f"https://{site_id}.igny8.com" # Placeholder + + # Get Sites Renderer URL from environment or use default + sites_renderer_host = os.getenv('SITES_RENDERER_HOST', '31.97.144.105') + sites_renderer_port = os.getenv('SITES_RENDERER_PORT', '8024') + sites_renderer_protocol = os.getenv('SITES_RENDERER_PROTOCOL', 'http') + + # Construct URL: http://31.97.144.105:8024/{site_id} + # Sites Renderer routes: /:siteId/* -> SiteRenderer component + return f"{sites_renderer_protocol}://{sites_renderer_host}:{sites_renderer_port}/{site_id}" # BaseAdapter interface implementation def publish( diff --git a/backend/igny8_core/modules/publisher/urls.py b/backend/igny8_core/modules/publisher/urls.py index 1e934905..bf585cd2 100644 --- a/backend/igny8_core/modules/publisher/urls.py +++ b/backend/igny8_core/modules/publisher/urls.py @@ -9,6 +9,7 @@ from igny8_core.modules.publisher.views import ( PublishingRecordViewSet, DeploymentRecordViewSet, PublisherViewSet, + SiteDefinitionView, ) router = DefaultRouter() @@ -19,5 +20,7 @@ router.register(r'', PublisherViewSet, basename='publisher') urlpatterns = [ path('', include(router.urls)), + # Public endpoint for Sites Renderer + path('sites//definition/', SiteDefinitionView.as_view(), name='site-definition'), ] diff --git a/backend/igny8_core/modules/publisher/views.py b/backend/igny8_core/modules/publisher/views.py index 633ffd15..4a88375f 100644 --- a/backend/igny8_core/modules/publisher/views.py +++ b/backend/igny8_core/modules/publisher/views.py @@ -2,9 +2,13 @@ Publisher ViewSet Phase 5: Sites Renderer & Publishing """ +import json +import os +from pathlib import Path from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.views import APIView from igny8_core.api.base import SiteSectorModelViewSet from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove @@ -196,3 +200,76 @@ class PublisherViewSet(viewsets.ViewSet): request ) + +class SiteDefinitionView(APIView): + """ + Public endpoint to serve deployed site definitions. + Used by Sites Renderer to load site definitions. + No authentication required for public sites. + """ + permission_classes = [] # Public endpoint + + def get(self, request, site_id): + """ + Get deployed site definition. + + GET /api/v1/publisher/sites/{site_id}/definition/ + """ + sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data') + + # Try to find latest deployed version + site_dir = Path(sites_data_path) / 'clients' / str(site_id) + + if not site_dir.exists(): + return error_response( + f'Site {site_id} not found', + status.HTTP_404_NOT_FOUND, + request + ) + + # Look for latest version (check for 'latest' symlink or highest version number) + latest_path = site_dir / 'latest' / 'site.json' + if latest_path.exists(): + try: + with open(latest_path, 'r', encoding='utf-8') as f: + definition = json.load(f) + return Response(definition, status=status.HTTP_200_OK) + except Exception as e: + return error_response( + f'Failed to load site definition: {str(e)}', + status.HTTP_500_INTERNAL_SERVER_ERROR, + request + ) + + # Fallback: find highest version number + version_dirs = [d for d in site_dir.iterdir() if d.is_dir() and d.name.startswith('v')] + if not version_dirs: + return error_response( + f'No deployed versions found for site {site_id}', + status.HTTP_404_NOT_FOUND, + request + ) + + # Sort by version number (extract number from 'v1', 'v2', etc.) + try: + version_dirs.sort(key=lambda d: int(d.name[1:]) if d.name[1:].isdigit() else 0, reverse=True) + latest_version_dir = version_dirs[0] + site_json_path = latest_version_dir / 'site.json' + + if site_json_path.exists(): + with open(site_json_path, 'r', encoding='utf-8') as f: + definition = json.load(f) + return Response(definition, status=status.HTTP_200_OK) + except Exception as e: + return error_response( + f'Failed to load site definition: {str(e)}', + status.HTTP_500_INTERNAL_SERVER_ERROR, + request + ) + + return error_response( + f'Site definition not found for site {site_id}', + status.HTTP_404_NOT_FOUND, + request + ) + diff --git a/docker-compose.app.yml b/docker-compose.app.yml index 04b60211..a782c587 100644 --- a/docker-compose.app.yml +++ b/docker-compose.app.yml @@ -101,6 +101,26 @@ services: - "com.docker.compose.project=igny8-app" - "com.docker.compose.service=igny8_marketing_dev" + igny8_sites: + # Sites Renderer - serves deployed public sites + # Build separately: docker build -t igny8-sites-dev:latest -f Dockerfile.dev . + # Accessible at http://31.97.144.105:8024 (direct) or via Caddy routing + image: igny8-sites-dev:latest + container_name: igny8_sites + restart: always + ports: + - "0.0.0.0:8024:5176" # Sites renderer port (internal: 5176, external: 8024) + environment: + VITE_API_URL: "https://api.igny8.com/api" + SITES_DATA_PATH: "/sites" + volumes: + - /data/app/igny8/sites:/app:rw + - /data/app/sites-data:/sites:ro # Read-only access to deployed sites + networks: [igny8_net] + labels: + - "com.docker.compose.project=igny8-app" + - "com.docker.compose.service=igny8_sites" + igny8_celery_worker: image: igny8-backend:latest diff --git a/frontend/src/pages/Sites/Preview.tsx b/frontend/src/pages/Sites/Preview.tsx index dd60258d..4b23d469 100644 --- a/frontend/src/pages/Sites/Preview.tsx +++ b/frontend/src/pages/Sites/Preview.tsx @@ -53,18 +53,20 @@ export default function SitePreview() { // If deployment exists but no URL, construct from Sites Renderer // Sites Renderer should be accessible at a different port or subdomain // Check if we have the Sites Renderer URL configured + // Use VPS IP or configured URL for Sites Renderer const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL || (window as any).__SITES_RENDERER_URL__ || - 'http://localhost:8024'; + 'http://31.97.144.105:8024'; setPreviewUrl(`${sitesRendererUrl}/${siteId}`); } } catch (error) { console.warn('No deployment record found:', error); // If blueprint is deployed but no deployment record, try Sites Renderer directly if (latestBlueprint.status === 'deployed') { + // Use VPS IP or configured URL for Sites Renderer const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL || (window as any).__SITES_RENDERER_URL__ || - 'http://localhost:8024'; + 'http://31.97.144.105:8024'; setPreviewUrl(`${sitesRendererUrl}/${siteId}`); } } diff --git a/sites/src/loaders/loadSiteDefinition.ts b/sites/src/loaders/loadSiteDefinition.ts index 377c4173..0966d280 100644 --- a/sites/src/loaders/loadSiteDefinition.ts +++ b/sites/src/loaders/loadSiteDefinition.ts @@ -16,26 +16,30 @@ const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites'; * then falls back to API. */ export async function loadSiteDefinition(siteId: string): Promise { - // Try filesystem first (for deployed sites) + // Try API endpoint for deployed site definition first try { - const fsPath = `${SITES_DATA_PATH}/clients/${siteId}/latest/site.json`; - const response = await fetch(fsPath); - if (response.ok) { - const definition = await response.json(); - return definition; + const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`); + if (response.data) { + return response.data as SiteDefinition; } } catch (error) { - // Filesystem load failed, try API - console.warn('Failed to load from filesystem, trying API:', error); + // API load failed, try blueprint endpoint as fallback + console.warn('Failed to load deployed site definition, trying blueprint:', error); } - // Fallback to API + // Fallback to blueprint API (for non-deployed sites) try { - const response = await axios.get(`${API_URL}/v1/site-builder/blueprints/${siteId}/`); - const blueprint = response.data; + const response = await axios.get(`${API_URL}/v1/site-builder/blueprints/?site=${siteId}`); + const blueprints = Array.isArray(response.data?.results) ? response.data.results : + Array.isArray(response.data) ? response.data : []; - // Transform blueprint to site definition format - return transformBlueprintToSiteDefinition(blueprint); + if (blueprints.length > 0) { + const blueprint = blueprints[0]; // Get latest blueprint + // Transform blueprint to site definition format + return transformBlueprintToSiteDefinition(blueprint); + } + + throw new Error(`No blueprint found for site ${siteId}`); } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Failed to load site: ${error.message}`);