diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule
index f09a9a93..8ee23a85 100644
Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ
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}
+
navigate(`/sites/builder/workflow/${blueprint.id}`)}
+ startIcon={ }
+ >
+ Continue Workflow
+
{(blueprint.status === 'ready' || blueprint.status === 'deployed') && (
handleDeploy(blueprint)}
- startIcon={deployingId === blueprint.id ? : }
+ startIcon={deployingId === blueprint.id ?
: }
>
{deployingId === blueprint.id ? "Deploying..." : blueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
@@ -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 (
);
}
@@ -132,7 +136,7 @@ export default function WorkflowWizard() {
if (loading && !context) {
return (
);
}
@@ -140,7 +144,7 @@ export default function WorkflowWizard() {
if (error) {
return (
);
}
@@ -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 (
@@ -31,34 +45,39 @@ export default function WizardProgress({ currentStep }: WizardProgressProps) {
const isCurrent = step.key === currentStep;
const isBlocked = blockingIssues.some(issue => issue.step === step.key);
const isAccessible = index <= currentIndex || isCompleted;
+ const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
return (
{/* Step Circle */}
-
handleStepClick(step.key, index)}
+ disabled={!canNavigate || isBlocked}
className={`
- flex items-center justify-center w-10 h-10 rounded-full border-2
+ flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all
${
isCompleted
- ? 'bg-green-500 border-green-500 text-white'
+ ? 'bg-green-500 border-green-500 text-white cursor-pointer hover:bg-green-600'
: isCurrent
- ? 'bg-primary border-primary text-white'
+ ? 'bg-primary border-primary text-white cursor-default'
: isBlocked
- ? 'bg-red-100 border-red-500 text-red-500'
- : isAccessible
- ? 'bg-gray-100 border-gray-300 text-gray-600'
- : 'bg-gray-50 border-gray-200 text-gray-400'
+ ? 'bg-red-100 border-red-500 text-red-500 cursor-not-allowed'
+ : canNavigate
+ ? 'bg-gray-100 border-gray-300 text-gray-600 cursor-pointer hover:bg-gray-200 hover:border-gray-400'
+ : 'bg-gray-50 border-gray-200 text-gray-400 cursor-not-allowed'
}
`}
+ title={isBlocked ? 'This step is blocked' : canNavigate ? `Go to ${step.label}` : 'Complete previous steps first'}
>
{isCompleted ? (
-
+
) : (
{index + 1}
)}
-
+
(null);
const [formData, setFormData] = useState({
name: '',
@@ -100,13 +100,52 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
});
setBlueprint(updated);
- // Mark step as complete
- await completeStep('business_details', {
- blueprint_name: formData.name,
- hosting_type: formData.hosting_type,
- });
+ // Mark step as complete - catch and handle workflow errors separately
+ try {
+ await completeStep('business_details', {
+ blueprint_name: formData.name,
+ hosting_type: formData.hosting_type,
+ });
+ // If successful, automatically advance to next step
+ const { goToStep } = useBuilderWorkflowStore.getState();
+ goToStep('clusters');
+ } catch (workflowErr: any) {
+ // If workflow update fails but blueprint was saved, mark step as complete locally and advance
+ const workflowErrorMsg = workflowErr?.response?.error ||
+ workflowErr?.response?.message ||
+ workflowErr?.message ||
+ 'Workflow step update failed';
+
+ // Check if it's a server error (500) - might be workflow service not enabled
+ const isServerError = workflowErr?.status === 500;
+ const errorDetail = workflowErr?.response?.error || workflowErr?.response?.message || '';
+
+ if (isServerError && errorDetail.includes('Workflow service not enabled')) {
+ // Workflow service is disabled - just advance without marking as complete
+ console.warn('Workflow service not enabled, advancing to next step');
+ const { goToStep } = useBuilderWorkflowStore.getState();
+ goToStep('clusters');
+ // Don't show error - workflow is optional
+ return;
+ }
+
+ console.warn('Workflow step update failed:', workflowErrorMsg, workflowErr);
+
+ // Mark step as completed locally so user can proceed
+ const { completedSteps, goToStep } = useBuilderWorkflowStore.getState();
+ const updatedCompletedSteps = new Set(completedSteps);
+ updatedCompletedSteps.add('business_details');
+ useBuilderWorkflowStore.setState({
+ completedSteps: updatedCompletedSteps,
+ currentStep: 'clusters' // Advance to step 2
+ });
+
+ // Show a non-blocking warning to the user
+ setError(`Blueprint saved successfully, but workflow step update failed: ${workflowErrorMsg}. You can continue to the next step.`);
+ }
} catch (err: any) {
- setError(err.message || 'Failed to save business details');
+ const errorMessage = err?.response?.error || err?.message || 'Failed to save business details';
+ setError(errorMessage);
} finally {
setSaving(false);
}
@@ -124,9 +163,7 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
{error && (
-
- {error}
-
+
)}
@@ -307,6 +344,7 @@ function BusinessDetailsStepStage1({
onChange={(e) => onChange('industry', e.target.value)}
placeholder={userPreferences?.selectedIndustry || "Supply chain automation"}
/>
+
Hosting preference
diff --git a/frontend/src/pages/Sites/Builder/steps/ClusterAssignmentStep.tsx b/frontend/src/pages/Sites/Builder/steps/ClusterAssignmentStep.tsx
index 23007c88..c0e30272 100644
--- a/frontend/src/pages/Sites/Builder/steps/ClusterAssignmentStep.tsx
+++ b/frontend/src/pages/Sites/Builder/steps/ClusterAssignmentStep.tsx
@@ -27,7 +27,7 @@ import {
} from '../../../../components/ui/table';
import { useToast } from '../../../../components/ui/toast/ToastContainer';
import { useSectorStore } from '../../../../store/sectorStore';
-import { Loader2, CheckCircle2Icon, XCircleIcon } from 'lucide-react';
+import { CheckCircleIcon, XCircleIcon } from '../../../../icons';
interface ClusterAssignmentStepProps {
blueprintId: number;
@@ -218,16 +218,16 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
Total Clusters
-
{context.cluster_summary.total}
+
{context.cluster_summary.clusters?.length || 0}
Attached
-
{context.cluster_summary.attached}
+
{context.cluster_summary.attached_count || 0}
Coverage
- {context.cluster_summary.coverage_stats.complete} / {context.cluster_summary.total}
+ {context.cluster_summary.coverage_counts?.complete || 0} / {context.cluster_summary.clusters?.length || 0}
@@ -291,7 +291,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
>
{attaching ? (
<>
-
+
Attaching...
>
) : (
@@ -305,7 +305,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
>
{detaching ? (
<>
-
+
Detaching...
>
) : (
@@ -319,7 +319,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
{/* Cluster Table */}
{loading ? (
) : filteredClusters.length === 0 ? (
@@ -371,7 +371,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
)}
-
{cluster.keywords_count || 0}
+
{cluster.keyword_count || 0}
{cluster.volume?.toLocaleString() || 0}
{isAttached ? (
-
+
Attached
) : (
@@ -433,7 +433,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
>
{workflowLoading ? (
<>
-
+
Loading...
>
) : (
diff --git a/frontend/src/pages/Sites/Builder/steps/CoverageValidationStep.tsx b/frontend/src/pages/Sites/Builder/steps/CoverageValidationStep.tsx
index 59bc5b99..bd180090 100644
--- a/frontend/src/pages/Sites/Builder/steps/CoverageValidationStep.tsx
+++ b/frontend/src/pages/Sites/Builder/steps/CoverageValidationStep.tsx
@@ -15,20 +15,22 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
const { context, completeStep, blockingIssues } = useBuilderWorkflowStore();
const coverageBlocking = blockingIssues.find(issue => issue.step === 'coverage');
- const clusterStats = context?.cluster_summary?.coverage_stats || {
- complete: 0,
- in_progress: 0,
- pending: 0,
+ const clusterStats = {
+ complete: context?.cluster_summary?.coverage_counts?.complete || 0,
+ in_progress: context?.cluster_summary?.coverage_counts?.in_progress || 0,
+ pending: context?.cluster_summary?.coverage_counts?.pending || 0,
};
- const totalClusters = context?.cluster_summary?.total || 0;
- const attachedClusters = context?.cluster_summary?.attached || 0;
+ const totalClusters = context?.cluster_summary?.clusters?.length || 0;
+ const attachedClusters = context?.cluster_summary?.attached_count || 0;
const clusterCoverage = totalClusters > 0 ? (attachedClusters / totalClusters) * 100 : 0;
- const totalTaxonomies = context?.taxonomy_summary?.total || 0;
- const taxonomyByType = context?.taxonomy_summary?.by_type || {};
+ const totalTaxonomies = context?.taxonomy_summary?.total_taxonomies || 0;
+ const taxonomyByType = context?.taxonomy_summary?.counts_by_type || {};
- const sitemapCoverage = context?.sitemap_summary?.coverage_percentage || 0;
- const totalPages = context?.sitemap_summary?.total_pages || 0;
+ // Calculate sitemap coverage from pages_by_status if available
+ const sitemapPages = context?.sitemap_summary?.pages_total || 0;
+ const sitemapCoverage = sitemapPages > 0 ? 100 : 0; // Simplified - backend doesn't provide coverage_percentage
+ const totalPages = sitemapPages;
const getCoverageStatus = (percentage: number) => {
if (percentage >= 90) return { status: 'excellent', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' };
@@ -152,9 +154,9 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
Total Pages:
{totalPages}
- {context?.sitemap_summary?.by_type && (
+ {context?.sitemap_summary?.pages_by_type && (
- {Object.entries(context.sitemap_summary.by_type).map(([type, count]) => (
+ {Object.entries(context.sitemap_summary.pages_by_type).map(([type, count]) => (
{type}:
{count as number}
diff --git a/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx b/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx
index 0c560efa..18f0b343 100644
--- a/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx
+++ b/frontend/src/pages/Sites/Builder/steps/ObjectivesStep.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import type { BuilderFormData } from "../../../../types/siteBuilder";
-import { Card } from "../../../../components/ui/card/Card";
+import { Card } from "../../../../components/ui/card";
import Button from "../../../../components/ui/button/Button";
const inputClass =
diff --git a/frontend/src/pages/Sites/Builder/steps/SitemapReviewStep.tsx b/frontend/src/pages/Sites/Builder/steps/SitemapReviewStep.tsx
index 27032d2d..816af5b1 100644
--- a/frontend/src/pages/Sites/Builder/steps/SitemapReviewStep.tsx
+++ b/frontend/src/pages/Sites/Builder/steps/SitemapReviewStep.tsx
@@ -135,16 +135,22 @@ export default function SitemapReviewStep({ blueprintId }: SitemapReviewStepProp
Total Pages:
- {context.sitemap_summary.total_pages}
+ {context.sitemap_summary.pages_total || 0}
-
Coverage:
-
{context.sitemap_summary.coverage_percentage}%
+
By Status:
+
+ {Object.entries(context.sitemap_summary.pages_by_status || {}).map(([status, count]) => (
+
+ {status}: {count}
+
+ ))}
+
By Type:
- {Object.entries(context.sitemap_summary.by_type).map(([type, count]) => (
+ {Object.entries(context.sitemap_summary.pages_by_type || {}).map(([type, count]) => (
{type}: {count}
diff --git a/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx b/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx
index 24b7a461..17fb3b9d 100644
--- a/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx
+++ b/frontend/src/pages/Sites/Builder/steps/StyleStep.tsx
@@ -6,7 +6,7 @@ import type {
} from "../../../../types/siteBuilder";
import { Card } from "../../../../components/ui/card";
import { Dropdown } from "../../../../components/ui/dropdown/Dropdown";
-import { Check } from "lucide-react";
+import { CheckLineIcon } from "../../../../icons";
const labelClass =
"text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
@@ -197,7 +197,7 @@ export function StyleStep({
)}
- {isSelected &&
}
+ {isSelected &&
}
);
})
@@ -312,7 +312,7 @@ export function StyleStep({
)}
- {isSelected &&
}
+ {isSelected &&
}
);
})
diff --git a/frontend/src/pages/Sites/Builder/steps/TaxonomyBuilderStep.tsx b/frontend/src/pages/Sites/Builder/steps/TaxonomyBuilderStep.tsx
index ccb2bbe1..551bf219 100644
--- a/frontend/src/pages/Sites/Builder/steps/TaxonomyBuilderStep.tsx
+++ b/frontend/src/pages/Sites/Builder/steps/TaxonomyBuilderStep.tsx
@@ -29,7 +29,7 @@ import {
} from '../../../../components/ui/table';
import { useToast } from '../../../../components/ui/toast/ToastContainer';
import { useSectorStore } from '../../../../store/sectorStore';
-import { Loader2, PlusIcon, UploadIcon, EditIcon, TrashIcon } from 'lucide-react';
+import { PlusIcon, DownloadIcon, PencilIcon, TrashBinIcon } from '../../../../icons';
import FormModal from '../../../../components/common/FormModal';
interface TaxonomyBuilderStepProps {
@@ -223,9 +223,9 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
Total Taxonomies
-
{context.taxonomy_summary.total}
+
{context.taxonomy_summary.total_taxonomies}
- {Object.entries(context.taxonomy_summary.by_type || {}).slice(0, 3).map(([type, count]) => (
+ {Object.entries(context.taxonomy_summary.counts_by_type || {}).slice(0, 3).map(([type, count]) => (
{type.replace('_', ' ')}
@@ -269,7 +269,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
onClick={handleImport}
variant="outline"
>
-
+
Import
@@ -279,7 +279,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
{/* Taxonomy Table */}
{loading ? (
) : filteredTaxonomies.length === 0 ? (
@@ -333,7 +333,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
size="sm"
onClick={() => handleOpenModal(taxonomy)}
>
-
+
@@ -414,7 +414,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
>
{workflowLoading ? (
<>
-
+
Loading...
>
) : (
diff --git a/frontend/src/pages/Sites/List.tsx b/frontend/src/pages/Sites/List.tsx
index b6d373ad..33659b1e 100644
--- a/frontend/src/pages/Sites/List.tsx
+++ b/frontend/src/pages/Sites/List.tsx
@@ -15,6 +15,7 @@ import FormModal, { FormField } from '../../components/common/FormModal';
import Alert from '../../components/ui/alert/Alert';
import Switch from '../../components/form/switch/Switch';
import ViewToggle from '../../components/common/ViewToggle';
+import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import {
PlusIcon,
PencilIcon,
@@ -108,9 +109,13 @@ export default function SiteList() {
}
} catch (error: any) {
// 404 means preferences don't exist yet - that's fine
- if (error.status !== 404) {
- console.warn('Failed to load user preferences:', error);
+ // 500 and other errors should be handled silently - user can still use the page
+ if (error?.status === 404) {
+ // Preferences don't exist yet - this is expected for new users
+ return;
}
+ // Silently handle other errors (500, network errors, etc.) - don't spam console
+ // User can still use the page without preferences
}
};
@@ -166,10 +171,9 @@ export default function SiteList() {
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
}
} catch (error: any) {
- // 404 means preferences don't exist yet - show all industries
- if (error.status !== 404) {
- console.warn('Failed to load user preferences for filtering:', error);
- }
+ // 404 means preferences don't exist yet - show all industries (expected for new users)
+ // 500 and other errors - show all industries (graceful degradation)
+ // Silently handle errors - user can still use the page
}
setIndustries(allIndustries);
@@ -678,6 +682,13 @@ export default function SiteList() {
);
}
+ // 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 (
@@ -687,6 +698,9 @@ export default function SiteList() {
hideSiteSector={true}
/>
+ {/* In-page navigation tabs */}
+
+
{/* Info Alert */}
;
- blocking_reason?: string;
completed: boolean;
+ blocking_reason?: string;
+ steps: Array<{
+ step: string;
+ status: string;
+ code?: string;
+ message?: string;
+ updated_at?: string;
+ }>;
updated_at: string;
}
export interface WizardContext {
workflow: WorkflowState;
cluster_summary: {
- total: number;
- attached: number;
- coverage_stats: {
- complete: number;
- in_progress: number;
- pending: number;
- };
+ attached_count: number;
+ coverage_counts: Record;
clusters: Array<{
id: number;
name: string;
- keywords_count: number;
+ keyword_count: number;
volume: number;
context_type?: string;
dimension_meta?: Record;
coverage_status: string;
role: string;
+ metadata?: Record;
+ suggested_taxonomies?: string[];
+ attribute_hints?: string[];
}>;
};
taxonomy_summary: {
- total: number;
- by_type: Record;
+ total_taxonomies: number;
+ counts_by_type: Record;
taxonomies: Array<{
id: number;
name: string;
slug: string;
taxonomy_type: string;
- cluster_count: number;
+ description?: string;
+ cluster_ids: number[];
+ metadata?: Record;
+ external_reference?: string;
}>;
};
- sitemap_summary: {
- total_pages: number;
- by_type: Record;
- coverage_percentage: number;
+ sitemap_summary?: {
+ pages_total: number;
+ pages_by_status: Record;
+ pages_by_type: Record;
+ };
+ next_actions?: {
+ step: string | null;
+ status: string;
+ message: string | null;
+ code: string | null;
};
- next_actions: Array<{
- step: string;
- action: string;
- message: string;
- }>;
}
export async function fetchSiteBlueprints(filters?: {
diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts
index a366efa1..a2aaf4cd 100644
--- a/frontend/src/store/authStore.ts
+++ b/frontend/src/store/authStore.ts
@@ -235,8 +235,23 @@ export const useAuthStore = create()(
set({ user: refreshedUser, isAuthenticated: true });
} catch (error: any) {
- console.warn('Failed to refresh user data:', error);
- set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
+ // Only logout on authentication/authorization errors, not on network errors
+ // Network errors (500, timeout, etc.) should not log the user out
+ const isAuthError = error?.code === 'ACCOUNT_REQUIRED' ||
+ error?.code === 'PLAN_REQUIRED' ||
+ error?.status === 401 ||
+ error?.status === 403 ||
+ (error?.message && error.message.includes('Not authenticated'));
+
+ if (isAuthError) {
+ // Real authentication error - logout user
+ console.warn('Authentication error during refresh, logging out:', error);
+ set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
+ } else {
+ // Network/server error - don't logout, just throw the error
+ // The caller (AppLayout) will handle it gracefully
+ console.debug('Non-auth error during refresh (will retry):', error);
+ }
throw error;
}
},
diff --git a/frontend/src/store/builderWorkflowStore.ts b/frontend/src/store/builderWorkflowStore.ts
index 07a54f49..6059c773 100644
--- a/frontend/src/store/builderWorkflowStore.ts
+++ b/frontend/src/store/builderWorkflowStore.ts
@@ -70,21 +70,38 @@ export const useBuilderWorkflowStore = create()(
set({ blueprintId, loading: true, error: null });
try {
const context = await fetchWizardContext(blueprintId);
- const workflow = context.workflow;
+ const workflow = context?.workflow;
+
+ // If workflow is null, initialize with defaults
+ if (!workflow) {
+ set({
+ blueprintId,
+ currentStep: DEFAULT_STEP,
+ completedSteps: new Set(),
+ blockingIssues: [],
+ workflowState: null,
+ context,
+ loading: false,
+ error: null,
+ });
+ return;
+ }
// Determine completed steps from workflow state
+ // Backend returns 'steps' as an array, not 'step_status' as an object
const completedSteps = new Set();
- Object.entries(workflow.step_status || {}).forEach(([step, status]) => {
- if (status.status === 'ready' || status.status === 'complete') {
- completedSteps.add(step as WizardStep);
+ const steps = workflow.steps || [];
+ steps.forEach((stepData: any) => {
+ if (stepData?.status === 'ready' || stepData?.status === 'complete') {
+ completedSteps.add(stepData.step as WizardStep);
}
});
// Extract blocking issues
const blockingIssues: Array<{ step: WizardStep; message: string }> = [];
- Object.entries(workflow.step_status || {}).forEach(([step, status]) => {
- if (status.status === 'blocked' && status.message) {
- blockingIssues.push({ step: step as WizardStep, message: status.message });
+ steps.forEach((stepData: any) => {
+ if (stepData?.status === 'blocked' && stepData?.message) {
+ blockingIssues.push({ step: stepData.step as WizardStep, message: stepData.message });
}
});
@@ -133,11 +150,17 @@ export const useBuilderWorkflowStore = create()(
},
completeStep: async (step: WizardStep, metadata?: Record) => {
- const { blueprintId } = get();
+ const { blueprintId, workflowState } = get();
if (!blueprintId) {
throw new Error('No blueprint initialized');
}
+ // Ensure workflow is initialized before updating
+ if (!workflowState) {
+ // Try to initialize first
+ await get().initialize(blueprintId);
+ }
+
set({ loading: true, error: null });
try {
const updatedState = await updateWorkflowStep(blueprintId, step, 'ready', metadata);
@@ -161,8 +184,13 @@ export const useBuilderWorkflowStore = create()(
// Emit telemetry
get().flushTelemetry();
} catch (error: any) {
+ // Extract more detailed error message if available
+ const errorMessage = error?.response?.error ||
+ error?.response?.message ||
+ error?.message ||
+ `Failed to complete step: ${step}`;
set({
- error: error.message || `Failed to complete step: ${step}`,
+ error: errorMessage,
loading: false,
});
throw error;