-
+
-
Acoount Limits Usage 12
-
Monitor your plan limits and usage statistics
+
Credit Usage & Limits
+
Monitor your credit usage and account management limits
- {/* Debug Info - Remove in production */}
- {import.meta.env.DEV && (
-
-
- Debug: Loading={limitsLoading ? 'Yes' : 'No'}, Limits={limits.length},
- Planner={groupedLimits.planner.length}, Writer={groupedLimits.writer.length},
- Images={groupedLimits.images.length}, AI={groupedLimits.ai.length}, General={groupedLimits.general.length}
-
-
- )}
+ {/* Credit Costs Reference */}
+
+ Credit Costs per Operation
+
+ {Object.entries(CREDIT_COSTS).map(([operation, info]) => (
+
+
+
+ {operation.replace(/_/g, ' ')}
+
+
+ {info.description}
+
+
+
+
+ {typeof info.cost === 'number' ? `${info.cost} credits` : info.cost}
+
+
+
+ ))}
+
+
- {/* Limit Cards by Category */}
+ {/* Credit Limits */}
{limitsLoading ? (
- ) : limits.length === 0 ? (
-
-
-
No usage limits data available.
-
The API endpoint may not be responding or your account may not have a plan configured.
-
Check browser console for errors. Endpoint: /v1/billing/credits/usage/limits/
-
-
) : (
- {/* Planner Limits */}
- {groupedLimits.planner.length > 0 && (
+ {/* Credit Usage Limits */}
+ {creditLimits.length > 0 && (
-
Planner Limits
+
Credit Usage
- {groupedLimits.planner.map((limit, idx) => (
+ {creditLimits.map((limit, idx) => (
))}
)}
- {/* Writer Limits */}
- {groupedLimits.writer.length > 0 && (
+ {/* Account Management Limits */}
+ {accountLimits.length > 0 && (
-
Writer Limits
+
Account Management
- {groupedLimits.writer.map((limit, idx) => (
+ {accountLimits.map((limit, idx) => (
))}
)}
- {/* Image Limits */}
- {groupedLimits.images.length > 0 && (
-
-
Image Generation Limits
-
- {groupedLimits.images.map((limit, idx) => (
-
- ))}
+ {creditLimits.length === 0 && accountLimits.length === 0 && (
+
+
+
No limits data available.
+
Your account may not have a plan configured.
-
- )}
-
- {/* AI Credits */}
- {groupedLimits.ai.length > 0 && (
-
-
AI Credits
-
- {groupedLimits.ai.map((limit, idx) => (
-
- ))}
-
-
- )}
-
- {/* General Limits */}
- {groupedLimits.general.length > 0 && (
-
-
General Limits
-
- {groupedLimits.general.map((limit, idx) => (
-
- ))}
-
-
+
)}
)}
@@ -219,22 +189,20 @@ export default function Usage() {
function LimitCardComponent({ limit }: { limit: LimitCard }) {
const getCategoryColor = (category: string) => {
switch (category) {
- case 'planner': return 'blue';
- case 'writer': return 'green';
- case 'images': return 'purple';
- case 'ai': return 'orange';
- case 'general': return 'gray';
+ case 'credits': return 'primary';
+ case 'account': return 'gray';
default: return 'gray';
}
};
- const getUsageStatus = (percentage: number) => {
+ const getUsageStatus = (percentage: number | null) => {
+ if (percentage === null) return 'info';
if (percentage >= 90) return 'danger';
if (percentage >= 75) return 'warning';
return 'success';
};
- const percentage = Math.min(limit.percentage, 100);
+ const percentage = limit.percentage !== null && limit.percentage !== undefined ? Math.min(limit.percentage, 100) : null;
const status = getUsageStatus(percentage);
const color = getCategoryColor(limit.category);
@@ -242,12 +210,16 @@ function LimitCardComponent({ limit }: { limit: LimitCard }) {
? 'bg-red-500'
: status === 'warning'
? 'bg-yellow-500'
+ : status === 'info'
+ ? 'bg-blue-500'
: 'bg-green-500';
const statusTextColor = status === 'danger'
? 'text-red-600 dark:text-red-400'
: status === 'warning'
? 'text-yellow-600 dark:text-yellow-400'
+ : status === 'info'
+ ? 'text-blue-600 dark:text-blue-400'
: 'text-green-600 dark:text-green-400';
return (
@@ -258,26 +230,44 @@ function LimitCardComponent({ limit }: { limit: LimitCard }) {
- {limit.used.toLocaleString()}
- / {limit.limit.toLocaleString()}
- {limit.unit}
+ {limit.limit !== null && limit.limit !== undefined ? (
+ <>
+ {limit.used.toLocaleString()}
+ / {limit.limit.toLocaleString()}
+ >
+ ) : (
+
+ {limit.available !== null && limit.available !== undefined ? limit.available.toLocaleString() : limit.used.toLocaleString()}
+
+ )}
+ {limit.unit && (
+ {limit.unit}
+ )}
-
-
-
+ {percentage !== null && (
+
-
+ )}
-
- {limit.available.toLocaleString()} available
-
-
- {percentage.toFixed(1)}% used
-
+ {limit.available !== null && limit.available !== undefined ? (
+
+ {limit.available.toLocaleString()} available
+
+ ) : (
+ Current value
+ )}
+ {percentage !== null && (
+
+ {percentage.toFixed(1)}% used
+
+ )}
);
diff --git a/frontend/src/pages/Help/Help.tsx b/frontend/src/pages/Help/Help.tsx
index 8e7f8f7f..2544f27d 100644
--- a/frontend/src/pages/Help/Help.tsx
+++ b/frontend/src/pages/Help/Help.tsx
@@ -76,7 +76,7 @@ export default function Help() {
},
{
question: "How do I set up automation?",
- answer: "Go to Dashboard > Automation Setup section. Enable automation for each step (Keywords, Ideas, Content, Images) and configure settings like how many keywords to process per cycle. Advanced settings are available in Schedules page."
+ answer: "Go to Dashboard > Automation Setup section. Enable automation for each step (Keywords, Ideas, Content, Images) and configure settings like how many keywords to process per cycle. Advanced scheduling settings are available in the Automation menu."
},
{
question: "Can I edit AI-generated content?",
@@ -539,7 +539,7 @@ export default function Help() {
- Note: Configure automation in Dashboard > Automation Setup. For advanced scheduling, go to Schedules page.
+ Note: Configure automation in Dashboard > Automation Setup. For advanced scheduling, go to the Automation menu.
diff --git a/frontend/src/pages/Settings/Modules.tsx b/frontend/src/pages/Settings/Modules.tsx
index 16276b8a..7952be3b 100644
--- a/frontend/src/pages/Settings/Modules.tsx
+++ b/frontend/src/pages/Settings/Modules.tsx
@@ -1,36 +1,48 @@
-import { useState, useEffect } from 'react';
+import { useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
-import { fetchAPI } from '../../services/api';
+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 [settings, setSettings] = useState
([]);
- const [loading, setLoading] = useState(true);
+ const {
+ moduleEnableSettings,
+ loadModuleEnableSettings,
+ updateModuleEnableSettings,
+ loading,
+ } = useSettingsStore();
useEffect(() => {
- loadSettings();
- }, []);
+ loadModuleEnableSettings();
+ }, [loadModuleEnableSettings]);
- const loadSettings = async () => {
+ const handleToggle = async (moduleName: string, enabled: boolean) => {
try {
- setLoading(true);
- const response = await fetchAPI('/v1/system/settings/modules/');
- setSettings(response.results || []);
+ 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 load module settings: ${error.message}`);
- } finally {
- setLoading(false);
+ 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 (
Module Settings
-
Module-specific configuration
+
Enable or disable modules for your account
{loading ? (
@@ -39,7 +51,38 @@ export default function ModuleSettings() {
) : (
- Module settings management interface coming soon.
+
+ {Object.entries(MODULES).map(([key, module]) => (
+
+
+
{module.icon}
+
+
+ {module.name}
+
+ {module.description && (
+
+ {module.description}
+
+ )}
+
+
+
+
+ {getModuleEnabled(key) ? 'Enabled' : 'Disabled'}
+
+ handleToggle(key, enabled)}
+ />
+
+
+ ))}
+
)}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index dbb1a98b..4a66392c 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -78,9 +78,16 @@ function getActiveSectorId(): number | null {
}
}
-// Get auth token from store
+// Get auth token from store - try Zustand store first, then localStorage as fallback
const getAuthToken = (): string | null => {
try {
+ // First try to get from Zustand store directly (faster, no parsing)
+ const authState = useAuthStore.getState();
+ if (authState?.token) {
+ return authState.token;
+ }
+
+ // Fallback to localStorage (for cases where store hasn't initialized yet)
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
@@ -92,9 +99,16 @@ const getAuthToken = (): string | null => {
return null;
};
-// Get refresh token from store
+// Get refresh token from store - try Zustand store first, then localStorage as fallback
const getRefreshToken = (): string | null => {
try {
+ // First try to get from Zustand store directly (faster, no parsing)
+ const authState = useAuthStore.getState();
+ if (authState?.refreshToken) {
+ return authState.refreshToken;
+ }
+
+ // Fallback to localStorage (for cases where store hasn't initialized yet)
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
@@ -148,9 +162,14 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
if (errorData?.detail?.includes('Authentication credentials') ||
errorData?.message?.includes('Authentication credentials') ||
errorData?.error?.includes('Authentication credentials')) {
- // Token is invalid - clear auth state and force re-login
- const { logout } = useAuthStore.getState();
- logout();
+ // Only logout if we actually have a token stored (means it's invalid)
+ // If no token, it might be a race condition after login - don't logout
+ const authState = useAuthStore.getState();
+ if (authState?.token || authState?.isAuthenticated) {
+ // Token exists but is invalid - clear auth state and force re-login
+ const { logout } = useAuthStore.getState();
+ logout();
+ }
// Don't throw here - let the error handling below show the error
}
} catch (e) {
@@ -175,13 +194,14 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
if (refreshResponse.ok) {
const refreshData = await refreshResponse.json();
- if (refreshData.success && refreshData.access) {
+ const accessToken = refreshData.data?.access || refreshData.access;
+ if (refreshData.success && accessToken) {
// Update token in store
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
- parsed.state.token = refreshData.access;
+ parsed.state.token = accessToken;
localStorage.setItem('auth-storage', JSON.stringify(parsed));
}
} catch (e) {
@@ -191,7 +211,7 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
// Retry original request with new token
const newHeaders = {
...headers,
- 'Authorization': `Bearer ${refreshData.access}`,
+ 'Authorization': `Bearer ${accessToken}`,
};
const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, {
@@ -1474,6 +1494,20 @@ export async function deleteAccountSetting(key: string): Promise
{
}
// 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;
@@ -1498,6 +1532,19 @@ export async function createModuleSetting(data: { module_name: string; key: stri
});
}
+export async function fetchModuleEnableSettings(): Promise {
+ const response = await fetchAPI('/v1/system/settings/modules/enable/');
+ return response;
+}
+
+export async function updateModuleEnableSettings(data: Partial): Promise {
+ 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; is_active: boolean }>): Promise {
return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, {
method: 'PUT',
diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts
index 7b765f42..ecb35b0f 100644
--- a/frontend/src/store/authStore.ts
+++ b/frontend/src/store/authStore.ts
@@ -60,14 +60,17 @@ export const useAuthStore = create()(
const data = await response.json();
if (!response.ok || !data.success) {
- throw new Error(data.message || 'Login failed');
+ throw new Error(data.error || data.message || 'Login failed');
}
- // Store user and JWT tokens
+ // Store user and JWT tokens (handle both old and new API formats)
+ const responseData = data.data || data;
+ // Support both formats: new (access/refresh at top level) and old (tokens.access/refresh)
+ const tokens = responseData.tokens || {};
set({
- user: data.user,
- token: data.tokens?.access || null,
- refreshToken: data.tokens?.refresh || null,
+ user: responseData.user || data.user,
+ token: responseData.access || tokens.access || data.access || null,
+ refreshToken: responseData.refresh || tokens.refresh || data.refresh || null,
isAuthenticated: true,
loading: false
});
@@ -119,8 +122,8 @@ export const useAuthStore = create()(
// Store user and JWT tokens
set({
user: data.user,
- token: data.tokens?.access || null,
- refreshToken: data.tokens?.refresh || null,
+ token: data.data?.access || data.access || null,
+ refreshToken: data.data?.refresh || data.refresh || null,
isAuthenticated: true,
loading: false
});
@@ -168,8 +171,8 @@ export const useAuthStore = create()(
throw new Error(data.message || 'Token refresh failed');
}
- // Update access token
- set({ token: data.access });
+ // Update access token (API returns access at top level of data)
+ set({ token: data.data?.access || data.access });
// Also refresh user data to get latest account/plan information
// This ensures account/plan changes are reflected immediately
diff --git a/frontend/src/store/settingsStore.ts b/frontend/src/store/settingsStore.ts
index 76f66ef6..453d9f74 100644
--- a/frontend/src/store/settingsStore.ts
+++ b/frontend/src/store/settingsStore.ts
@@ -13,13 +13,17 @@ import {
fetchModuleSettings,
createModuleSetting,
updateModuleSetting,
+ fetchModuleEnableSettings,
+ updateModuleEnableSettings,
AccountSetting,
ModuleSetting,
+ ModuleEnableSettings,
} from '../services/api';
interface SettingsState {
accountSettings: Record;
moduleSettings: Record>;
+ moduleEnableSettings: ModuleEnableSettings | null;
loading: boolean;
error: string | null;
@@ -29,6 +33,9 @@ interface SettingsState {
updateAccountSetting: (key: string, value: any) => Promise;
loadModuleSettings: (moduleName: string) => Promise;
updateModuleSetting: (moduleName: string, key: string, value: any) => Promise;
+ loadModuleEnableSettings: () => Promise;
+ updateModuleEnableSettings: (data: Partial) => Promise;
+ isModuleEnabled: (moduleName: string) => boolean;
reset: () => void;
}
@@ -37,6 +44,7 @@ export const useSettingsStore = create()(
(set, get) => ({
accountSettings: {},
moduleSettings: {},
+ moduleEnableSettings: null,
loading: false,
error: null,
@@ -135,10 +143,40 @@ export const useSettingsStore = create()(
}
},
+ loadModuleEnableSettings: async () => {
+ set({ loading: true, error: null });
+ try {
+ const settings = await fetchModuleEnableSettings();
+ set({ moduleEnableSettings: settings, loading: false });
+ } catch (error: any) {
+ set({ error: error.message, loading: false });
+ }
+ },
+
+ updateModuleEnableSettings: async (data: Partial) => {
+ 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,
});
@@ -149,6 +187,7 @@ export const useSettingsStore = create()(
partialize: (state) => ({
accountSettings: state.accountSettings,
moduleSettings: state.moduleSettings,
+ moduleEnableSettings: state.moduleEnableSettings,
}),
}
)