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 9276c3ea..b8b8e9da 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/frontend/src/pages/Sites/Builder/Blueprints.tsx b/frontend/src/pages/Sites/Builder/Blueprints.tsx index 2da058b8..4f5e5a00 100644 --- a/frontend/src/pages/Sites/Builder/Blueprints.tsx +++ b/frontend/src/pages/Sites/Builder/Blueprints.tsx @@ -4,7 +4,7 @@ import PageMeta from "../../../components/common/PageMeta"; import { Card } from "../../../components/ui/card"; import Button from "../../../components/ui/button/Button"; import { useToast } from "../../../components/ui/toast/ToastContainer"; -import { FileText, Loader2, Plus, Trash2, CheckSquare, Square } from "lucide-react"; +import { FileText, Loader2, Plus, Trash2, CheckSquare, Square, Rocket } from "lucide-react"; import { useSiteStore } from "../../../store/siteStore"; import { useBuilderStore } from "../../../store/builderStore"; import { siteBuilderApi } from "../../../services/siteBuilder.api"; @@ -25,6 +25,7 @@ export default function SiteBuilderBlueprints() { blueprint: SiteBlueprint | null; isBulk: boolean; }>({ 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; + }, };