diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py
index c4d8d47f..db4e68f1 100644
--- a/backend/igny8_core/modules/system/settings_views.py
+++ b/backend/igny8_core/modules/system/settings_views.py
@@ -13,10 +13,11 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu
from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
-from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings
+from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
+from .global_settings_models import GlobalModuleSettings
from .settings_serializers import (
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
- ModuleSettingsSerializer, ModuleEnableSettingsSerializer, AISettingsSerializer
+ ModuleSettingsSerializer, AISettingsSerializer
)
@@ -288,166 +289,48 @@ class ModuleSettingsViewSet(AccountModelViewSet):
@extend_schema_view(
list=extend_schema(tags=['System']),
retrieve=extend_schema(tags=['System']),
- update=extend_schema(tags=['System']),
- partial_update=extend_schema(tags=['System']),
)
-class ModuleEnableSettingsViewSet(AccountModelViewSet):
+class ModuleEnableSettingsViewSet(viewsets.ViewSet):
"""
- ViewSet for managing module enable/disable settings
- Unified API Standard v1.0 compliant
- One record per account
- Read access: All authenticated users
- Write access: Admins/Owners only
+ ViewSet for GLOBAL module enable/disable settings (read-only, public).
+ Returns platform-wide module availability from GlobalModuleSettings singleton.
+ No authentication required - these are public platform-wide settings.
+ Only superadmin can modify via Django Admin at /admin/system/globalmodulesettings/.
"""
- queryset = ModuleEnableSettings.objects.all()
- serializer_class = ModuleEnableSettingsSerializer
- authentication_classes = [JWTAuthentication]
+ authentication_classes = []
+ permission_classes = []
throttle_scope = 'system'
throttle_classes = [DebugScopedRateThrottle]
- def get_permissions(self):
- """
- Allow read access to all authenticated users,
- but restrict write access to admins/owners
- """
- if self.action in ['list', 'retrieve', 'get_current']:
- permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
- else:
- permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
- return [permission() for permission in permission_classes]
-
- def get_queryset(self):
- """Get module enable settings for current account"""
- # Return queryset filtered by account - but list() will handle get_or_create
- queryset = super().get_queryset()
- # Filter by account if available
- account = getattr(self.request, 'account', None)
- if not account:
- user = getattr(self.request, 'user', None)
- if user:
- account = getattr(user, 'account', None)
- if account:
- queryset = queryset.filter(account=account)
- return queryset
-
- @action(detail=False, methods=['get', 'put'], url_path='current', url_name='current')
- def get_current(self, request):
- """Get or update current account's module enable settings"""
- if request.method == 'GET':
- return self.list(request)
- else:
- return self.update(request, pk=None)
-
def list(self, request, *args, **kwargs):
- """Get or create module enable settings for current account"""
+ """Return global module settings (platform-wide, read-only)"""
try:
- account = getattr(request, 'account', None)
- if not account:
- user = getattr(request, 'user', None)
- if user and hasattr(user, 'account'):
- account = user.account
+ global_settings = GlobalModuleSettings.get_instance()
- if not account:
- return error_response(
- error='Account not found',
- status_code=status.HTTP_400_BAD_REQUEST,
- request=request
- )
-
- # Check if table exists (migration might not have been run)
- try:
- # Get or create settings for account (one per account)
- try:
- settings = ModuleEnableSettings.objects.get(account=account)
- except ModuleEnableSettings.DoesNotExist:
- # Create default settings for account
- settings = ModuleEnableSettings.objects.create(account=account)
-
- serializer = self.get_serializer(settings)
- return success_response(data=serializer.data, request=request)
- except Exception as db_error:
- # Check if it's a "table does not exist" error
- error_str = str(db_error)
- if 'does not exist' in error_str.lower() or 'relation' in error_str.lower():
- import logging
- logger = logging.getLogger(__name__)
- logger.error(f"ModuleEnableSettings table does not exist. Migration 0007_add_module_enable_settings needs to be run: {error_str}")
- return error_response(
- error='Module enable settings table not found. Please run migration: python manage.py migrate igny8_core_modules_system 0007',
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- request=request
- )
- # Re-raise other database errors
- raise
+ data = {
+ 'id': 1,
+ 'planner_enabled': global_settings.planner_enabled,
+ 'writer_enabled': global_settings.writer_enabled,
+ 'thinker_enabled': global_settings.thinker_enabled,
+ 'automation_enabled': global_settings.automation_enabled,
+ 'site_builder_enabled': global_settings.site_builder_enabled,
+ 'linker_enabled': global_settings.linker_enabled,
+ 'optimizer_enabled': global_settings.optimizer_enabled,
+ 'publisher_enabled': global_settings.publisher_enabled,
+ 'created_at': global_settings.created_at.isoformat() if global_settings.created_at else None,
+ 'updated_at': global_settings.updated_at.isoformat() if global_settings.updated_at else None,
+ }
+ return success_response(data=data, request=request)
except Exception as e:
- import traceback
- error_trace = traceback.format_exc()
return error_response(
- error=f'Failed to load module enable settings: {str(e)}',
+ error=f'Failed to load global module settings: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
def retrieve(self, request, pk=None, *args, **kwargs):
- """Get module enable settings for current account"""
- try:
- account = getattr(request, 'account', None)
- if not account:
- user = getattr(request, 'user', None)
- if user:
- account = getattr(user, 'account', None)
-
- if not account:
- return error_response(
- error='Account not found',
- status_code=status.HTTP_400_BAD_REQUEST,
- request=request
- )
-
- # Get or create settings for account
- settings, created = ModuleEnableSettings.objects.get_or_create(account=account)
- serializer = self.get_serializer(settings)
- return success_response(data=serializer.data, request=request)
- except Exception as e:
- return error_response(
- error=f'Failed to load module enable settings: {str(e)}',
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- request=request
- )
-
- def update(self, request, pk=None):
- """Update module enable settings for current account"""
- account = getattr(request, 'account', None)
- if not account:
- user = getattr(request, 'user', None)
- if user:
- account = getattr(user, 'account', None)
-
- if not account:
- return error_response(
- error='Account not found',
- status_code=status.HTTP_400_BAD_REQUEST,
- request=request
- )
-
- # Get or create settings for account
- settings = ModuleEnableSettings.get_or_create_for_account(account)
- serializer = self.get_serializer(settings, data=request.data, partial=True)
-
- if serializer.is_valid():
- serializer.save()
- return success_response(data=serializer.data, request=request)
-
- return error_response(
- error='Validation failed',
- errors=serializer.errors,
- status_code=status.HTTP_400_BAD_REQUEST,
- request=request
- )
-
- def partial_update(self, request, pk=None):
- """Partial update module enable settings"""
- return self.update(request, pk)
+ """Same as list - return global settings (singleton, pk ignored)"""
+ return self.list(request, *args, **kwargs)
@extend_schema_view(
diff --git a/backend/igny8_core/modules/system/urls.py b/backend/igny8_core/modules/system/urls.py
index 9c1508b2..aa2a08e4 100644
--- a/backend/igny8_core/modules/system/urls.py
+++ b/backend/igny8_core/modules/system/urls.py
@@ -52,11 +52,9 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
# Custom view for module enable settings to avoid URL routing conflict with ModuleSettingsViewSet
# This must be defined as a custom path BEFORE router.urls to ensure it matches first
-# The update method handles pk=None correctly, so we can use as_view
+# Read-only viewset - only GET is supported (modification via Django Admin only)
module_enable_viewset = ModuleEnableSettingsViewSet.as_view({
'get': 'list',
- 'put': 'update',
- 'patch': 'partial_update',
})
urlpatterns = [
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c77041ab..ec5620e5 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -9,6 +9,7 @@ import { AwsAdminGuard } from "./components/auth/AwsAdminGuard";
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
import { useAuthStore } from "./store/authStore";
+import { useModuleStore } from "./store/moduleStore";
// Auth pages - loaded immediately (needed for login)
import SignIn from "./pages/AuthPages/SignIn";
@@ -117,7 +118,13 @@ const Components = lazy(() => import("./pages/Components"));
export default function App() {
- // All session validation removed - API interceptor handles authentication
+ const { isAuthenticated } = useAuthStore();
+ const { loadModuleSettings } = useModuleStore();
+
+ // Load global module settings immediately on mount (public endpoint, no auth required)
+ useEffect(() => {
+ loadModuleSettings();
+ }, [loadModuleSettings]);
return (
<>
diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx
index 7076d19b..804d6567 100644
--- a/frontend/src/layout/AppSidebar.tsx
+++ b/frontend/src/layout/AppSidebar.tsx
@@ -23,12 +23,14 @@ import SidebarWidget from "./SidebarWidget";
import { APP_VERSION } from "../config/version";
import { useAuthStore } from "../store/authStore";
import { useSettingsStore } from "../store/settingsStore";
+import { useModuleStore } from "../store/moduleStore";
type NavItem = {
name: string;
icon: React.ReactNode;
path?: string;
subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[];
+ adminOnly?: boolean;
};
type MenuSection = {
@@ -40,17 +42,8 @@ const AppSidebar: React.FC = () => {
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
const location = useLocation();
const { user, isAuthenticated } = useAuthStore();
- const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled, loadModuleEnableSettings, loading: settingsLoading } = useSettingsStore();
+ const { isModuleEnabled, settings: moduleSettings } = useModuleStore();
- // Show admin menu only for aws-admin account users
- const isAwsAdminAccount = Boolean(user?.account?.slug === 'aws-admin');
-
- // Helper to check if module is enabled - memoized to prevent infinite loops
- const moduleEnabled = useCallback((moduleName: string): boolean => {
- if (!moduleEnableSettings) return true; // Default to enabled if not loaded
- return checkModuleEnabled(moduleName);
- }, [moduleEnableSettings, checkModuleEnabled]);
-
const [openSubmenu, setOpenSubmenu] = useState<{
sectionIndex: number;
itemIndex: number;
@@ -65,33 +58,9 @@ const AppSidebar: React.FC = () => {
[location.pathname]
);
- // Load module enable settings on mount (only once) - but only if user is authenticated
- useEffect(() => {
- // Only load if user is authenticated and settings aren't already loaded
- // Skip for non-module pages to reduce unnecessary calls (e.g., account/billing/signup)
- const path = location.pathname || '';
- const isModulePage = [
- '/planner',
- '/writer',
- '/automation',
- '/thinker',
- '/linker',
- '/optimizer',
- '/publisher',
- '/dashboard',
- '/home',
- ].some((p) => path.startsWith(p));
-
- if (user && isAuthenticated && isModulePage && !moduleEnableSettings && !settingsLoading) {
- loadModuleEnableSettings().catch((error) => {
- console.warn('Failed to load module enable settings:', error);
- });
- }
- }, [user, isAuthenticated, location.pathname]); // Only run when user/auth or route changes
-
// Define menu sections with useMemo to prevent recreation on every render
- // Filter out disabled modules based on module enable settings
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
+ // Module visibility is controlled by GlobalModuleSettings (Django Admin only)
const menuSections: MenuSection[] = useMemo(() => {
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
const setupItems: NavItem[] = [
@@ -100,15 +69,19 @@ const AppSidebar: React.FC = () => {
name: "Add Keywords",
path: "/setup/add-keywords",
},
- {
+ ];
+
+ // Add Sites (Site Builder) if enabled
+ if (isModuleEnabled('site_builder')) {
+ setupItems.push({
icon: