feat(migrations): Rename indexes and update global integration settings fields for improved clarity and functionality

feat(admin): Add API monitoring, debug console, and system health templates for enhanced admin interface

docs: Add AI system cleanup summary and audit report detailing architecture, token management, and recommendations

docs: Introduce credits and tokens system guide outlining configuration, data flow, and monitoring strategies
This commit is contained in:
IGNY8 VPS (Salman)
2025-12-20 12:55:05 +00:00
parent eb6cba7920
commit 3283a83b42
51 changed files with 3578 additions and 5434 deletions

View File

@@ -5,7 +5,6 @@ import AppLayout from "./layout/AppLayout";
import { ScrollToTop } from "./components/common/ScrollToTop";
import ProtectedRoute from "./components/auth/ProtectedRoute";
import ModuleGuard from "./components/common/ModuleGuard";
import { AwsAdminGuard } from "./components/auth/AwsAdminGuard";
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
import { useAuthStore } from "./store/authStore";
@@ -68,9 +67,6 @@ const AccountSettingsPage = lazy(() => import("./pages/account/AccountSettingsPa
const TeamManagementPage = lazy(() => import("./pages/account/TeamManagementPage"));
const UsageAnalyticsPage = lazy(() => import("./pages/account/UsageAnalyticsPage"));
// Admin Module - Only dashboard for aws-admin users
const AdminSystemDashboard = lazy(() => import("./pages/admin/AdminSystemDashboard"));
// Reference Data - Lazy loaded
const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
@@ -270,13 +266,6 @@ export default function App() {
<Route path="/account/team" element={<TeamManagementPage />} />
<Route path="/account/usage" element={<UsageAnalyticsPage />} />
{/* Admin Routes - Only Dashboard for aws-admin users */}
<Route path="/admin/dashboard" element={
<AwsAdminGuard>
<AdminSystemDashboard />
</AwsAdminGuard>
} />
{/* Reference Data */}
<Route path="/reference/seed-keywords" element={<SeedKeywords />} />
<Route path="/planner/keyword-opportunities" element={<KeywordOpportunities />} />

View File

@@ -1,31 +0,0 @@
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../../store/authStore';
interface AwsAdminGuardProps {
children: React.ReactNode;
}
/**
* Route guard that only allows access to users of the aws-admin account
* Used for the single remaining admin dashboard page
*/
export const AwsAdminGuard: React.FC<AwsAdminGuardProps> = ({ children }) => {
const { user, loading } = useAuthStore();
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
);
}
// Check if user belongs to aws-admin account
const isAwsAdmin = user?.account?.slug === 'aws-admin';
if (!isAwsAdmin) {
return <Navigate to="/dashboard" replace />;
}
return <>{children}</>;
};

View File

@@ -32,14 +32,6 @@ interface ImageQueueModalProps {
model?: string;
provider?: string;
onUpdateQueue?: (queue: ImageQueueItem[]) => void;
onLog?: (log: {
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}) => void;
}
export default function ImageQueueModal({
@@ -51,7 +43,6 @@ export default function ImageQueueModal({
model,
provider,
onUpdateQueue,
onLog,
}: ImageQueueModalProps) {
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
// Track smooth progress animation for each item
@@ -250,43 +241,6 @@ export default function ImageQueueModal({
console.log(`[ImageQueueModal] Task completed with state:`, taskState);
clearInterval(pollInterval);
// Log completion status
if (onLog) {
if (taskState === 'SUCCESS') {
const result = data.result || (data.meta && data.meta.result);
const completed = result?.completed || 0;
const failed = result?.failed || 0;
const total = result?.total_images || totalImages;
onLog({
timestamp: new Date().toISOString(),
type: failed > 0 ? 'error' : 'success',
action: 'generate_images',
stepName: 'Task Completed',
data: {
state: 'SUCCESS',
completed,
failed,
total,
results: result?.results || []
}
});
} else {
// FAILURE
onLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'generate_images',
stepName: 'Task Failed',
data: {
state: 'FAILURE',
error: data.error || data.meta?.error || 'Task failed',
meta: data.meta
}
});
}
}
// Update final state
if (taskState === 'SUCCESS' && data.result) {
console.log(`[ImageQueueModal] Updating queue from result:`, data.result);

View File

@@ -1,435 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { useAuthStore } from '../../store/authStore';
import { API_BASE_URL } from '../../services/api';
interface RequestMetrics {
request_id: string;
path: string;
method: string;
elapsed_time_ms: number;
cpu: {
user_time_ms: number;
system_time_ms: number;
total_time_ms: number;
system_percent: number;
};
memory: {
delta_bytes: number;
delta_mb: number;
final_rss_mb: number;
system_used_percent: number;
};
io: {
read_bytes: number;
read_mb: number;
write_bytes: number;
write_mb: number;
};
timestamp: number;
}
interface ResourceDebugOverlayProps {
enabled: boolean;
}
export default function ResourceDebugOverlay({ enabled }: ResourceDebugOverlayProps) {
const { user } = useAuthStore();
const [metrics, setMetrics] = useState<RequestMetrics[]>([]);
const [isVisible, setIsVisible] = useState(false);
const [pageLoadStart, setPageLoadStart] = useState<number | null>(null);
const requestIdRef = useRef<string | null>(null);
const metricsRef = useRef<RequestMetrics[]>([]);
const originalFetchRef = useRef<typeof fetch | null>(null);
const nativeFetchRef = useRef<typeof fetch | null>(null); // Store native fetch separately
// Check if user is admin/developer
const isAdminOrDeveloper = user?.role === 'admin' || user?.role === 'developer';
// Track page load start and intercept fetch requests
useEffect(() => {
if (!enabled || !isAdminOrDeveloper) {
// Restore native fetch if disabled
if (nativeFetchRef.current) {
window.fetch = nativeFetchRef.current;
nativeFetchRef.current = null;
originalFetchRef.current = null;
}
return;
}
setPageLoadStart(performance.now());
// Store native fetch and create bound version
if (!nativeFetchRef.current) {
nativeFetchRef.current = window.fetch; // Store actual native fetch
originalFetchRef.current = window.fetch.bind(window); // Create bound version for calling
}
// Intercept fetch requests to track API calls
window.fetch = async function(...args) {
const startTime = performance.now();
const [url, options = {}] = args;
// Don't intercept our own metrics fetch calls to avoid infinite loops
const urlString = typeof url === 'string' ? url : url.toString();
if (urlString.includes('/request-metrics/')) {
// Use native fetch directly for metrics calls
return nativeFetchRef.current!.apply(window, args as [RequestInfo | URL, RequestInit?]);
}
// Add debug header to enable tracking
const headers = new Headers(options.headers || {});
headers.set('X-Debug-Resource-Tracking', 'true');
// Use bound fetch to preserve context
const response = await originalFetchRef.current!(url, {
...options,
headers,
});
const endTime = performance.now();
// Get request ID from response header
const requestId = response.headers.get('X-Resource-Tracking-ID');
if (requestId) {
requestIdRef.current = requestId;
// Fetch metrics after a delay to ensure backend has stored them
// Use a slightly longer delay to avoid race conditions
setTimeout(() => fetchRequestMetrics(requestId), 300);
}
return response;
};
return () => {
// Restore native fetch on cleanup
if (nativeFetchRef.current) {
window.fetch = nativeFetchRef.current;
nativeFetchRef.current = null;
originalFetchRef.current = null;
}
};
}, [enabled, isAdminOrDeveloper]);
// Fetch metrics for a request - use fetchAPI to get proper authentication handling
const fetchRequestMetrics = async (requestId: string, retryCount = 0) => {
try {
// Use fetchAPI which handles token refresh and authentication properly
// But we need to use native fetch to avoid interception loop
const nativeFetch = nativeFetchRef.current || window.fetch;
const { token } = useAuthStore.getState();
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
// Add JWT token if available
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Silently handle 404s and other errors - metrics might not exist for all requests
try {
const response = await nativeFetch.call(window, `${API_BASE_URL}/v1/system/request-metrics/${requestId}/`, {
method: 'GET',
headers,
credentials: 'include', // Include session cookies for authentication
});
// Silently ignore 404s - metrics endpoint might not exist for all requests
if (response.status === 404) {
return; // Don't log or retry 404s
}
if (response.ok) {
const responseData = await response.json();
// Extract data from unified API response format: {success: true, data: {...}}
const data = responseData?.data || responseData;
// Only log in debug mode to reduce console noise
if (import.meta.env.DEV) {
console.debug('Fetched metrics for request:', requestId, data);
}
// Validate data structure before adding
if (data && typeof data === 'object' && data.request_id) {
metricsRef.current = [...metricsRef.current, data];
setMetrics([...metricsRef.current]);
}
} else if (response.status === 401) {
// Token might be expired - try to refresh and retry once
try {
await useAuthStore.getState().refreshToken();
const newToken = useAuthStore.getState().token;
if (newToken) {
const retryHeaders: HeadersInit = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${newToken}`,
};
const retryResponse = await nativeFetch.call(window, `${API_BASE_URL}/v1/system/request-metrics/${requestId}/`, {
method: 'GET',
headers: retryHeaders,
credentials: 'include',
});
if (retryResponse.ok) {
const responseData = await retryResponse.json();
// Extract data from unified API response format: {success: true, data: {...}}
const data = responseData?.data || responseData;
// Validate data structure before adding
if (data && typeof data === 'object' && data.request_id) {
metricsRef.current = [...metricsRef.current, data];
setMetrics([...metricsRef.current]);
}
return;
}
}
} catch (refreshError) {
// Refresh failed - silently ignore
}
// Silently ignore 401 errors - user might not be authenticated
} else if (response.status === 404) {
// Metrics not found - silently ignore (metrics might not exist for all requests)
return;
} else {
// Other errors - silently ignore
return;
}
} catch (error) {
// Silently ignore all fetch errors (network errors, etc.)
// Metrics are optional and not critical for functionality
return;
}
} catch (error) {
// Silently ignore all errors
return;
}
};
// Calculate page load time
const pageLoadTime = pageLoadStart ? performance.now() - pageLoadStart : null;
// Calculate totals - with null safety checks
const totals = metrics.reduce((acc, m) => {
// Safely access nested properties with defaults
const elapsed = m?.elapsed_time_ms || 0;
const cpuTotal = m?.cpu?.total_time_ms || 0;
const memoryDelta = m?.memory?.delta_mb || 0;
const ioRead = m?.io?.read_mb || 0;
const ioWrite = m?.io?.write_mb || 0;
return {
elapsed_time_ms: acc.elapsed_time_ms + elapsed,
cpu_total_ms: acc.cpu_total_ms + cpuTotal,
memory_delta_mb: acc.memory_delta_mb + memoryDelta,
io_read_mb: acc.io_read_mb + ioRead,
io_write_mb: acc.io_write_mb + ioWrite,
};
}, {
elapsed_time_ms: 0,
cpu_total_ms: 0,
memory_delta_mb: 0,
io_read_mb: 0,
io_write_mb: 0,
});
// Find the slowest request - with null safety
const slowestRequest = metrics.length > 0
? metrics.reduce((prev, current) => {
const prevTime = prev?.elapsed_time_ms || 0;
const currentTime = current?.elapsed_time_ms || 0;
return (currentTime > prevTime) ? current : prev;
})
: null;
// Find the request with highest CPU usage - with null safety
const highestCpuRequest = metrics.length > 0
? metrics.reduce((prev, current) => {
const prevCpu = prev?.cpu?.total_time_ms || 0;
const currentCpu = current?.cpu?.total_time_ms || 0;
return (currentCpu > prevCpu) ? current : prev;
})
: null;
// Find the request with highest memory usage - with null safety
const highestMemoryRequest = metrics.length > 0
? metrics.reduce((prev, current) => {
const prevMemory = prev?.memory?.delta_mb || 0;
const currentMemory = current?.memory?.delta_mb || 0;
return (currentMemory > prevMemory) ? current : prev;
})
: null;
if (!enabled || !isAdminOrDeveloper) return null;
return (
<>
{/* Toggle Button - Fixed position */}
<button
onClick={() => setIsVisible(!isVisible)}
className="fixed bottom-4 right-4 z-[99999] bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg shadow-lg text-sm font-medium flex items-center gap-2"
title="Toggle Resource Debug Overlay"
>
<span>🔍</span>
<span>Debug ({metrics.length})</span>
</button>
{/* Overlay */}
{isVisible && (
<div className="fixed bottom-20 right-4 z-[99998] bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg shadow-2xl w-[500px] max-h-[85vh] overflow-auto">
<div className="sticky top-0 bg-gray-100 dark:bg-gray-800 px-4 py-3 border-b border-gray-300 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-semibold text-gray-900 dark:text-white">Resource Debug</h3>
<button
onClick={() => setIsVisible(false)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
</button>
</div>
<div className="p-4 space-y-4">
{/* Page Load Summary */}
{pageLoadTime && (
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded border border-blue-200 dark:border-blue-800">
<h4 className="font-semibold text-blue-900 dark:text-blue-200 mb-2">Page Load Time</h4>
<div className="text-sm text-blue-800 dark:text-blue-300">
{pageLoadTime.toFixed(2)} ms
</div>
</div>
)}
{/* Performance Summary - Highlight Culprits */}
{metrics.length > 0 && (
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded border border-yellow-200 dark:border-yellow-800">
<h4 className="font-semibold text-yellow-900 dark:text-yellow-200 mb-2"> Performance Culprits</h4>
<div className="text-xs space-y-2 text-yellow-800 dark:text-yellow-300">
{slowestRequest && (
<div>
<span className="font-semibold">Slowest Request:</span> {slowestRequest.method || 'N/A'} {slowestRequest.path || 'N/A'}
<br />
<span className="ml-4">Time: {(slowestRequest.elapsed_time_ms || 0).toFixed(2)} ms</span>
</div>
)}
{highestCpuRequest && highestCpuRequest.cpu && (highestCpuRequest.cpu.total_time_ms || 0) > 100 && (
<div>
<span className="font-semibold">Highest CPU:</span> {highestCpuRequest.method || 'N/A'} {highestCpuRequest.path || 'N/A'}
<br />
<span className="ml-4">CPU: {(highestCpuRequest.cpu.total_time_ms || 0).toFixed(2)} ms (System: {(highestCpuRequest.cpu.system_percent || 0).toFixed(1)}%)</span>
</div>
)}
{highestMemoryRequest && highestMemoryRequest.memory && (highestMemoryRequest.memory.delta_mb || 0) > 1 && (
<div>
<span className="font-semibold">Highest Memory:</span> {highestMemoryRequest.method || 'N/A'} {highestMemoryRequest.path || 'N/A'}
<br />
<span className="ml-4">Memory: {(highestMemoryRequest.memory.delta_mb || 0) > 0 ? '+' : ''}{(highestMemoryRequest.memory.delta_mb || 0).toFixed(2)} MB</span>
</div>
)}
</div>
</div>
)}
{/* Totals */}
{metrics.length > 0 && (
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded border border-gray-200 dark:border-gray-700">
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Request Totals</h4>
<div className="text-xs space-y-1 text-gray-700 dark:text-gray-300">
<div>Total Requests: {metrics.length}</div>
<div>Total Time: {totals.elapsed_time_ms.toFixed(2)} ms</div>
<div>Total CPU Time: {totals.cpu_total_ms.toFixed(2)} ms</div>
<div>Total Memory Delta: {totals.memory_delta_mb > 0 ? '+' : ''}{totals.memory_delta_mb.toFixed(2)} MB</div>
<div>Total I/O Read: {totals.io_read_mb.toFixed(2)} MB</div>
<div>Total I/O Write: {totals.io_write_mb.toFixed(2)} MB</div>
</div>
</div>
)}
{/* Individual Requests - Detailed View */}
<div className="space-y-2">
<h4 className="font-semibold text-gray-900 dark:text-white">All Requests (Detailed)</h4>
{metrics.length === 0 ? (
<div className="text-sm text-gray-500 dark:text-gray-400">
No requests tracked yet. Navigate to trigger API calls.
<br />
<span className="text-xs">Make sure debug toggle is enabled in header.</span>
</div>
) : (
metrics.map((m, idx) => {
// Safely access properties with defaults
const elapsedTime = m?.elapsed_time_ms || 0;
const cpuTotal = m?.cpu?.total_time_ms || 0;
const memoryDelta = m?.memory?.delta_mb || 0;
const isSlow = elapsedTime > 1000;
const isHighCpu = cpuTotal > 100;
const isHighMemory = memoryDelta > 1;
return (
<div
key={idx}
className={`p-3 rounded border text-xs ${
isSlow || isHighCpu || isHighMemory
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
: 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700'
}`}
>
<div className="font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<span>{m.method}</span>
<span className="text-gray-600 dark:text-gray-400 truncate">{m.path}</span>
{(isSlow || isHighCpu || isHighMemory) && (
<span className="text-red-600 dark:text-red-400 text-xs"></span>
)}
</div>
<div className="space-y-1 text-gray-700 dark:text-gray-300">
<div className={isSlow ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
Time: {elapsedTime.toFixed(2)} ms
</div>
{m?.cpu && (
<div className={isHighCpu ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
🔥 CPU: {cpuTotal.toFixed(2)} ms
<span className="text-gray-500"> (User: {(m.cpu.user_time_ms || 0).toFixed(2)}ms, System: {(m.cpu.system_time_ms || 0).toFixed(2)}ms)</span>
<br />
<span className="ml-4 text-gray-500">System CPU: {(m.cpu.system_percent || 0).toFixed(1)}%</span>
</div>
)}
{m?.memory && (
<div className={isHighMemory ? 'font-semibold text-red-700 dark:text-red-300' : ''}>
💾 Memory: {memoryDelta > 0 ? '+' : ''}{memoryDelta.toFixed(2)} MB
<span className="text-gray-500"> (Final RSS: {(m.memory.final_rss_mb || 0).toFixed(2)} MB)</span>
<br />
<span className="ml-4 text-gray-500">System Memory: {(m.memory.system_used_percent || 0).toFixed(1)}%</span>
</div>
)}
{m?.io?.read_mb > 0 && (
<div>
📖 I/O Read: {m.io.read_mb.toFixed(2)} MB ({(m.io.read_bytes || 0).toLocaleString()} bytes)
</div>
)}
{m?.io?.write_mb > 0 && (
<div>
📝 I/O Write: {m.io.write_mb.toFixed(2)} MB ({(m.io.write_bytes || 0).toLocaleString()} bytes)
</div>
)}
</div>
</div>
);
})
)}
</div>
{/* Clear Button */}
{metrics.length > 0 && (
<button
onClick={() => {
setMetrics([]);
metricsRef.current = [];
setPageLoadStart(performance.now());
}}
className="w-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white px-3 py-2 rounded text-sm"
>
Clear Metrics
</button>
)}
</div>
</div>
)}
</>
);
}

View File

@@ -1,42 +0,0 @@
import { useState, useEffect } from 'react';
import { useAuthStore } from '../../store/authStore';
export default function ResourceDebugToggle() {
const { user } = useAuthStore();
const [enabled, setEnabled] = useState(false);
const isAdminOrDeveloper = user?.role === 'admin' || user?.role === 'developer';
// Load saved state from localStorage
useEffect(() => {
if (isAdminOrDeveloper) {
const saved = localStorage.getItem('debug_resource_tracking_enabled');
setEnabled(saved === 'true');
}
}, [isAdminOrDeveloper]);
const toggle = () => {
const newValue = !enabled;
setEnabled(newValue);
localStorage.setItem('debug_resource_tracking_enabled', String(newValue));
// Dispatch event for overlay component
window.dispatchEvent(new CustomEvent('debug-resource-tracking-toggle', { detail: newValue }));
};
if (!isAdminOrDeveloper) return null;
return (
<button
onClick={toggle}
className={`flex items-center justify-center w-10 h-10 rounded-lg transition-colors ${
enabled
? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400'
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'
}`}
title={enabled ? 'Disable Resource Debug' : 'Enable Resource Debug'}
>
🔍
</button>
);
}

View File

@@ -1,30 +0,0 @@
import { useState, useEffect } from 'react';
/**
* Hook to check if Resource Debug is enabled
* This controls both Resource Debug overlay and AI Function Logs
*/
export function useResourceDebug(): boolean {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
// Load initial state
const saved = localStorage.getItem('debug_resource_tracking_enabled');
setEnabled(saved === 'true');
// Listen for toggle changes
const handleToggle = (e: Event) => {
const customEvent = e as CustomEvent;
setEnabled(customEvent.detail);
};
window.addEventListener('debug-resource-tracking-toggle', handleToggle);
return () => {
window.removeEventListener('debug-resource-tracking-toggle', handleToggle);
};
}, []);
return enabled;
}

View File

@@ -6,7 +6,6 @@ import { ThemeToggleButton } from "../components/common/ThemeToggleButton";
import NotificationDropdown from "../components/header/NotificationDropdown";
import UserDropdown from "../components/header/UserDropdown";
import { HeaderMetrics } from "../components/header/HeaderMetrics";
import ResourceDebugToggle from "../components/debug/ResourceDebugToggle";
const AppHeader: React.FC = () => {
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
@@ -163,8 +162,6 @@ const AppHeader: React.FC = () => {
<HeaderMetrics />
{/* <!-- Dark Mode Toggler --> */}
<ThemeToggleButton />
{/* <!-- Resource Debug Toggle (Admin only) --> */}
<ResourceDebugToggle />
{/* <!-- Notification Menu Area --> */}
<NotificationDropdown />
{/* <!-- Notification Menu Area --> */}

View File

@@ -10,7 +10,6 @@ import { useBillingStore } from "../store/billingStore";
import { useHeaderMetrics } from "../context/HeaderMetricsContext";
import { useErrorHandler } from "../hooks/useErrorHandler";
import { trackLoading } from "../components/common/LoadingStateMonitor";
import ResourceDebugOverlay from "../components/debug/ResourceDebugOverlay";
import PendingPaymentBanner from "../components/billing/PendingPaymentBanner";
const LayoutContent: React.FC = () => {
@@ -22,7 +21,6 @@ const LayoutContent: React.FC = () => {
const { addError } = useErrorHandler('AppLayout');
const hasLoadedSite = useRef(false);
const isLoadingSite = useRef(false);
const [debugEnabled, setDebugEnabled] = useState(false);
const lastUserRefresh = useRef<number>(0);
// Initialize site store on mount - only once, but only if authenticated
@@ -145,22 +143,6 @@ const LayoutContent: React.FC = () => {
}]);
}, [balance, isAuthenticated, setMetrics]);
// Listen for debug toggle changes
useEffect(() => {
const saved = localStorage.getItem('debug_resource_tracking_enabled');
setDebugEnabled(saved === 'true');
const handleToggle = (e: Event) => {
const customEvent = e as CustomEvent;
setDebugEnabled(customEvent.detail);
};
window.addEventListener('debug-resource-tracking-toggle', handleToggle);
return () => {
window.removeEventListener('debug-resource-tracking-toggle', handleToggle);
};
}, []);
return (
<div className="min-h-screen xl:flex">
<div>
@@ -178,8 +160,6 @@ const LayoutContent: React.FC = () => {
<div className="p-4 pb-20 md:p-6 md:pb-24">
<Outlet />
</div>
{/* Resource Debug Overlay - Only visible when enabled by admin */}
<ResourceDebugOverlay enabled={debugEnabled} />
</div>
</div>
);

View File

@@ -17,6 +17,7 @@ import {
FileIcon,
UserIcon,
UserCircleIcon,
BoxCubeIcon,
} from "../icons";
import { useSidebar } from "../context/SidebarContext";
import SidebarWidget from "./SidebarWidget";
@@ -29,6 +30,7 @@ type NavItem = {
icon: React.ReactNode;
path?: string;
subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[];
adminOnly?: boolean;
};
type MenuSection = {
@@ -42,9 +44,6 @@ const AppSidebar: React.FC = () => {
const { user, isAuthenticated } = useAuthStore();
const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled, loadModuleEnableSettings, loading: settingsLoading } = useSettingsStore();
// 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
@@ -224,6 +223,30 @@ const AppSidebar: React.FC = () => {
path: "/settings/integration",
adminOnly: true,
},
// Global Settings - Admin only, dropdown with global config pages
{
icon: <BoxCubeIcon />,
name: "Global Settings",
adminOnly: true,
subItems: [
{
name: "Platform API Keys",
path: "/admin/system/globalintegrationsettings/",
},
{
name: "Global Prompts",
path: "/admin/system/globalaiprompt/",
},
{
name: "Global Author Profiles",
path: "/admin/system/globalauthorprofile/",
},
{
name: "Global Strategies",
path: "/admin/system/globalstrategy/",
},
],
},
{
icon: <PageIcon />,
name: "Publishing",
@@ -249,32 +272,10 @@ const AppSidebar: React.FC = () => {
];
}, [moduleEnabled]);
// Admin section - only shown for aws-admin account users
const adminSection: MenuSection = useMemo(() => ({
label: "ADMIN",
items: [
{
icon: <GridIcon />,
name: "System Dashboard",
path: "/admin/dashboard",
},
],
}), []);
// Combine all sections, including admin if user is in aws-admin account
// Combine all sections
const allSections = useMemo(() => {
const baseSections = menuSections.map(section => {
// Filter adminOnly items for non-system users
const filteredItems = section.items.filter((item: any) => {
if ((item as any).adminOnly && !isAwsAdminAccount) return false;
return true;
});
return { ...section, items: filteredItems };
});
return isAwsAdminAccount
? [...baseSections, adminSection]
: baseSections;
}, [isAwsAdminAccount, menuSections, adminSection]);
return menuSections;
}, [menuSections]);
useEffect(() => {
const currentPath = location.pathname;
@@ -355,7 +356,15 @@ const AppSidebar: React.FC = () => {
const renderMenuItems = (items: NavItem[], sectionIndex: number) => (
<ul className="flex flex-col gap-2">
{items.map((nav, itemIndex) => (
{items
.filter((nav) => {
// Filter out admin-only items for non-admin users
if (nav.adminOnly && user?.role !== 'admin' && !user?.is_staff) {
return false;
}
return true;
})
.map((nav, itemIndex) => (
<li key={nav.name}>
{nav.subItems ? (
<button

View File

@@ -31,7 +31,6 @@ import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/di
import FormModal from '../../components/common/FormModal';
import ProgressModal from '../../components/common/ProgressModal';
import { useProgressModal } from '../../hooks/useProgressModal';
import { useResourceDebug } from '../../hooks/useResourceDebug';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { ArrowUpIcon, PlusIcon, ListIcon, DownloadIcon, GroupIcon, BoltIcon } from '../../icons';
import { useKeywordsImportExport } from '../../config/import-export.config';
@@ -90,37 +89,6 @@ export default function Keywords() {
const progressModal = useProgressModal();
const hasReloadedRef = useRef(false);
// Resource Debug toggle - controls AI Function Logs
const resourceDebugEnabled = useResourceDebug();
// AI Function Logs state
const [aiLogs, setAiLogs] = useState<Array<{
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}>>([]);
// Track last logged step to avoid duplicates
const lastLoggedStepRef = useRef<string | null>(null);
const lastLoggedPercentageRef = useRef<number>(-1);
// Helper function to add log entry (only if Resource Debug is enabled)
const addAiLog = useCallback((log: {
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}) => {
if (resourceDebugEnabled) {
setAiLogs(prev => [...prev, log]);
}
}, [resourceDebugEnabled]);
// Load sectors for active site using sector store
useEffect(() => {
if (activeSite) {
@@ -332,21 +300,6 @@ export default function Keywords() {
const numIds = ids.map(id => parseInt(id));
const sectorId = activeSector?.id;
const selectedKeywords = keywords.filter(k => numIds.includes(k.id));
const requestData = {
ids: numIds,
keyword_count: numIds.length,
keyword_names: selectedKeywords.map(k => k.keyword),
sector_id: sectorId,
};
// Log request (only if Resource Debug is enabled)
addAiLog({
timestamp: new Date().toISOString(),
type: 'request',
action: 'auto_cluster (Bulk Action)',
data: requestData,
});
try {
const result = await autoClusterKeywords(numIds, sectorId);
@@ -354,43 +307,17 @@ export default function Keywords() {
if (result && result.success === false) {
// Error response from API
const errorMsg = result.error || 'Failed to cluster keywords';
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'auto_cluster (Bulk Action)',
data: { error: errorMsg, keyword_count: numIds.length },
});
toast.error(errorMsg);
return;
}
if (result && result.success) {
if (result.task_id) {
// Log success with task_id
addAiLog({
timestamp: new Date().toISOString(),
type: 'success',
action: 'auto_cluster (Bulk Action)',
data: { task_id: result.task_id, message: result.message, keyword_count: numIds.length },
});
// Async task - open progress modal
hasReloadedRef.current = false;
progressModal.openModal(result.task_id, 'Auto-Clustering Keywords', 'ai-auto-cluster-01');
// Don't show toast - progress modal will show status
} else {
// Log success with results
addAiLog({
timestamp: new Date().toISOString(),
type: 'success',
action: 'auto_cluster (Bulk Action)',
data: {
clusters_created: result.clusters_created || 0,
keywords_updated: result.keywords_updated || 0,
keyword_count: numIds.length,
message: result.message,
},
});
// Synchronous completion
toast.success(`Clustering complete: ${result.clusters_created || 0} clusters created, ${result.keywords_updated || 0} keywords updated`);
if (!hasReloadedRef.current) {
@@ -401,13 +328,6 @@ export default function Keywords() {
} else {
// Unexpected response format - show error
const errorMsg = result?.error || 'Unexpected response format';
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'auto_cluster (Bulk Action)',
data: { error: errorMsg, keyword_count: numIds.length },
});
toast.error(errorMsg);
}
} catch (error: any) {
@@ -420,13 +340,6 @@ export default function Keywords() {
errorMsg = error.message;
}
}
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'auto_cluster (Bulk Action)',
data: { error: errorMsg, keyword_count: numIds.length },
});
toast.error(errorMsg);
}
} else {
@@ -434,96 +347,9 @@ export default function Keywords() {
}
}, [toast, activeSector, loadKeywords, progressModal, keywords]);
// Log AI function progress steps
useEffect(() => {
if (!progressModal.taskId || !progressModal.isOpen) {
return;
}
const progress = progressModal.progress;
const currentStep = progress.details?.phase || '';
const currentPercentage = progress.percentage;
const currentMessage = progress.message;
const currentStatus = progress.status;
// Log step changes
if (currentStep && currentStep !== lastLoggedStepRef.current) {
const stepType = currentStatus === 'error' ? 'error' :
currentStatus === 'completed' ? 'success' : 'step';
addAiLog({
timestamp: new Date().toISOString(),
type: stepType,
action: progressModal.title || 'AI Function',
stepName: currentStep,
percentage: currentPercentage,
data: {
step: currentStep,
message: currentMessage,
percentage: currentPercentage,
status: currentStatus,
details: progress.details,
},
});
lastLoggedStepRef.current = currentStep;
lastLoggedPercentageRef.current = currentPercentage;
}
// Log percentage changes for same step (if significant change)
else if (currentStep && Math.abs(currentPercentage - lastLoggedPercentageRef.current) >= 10) {
const stepType = currentStatus === 'error' ? 'error' :
currentStatus === 'completed' ? 'success' : 'step';
addAiLog({
timestamp: new Date().toISOString(),
type: stepType,
action: progressModal.title || 'AI Function',
stepName: currentStep,
percentage: currentPercentage,
data: {
step: currentStep,
message: currentMessage,
percentage: currentPercentage,
status: currentStatus,
details: progress.details,
},
});
lastLoggedPercentageRef.current = currentPercentage;
}
// Log status changes (error, completed)
else if (currentStatus === 'error' || currentStatus === 'completed') {
// Only log if we haven't already logged this status for this step
if (currentStep !== lastLoggedStepRef.current ||
(currentStatus === 'error' && lastLoggedStepRef.current !== 'error') ||
(currentStatus === 'completed' && lastLoggedStepRef.current !== 'completed')) {
const stepType = currentStatus === 'error' ? 'error' : 'success';
addAiLog({
timestamp: new Date().toISOString(),
type: stepType,
action: progressModal.title || 'AI Function',
stepName: currentStep || 'Final',
percentage: currentPercentage,
data: {
step: currentStep || 'Final',
message: currentMessage,
percentage: currentPercentage,
status: currentStatus,
details: progress.details,
},
});
lastLoggedStepRef.current = currentStep || currentStatus;
}
}
}, [progressModal.progress, progressModal.taskId, progressModal.isOpen, progressModal.title, addAiLog]);
// Reset step tracking when modal closes or opens
// Reset reload flag when modal closes or opens
useEffect(() => {
if (!progressModal.isOpen) {
lastLoggedStepRef.current = null;
lastLoggedPercentageRef.current = -1;
hasReloadedRef.current = false; // Reset reload flag when modal closes
} else {
// Reset reload flag when modal opens for a new task
@@ -984,74 +810,6 @@ export default function Keywords() {
}
}}
/>
{/* AI Function Logs - Display below table (only when Resource Debug is enabled) */}
{resourceDebugEnabled && aiLogs.length > 0 && (
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
AI Function Logs
</h3>
<button
onClick={() => setAiLogs([])}
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
Clear Logs
</button>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{aiLogs.slice().reverse().map((log, index) => (
<div
key={index}
className={`p-3 rounded border text-xs font-mono ${
log.type === 'request'
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
: log.type === 'success'
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: log.type === 'error'
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2 flex-wrap">
<span className={`font-semibold ${
log.type === 'request'
? 'text-blue-700 dark:text-blue-300'
: log.type === 'success'
? 'text-green-700 dark:text-green-300'
: log.type === 'error'
? 'text-red-700 dark:text-red-300'
: 'text-purple-700 dark:text-purple-300'
}`}>
[{log.type.toUpperCase()}]
</span>
<span className="text-gray-700 dark:text-gray-300">
{log.action}
</span>
{log.stepName && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
{log.stepName}
</span>
)}
{log.percentage !== undefined && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{log.percentage}%
</span>
)}
</div>
<span className="text-gray-500 dark:text-gray-400">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
</div>
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
{JSON.stringify(log.data, null, 2)}
</pre>
</div>
))}
</div>
</div>
)}
</>
);
}

View File

@@ -119,7 +119,6 @@ export default function Integration() {
const validateIntegration = useCallback(async (
integrationId: string,
enabled: boolean,
apiKey?: string,
model?: string
) => {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
@@ -129,10 +128,8 @@ export default function Integration() {
return;
}
// Check if integration is enabled and has API key configured
const hasApiKey = apiKey && apiKey.trim() !== '';
if (!hasApiKey || !enabled) {
// Check if integration is enabled
if (!enabled) {
// Not configured or disabled - set status accordingly
setValidationStatuses(prev => ({
...prev,
@@ -147,12 +144,10 @@ export default function Integration() {
[integrationId]: 'pending',
}));
// Test connection asynchronously
// Test connection asynchronously (uses platform API key)
try {
// Build request body based on integration type
const requestBody: any = {
apiKey: apiKey,
};
const requestBody: any = {};
// OpenAI needs model in config, Runware doesn't
if (integrationId === 'openai') {
@@ -195,11 +190,10 @@ export default function Integration() {
if (!integration) return;
const enabled = integration.enabled === true;
const apiKey = integration.apiKey;
const model = integration.model;
// Validate with current state (fire and forget - don't await)
validateIntegration(id, enabled, apiKey, model);
validateIntegration(id, enabled, model);
});
// Return unchanged - we're just reading state
@@ -216,7 +210,7 @@ export default function Integration() {
useEffect(() => {
// Only validate if integrations have been loaded (not initial empty state)
const hasLoadedData = Object.values(integrations).some(integ =>
integ.apiKey !== undefined || integ.enabled !== undefined
integ.enabled !== undefined
);
if (!hasLoadedData) return;
@@ -227,7 +221,7 @@ export default function Integration() {
return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [integrations.openai.enabled, integrations.runware.enabled, integrations.openai.apiKey, integrations.runware.apiKey]);
}, [integrations.openai.enabled, integrations.runware.enabled]);
const loadIntegrationSettings = async () => {
try {
@@ -294,12 +288,6 @@ export default function Integration() {
}
const config = integrations[selectedIntegration];
const apiKey = config.apiKey;
if (!apiKey) {
toast.error('Please enter an API key first');
return;
}
setIsTesting(true);
@@ -312,12 +300,12 @@ export default function Integration() {
}
try {
// Test uses platform API key (no apiKey parameter needed)
// fetchAPI extracts data from unified format {success: true, data: {...}}
// So data is the extracted response payload
const data = await fetchAPI(`/v1/system/settings/integrations/${selectedIntegration}/test/`, {
method: 'POST',
body: JSON.stringify({
apiKey,
config: config,
}),
});
@@ -477,20 +465,6 @@ export default function Integration() {
if (integrationId === 'openai') {
return [
{
key: 'apiKey',
label: 'OpenAI API Key',
type: 'password',
value: config.apiKey || '',
onChange: (value) => {
setIntegrations({
...integrations,
[integrationId]: { ...config, apiKey: value },
});
},
placeholder: 'Enter your OpenAI API key',
required: true,
},
{
key: 'model',
label: 'AI Model',
@@ -513,20 +487,7 @@ export default function Integration() {
];
} else if (integrationId === 'runware') {
return [
{
key: 'apiKey',
label: 'Runware API Key',
type: 'password',
value: config.apiKey || '',
onChange: (value) => {
setIntegrations({
...integrations,
[integrationId]: { ...config, apiKey: value },
});
},
placeholder: 'Enter your Runware API key',
required: true,
},
// Runware doesn't have model selection, just using platform API key
];
} else if (integrationId === 'image_generation') {
const service = config.service || 'openai';
@@ -912,6 +873,13 @@ export default function Integration() {
<PageMeta title="API Integration - IGNY8" description="External integrations" />
<div className="space-y-8">
{/* Platform API Keys Info */}
<Alert
variant="info"
title="Platform API Keys"
message="API keys are managed at the platform level by administrators. You can customize which AI models and parameters to use for your account. Free plan users can view settings but cannot customize them."
/>
{/* Integration Cards with Validation Cards */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
{/* OpenAI Integration + Validation */}
@@ -924,12 +892,10 @@ export default function Integration() {
integrationId="openai"
onToggleSuccess={(enabled, data) => {
// Refresh status circle when toggle changes
// Use API key from hook's data (most up-to-date) or fallback to integrations state
const apiKey = data?.apiKey || integrations.openai.apiKey;
const model = data?.model || integrations.openai.model;
// Validate with current enabled state and API key
validateIntegration('openai', enabled, apiKey, model);
// Validate with current enabled state and model
validateIntegration('openai', enabled, model);
}}
onSettings={() => handleSettings('openai')}
onDetails={() => handleDetails('openai')}
@@ -965,11 +931,8 @@ export default function Integration() {
}
onToggleSuccess={(enabled, data) => {
// Refresh status circle when toggle changes
// Use API key from hook's data (most up-to-date) or fallback to integrations state
const apiKey = data?.apiKey || integrations.runware.apiKey;
// Validate with current enabled state and API key
validateIntegration('runware', enabled, apiKey);
// Validate with current enabled state
validateIntegration('runware', enabled);
}}
onSettings={() => handleSettings('runware')}
onDetails={() => handleDetails('runware')}
@@ -1003,11 +966,7 @@ export default function Integration() {
<Alert
variant="info"
title="AI Integration & Image Generation Testing"
message="Configure and test your AI integrations on this page.
Set up OpenAI and Runware API keys, validate connections, and test image generation with different models and parameters.
Before you start, please read the documentation for each integration.
Make sure to use the correct API keys and models for each integration."
message="Test your AI integrations and image generation on this page. The platform provides API keys - you can customize model preferences and parameters based on your plan. Test connections to verify everything is working correctly."
/>
</div>
</div>
@@ -1093,7 +1052,7 @@ export default function Integration() {
onClick={() => {
handleTestConnection();
}}
disabled={isTesting || isSaving || !integrations[selectedIntegration]?.apiKey}
disabled={isTesting || isSaving}
className="flex items-center gap-2"
>
{isTesting ? 'Testing...' : 'Test Connection'}

View File

@@ -61,9 +61,6 @@ export default function IndustriesSectorsKeywords() {
const [countryFilter, setCountryFilter] = useState('');
const [difficultyFilter, setDifficultyFilter] = useState('');
// Check if user is admin/superuser (role is 'admin' or 'developer')
const isAdmin = user?.role === 'admin' || user?.role === 'developer';
// Import modal state
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isImporting, setIsImporting] = useState(false);
@@ -706,18 +703,6 @@ export default function IndustriesSectorsKeywords() {
}
}}
bulkActions={pageConfig.bulkActions}
customActions={
isAdmin ? (
<Button
variant="secondary"
size="sm"
onClick={handleImportClick}
>
<PlusIcon className="w-4 h-4 mr-2" />
Import Keywords
</Button>
) : undefined
}
pagination={{
currentPage,
totalPages,
@@ -733,8 +718,7 @@ export default function IndustriesSectorsKeywords() {
selectedIds,
onSelectionChange: setSelectedIds,
}}
// Only show row actions for admin users
onEdit={isAdmin ? undefined : undefined}
onEdit={undefined}
onDelete={undefined}
/>

View File

@@ -22,7 +22,6 @@ import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon
import { createImagesPageConfig } from '../../config/pages/images.config';
import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal';
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
import { useResourceDebug } from '../../hooks/useResourceDebug';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import { Modal } from '../../components/ui/modal';
@@ -30,33 +29,6 @@ import { Modal } from '../../components/ui/modal';
export default function Images() {
const toast = useToast();
// Resource Debug toggle - controls AI Function Logs
const resourceDebugEnabled = useResourceDebug();
// AI Function Logs state
const [aiLogs, setAiLogs] = useState<Array<{
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}>>([]);
// Helper function to add log entry (only if Resource Debug is enabled)
const addAiLog = useCallback((log: {
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}) => {
if (resourceDebugEnabled) {
setAiLogs(prev => [...prev, log]);
}
}, [resourceDebugEnabled]);
// Data state
const [images, setImages] = useState<ContentImagesGroup[]>([]);
const [loading, setLoading] = useState(true);
@@ -373,36 +345,16 @@ export default function Images() {
console.log('[Generate Images] Max in-article images from settings:', maxInArticleImages);
// STAGE 2: Start actual generation
addAiLog({
timestamp: new Date().toISOString(),
type: 'request',
action: 'generate_images',
data: { imageIds, contentId, totalImages: imageIds.length }
});
const result = await generateImages(imageIds, contentId);
if (result.success && result.task_id) {
// Task started successfully - polling will be handled by ImageQueueModal
setTaskId(result.task_id);
console.log('[Generate Images] Stage 2: Task started with ID:', result.task_id);
addAiLog({
timestamp: new Date().toISOString(),
type: 'step',
action: 'generate_images',
stepName: 'Task Queued',
data: { task_id: result.task_id, message: 'Image generation task queued' }
});
} else {
toast.error(result.error || 'Failed to start image generation');
setIsQueueModalOpen(false);
setTaskId(null);
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'generate_images',
data: { error: result.error || 'Failed to start image generation' }
});
}
} catch (error: any) {
@@ -581,7 +533,6 @@ export default function Images() {
model={imageModel || undefined}
provider={imageProvider || undefined}
onUpdateQueue={setImageQueue}
onLog={addAiLog}
/>
{/* Status Update Modal */}
@@ -623,74 +574,6 @@ export default function Images() {
</div>
)}
</Modal>
{/* AI Function Logs - Display below table (only when Resource Debug is enabled) */}
{resourceDebugEnabled && aiLogs.length > 0 && (
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
AI Function Logs
</h3>
<button
onClick={() => setAiLogs([])}
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
Clear Logs
</button>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{aiLogs.slice().reverse().map((log, index) => (
<div
key={index}
className={`p-3 rounded border text-xs font-mono ${
log.type === 'request'
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
: log.type === 'success'
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: log.type === 'error'
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2 flex-wrap">
<span className={`font-semibold ${
log.type === 'request'
? 'text-blue-700 dark:text-blue-300'
: log.type === 'success'
? 'text-green-700 dark:text-green-300'
: log.type === 'error'
? 'text-red-700 dark:text-red-300'
: 'text-purple-700 dark:text-purple-300'
}`}>
[{log.type.toUpperCase()}]
</span>
<span className="text-gray-700 dark:text-gray-300">
{log.action}
</span>
{log.stepName && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
{log.stepName}
</span>
)}
{log.percentage !== undefined && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{log.percentage}%
</span>
)}
</div>
<span className="text-gray-500 dark:text-gray-400">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
</div>
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
{JSON.stringify(log.data, null, 2)}
</pre>
</div>
))}
</div>
</div>
)}
</>
);
}

View File

@@ -23,7 +23,6 @@ import {
import FormModal from '../../components/common/FormModal';
import ProgressModal from '../../components/common/ProgressModal';
import { useProgressModal } from '../../hooks/useProgressModal';
import { useResourceDebug } from '../../hooks/useResourceDebug';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { TaskIcon, PlusIcon, DownloadIcon, FileIcon, ImageIcon, CheckCircleIcon } from '../../icons';
import { createTasksPageConfig } from '../../config/pages/tasks.config';
@@ -139,36 +138,11 @@ export default function Tasks() {
}, [tasks, totalCount]);
// AI Function Logs state
const [aiLogs, setAiLogs] = useState<Array<{
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}>>([]);
// Resource Debug toggle - controls AI Function Logs
const resourceDebugEnabled = useResourceDebug();
// Track last logged step to avoid duplicates
const lastLoggedStepRef = useRef<string | null>(null);
const lastLoggedPercentageRef = useRef<number>(-1);
const hasReloadedRef = useRef<boolean>(false);
// Helper function to add log entry (only if Resource Debug is enabled)
const addAiLog = useCallback((log: {
timestamp: string;
type: 'request' | 'success' | 'error' | 'step';
action: string;
data: any;
stepName?: string;
percentage?: number;
}) => {
if (resourceDebugEnabled) {
setAiLogs(prev => [...prev, log]);
}
}, [resourceDebugEnabled]);
// Load clusters for filter dropdown
useEffect(() => {
@@ -311,65 +285,23 @@ export default function Tasks() {
// return;
// }
const requestData = {
ids: [row.id],
task_title: row.title,
task_id: row.id,
};
// Log request
addAiLog({
timestamp: new Date().toISOString(),
type: 'request',
action: 'generate_content (Row Action)',
data: requestData,
});
try {
const result = await autoGenerateContent([row.id]);
if (result.success) {
if (result.task_id) {
// Log success with task_id
addAiLog({
timestamp: new Date().toISOString(),
type: 'success',
action: 'generate_content (Row Action)',
data: { task_id: result.task_id, message: result.message },
});
// Async task - show progress modal
progressModal.openModal(result.task_id, 'Generating Content', 'ai-generate-content-03');
toast.success('Content generation started');
} else {
// Log success with results
addAiLog({
timestamp: new Date().toISOString(),
type: 'success',
action: 'generate_content (Row Action)',
data: { tasks_updated: result.tasks_updated || 0, message: result.message },
});
// Synchronous completion
toast.success(`Content generated successfully: ${result.tasks_updated || 0} article generated`);
await loadTasks();
}
} else {
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'generate_content (Row Action)',
data: { error: result.error || 'Failed to generate content' },
});
toast.error(result.error || 'Failed to generate content');
}
} catch (error: any) {
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'generate_content (Row Action)',
data: { error: error.message || 'Unknown error occurred' },
});
toast.error(`Failed to generate content: ${error.message}`);
}
}
@@ -389,64 +321,23 @@ export default function Tasks() {
}
const numIds = ids.map(id => parseInt(id));
const selectedTasks = tasks.filter(t => numIds.includes(t.id));
const requestData = {
ids: numIds,
task_count: numIds.length,
task_titles: selectedTasks.map(t => t.title),
};
// Log request
addAiLog({
timestamp: new Date().toISOString(),
type: 'request',
action: 'generate_images (Bulk Action)',
data: requestData,
});
try {
const result = await autoGenerateImages(numIds);
if (result.success) {
if (result.task_id) {
// Log success with task_id
addAiLog({
timestamp: new Date().toISOString(),
type: 'success',
action: 'generate_images (Bulk Action)',
data: { task_id: result.task_id, message: result.message, task_count: numIds.length },
});
// Async task - show progress modal
progressModal.openModal(result.task_id, 'Generating Images');
toast.success('Image generation started');
} else {
// Log success with results
addAiLog({
timestamp: new Date().toISOString(),
type: 'success',
action: 'generate_images (Bulk Action)',
data: { images_created: result.images_created || 0, message: result.message, task_count: numIds.length },
});
// Synchronous completion
toast.success(`Image generation complete: ${result.images_created || 0} images generated`);
await loadTasks();
}
} else {
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'generate_images (Bulk Action)',
data: { error: result.error || 'Failed to generate images', task_count: numIds.length },
});
toast.error(result.error || 'Failed to generate images');
}
} catch (error: any) {
// Log error
addAiLog({
timestamp: new Date().toISOString(),
type: 'error',
action: 'generate_images (Bulk Action)',
data: { error: error.message || 'Unknown error occurred', task_count: numIds.length },
});
toast.error(`Failed to generate images: ${error.message}`);
}
} else {
@@ -454,96 +345,9 @@ export default function Tasks() {
}
}, [toast, loadTasks, progressModal, tasks]);
// Log AI function progress steps
useEffect(() => {
if (!progressModal.taskId || !progressModal.isOpen) {
return;
}
const progress = progressModal.progress;
const currentStep = progress.details?.phase || '';
const currentPercentage = progress.percentage;
const currentMessage = progress.message;
const currentStatus = progress.status;
// Log step changes
if (currentStep && currentStep !== lastLoggedStepRef.current) {
const stepType = currentStatus === 'error' ? 'error' :
currentStatus === 'completed' ? 'success' : 'step';
addAiLog({
timestamp: new Date().toISOString(),
type: stepType,
action: progressModal.title || 'AI Function',
stepName: currentStep,
percentage: currentPercentage,
data: {
step: currentStep,
message: currentMessage,
percentage: currentPercentage,
status: currentStatus,
details: progress.details,
},
});
lastLoggedStepRef.current = currentStep;
lastLoggedPercentageRef.current = currentPercentage;
}
// Log percentage changes for same step (if significant change)
else if (currentStep && Math.abs(currentPercentage - lastLoggedPercentageRef.current) >= 10) {
const stepType = currentStatus === 'error' ? 'error' :
currentStatus === 'completed' ? 'success' : 'step';
addAiLog({
timestamp: new Date().toISOString(),
type: stepType,
action: progressModal.title || 'AI Function',
stepName: currentStep,
percentage: currentPercentage,
data: {
step: currentStep,
message: currentMessage,
percentage: currentPercentage,
status: currentStatus,
details: progress.details,
},
});
lastLoggedPercentageRef.current = currentPercentage;
}
// Log status changes (error, completed)
else if (currentStatus === 'error' || currentStatus === 'completed') {
// Only log if we haven't already logged this status for this step
if (currentStep !== lastLoggedStepRef.current ||
(currentStatus === 'error' && lastLoggedStepRef.current !== 'error') ||
(currentStatus === 'completed' && lastLoggedStepRef.current !== 'completed')) {
const stepType = currentStatus === 'error' ? 'error' : 'success';
addAiLog({
timestamp: new Date().toISOString(),
type: stepType,
action: progressModal.title || 'AI Function',
stepName: currentStep || 'Final',
percentage: currentPercentage,
data: {
step: currentStep || 'Final',
message: currentMessage,
percentage: currentPercentage,
status: currentStatus,
details: progress.details,
},
});
lastLoggedStepRef.current = currentStep || currentStatus;
}
}
}, [progressModal.progress, progressModal.taskId, progressModal.isOpen, progressModal.title, addAiLog]);
// Reset step tracking when modal closes or opens
// Reset reload flag when modal closes or opens
useEffect(() => {
if (!progressModal.isOpen) {
lastLoggedStepRef.current = null;
lastLoggedPercentageRef.current = -1;
hasReloadedRef.current = false; // Reset reload flag when modal closes
} else {
// Reset reload flag when modal opens for a new task
@@ -804,74 +608,6 @@ export default function Tasks() {
}}
/>
{/* AI Function Logs - Display below table (only when Resource Debug is enabled) */}
{resourceDebugEnabled && aiLogs.length > 0 && (
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
AI Function Logs
</h3>
<button
onClick={() => setAiLogs([])}
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
Clear Logs
</button>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{aiLogs.slice().reverse().map((log, index) => (
<div
key={index}
className={`p-3 rounded border text-xs font-mono ${
log.type === 'request'
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
: log.type === 'success'
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: log.type === 'error'
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2 flex-wrap">
<span className={`font-semibold ${
log.type === 'request'
? 'text-blue-700 dark:text-blue-300'
: log.type === 'success'
? 'text-green-700 dark:text-green-300'
: log.type === 'error'
? 'text-red-700 dark:text-red-300'
: 'text-purple-700 dark:text-purple-300'
}`}>
[{log.type.toUpperCase()}]
</span>
<span className="text-gray-700 dark:text-gray-300">
{log.action}
</span>
{log.stepName && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
{log.stepName}
</span>
)}
{log.percentage !== undefined && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{log.percentage}%
</span>
)}
</div>
<span className="text-gray-500 dark:text-gray-400">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
</div>
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
{JSON.stringify(log.data, null, 2)}
</pre>
</div>
))}
</div>
</div>
)}
{/* Create/Edit Modal */}
<FormModal
isOpen={isModalOpen}

View File

@@ -1,232 +0,0 @@
/**
* Admin System Dashboard
* Overview page with stats, alerts, revenue, active accounts, pending approvals
*/
import { useState, useEffect } from 'react';
import {
Users,
CheckCircle,
DollarSign,
Clock,
AlertCircle,
Activity,
Loader2,
ExternalLink,
Globe,
Database,
Folder,
Server,
GitBranch,
FileText,
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import { getAdminBillingStats } from '../../services/billing.api';
export default function AdminSystemDashboard() {
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<any>(null);
const [error, setError] = useState<string>('');
const totalUsers = Number(stats?.total_users ?? 0);
const activeUsers = Number(stats?.active_users ?? 0);
const issuedCredits = Number(stats?.total_credits_issued ?? stats?.credits_issued_30d ?? 0);
const usedCredits = Number(stats?.total_credits_used ?? stats?.credits_used_30d ?? 0);
const creditScale = Math.max(issuedCredits, usedCredits, 1);
const issuedPct = Math.min(100, Math.round((issuedCredits / creditScale) * 100));
const usedPct = Math.min(100, Math.round((usedCredits / creditScale) * 100));
const adminLinks = [
{ label: 'Marketing Site', url: 'https://igny8.com', icon: <Globe className="w-5 h-5 text-blue-600" />, note: 'Public marketing site' },
{ label: 'IGNY8 App', url: 'https://app.igny8.com', icon: <Globe className="w-5 h-5 text-green-600" />, note: 'Main SaaS UI' },
{ label: 'Django Admin', url: 'https://api.igny8.com/admin', icon: <Server className="w-5 h-5 text-indigo-600" />, note: 'Backend admin UI' },
{ label: 'PgAdmin', url: 'http://31.97.144.105:5050/', icon: <Database className="w-5 h-5 text-amber-600" />, note: 'Postgres console' },
{ label: 'File Manager', url: 'https://files.igny8.com', icon: <Folder className="w-5 h-5 text-teal-600" />, note: 'File manager UI' },
{ label: 'Portainer', url: 'http://31.97.144.105:9443', icon: <Server className="w-5 h-5 text-purple-600" />, note: 'Container management' },
{ label: 'API Docs (Swagger)', url: 'https://api.igny8.com/api/docs/', icon: <FileText className="w-5 h-5 text-orange-600" />, note: 'Swagger UI' },
{ label: 'API Docs (ReDoc)', url: 'https://api.igny8.com/api/redoc/', icon: <FileText className="w-5 h-5 text-rose-600" />, note: 'ReDoc docs' },
{ label: 'Gitea Repo', url: 'https://git.igny8.com/salman/igny8', icon: <GitBranch className="w-5 h-5 text-gray-700" />, note: 'Source control' },
];
useEffect(() => {
loadStats();
}, []);
const loadStats = async () => {
try {
setLoading(true);
const data = await getAdminBillingStats();
setStats(data);
} catch (err: any) {
setError(err.message || 'Failed to load system stats');
console.error('Admin stats load error:', err);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">System Dashboard</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Overview of system health and billing activity
</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-600" />
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Users</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{totalUsers.toLocaleString()}
</div>
</div>
<Users className="w-12 h-12 text-blue-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Active Users</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{activeUsers.toLocaleString()}
</div>
</div>
<CheckCircle className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Credits Issued</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{issuedCredits.toLocaleString()}
</div>
<div className="text-sm text-gray-500 mt-1">lifetime total</div>
</div>
<DollarSign className="w-12 h-12 text-green-600 opacity-50" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Credits Used</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{usedCredits.toLocaleString()}
</div>
<div className="text-sm text-gray-500 mt-1">lifetime total</div>
</div>
<Clock className="w-12 h-12 text-yellow-600 opacity-50" />
</div>
</Card>
</div>
{/* System Health */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Activity className="w-5 h-5" />
System Health
</h2>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">API Status</span>
<Badge variant="light" color="success">Operational</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">Database</span>
<Badge variant="light" color="success">Healthy</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">Background Jobs</span>
<Badge variant="light" color="success">Running</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">Last Check</span>
<span className="text-sm text-gray-600 dark:text-gray-400">
{stats?.system_health?.last_check || 'Just now'}
</span>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Credit Usage</h2>
<div className="space-y-4">
<div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600 dark:text-gray-400">Issued (30 days)</span>
<span className="font-semibold">{issuedCredits.toLocaleString()}</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: `${issuedPct}%` }}></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600 dark:text-gray-400">Used (30 days)</span>
<span className="font-semibold">{usedCredits.toLocaleString()}</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div className="bg-green-600 h-2 rounded-full" style={{ width: `${usedPct}%` }}></div>
</div>
</div>
</div>
</Card>
</div>
{/* Admin Quick Access */}
<Card className="p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold">Admin Quick Access</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Open common admin tools directly</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{adminLinks.map((link) => (
<a
key={link.url}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-start justify-between rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-3">
<div className="mt-0.5">{link.icon}</div>
<div>
<p className="text-sm font-semibold text-gray-900 dark:text-white">{link.label}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{link.note}</p>
</div>
</div>
<ExternalLink className="w-4 h-4 text-gray-400" />
</a>
))}
</div>
</Card>
</div>
);
}

View File

@@ -19,6 +19,7 @@ interface User {
email: string;
username: string;
role: string;
is_staff?: boolean;
account?: {
id: number;
name: string;