From c378e503d8069e270262599e7da99cbebfd9d87a Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 18 Nov 2025 18:58:48 +0000 Subject: [PATCH] Enhance site deployment and preview functionality. Updated Preview.tsx to improve deployment record handling and fallback URL logic. Added deploy functionality in Blueprints.tsx and Preview.tsx, including loading states and user feedback. Introduced ProgressModal for page generation status updates. Updated siteBuilder.api.ts to include deployBlueprint API method. --- SITE_BUILDER_URLS_AND_FILES.md | 139 ++++++++++++++++++ backend/celerybeat-schedule | Bin 16384 -> 16384 bytes .../src/pages/Sites/Builder/Blueprints.tsx | 36 ++++- frontend/src/pages/Sites/Builder/Preview.tsx | 111 +++++++++++++- frontend/src/pages/Sites/Preview.tsx | 82 ++++++++--- frontend/src/services/siteBuilder.api.ts | 11 ++ 6 files changed, 351 insertions(+), 28 deletions(-) create mode 100644 SITE_BUILDER_URLS_AND_FILES.md diff --git a/SITE_BUILDER_URLS_AND_FILES.md b/SITE_BUILDER_URLS_AND_FILES.md new file mode 100644 index 00000000..9c54d1c1 --- /dev/null +++ b/SITE_BUILDER_URLS_AND_FILES.md @@ -0,0 +1,139 @@ +# Site Builder URLs and File Management + +## Summary of Implementation + +### ✅ Generate Page Content Step + +**Location**: `frontend/src/pages/Sites/Builder/Preview.tsx` + +**Implementation**: +- Added "Generate All Pages" button (shown when blueprint status is `'ready'`) +- Button triggers `generateAllPages()` from `builderStore` +- Shows ProgressModal during generation +- Uses existing `PageGenerationService.bulk_generate_pages()` backend function + +**Queue Function**: ✅ **EXISTS** +- `PageGenerationService.bulk_generate_pages()` creates Writer Tasks +- Tasks are queued via `ContentGenerationService.generate_content()` +- Each page blueprint gets a Writer Task with title: `"[Site Builder] {page_title}"` +- Tasks are processed by Celery workers + +--- + +## URL Standards + +### Public Site URLs (Deployed Sites) + +**Current Implementation** (Placeholder): +- Pattern: `https://{site_id}.igny8.com` +- Generated by: `SitesRendererAdapter._get_deployment_url()` +- Location: `backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py:191` + +**Planned Implementation** (from docs): +- Custom domains: `clientdomain.com` → routed via Caddy +- Subdomain: `mysite.igny8.com` → routed via Caddy +- Marketing site: `igny8.com` → `/igny8-sites/marketing/` + +**Sites Renderer Routes**: +- Public routes: `/:siteId/*` (no auth required) +- Loads from: `/data/app/sites-data/clients/{site_id}/v{version}/` + +--- + +### Admin/Management URLs (Frontend App) + +**Site Management Routes** (from `frontend/src/App.tsx`): +- `/sites` - All sites list +- `/sites/:id` - Site dashboard +- `/sites/:id/content` - Site content list +- `/sites/:id/editor` - Site content editor +- `/sites/:id/pages` - Page manager +- `/sites/:id/pages/new` - Create new page +- `/sites/:id/pages/:pageId/edit` - Edit page +- `/sites/:id/posts/:postId` - View/edit post +- `/sites/:id/posts/:postId/edit` - Edit post +- `/sites/:id/preview` - Site preview +- `/sites/:id/settings` - Site settings (General, SEO, OG, Schema, Integrations) +- `/sites/manage` - Site management dashboard + +**Site Builder Routes**: +- `/sites/builder` - Site Builder wizard +- `/sites/builder/preview` - Preview blueprint (with Generate All Pages button) +- `/sites/blueprints` - Blueprints list + +--- + +## File Management + +### File Storage Structure + +**Site Files**: +``` +/data/app/sites-data/ +└── clients/ + └── {site_id}/ + └── v{version}/ + ├── site.json + ├── pages/ + │ ├── home.json + │ ├── about.json + │ └── ... + └── assets/ # User-managed files + ├── images/ + ├── documents/ + └── media/ +``` + +**Service**: `SiteBuilderFileService` +- Location: `backend/igny8_core/business/site_building/services/file_management_service.py` +- Base path: `/data/app/sites-data/clients` +- Max file size: 10MB per file +- Max storage per site: 100MB + +### User Access Rules + +- **Owner/Admin**: Full access to all account sites +- **Editor**: Access to granted sites (via SiteUserAccess) +- **Viewer**: Read-only access to granted sites +- File operations scoped to user's accessible sites only + +### File Manager UI + +**Status**: ⚠️ **NOT YET IMPLEMENTED** + +**Planned** (from Phase 3 docs): +- File Browser UI: `site-builder/src/components/files/FileBrowser.tsx` +- File Upload API: `modules/site_builder/views.py` +- Storage quota check: `infrastructure/storage/file_storage.py` + +**Expected Routes** (not yet in App.tsx): +- `/sites/:id/files` - File manager for site assets +- `/sites/:id/files/upload` - Upload files +- `/sites/:id/files/:fileId` - View/edit file + +--- + +## Docker Volumes + +**From `docker-compose.app.yml`**: +```yaml +igny8_sites: + volumes: + - /data/app/igny8/sites:/app + - /data/app/sites-data:/sites # Site definitions and assets +``` + +**Environment Variable**: +- `SITES_DATA_PATH=/sites` (inside container) +- Maps to `/data/app/sites-data` on host + +--- + +## Next Steps + +1. ✅ **Generate All Pages button** - Added to Preview page +2. ⏳ **File Manager UI** - Needs to be implemented +3. ⏳ **Deployment URL generation** - Currently placeholder, needs real domain mapping +4. ⏳ **Caddy routing configuration** - For custom domains +5. ⏳ **File upload API endpoints** - For user file management + diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 9276c3eae92f2c2f3cc29e28d33dc94f9a5e9f75..b8b8e9da58fc519652d9ee4590d4a0d10cf7af17 100644 GIT binary patch delta 30 lcmZo@U~Fh$+@NT}FJ#2Pz|b-!L$qy5&=hZn%?2hHxB-dM32guX delta 30 lcmZo@U~Fh$+@NT}&o9Ejz)(9SL$qy5&=ha6%?2hHxB-Rt2^#({ isOpen: false, blueprint: null, isBulk: false }); + const [deployingId, setDeployingId] = useState(null); const loadBlueprints = async (siteId: number) => { try { @@ -113,6 +114,27 @@ export default function SiteBuilderBlueprints() { } }; + const handleDeploy = async (blueprint: SiteBlueprint) => { + try { + setDeployingId(blueprint.id); + const result = await siteBuilderApi.deployBlueprint(blueprint.id); + + if (result.success) { + toast.success("Site deployed successfully!"); + // Reload blueprints to get updated status + if (activeSite?.id) { + await loadBlueprints(activeSite.id); + } + } else { + toast.error("Failed to deploy site"); + } + } catch (error: any) { + toast.error(error?.message || "Failed to deploy site"); + } finally { + setDeployingId(null); + } + }; + return (
@@ -238,6 +260,17 @@ export default function SiteBuilderBlueprints() { {blueprint.status}
+ {(blueprint.status === 'ready' || blueprint.status === 'deployed') && ( + + )}
)} diff --git a/frontend/src/pages/Sites/Builder/Preview.tsx b/frontend/src/pages/Sites/Builder/Preview.tsx index 38c10537..58cda57c 100644 --- a/frontend/src/pages/Sites/Builder/Preview.tsx +++ b/frontend/src/pages/Sites/Builder/Preview.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import PageMeta from "../../../components/common/PageMeta"; import { @@ -10,17 +10,69 @@ import Button from "../../../components/ui/button/Button"; import Alert from "../../../components/ui/alert/Alert"; import { useBuilderStore } from "../../../store/builderStore"; import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore"; -import { Eye } from "lucide-react"; +import ProgressModal from "../../../components/common/ProgressModal"; +import { Eye, Play, Loader2, Rocket } from "lucide-react"; +import { useToast } from "../../../components/ui/toast/ToastContainer"; +import { siteBuilderApi } from "../../../services/siteBuilder.api"; export default function SiteBuilderPreview() { const navigate = useNavigate(); - const { activeBlueprint, pages } = useBuilderStore(); + const toast = useToast(); + const { + activeBlueprint, + pages, + generateAllPages, + isGenerating, + generationProgress + } = useBuilderStore(); const { structure, selectedSlug, selectPage } = useSiteDefinitionStore(); + const [showProgress, setShowProgress] = useState(false); + const [isDeploying, setIsDeploying] = useState(false); const selectedPageDefinition = useMemo(() => { return structure?.pages?.find((page) => page.slug === selectedSlug); }, [structure, selectedSlug]); + useEffect(() => { + if (generationProgress?.celeryTaskId) { + setShowProgress(true); + } + }, [generationProgress?.celeryTaskId]); + + const handleGenerateAll = async () => { + if (!activeBlueprint) return; + + setShowProgress(true); + try { + await generateAllPages(activeBlueprint.id); + toast.success("Page generation queued successfully"); + } catch (error: any) { + toast.error(error?.message || "Failed to generate pages"); + setShowProgress(false); + } + }; + + const handleDeploy = async () => { + if (!activeBlueprint) return; + + try { + setIsDeploying(true); + const result = await siteBuilderApi.deployBlueprint(activeBlueprint.id); + + if (result.success) { + toast.success("Site deployed successfully!"); + // Reload blueprint to get updated status + await useBuilderStore.getState().loadBlueprint(activeBlueprint.id); + } else { + toast.error("Failed to deploy site"); + } + } catch (error: any) { + toast.error(error?.message || "Failed to deploy site"); + } finally { + setIsDeploying(false); + } + }; + if (!activeBlueprint) { return (
@@ -58,9 +110,33 @@ export default function SiteBuilderPreview() { Inspect the generated structure before publishing or deploying.

- +
+ {activeBlueprint.status === 'ready' && ( + + )} + {(activeBlueprint.status === 'ready' || activeBlueprint.status === 'deployed') && ( + + )} + +
+ + setShowProgress(false)} + title="Generating Pages" + percentage={isGenerating ? 50 : 100} + status={isGenerating ? 'processing' : generationProgress ? 'completed' : 'pending'} + message={ + isGenerating + ? `Generating content for ${generationProgress?.pagesQueued || pages.length} page${(generationProgress?.pagesQueued || pages.length) !== 1 ? 's' : ''}...` + : 'Generation completed!' + } + details={ + generationProgress + ? { + current: generationProgress.pagesQueued, + total: generationProgress.pagesQueued, + completed: generationProgress.pagesQueued, + } + : undefined + } + taskId={generationProgress?.celeryTaskId} + /> ); } diff --git a/frontend/src/pages/Sites/Preview.tsx b/frontend/src/pages/Sites/Preview.tsx index 52f9d74d..dd60258d 100644 --- a/frontend/src/pages/Sites/Preview.tsx +++ b/frontend/src/pages/Sites/Preview.tsx @@ -37,30 +37,51 @@ export default function SitePreview() { const latestBlueprint = blueprints[0]; setBlueprint(latestBlueprint); - // Get publishing status to find preview URL - if (latestBlueprint.deployed_version) { - // Try to get publishing record + // Get deployment record to find preview URL + if (latestBlueprint.deployed_version || latestBlueprint.status === 'deployed') { try { - const publishingData = await fetchAPI(`/v1/publishing/records/?site_blueprint=${latestBlueprint.id}`); - const records = Array.isArray(publishingData?.results) ? publishingData.results : Array.isArray(publishingData) ? publishingData : []; + // Get deployment records for this blueprint + const deploymentsData = await fetchAPI(`/v1/publisher/deployments/?site_blueprint=${latestBlueprint.id}`); + const deployments = Array.isArray(deploymentsData?.results) ? deploymentsData.results : Array.isArray(deploymentsData) ? deploymentsData : []; - if (records.length > 0) { - const record = records.find((r: any) => r.status === 'published') || records[0]; - if (record?.published_url) { - setPreviewUrl(record.published_url); - } + // Find the latest deployed record + const deployedRecord = deployments.find((d: any) => d.status === 'deployed') || deployments[0]; + + if (deployedRecord?.deployment_url) { + setPreviewUrl(deployedRecord.deployment_url); + } else if (deployedRecord) { + // 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 + const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL || + (window as any).__SITES_RENDERER_URL__ || + 'http://localhost:8024'; + setPreviewUrl(`${sitesRendererUrl}/${siteId}`); } } catch (error) { - // If no publishing record, construct preview URL from blueprint - console.warn('No publishing record found, using fallback URL'); + console.warn('No deployment record found:', error); + // If blueprint is deployed but no deployment record, try Sites Renderer directly + if (latestBlueprint.status === 'deployed') { + const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL || + (window as any).__SITES_RENDERER_URL__ || + 'http://localhost:8024'; + setPreviewUrl(`${sitesRendererUrl}/${siteId}`); + } } } - // Fallback: construct preview URL from blueprint - if (!previewUrl && latestBlueprint.id) { - // Assuming sites are hosted at a subdomain or path - const baseUrl = window.location.origin; - setPreviewUrl(`${baseUrl}/sites/${siteId}/preview/${latestBlueprint.id}`); + // If still no preview URL, check blueprint status + if (!previewUrl) { + if (latestBlueprint.status === 'ready' || latestBlueprint.status === 'generating' || latestBlueprint.status === 'draft') { + // Blueprint exists but not deployed yet - don't set preview URL + setPreviewUrl(null); + } else if (latestBlueprint.status === 'deployed') { + // Blueprint is deployed but no deployment record found - try Sites Renderer + const sitesRendererUrl = import.meta.env.VITE_SITES_RENDERER_URL || + (window as any).__SITES_RENDERER_URL__ || + 'http://localhost:8024'; + setPreviewUrl(`${sitesRendererUrl}/${siteId}`); + } } } } catch (error: any) { @@ -110,12 +131,31 @@ export default function SitePreview() {

- No preview available. Please deploy your site first. + {blueprint?.status === 'ready' || blueprint?.status === 'generating' + ? 'Blueprint is ready but not yet deployed. Deploy your site to preview it.' + : blueprint?.status === 'draft' + ? 'Blueprint is still in draft. Complete the wizard to generate the site structure.' + : 'No preview available. Please deploy your site first.'}

{blueprint && ( -

- Blueprint: {blueprint.name} ({blueprint.status}) -

+
+

+ Blueprint: {blueprint.name} ({blueprint.status}) +

+ {blueprint.status === 'ready' && ( + + )} +
)}
diff --git a/frontend/src/services/siteBuilder.api.ts b/frontend/src/services/siteBuilder.api.ts index 456892d8..1f6e5062 100644 --- a/frontend/src/services/siteBuilder.api.ts +++ b/frontend/src/services/siteBuilder.api.ts @@ -157,5 +157,16 @@ export const siteBuilderApi = { body: JSON.stringify({ ids }), }); }, + + /** + * Deploy a blueprint to Sites renderer + */ + async deployBlueprint(blueprintId: number): Promise<{ success: boolean; deployment_url?: string; deployment_id?: number }> { + const response = await fetchAPI(`/v1/publisher/deploy/${blueprintId}/`, { + method: 'POST', + }); + // Handle unified response format + return response?.data || response; + }, };