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

View File

@@ -1,8 +1,4 @@
import { ReactNode, useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useSettingsStore } from '../../store/settingsStore';
import { isModuleEnabled } from '../../config/modules.config';
import { isUpgradeError } from '../../utils/upgrade';
import { ReactNode } from 'react';
interface ModuleGuardProps {
module: string;
@@ -11,31 +7,12 @@ interface ModuleGuardProps {
}
/**
* ModuleGuard - Protects routes based on module enable status
* Redirects to settings page if module is disabled
* ModuleGuard - DEPRECATED
* 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) {
const { moduleEnableSettings, loadModuleEnableSettings, loading } = useSettingsStore();
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 />;
}
export default function ModuleGuard({ children }: ModuleGuardProps) {
// Module filtering removed - all modules always accessible
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[] {
return Object.entries(MODULES)
.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);
export function getEnabledModules(): ModuleConfig[] {
return Object.values(MODULES).filter(module => module.enabled);
}
/**
* 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];
if (!module) return false;
if (moduleEnableSettings) {
const enabledKey = `${moduleName}_enabled` as keyof typeof moduleEnableSettings;
return moduleEnableSettings[enabledKey] !== false; // Default to true if not set
}
return module.enabled;
return module ? module.enabled : false;
}

View File

@@ -41,14 +41,7 @@ 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();
// 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;
@@ -64,31 +57,10 @@ const AppSidebar: React.FC = () => {
);
// 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
// REMOVED: Module enable settings functionality - all modules always shown
// Module enable/disable is now controlled ONLY via Django Admin (GlobalModuleSettings)
// 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
const menuSections: MenuSection[] = useMemo(() => {
// 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)
if (moduleEnabled('thinker')) {
setupItems.push({
icon: <BoltIcon />,
name: "Thinker",
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
});
}
// Add Thinker (always shown)
setupItems.push({
icon: <BoltIcon />,
name: "Thinker",
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[] = [];
// Add Planner if enabled (single item, no dropdown)
if (moduleEnabled('planner')) {
workflowItems.push({
icon: <ListIcon />,
name: "Planner",
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
});
}
// Add Planner
workflowItems.push({
icon: <ListIcon />,
name: "Planner",
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
});
// Add Writer if enabled (single item, no dropdown)
if (moduleEnabled('writer')) {
workflowItems.push({
icon: <TaskIcon />,
name: "Writer",
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
});
}
// Add Writer
workflowItems.push({
icon: <TaskIcon />,
name: "Writer",
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
});
// Add Automation (always available if Writer is enabled)
if (moduleEnabled('writer')) {
workflowItems.push({
icon: <BoltIcon />,
name: "Automation",
path: "/automation",
});
}
// Add Automation
workflowItems.push({
icon: <BoltIcon />,
name: "Automation",
path: "/automation",
});
// Add Linker if enabled (single item, no dropdown)
if (moduleEnabled('linker')) {
workflowItems.push({
icon: <PlugInIcon />,
name: "Linker",
path: "/linker/content",
});
}
// Add Linker
workflowItems.push({
icon: <PlugInIcon />,
name: "Linker",
path: "/linker/content",
});
// Add Optimizer if enabled (single item, no dropdown)
if (moduleEnabled('optimizer')) {
workflowItems.push({
icon: <BoltIcon />,
name: "Optimizer",
path: "/optimizer/content",
});
}
// Add Optimizer
workflowItems.push({
icon: <BoltIcon />,
name: "Optimizer",
path: "/optimizer/content",
});
return [
// Dashboard is standalone (no section header)
@@ -243,7 +203,7 @@ const AppSidebar: React.FC = () => {
],
},
];
}, [moduleEnabled]);
}, []); // No dependencies - always show all modules
// Combine all sections
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
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 {
id: number;
module_name: string;
@@ -1834,9 +1820,6 @@ export interface ModuleSetting {
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[]> {
// fetchAPI extracts data from unified format {success: true, data: [...]}
// 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> {
return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, {
method: 'PUT',

View File

@@ -13,16 +13,13 @@ import {
fetchModuleSettings,
createModuleSetting,
updateModuleSetting,
fetchModuleEnableSettings,
updateModuleEnableSettings,
AccountSetting,
ModuleSetting,
ModuleEnableSettings,
AccountSettingsError,
} from '../services/api';
// Version for cache busting - increment when structure changes
const SETTINGS_STORE_VERSION = 2;
const SETTINGS_STORE_VERSION = 4;
const getAccountSettingsErrorMessage = (error: AccountSettingsError): string => {
switch (error.type) {
@@ -38,7 +35,6 @@ const getAccountSettingsErrorMessage = (error: AccountSettingsError): string =>
interface SettingsState {
accountSettings: Record<string, AccountSetting>;
moduleSettings: Record<string, Record<string, ModuleSetting>>;
moduleEnableSettings: ModuleEnableSettings | null;
loading: boolean;
error: string | null;
@@ -48,9 +44,6 @@ interface SettingsState {
updateAccountSetting: (key: string, value: any) => Promise<void>;
loadModuleSettings: (moduleName: string) => 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;
}
@@ -59,11 +52,8 @@ export const useSettingsStore = create<SettingsState>()(
(set, get) => ({
accountSettings: {},
moduleSettings: {},
moduleEnableSettings: null,
loading: false,
error: null,
_moduleEnableLastFetched: 0 as number | undefined,
_moduleEnableInFlight: null as Promise<ModuleEnableSettings> | null,
loadAccountSettings: async () => {
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: () => {
set({
accountSettings: {},
moduleSettings: {},
moduleEnableSettings: null,
loading: false,
error: null,
});
@@ -244,23 +184,11 @@ export const useSettingsStore = create<SettingsState>()(
}),
{
name: 'settings-storage',
version: SETTINGS_STORE_VERSION, // Add version for cache busting
version: SETTINGS_STORE_VERSION,
partialize: (state) => ({
accountSettings: state.accountSettings,
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;
},
}
)
);