moduels setigns rmeove from frotneend

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-20 22:18:32 +00:00
parent 7a1e952a57
commit 5c9ef81aba
10 changed files with 90 additions and 597 deletions

View File

@@ -4,7 +4,7 @@ Settings Models Admin
from django.contrib import admin from django.contrib import admin
from unfold.admin import ModelAdmin from unfold.admin import ModelAdmin
from igny8_core.admin.base import AccountAdminMixin from igny8_core.admin.base import AccountAdminMixin
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
@admin.register(SystemSettings) @admin.register(SystemSettings)
@@ -93,19 +93,6 @@ class AISettingsAdmin(AccountAdminMixin, ModelAdmin):
get_account_display.short_description = 'Account' get_account_display.short_description = 'Account'
@admin.register(ModuleEnableSettings) # ModuleEnableSettings is DEPRECATED - use GlobalModuleSettings instead
class ModuleEnableSettingsAdmin(AccountAdminMixin, ModelAdmin): # GlobalModuleSettings is registered in admin.py (platform-wide, singleton)
list_display = [ # Old per-account ModuleEnableSettings model is no longer used
'account',
'planner_enabled',
'writer_enabled',
'thinker_enabled',
'automation_enabled',
'site_builder_enabled',
'linker_enabled',
'optimizer_enabled',
'publisher_enabled',
]
list_filter = ['planner_enabled', 'writer_enabled', 'thinker_enabled', 'automation_enabled', 'site_builder_enabled', 'linker_enabled', 'optimizer_enabled', 'publisher_enabled']
search_fields = ['account__name']

View File

@@ -2,7 +2,7 @@
Serializers for Settings Models Serializers for Settings Models
""" """
from rest_framework import serializers from rest_framework import serializers
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
from .validators import validate_settings_schema from .validators import validate_settings_schema
@@ -58,15 +58,8 @@ class ModuleSettingsSerializer(serializers.ModelSerializer):
return value return value
class ModuleEnableSettingsSerializer(serializers.ModelSerializer): # ModuleEnableSettingsSerializer is DEPRECATED - use GlobalModuleSettings instead
class Meta: # GlobalModuleSettings is managed via Django Admin only (no API serializer needed)
model = ModuleEnableSettings
fields = [
'id', 'planner_enabled', 'writer_enabled', 'thinker_enabled',
'automation_enabled', 'site_builder_enabled', 'linker_enabled',
'optimizer_enabled', 'publisher_enabled', 'created_at', 'updated_at'
]
read_only_fields = ['created_at', 'updated_at', 'account']
class AISettingsSerializer(serializers.ModelSerializer): class AISettingsSerializer(serializers.ModelSerializer):

View File

@@ -13,10 +13,11 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu
from igny8_core.api.pagination import CustomPageNumberPagination from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.api.throttles import DebugScopedRateThrottle from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner 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 ( from .settings_serializers import (
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer, SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
ModuleSettingsSerializer, ModuleEnableSettingsSerializer, AISettingsSerializer ModuleSettingsSerializer, AISettingsSerializer
) )
@@ -291,31 +292,20 @@ class ModuleSettingsViewSet(AccountModelViewSet):
update=extend_schema(tags=['System']), update=extend_schema(tags=['System']),
partial_update=extend_schema(tags=['System']), partial_update=extend_schema(tags=['System']),
) )
class ModuleEnableSettingsViewSet(AccountModelViewSet): class ModuleEnableSettingsViewSet(viewsets.ViewSet):
""" """
ViewSet for GLOBAL module enable/disable settings (read-only). ViewSet for GLOBAL module enable/disable settings (read-only).
Returns platform-wide module availability. Returns platform-wide module availability from GlobalModuleSettings singleton.
Only superadmin can modify via Django Admin. Only superadmin can modify via Django Admin at /admin/system/globalmodulesettings/.
""" """
queryset = ModuleEnableSettings.objects.all()
serializer_class = ModuleEnableSettingsSerializer
http_method_names = ['get'] # Read-only
authentication_classes = [JWTAuthentication] authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
throttle_scope = 'system' throttle_scope = 'system'
throttle_classes = [DebugScopedRateThrottle] throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
"""Read-only for all authenticated users"""
return [IsAuthenticatedAndActive(), HasTenantAccess()]
def get_queryset(self):
"""Return empty queryset (not used - we return global settings)"""
return ModuleEnableSettings.objects.none()
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
"""Return global module settings (platform-wide)""" """Return global module settings (platform-wide, read-only)"""
try: try:
from igny8_core.modules.system.global_settings_models import GlobalModuleSettings
global_settings = GlobalModuleSettings.get_instance() global_settings = GlobalModuleSettings.get_instance()
data = { data = {
@@ -334,134 +324,14 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
return success_response(data=data, request=request) return success_response(data=data, request=request)
except Exception as e: except Exception as e:
return error_response( return error_response(
error=str(e), error=f'Failed to load global module settings: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request request=request
) )
def retrieve(self, request, pk=None, *args, **kwargs): def retrieve(self, request, pk=None, *args, **kwargs):
"""Same as list - return global settings""" """Same as list - return global settings (singleton, pk ignored)"""
return self.list(request) return self.list(request, *args, **kwargs)
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"""
try:
account = getattr(request, 'account', None)
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'account'):
account = user.account
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
except Exception as e:
import traceback
error_trace = traceback.format_exc()
return error_response(
error=f'Failed to load module enable 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)
@extend_schema_view( @extend_schema_view(

View File

@@ -4,7 +4,6 @@ import { HelmetProvider } from "react-helmet-async";
import AppLayout from "./layout/AppLayout"; import AppLayout from "./layout/AppLayout";
import { ScrollToTop } from "./components/common/ScrollToTop"; import { ScrollToTop } from "./components/common/ScrollToTop";
import ProtectedRoute from "./components/auth/ProtectedRoute"; import ProtectedRoute from "./components/auth/ProtectedRoute";
import ModuleGuard from "./components/common/ModuleGuard";
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay"; import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
import LoadingStateMonitor from "./components/common/LoadingStateMonitor"; import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
import { useAuthStore } from "./store/authStore"; import { useAuthStore } from "./store/authStore";
@@ -81,7 +80,6 @@ const Users = lazy(() => import("./pages/Settings/Users"));
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions")); const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
const SystemSettings = lazy(() => import("./pages/Settings/System")); const SystemSettings = lazy(() => import("./pages/Settings/System"));
const AccountSettings = lazy(() => import("./pages/Settings/Account")); const AccountSettings = lazy(() => import("./pages/Settings/Account"));
const ModuleSettings = lazy(() => import("./pages/Settings/Modules"));
const AISettings = lazy(() => import("./pages/Settings/AI")); const AISettings = lazy(() => import("./pages/Settings/AI"));
const Plans = lazy(() => import("./pages/Settings/Plans")); const Plans = lazy(() => import("./pages/Settings/Plans"));
const Industries = lazy(() => import("./pages/Settings/Industries")); const Industries = lazy(() => import("./pages/Settings/Industries"));
@@ -142,115 +140,42 @@ export default function App() {
{/* Planner Module - Redirect dashboard to keywords */} {/* Planner Module - Redirect dashboard to keywords */}
<Route path="/planner" element={<Navigate to="/planner/keywords" replace />} /> <Route path="/planner" element={<Navigate to="/planner/keywords" replace />} />
<Route path="/planner/keywords" element={ <Route path="/planner/keywords" element={<Keywords />} />
<ModuleGuard module="planner"> <Route path="/planner/clusters" element={<Clusters />} />
<Keywords /> <Route path="/planner/clusters/:id" element={<ClusterDetail />} />
</ModuleGuard> <Route path="/planner/ideas" element={<Ideas />} />
} />
<Route path="/planner/clusters" element={
<ModuleGuard module="planner">
<Clusters />
</ModuleGuard>
} />
<Route path="/planner/clusters/:id" element={
<ModuleGuard module="planner">
<ClusterDetail />
</ModuleGuard>
} />
<Route path="/planner/ideas" element={
<ModuleGuard module="planner">
<Ideas />
</ModuleGuard>
} />
{/* Writer Module - Redirect dashboard to tasks */} {/* Writer Module - Redirect dashboard to tasks */}
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} /> <Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
<Route path="/writer/tasks" element={ <Route path="/writer/tasks" element={<Tasks />} />
<ModuleGuard module="writer">
<Tasks />
</ModuleGuard>
} />
{/* Writer Content Routes - Order matters: list route must come before detail route */} {/* Writer Content Routes - Order matters: list route must come before detail route */}
<Route path="/writer/content" element={ <Route path="/writer/content" element={<Content />} />
<ModuleGuard module="writer">
<Content />
</ModuleGuard>
} />
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */} {/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
<Route path="/writer/content/:id" element={ <Route path="/writer/content/:id" element={<ContentView />} />
<ModuleGuard module="writer">
<ContentView />
</ModuleGuard>
} />
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} /> <Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
<Route path="/writer/images" element={ <Route path="/writer/images" element={<Images />} />
<ModuleGuard module="writer"> <Route path="/writer/review" element={<Review />} />
<Images /> <Route path="/writer/published" element={<Published />} />
</ModuleGuard>
} />
<Route path="/writer/review" element={
<ModuleGuard module="writer">
<Review />
</ModuleGuard>
} />
<Route path="/writer/published" element={
<ModuleGuard module="writer">
<Published />
</ModuleGuard>
} />
{/* Automation Module */} {/* Automation Module */}
<Route path="/automation" element={<AutomationPage />} /> <Route path="/automation" element={<AutomationPage />} />
{/* Linker Module - Redirect dashboard to content */} {/* Linker Module - Redirect dashboard to content */}
<Route path="/linker" element={<Navigate to="/linker/content" replace />} /> <Route path="/linker" element={<Navigate to="/linker/content" replace />} />
<Route path="/linker/content" element={ <Route path="/linker/content" element={<LinkerContentList />} />
<ModuleGuard module="linker">
<LinkerContentList />
</ModuleGuard>
} />
{/* Optimizer Module - Redirect dashboard to content */} {/* Optimizer Module - Redirect dashboard to content */}
<Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} /> <Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} />
<Route path="/optimizer/content" element={ <Route path="/optimizer/content" element={<OptimizerContentSelector />} />
<ModuleGuard module="optimizer"> <Route path="/optimizer/analyze/:id" element={<AnalysisPreview />} />
<OptimizerContentSelector />
</ModuleGuard>
} />
<Route path="/optimizer/analyze/:id" element={
<ModuleGuard module="optimizer">
<AnalysisPreview />
</ModuleGuard>
} />
{/* Thinker Module */}
{/* Thinker Module - Redirect dashboard to prompts */} {/* Thinker Module - Redirect dashboard to prompts */}
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} /> <Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
<Route path="/thinker/prompts" element={ <Route path="/thinker/prompts" element={<Prompts />} />
<ModuleGuard module="thinker"> <Route path="/thinker/author-profiles" element={<AuthorProfiles />} />
<Prompts /> <Route path="/thinker/profile" element={<ThinkerProfile />} />
</ModuleGuard> <Route path="/thinker/strategies" element={<Strategies />} />
} /> <Route path="/thinker/image-testing" element={<ImageTesting />} />
<Route path="/thinker/author-profiles" element={
<ModuleGuard module="thinker">
<AuthorProfiles />
</ModuleGuard>
} />
<Route path="/thinker/profile" element={
<ModuleGuard module="thinker">
<ThinkerProfile />
</ModuleGuard>
} />
<Route path="/thinker/strategies" element={
<ModuleGuard module="thinker">
<Strategies />
</ModuleGuard>
} />
<Route path="/thinker/image-testing" element={
<ModuleGuard module="thinker">
<ImageTesting />
</ModuleGuard>
} />
{/* Billing Module */} {/* Billing Module */}
<Route path="/billing" element={<Navigate to="/billing/overview" replace />} /> <Route path="/billing" element={<Navigate to="/billing/overview" replace />} />
@@ -283,7 +208,6 @@ export default function App() {
<Route path="/settings/subscriptions" element={<Subscriptions />} /> <Route path="/settings/subscriptions" element={<Subscriptions />} />
<Route path="/settings/system" element={<SystemSettings />} /> <Route path="/settings/system" element={<SystemSettings />} />
<Route path="/settings/account" element={<AccountSettings />} /> <Route path="/settings/account" element={<AccountSettings />} />
<Route path="/settings/modules" element={<ModuleSettings />} />
<Route path="/settings/ai" element={<AISettings />} /> <Route path="/settings/ai" element={<AISettings />} />
<Route path="/settings/plans" element={<Plans />} /> <Route path="/settings/plans" element={<Plans />} />
<Route path="/settings/industries" element={<Industries />} /> <Route path="/settings/industries" element={<Industries />} />

View File

@@ -1,8 +1,4 @@
import { ReactNode, useEffect } from 'react'; import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';
import { useSettingsStore } from '../../store/settingsStore';
import { isModuleEnabled } from '../../config/modules.config';
import { isUpgradeError } from '../../utils/upgrade';
interface ModuleGuardProps { interface ModuleGuardProps {
module: string; module: string;
@@ -11,31 +7,12 @@ interface ModuleGuardProps {
} }
/** /**
* ModuleGuard - Protects routes based on module enable status * ModuleGuard - DEPRECATED
* Redirects to settings page if module is disabled * Module enable/disable is now controlled ONLY via Django Admin (GlobalModuleSettings)
* This component no longer checks module status - all modules are accessible
*/ */
export default function ModuleGuard({ module, children, redirectTo = '/settings/modules' }: ModuleGuardProps) { export default function ModuleGuard({ children }: ModuleGuardProps) {
const { moduleEnableSettings, loadModuleEnableSettings, loading } = useSettingsStore(); // Module filtering removed - all modules always accessible
useEffect(() => {
// Load module enable settings if not already loaded
if (!moduleEnableSettings && !loading) {
loadModuleEnableSettings();
}
}, [moduleEnableSettings, loading, loadModuleEnableSettings]);
// While loading, show children (optimistic rendering)
if (loading || !moduleEnableSettings) {
return <>{children}</>;
}
// Check if module is enabled
const enabled = isModuleEnabled(module, moduleEnableSettings as any);
if (!enabled) {
return <Navigate to={redirectTo} replace />;
}
return <>{children}</>; return <>{children}</>;
} }

View File

@@ -78,33 +78,17 @@ export function getModuleConfig(moduleName: string): ModuleConfig | undefined {
} }
/** /**
* Get all enabled modules * Get all enabled modules (all modules always enabled)
*/ */
export function getEnabledModules(moduleEnableSettings?: Record<string, boolean>): ModuleConfig[] { export function getEnabledModules(): ModuleConfig[] {
return Object.entries(MODULES) return Object.values(MODULES).filter(module => module.enabled);
.filter(([key, module]) => {
// If moduleEnableSettings provided, use it; otherwise default to enabled
if (moduleEnableSettings) {
const enabledKey = `${key}_enabled` as keyof typeof moduleEnableSettings;
return moduleEnableSettings[enabledKey] !== false; // Default to true if not set
}
return module.enabled;
})
.map(([, module]) => module);
} }
/** /**
* Check if a module is enabled * Check if a module is enabled (all modules always enabled)
*/ */
export function isModuleEnabled(moduleName: string, moduleEnableSettings?: Record<string, boolean>): boolean { export function isModuleEnabled(moduleName: string): boolean {
const module = MODULES[moduleName]; const module = MODULES[moduleName];
if (!module) return false; return module ? module.enabled : false;
if (moduleEnableSettings) {
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
return moduleEnableSettings[enabledKey] !== false; // Default to true if not set
}
return module.enabled;
} }

View File

@@ -41,13 +41,6 @@ const AppSidebar: React.FC = () => {
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar(); const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
const location = useLocation(); const location = useLocation();
const { user, isAuthenticated } = useAuthStore(); const { user, isAuthenticated } = useAuthStore();
const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled, loadModuleEnableSettings, loading: settingsLoading } = useSettingsStore();
// 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<{ const [openSubmenu, setOpenSubmenu] = useState<{
sectionIndex: number; sectionIndex: number;
@@ -64,31 +57,10 @@ const AppSidebar: React.FC = () => {
); );
// Load module enable settings on mount (only once) - but only if user is authenticated // Load module enable settings on mount (only once) - but only if user is authenticated
useEffect(() => { // REMOVED: Module enable settings functionality - all modules always shown
// Only load if user is authenticated and settings aren't already loaded // Module enable/disable is now controlled ONLY via Django Admin (GlobalModuleSettings)
// 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 // 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 // New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
const menuSections: MenuSection[] = useMemo(() => { const menuSections: MenuSection[] = useMemo(() => {
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation) // SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
@@ -105,62 +77,50 @@ const AppSidebar: React.FC = () => {
}, },
]; ];
// Add Thinker if enabled (single item, no dropdown) // Add Thinker (always shown)
if (moduleEnabled('thinker')) { setupItems.push({
setupItems.push({ icon: <BoltIcon />,
icon: <BoltIcon />, name: "Thinker",
name: "Thinker", path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation });
});
}
// WORKFLOW section items (single items, no dropdowns - submenus shown as in-page navigation) // WORKFLOW section items (all modules always shown)
const workflowItems: NavItem[] = []; const workflowItems: NavItem[] = [];
// Add Planner if enabled (single item, no dropdown) // Add Planner
if (moduleEnabled('planner')) { workflowItems.push({
workflowItems.push({ icon: <ListIcon />,
icon: <ListIcon />, name: "Planner",
name: "Planner", path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation });
});
}
// Add Writer if enabled (single item, no dropdown) // Add Writer
if (moduleEnabled('writer')) { workflowItems.push({
workflowItems.push({ icon: <TaskIcon />,
icon: <TaskIcon />, name: "Writer",
name: "Writer", path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation });
});
}
// Add Automation (always available if Writer is enabled) // Add Automation
if (moduleEnabled('writer')) { workflowItems.push({
workflowItems.push({ icon: <BoltIcon />,
icon: <BoltIcon />, name: "Automation",
name: "Automation", path: "/automation",
path: "/automation", });
});
}
// Add Linker if enabled (single item, no dropdown) // Add Linker
if (moduleEnabled('linker')) { workflowItems.push({
workflowItems.push({ icon: <PlugInIcon />,
icon: <PlugInIcon />, name: "Linker",
name: "Linker", path: "/linker/content",
path: "/linker/content", });
});
}
// Add Optimizer if enabled (single item, no dropdown) // Add Optimizer
if (moduleEnabled('optimizer')) { workflowItems.push({
workflowItems.push({ icon: <BoltIcon />,
icon: <BoltIcon />, name: "Optimizer",
name: "Optimizer", path: "/optimizer/content",
path: "/optimizer/content", });
});
}
return [ return [
// Dashboard is standalone (no section header) // Dashboard is standalone (no section header)
@@ -243,7 +203,7 @@ const AppSidebar: React.FC = () => {
], ],
}, },
]; ];
}, [moduleEnabled]); }, []); // No dependencies - always show all modules
// Combine all sections // Combine all sections
const allSections = useMemo(() => { const allSections = useMemo(() => {

View File

@@ -1,91 +0,0 @@
import { useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { useSettingsStore } from '../../store/settingsStore';
import { MODULES } from '../../config/modules.config';
import { Card } from '../../components/ui/card';
import Switch from '../../components/form/switch/Switch';
export default function ModuleSettings() {
const toast = useToast();
const {
moduleEnableSettings,
loadModuleEnableSettings,
updateModuleEnableSettings,
loading,
} = useSettingsStore();
useEffect(() => {
loadModuleEnableSettings();
}, [loadModuleEnableSettings]);
const handleToggle = async (moduleName: string, enabled: boolean) => {
try {
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
await updateModuleEnableSettings({
[enabledKey]: enabled,
} as any);
toast.success(`${MODULES[moduleName]?.name || moduleName} ${enabled ? 'enabled' : 'disabled'}`);
} catch (error: any) {
toast.error(`Failed to update module: ${error.message}`);
}
};
const getModuleEnabled = (moduleName: string): boolean => {
if (!moduleEnableSettings) return true; // Default to enabled
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
return moduleEnableSettings[enabledKey] !== false;
};
return (
<div className="p-6">
<PageMeta title="Module Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Module Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Enable or disable modules for your account</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="space-y-6">
{Object.entries(MODULES).map(([key, module]) => (
<div
key={key}
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-4">
<div className="text-2xl">{module.icon}</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{module.name}
</h3>
{module.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{module.description}
</p>
)}
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-600 dark:text-gray-400">
{getModuleEnabled(key) ? 'Enabled' : 'Disabled'}
</span>
<Switch
label=""
checked={getModuleEnabled(key)}
onChange={(enabled) => handleToggle(key, enabled)}
/>
</div>
</div>
))}
</div>
</Card>
)}
</div>
);
}

View File

@@ -1810,20 +1810,6 @@ export async function deleteUserSetting(key: string): Promise<void> {
} }
// Module Settings // Module Settings
export interface ModuleEnableSettings {
id: number;
planner_enabled: boolean;
writer_enabled: boolean;
thinker_enabled: boolean;
automation_enabled: boolean;
site_builder_enabled: boolean;
linker_enabled: boolean;
optimizer_enabled: boolean;
publisher_enabled: boolean;
created_at: string;
updated_at: string;
}
export interface ModuleSetting { export interface ModuleSetting {
id: number; id: number;
module_name: string; module_name: string;
@@ -1834,9 +1820,6 @@ export interface ModuleSetting {
updated_at: string; updated_at: string;
} }
// Deduplicate module-enable fetches to prevent 429s for normal users
let moduleEnableSettingsInFlight: Promise<ModuleEnableSettings> | null = null;
export async function fetchModuleSettings(moduleName: string): Promise<ModuleSetting[]> { export async function fetchModuleSettings(moduleName: string): Promise<ModuleSetting[]> {
// fetchAPI extracts data from unified format {success: true, data: [...]} // fetchAPI extracts data from unified format {success: true, data: [...]}
// So response IS the array, not an object with results // So response IS the array, not an object with results
@@ -1851,28 +1834,6 @@ export async function createModuleSetting(data: { module_name: string; key: stri
}); });
} }
export async function fetchModuleEnableSettings(): Promise<ModuleEnableSettings> {
if (moduleEnableSettingsInFlight) {
return moduleEnableSettingsInFlight;
}
moduleEnableSettingsInFlight = fetchAPI('/v1/system/settings/modules/enable/');
try {
const response = await moduleEnableSettingsInFlight;
return response;
} finally {
moduleEnableSettingsInFlight = null;
}
}
export async function updateModuleEnableSettings(data: Partial<ModuleEnableSettings>): Promise<ModuleEnableSettings> {
const response = await fetchAPI('/v1/system/settings/modules/enable/', {
method: 'PUT',
body: JSON.stringify(data),
});
return response;
}
export async function updateModuleSetting(moduleName: string, key: string, data: Partial<{ config: Record<string, any>; is_active: boolean }>): Promise<ModuleSetting> { export async function updateModuleSetting(moduleName: string, key: string, data: Partial<{ config: Record<string, any>; is_active: boolean }>): Promise<ModuleSetting> {
return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, { return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, {
method: 'PUT', method: 'PUT',

View File

@@ -13,16 +13,13 @@ import {
fetchModuleSettings, fetchModuleSettings,
createModuleSetting, createModuleSetting,
updateModuleSetting, updateModuleSetting,
fetchModuleEnableSettings,
updateModuleEnableSettings,
AccountSetting, AccountSetting,
ModuleSetting, ModuleSetting,
ModuleEnableSettings,
AccountSettingsError, AccountSettingsError,
} from '../services/api'; } from '../services/api';
// Version for cache busting - increment when structure changes // Version for cache busting - increment when structure changes
const SETTINGS_STORE_VERSION = 2; const SETTINGS_STORE_VERSION = 4;
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => { const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
switch (error.type) { switch (error.type) {
@@ -38,7 +35,6 @@ const getAccountSettingsErrorMessage = (error: AccountSettingsError): string =>
interface SettingsState { interface SettingsState {
accountSettings: Record<string, AccountSetting>; accountSettings: Record<string, AccountSetting>;
moduleSettings: Record<string, Record<string, ModuleSetting>>; moduleSettings: Record<string, Record<string, ModuleSetting>>;
moduleEnableSettings: ModuleEnableSettings | null;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
@@ -48,9 +44,6 @@ interface SettingsState {
updateAccountSetting: (key: string, value: any) => Promise<void>; updateAccountSetting: (key: string, value: any) => Promise<void>;
loadModuleSettings: (moduleName: string) => Promise<void>; loadModuleSettings: (moduleName: string) => Promise<void>;
updateModuleSetting: (moduleName: string, key: string, value: any) => Promise<void>; updateModuleSetting: (moduleName: string, key: string, value: any) => Promise<void>;
loadModuleEnableSettings: () => Promise<void>;
updateModuleEnableSettings: (data: Partial<ModuleEnableSettings>) => Promise<void>;
isModuleEnabled: (moduleName: string) => boolean;
reset: () => void; reset: () => void;
} }
@@ -59,11 +52,8 @@ export const useSettingsStore = create<SettingsState>()(
(set, get) => ({ (set, get) => ({
accountSettings: {}, accountSettings: {},
moduleSettings: {}, moduleSettings: {},
moduleEnableSettings: null,
loading: false, loading: false,
error: null, error: null,
_moduleEnableLastFetched: 0 as number | undefined,
_moduleEnableInFlight: null as Promise<ModuleEnableSettings> | null,
loadAccountSettings: async () => { loadAccountSettings: async () => {
set({ loading: true, error: null }); set({ loading: true, error: null });
@@ -183,60 +173,10 @@ export const useSettingsStore = create<SettingsState>()(
} }
}, },
loadModuleEnableSettings: async () => {
const state = get() as any;
const now = Date.now();
// Use cached value if fetched within last 60s
if (state.moduleEnableSettings && state._moduleEnableLastFetched && now - state._moduleEnableLastFetched < 60000) {
return;
}
// Coalesce concurrent calls
if (state._moduleEnableInFlight) {
await state._moduleEnableInFlight;
return;
}
set({ loading: true, error: null });
try {
const inFlight = fetchModuleEnableSettings();
(state as any)._moduleEnableInFlight = inFlight;
const settings = await inFlight;
set({ moduleEnableSettings: settings, loading: false, _moduleEnableLastFetched: Date.now() });
} catch (error: any) {
// On 429/403, avoid loops; cache the failure timestamp and do not retry automatically
if (error?.status === 429 || error?.status === 403) {
set({ loading: false, _moduleEnableLastFetched: Date.now() });
return;
}
set({ error: error.message, loading: false, _moduleEnableLastFetched: Date.now() });
} finally {
(get() as any)._moduleEnableInFlight = null;
}
},
updateModuleEnableSettings: async (data: Partial<ModuleEnableSettings>) => {
set({ loading: true, error: null });
try {
const settings = await updateModuleEnableSettings(data);
set({ moduleEnableSettings: settings, loading: false });
} catch (error: any) {
set({ error: error.message, loading: false });
throw error;
}
},
isModuleEnabled: (moduleName: string): boolean => {
const settings = get().moduleEnableSettings;
if (!settings) return true; // Default to enabled if not loaded
const enabledKey = `${moduleName}_enabled` as keyof ModuleEnableSettings;
return settings[enabledKey] !== false; // Default to true if not set
},
reset: () => { reset: () => {
set({ set({
accountSettings: {}, accountSettings: {},
moduleSettings: {}, moduleSettings: {},
moduleEnableSettings: null,
loading: false, loading: false,
error: null, error: null,
}); });
@@ -244,23 +184,11 @@ export const useSettingsStore = create<SettingsState>()(
}), }),
{ {
name: 'settings-storage', name: 'settings-storage',
version: SETTINGS_STORE_VERSION, // Add version for cache busting version: SETTINGS_STORE_VERSION,
partialize: (state) => ({ partialize: (state) => ({
accountSettings: state.accountSettings, accountSettings: state.accountSettings,
moduleSettings: state.moduleSettings, moduleSettings: state.moduleSettings,
moduleEnableSettings: state.moduleEnableSettings,
}), }),
// Migrate function to handle version changes
migrate: (persistedState: any, version: number) => {
if (version < SETTINGS_STORE_VERSION) {
// Clear module enable settings on version upgrade
return {
...persistedState,
moduleEnableSettings: null,
};
}
return persistedState;
},
} }
) )
); );