From 1b4cd59e5b1741de822bcb05a7f067e8ab5e4cc1 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Thu, 20 Nov 2025 21:50:16 +0000 Subject: [PATCH] Refactor site building workflow and context handling; update API response structure for improved clarity and consistency. Adjust frontend components to align with new data structure, including error handling and loading states. --- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes .../services/wizard_context_service.py | 8 ++- .../services/workflow_state_service.py | 30 +++++++-- .../igny8_core/modules/site_builder/views.py | 55 +++++++++++------ frontend/src/App.tsx | 8 +-- frontend/src/icons/index.ts | 1 + .../pages/Setup/IndustriesSectorsKeywords.tsx | 9 ++- .../src/pages/Sites/Builder/Blueprints.tsx | 55 +++++++++++++---- frontend/src/pages/Sites/Builder/Preview.tsx | 12 ++-- frontend/src/pages/Sites/Builder/Wizard.tsx | 16 +++++ .../pages/Sites/Builder/WorkflowWizard.tsx | 22 ++++--- .../Sites/Builder/components/HelperDrawer.tsx | 8 +-- .../Builder/components/WizardProgress.tsx | 43 +++++++++---- .../Builder/steps/BusinessDetailsStep.tsx | 58 +++++++++++++++--- .../Builder/steps/ClusterAssignmentStep.tsx | 20 +++--- .../Builder/steps/CoverageValidationStep.tsx | 26 ++++---- .../Sites/Builder/steps/ObjectivesStep.tsx | 2 +- .../Sites/Builder/steps/SitemapReviewStep.tsx | 14 +++-- .../pages/Sites/Builder/steps/StyleStep.tsx | 6 +- .../Builder/steps/TaxonomyBuilderStep.tsx | 14 ++--- frontend/src/pages/Sites/List.tsx | 26 ++++++-- frontend/src/services/api.ts | 52 +++++++++------- frontend/src/store/authStore.ts | 19 +++++- frontend/src/store/builderWorkflowStore.ts | 46 +++++++++++--- 24 files changed, 386 insertions(+), 164 deletions(-) diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index f09a9a93d9cdbbefc810111cc280fe189b4e3c72..8ee23a85a2661b8280babd1e45b1ff6481fa296a 100644 GIT binary patch delta 36 scmZo@U~Fh$++boRC?cxMz`zhbB}24rO3)PF#Y~e8Oq4fgn9bk>0KoHq)$ delta 36 scmZo@U~Fh$++boRC@gBuz`!tRN``3Ll%OfTt&EcmOq4fgn9bk>0L3H=4*&oF diff --git a/backend/igny8_core/business/site_building/services/wizard_context_service.py b/backend/igny8_core/business/site_building/services/wizard_context_service.py index 257ff885..b6d31ed1 100644 --- a/backend/igny8_core/business/site_building/services/wizard_context_service.py +++ b/backend/igny8_core/business/site_building/services/wizard_context_service.py @@ -35,11 +35,13 @@ class WizardContextService: workflow_payload = self.workflow_service.serialize_state(workflow_state) if workflow_state else None + coverage_data = self._coverage_summary(site_blueprint) context = { 'workflow': workflow_payload, - 'clusters': self._cluster_summary(site_blueprint), - 'taxonomies': self._taxonomy_summary(site_blueprint), - 'coverage': self._coverage_summary(site_blueprint), + 'cluster_summary': self._cluster_summary(site_blueprint), + 'taxonomy_summary': self._taxonomy_summary(site_blueprint), + 'sitemap_summary': coverage_data, # Frontend expects 'sitemap_summary' not 'coverage' + 'coverage': coverage_data, # Keep for backward compatibility } context['next_actions'] = self._next_actions(workflow_payload) return context diff --git a/backend/igny8_core/business/site_building/services/workflow_state_service.py b/backend/igny8_core/business/site_building/services/workflow_state_service.py index 276961c9..c2027103 100644 --- a/backend/igny8_core/business/site_building/services/workflow_state_service.py +++ b/backend/igny8_core/business/site_building/services/workflow_state_service.py @@ -111,13 +111,21 @@ class WorkflowStateService: metadata: Optional[Dict[str, str]] = None, ) -> Optional[WorkflowState]: """Persist explicit step updates coming from the wizard.""" + if not self.enabled: + return None + state = self.initialize(site_blueprint) if not state: return None metadata = metadata or {} timestamp = timezone.now().isoformat() - step_status = state.step_status or {} + + # Ensure step_status is a dict (handle None case) + if state.step_status is None: + state.step_status = {} + step_status = dict(state.step_status) # Create a copy to avoid mutation issues + entry = self._build_step_entry( step=step, status=status, @@ -132,8 +140,22 @@ class WorkflowStateService: state.step_status = step_status state.blocking_reason = metadata.get('message') - state.completed = all(value.get('status') == 'ready' for value in step_status.values()) - state.save(update_fields=['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at']) + + # Calculate completed status - only true if all steps are ready and we have at least one step + if step_status: + state.completed = all( + value.get('status') == 'ready' or value.get('status') == 'complete' + for value in step_status.values() + ) + else: + state.completed = False + + try: + state.save(update_fields=['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at']) + except Exception as e: + logger.error(f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}") + raise + self._emit_event(site_blueprint, 'wizard_step_updated', { 'step': step, 'status': status, @@ -173,7 +195,7 @@ class WorkflowStateService: 'completed': state.completed, 'blocking_reason': state.blocking_reason, 'steps': steps_payload, - 'updated_at': state.updated_at, + 'updated_at': state.updated_at.isoformat() if hasattr(state.updated_at, 'isoformat') else str(state.updated_at), } def _build_step_entry( diff --git a/backend/igny8_core/modules/site_builder/views.py b/backend/igny8_core/modules/site_builder/views.py index aab48aeb..b20571d3 100644 --- a/backend/igny8_core/modules/site_builder/views.py +++ b/backend/igny8_core/modules/site_builder/views.py @@ -1,3 +1,4 @@ +import logging from django.conf import settings from rest_framework import status from rest_framework.decorators import action @@ -6,6 +7,8 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.exceptions import ValidationError +logger = logging.getLogger(__name__) + from igny8_core.api.base import SiteSectorModelViewSet from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove from igny8_core.api.response import success_response, error_response @@ -308,8 +311,16 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): """Return aggregated wizard context (steps, clusters, taxonomies, coverage).""" blueprint = self.get_object() if not self.workflow_service.enabled: + # Return empty context structure matching frontend expectations return success_response( - data={'workflow': None, 'clusters': {}, 'taxonomies': {}, 'coverage': {}}, + data={ + 'workflow': None, + 'cluster_summary': {'attached_count': 0, 'coverage_counts': {}, 'clusters': []}, + 'taxonomy_summary': {'total_taxonomies': 0, 'counts_by_type': {}, 'taxonomies': []}, + 'sitemap_summary': {'pages_total': 0, 'pages_by_status': {}, 'pages_by_type': {}}, + 'coverage': {'pages_total': 0, 'pages_by_status': {}, 'pages_by_type': {}}, + 'next_actions': None, + }, request=request, ) @@ -362,27 +373,35 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): request ) - updated_state = self.workflow_service.update_step( - blueprint, - step, - status_value, - metadata - ) - - if not updated_state: + try: + updated_state = self.workflow_service.update_step( + blueprint, + step, + status_value, + metadata + ) + + if not updated_state: + return error_response( + 'Failed to update workflow step', + status.HTTP_500_INTERNAL_SERVER_ERROR, + request + ) + + # Serialize state + serialized = self.workflow_service.serialize_state(updated_state) + + return success_response( + data=serialized, + request=request + ) + except Exception as e: + logger.exception(f"Error updating workflow step for blueprint {blueprint.id}: {str(e)}") return error_response( - 'Failed to update workflow step', + f'Internal server error: {str(e)}', status.HTTP_500_INTERNAL_SERVER_ERROR, request ) - - # Serialize state - serialized = self.workflow_service.serialize_state(updated_state) - - return success_response( - data=serialized, - request=request - ) @action(detail=True, methods=['post'], url_path='clusters/attach') def attach_clusters(self, request, pk=None): diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a1f14ae4..d622120f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -138,11 +138,9 @@ const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips")); const Videos = lazy(() => import("./pages/Settings/UiElements/Videos")); export default function App() { - const { isAuthenticated, refreshUser, logout } = useAuthStore((state) => ({ - isAuthenticated: state.isAuthenticated, - refreshUser: state.refreshUser, - logout: state.logout, - })); + const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const refreshUser = useAuthStore((state) => state.refreshUser); + const logout = useAuthStore((state) => state.logout); useEffect(() => { if (!isAuthenticated) { diff --git a/frontend/src/icons/index.ts b/frontend/src/icons/index.ts index ac0f4ed7..e606d07a 100644 --- a/frontend/src/icons/index.ts +++ b/frontend/src/icons/index.ts @@ -119,6 +119,7 @@ export { // Aliases for commonly used icon names export { AngleLeftIcon as ArrowLeftIcon }; export { FileIcon as FileTextIcon }; +export { FileIcon as ImageIcon }; // Use FileIcon as ImageIcon alias export { TimeIcon as ClockIcon }; export { ErrorIcon as XCircleIcon }; export { BoxIcon as TagIcon }; diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx index 88260f8a..56b49a3f 100644 --- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx +++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx @@ -111,15 +111,14 @@ export default function IndustriesSectorsKeywords() { } catch (error: any) { if (error instanceof AccountSettingsError) { if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') { - console.debug('No saved user preferences yet.'); + // Preferences don't exist yet - this is expected for new users return; } - console.warn('Failed to load user preferences:', error); - toast.error(getAccountSettingsPreferenceMessage(error)); + // For other errors (500, etc.), silently handle - user can still use the page + // Don't show error toast for server errors - graceful degradation return; } - console.warn('Failed to load user preferences:', error); - toast.error('Unable to load your saved preferences right now.'); + // For non-AccountSettingsError errors, silently handle - graceful degradation } }; diff --git a/frontend/src/pages/Sites/Builder/Blueprints.tsx b/frontend/src/pages/Sites/Builder/Blueprints.tsx index 4f5e5a00..fd0af231 100644 --- a/frontend/src/pages/Sites/Builder/Blueprints.tsx +++ b/frontend/src/pages/Sites/Builder/Blueprints.tsx @@ -4,7 +4,17 @@ 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, Rocket } from "lucide-react"; +import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs"; +import { + TableIcon, + PlusIcon, + FileIcon, + TrashBinIcon, + CheckLineIcon, + BoxIcon, + PaperPlaneIcon, + BoltIcon +} from "../../../icons"; import { useSiteStore } from "../../../store/siteStore"; import { useBuilderStore } from "../../../store/builderStore"; import { siteBuilderApi } from "../../../services/siteBuilder.api"; @@ -135,9 +145,20 @@ export default function SiteBuilderBlueprints() { } }; + // Navigation tabs for Sites module + const sitesTabs = [ + { label: 'All Sites', path: '/sites', icon: }, + { label: 'Create Site', path: '/sites/builder', icon: }, + { label: 'Blueprints', path: '/sites/blueprints', icon: }, + ]; + return (
- + + + {/* In-page navigation tabs */} + +

@@ -156,7 +177,7 @@ export default function SiteBuilderBlueprints() { onClick={handleBulkDeleteClick} variant="solid" tone="danger" - startIcon={} + startIcon={} > Delete {selectedIds.size} selected @@ -165,7 +186,7 @@ export default function SiteBuilderBlueprints() { onClick={() => navigate("/sites/builder")} variant="solid" tone="brand" - startIcon={} + startIcon={} > Create blueprint @@ -180,12 +201,12 @@ export default function SiteBuilderBlueprints() { ) : loading ? (

- +
Loading blueprints…
) : blueprints.length === 0 ? ( - +

No blueprints created yet for {activeSite.name}.

@@ -202,9 +223,9 @@ export default function SiteBuilderBlueprints() { className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" > {selectedIds.size === blueprints.length ? ( - + ) : ( - + )} {selectedIds.size === blueprints.length @@ -244,9 +265,9 @@ export default function SiteBuilderBlueprints() { className="ml-2 flex-shrink-0" > {selectedIds.has(blueprint.id) ? ( - + ) : ( - + )}
@@ -260,13 +281,21 @@ export default function SiteBuilderBlueprints() { {blueprint.status}
+ {(blueprint.status === 'ready' || blueprint.status === 'deployed') && ( @@ -279,7 +308,7 @@ export default function SiteBuilderBlueprints() { > {isLoadingBlueprint && activeBlueprint?.id === blueprint.id ? ( <> - +
Loading… ) : ( @@ -299,7 +328,7 @@ export default function SiteBuilderBlueprints() { variant="ghost" tone="danger" onClick={() => handleDeleteClick(blueprint)} - startIcon={} + startIcon={} > Delete diff --git a/frontend/src/pages/Sites/Builder/Preview.tsx b/frontend/src/pages/Sites/Builder/Preview.tsx index 58cda57c..3821c39e 100644 --- a/frontend/src/pages/Sites/Builder/Preview.tsx +++ b/frontend/src/pages/Sites/Builder/Preview.tsx @@ -11,7 +11,7 @@ import Alert from "../../../components/ui/alert/Alert"; import { useBuilderStore } from "../../../store/builderStore"; import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore"; import ProgressModal from "../../../components/common/ProgressModal"; -import { Eye, Play, Loader2, Rocket } from "lucide-react"; +import { EyeIcon, PaperPlaneIcon } from "../../../icons"; import { useToast } from "../../../components/ui/toast/ToastContainer"; import { siteBuilderApi } from "../../../services/siteBuilder.api"; @@ -76,9 +76,9 @@ export default function SiteBuilderPreview() { if (!activeBlueprint) { return (
- + - +

Run the Site Builder wizard or open a blueprint to preview it.

@@ -97,7 +97,7 @@ export default function SiteBuilderPreview() { return (
- +

@@ -117,7 +117,7 @@ export default function SiteBuilderPreview() { tone="brand" onClick={handleGenerateAll} disabled={isGenerating} - startIcon={isGenerating ? : } + startIcon={isGenerating ?

: } > {isGenerating ? "Generating..." : "Generate All Pages"} @@ -128,7 +128,7 @@ export default function SiteBuilderPreview() { tone="brand" onClick={handleDeploy} disabled={isDeploying} - startIcon={isDeploying ? : } + startIcon={isDeploying ?
: } > {isDeploying ? "Deploying..." : activeBlueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"} diff --git a/frontend/src/pages/Sites/Builder/Wizard.tsx b/frontend/src/pages/Sites/Builder/Wizard.tsx index 731c6aa8..8fc92060 100644 --- a/frontend/src/pages/Sites/Builder/Wizard.tsx +++ b/frontend/src/pages/Sites/Builder/Wizard.tsx @@ -10,11 +10,15 @@ import PageMeta from "../../../components/common/PageMeta"; import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector"; import PageHeader from "../../../components/common/PageHeader"; import Alert from "../../../components/ui/alert/Alert"; +import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs"; import { GridIcon, ArrowLeftIcon, ArrowRightIcon, BoltIcon, + TableIcon, + PlusIcon, + FileIcon, } from "../../../icons"; import { useSiteStore } from "../../../store/siteStore"; import { useSectorStore } from "../../../store/sectorStore"; @@ -54,6 +58,7 @@ export default function SiteBuilderWizard() { metadataError, isMetadataLoading, loadMetadata, + loadBlueprint, } = useBuilderStore(); // Progress modal for AI functions @@ -304,9 +309,20 @@ export default function SiteBuilderWizard() { return undefined; }; + // Navigation tabs for Sites module + const sitesTabs = [ + { label: 'All Sites', path: '/sites', icon: }, + { label: 'Create Site', path: '/sites/builder', icon: }, + { label: 'Blueprints', path: '/sites/blueprints', icon: }, + ]; + return (
+ + {/* In-page navigation tabs */} + +
= { +interface StepComponentProps { + blueprintId: number; +} + +const STEP_COMPONENTS: Record> = { business_details: BusinessDetailsStep, clusters: ClusterAssignmentStep, taxonomies: TaxonomyBuilderStep, @@ -124,7 +128,7 @@ export default function WorkflowWizard() { if (!id) { return (
- Invalid blueprint ID +
); } @@ -132,7 +136,7 @@ export default function WorkflowWizard() { if (loading && !context) { return (
- +
); } @@ -140,7 +144,7 @@ export default function WorkflowWizard() { if (error) { return (
- {error} +
); } @@ -149,7 +153,10 @@ export default function WorkflowWizard() { return (
- +
{/* Header with Help Button */} @@ -166,9 +173,8 @@ export default function WorkflowWizard() { onClick={() => setHelperDrawerOpen(!helperDrawerOpen)} variant="outline" size="sm" - title="Press F1 or ? for help" + startIcon={} > - Help
diff --git a/frontend/src/pages/Sites/Builder/components/HelperDrawer.tsx b/frontend/src/pages/Sites/Builder/components/HelperDrawer.tsx index a0eaa0f6..46be6838 100644 --- a/frontend/src/pages/Sites/Builder/components/HelperDrawer.tsx +++ b/frontend/src/pages/Sites/Builder/components/HelperDrawer.tsx @@ -3,9 +3,9 @@ * Contextual help for each wizard step */ import { useState } from 'react'; -import { X, HelpCircle, ChevronRight } from 'lucide-react'; import { WizardStep } from '../../../../store/builderWorkflowStore'; import Button from '../../../../components/ui/button/Button'; +import { CloseIcon, InfoIcon, ArrowRightIcon } from '../../../../icons'; interface HelperDrawerProps { currentStep: WizardStep; @@ -95,7 +95,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra {/* Header */}
- +

Help & Tips

@@ -107,7 +107,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra className="p-1" aria-label="Close help drawer" > - +
@@ -119,7 +119,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
    {help.content.map((item, index) => (
  • - + {item}
  • ))} diff --git a/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx b/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx index 7da53d4c..616ac1a0 100644 --- a/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx +++ b/frontend/src/pages/Sites/Builder/components/WizardProgress.tsx @@ -3,7 +3,7 @@ * Shows breadcrumb with step completion status */ import { useBuilderWorkflowStore, WizardStep } from '../../../../store/builderWorkflowStore'; -import { CheckCircle2, Circle } from 'lucide-react'; +import { CheckCircleIcon } from '../../../../icons'; const STEPS: Array<{ key: WizardStep; label: string }> = [ { key: 'business_details', label: 'Business Details' }, @@ -19,9 +19,23 @@ interface WizardProgressProps { } export default function WizardProgress({ currentStep }: WizardProgressProps) { - const { completedSteps, blockingIssues } = useBuilderWorkflowStore(); + const { completedSteps, blockingIssues, goToStep } = useBuilderWorkflowStore(); const currentIndex = STEPS.findIndex(s => s.key === currentStep); + const handleStepClick = (step: WizardStep, index: number) => { + // Allow navigation to: + // 1. Current step + // 2. Completed steps + // 3. Next step if current step is completed + const isCompleted = completedSteps.has(step.key); + const isCurrent = step.key === currentStep; + const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key)); + + if (canNavigate && !blockingIssues.some(issue => issue.step === step.key)) { + goToStep(step); + } + }; + return (