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

@@ -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>
);
}