import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; import { Users, UserPlus, Trash2, Key, Shield, ShieldOff, Settings as SettingsIcon, Cpu, Clock, Bell, Globe, ChevronDown, ChevronUp, RefreshCw, RotateCcw, Edit2, Sparkles, Eye, Code, Brain, DollarSign, Filter, TrendingDown, CheckCircle2, Package } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { cn } from '@/lib/utils'; import { useAuth } from '@/hooks/use-auth'; const API_URL = '/api'; const categoryIcons = { chat: Sparkles, vision: Eye, code: Code, reasoning: Brain, free: DollarSign }; export default function Admin() { const navigate = useNavigate(); const { user, getAuthHeaders, isAdmin, changePassword } = useAuth(); const [activeTab, setActiveTab] = useState('users'); // User management state const [users, setUsers] = useState([]); const [newUser, setNewUser] = useState({ username: '', password: '', is_admin: false }); const [passwordChange, setPasswordChange] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }); const [adminPasswordChange, setAdminPasswordChange] = useState({ userId: null, newPassword: '' }); // Settings state (from Settings.jsx) // Settings state (from Settings.jsx) const [channels, setChannels] = useState([]); const [jobConfig, setJobConfig] = useState({ refresh_interval_minutes: 60, next_run: null, running: false }); const [config, setConfig] = useState({ ai_provider: 'ollama', ai_model: 'moondream', ai_api_key: '', ai_api_base: 'http://ollama:11434', ai_temperature: 0.1, ai_max_tokens: 300, confidence_threshold_price: 0.5, confidence_threshold_stock: 0.5, enable_json_repair: true, smart_scroll_enabled: false, smart_scroll_pixels: 350, text_context_enabled: false, text_context_length: 5000, scraper_timeout: 90000 }); const [newChannel, setNewChannel] = useState({ name: '', type: 'email', configuration: '', // Helper fields for UI email_user: '', email_pass: '', email_host: '', email_port: '', discord_webhook: '', mattermost_webhook: '', is_active: true }); const [editingChannelId, setEditingChannelId] = useState(null); const [showAdvancedAI, setShowAdvancedAI] = useState(false); const [searchSites, setSearchSites] = useState([]); const [resettingSites, setResettingSites] = useState(false); const [openrouterModels, setOpenrouterModels] = useState([]); const [openrouterLoading, setOpenrouterLoading] = useState(false); const [openrouterCategory, setOpenrouterCategory] = useState('all'); const [openrouterCategories] = useState([ { id: 'all', name: 'Tous', description: 'Tous les modèles' }, { id: 'chat', name: 'Chat', description: 'Conversation générale' }, { id: 'vision', name: 'Vision', description: 'Analyse d\'images' }, { id: 'code', name: 'Code', description: 'Programmation' }, { id: 'reasoning', name: 'Raisonnement', description: 'Raisonnement avancé' }, { id: 'free', name: 'Gratuit', description: 'Modèles gratuits' } ]); useEffect(() => { if (!isAdmin) { navigate('/'); return; } fetchAll(); }, [isAdmin, navigate]); useEffect(() => { if (config.ai_provider === 'openrouter') { fetchOpenRouterModels(); } }, [config.ai_provider, openrouterCategory]); const fetchAll = async () => { try { const headers = getAuthHeaders(); const [usersRes, channelsRes, settingsRes, jobRes, sitesRes] = await Promise.all([ axios.get(`${API_URL}/auth/users`, { headers }), axios.get(`${API_URL}/notifications/channels`), axios.get(`${API_URL}/settings`), axios.get(`${API_URL}/jobs/config`), axios.get(`${API_URL}/search-sites`) ]); setUsers(usersRes.data); setChannels(channelsRes.data); setJobConfig(jobRes.data); setSearchSites(sitesRes.data); const settingsMap = {}; settingsRes.data.forEach(s => settingsMap[s.key] = s.value); setConfig({ ai_provider: settingsMap['ai_provider'] || 'ollama', ai_model: settingsMap['ai_model'] || 'moondream', ai_api_key: settingsMap['ai_api_key'] || '', ai_api_base: settingsMap['ai_api_base'] || 'http://ollama:11434', ai_temperature: parseFloat(settingsMap['ai_temperature'] || '0.1'), ai_max_tokens: parseInt(settingsMap['ai_max_tokens'] || '300'), confidence_threshold_price: parseFloat(settingsMap['confidence_threshold_price'] || '0.5'), confidence_threshold_stock: parseFloat(settingsMap['confidence_threshold_stock'] || '0.5'), enable_json_repair: settingsMap['enable_json_repair'] !== 'false', smart_scroll_enabled: settingsMap['smart_scroll_enabled'] === 'true', smart_scroll_pixels: parseInt(settingsMap['smart_scroll_pixels'] || '350'), text_context_enabled: settingsMap['text_context_enabled'] === 'true', text_context_length: parseInt(settingsMap['text_context_length'] || '5000'), scraper_timeout: parseInt(settingsMap['scraper_timeout'] || '90000') }); } catch (error) { console.error('Error fetching data:', error); toast.error('Erreur lors du chargement des données'); } }; // User management functions const handleCreateUser = async (e) => { e.preventDefault(); if (!newUser.username.trim() || !newUser.password) { toast.error('Veuillez remplir tous les champs'); return; } try { await axios.post(`${API_URL}/auth/users`, newUser, { headers: getAuthHeaders() }); toast.success('Utilisateur créé'); setNewUser({ username: '', password: '', is_admin: false }); fetchAll(); } catch (error) { toast.error(error.response?.data?.detail || 'Erreur lors de la création'); } }; const handleDeleteUser = async (userId) => { if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) return; try { await axios.delete(`${API_URL}/auth/users/${userId}`, { headers: getAuthHeaders() }); toast.success('Utilisateur supprimé'); fetchAll(); } catch (error) { toast.error(error.response?.data?.detail || 'Erreur lors de la suppression'); } }; const handleToggleUserActive = async (userId) => { try { await axios.put(`${API_URL}/auth/users/${userId}/toggle-active`, {}, { headers: getAuthHeaders() }); toast.success('Statut modifié'); fetchAll(); } catch (error) { toast.error(error.response?.data?.detail || 'Erreur lors de la modification'); } }; const handleChangeMyPassword = async (e) => { e.preventDefault(); if (passwordChange.newPassword !== passwordChange.confirmPassword) { toast.error('Les mots de passe ne correspondent pas'); return; } try { await changePassword(passwordChange.currentPassword, passwordChange.newPassword); toast.success('Mot de passe modifié'); setPasswordChange({ currentPassword: '', newPassword: '', confirmPassword: '' }); } catch (error) { toast.error(error.response?.data?.detail || 'Erreur lors du changement de mot de passe'); } }; const handleAdminChangePassword = async (e) => { e.preventDefault(); if (!adminPasswordChange.userId || !adminPasswordChange.newPassword) return; try { await axios.put( `${API_URL}/auth/users/${adminPasswordChange.userId}/password`, { new_password: adminPasswordChange.newPassword }, { headers: getAuthHeaders() } ); toast.success('Mot de passe modifié'); setAdminPasswordChange({ userId: null, newPassword: '' }); } catch (error) { toast.error(error.response?.data?.detail || 'Erreur lors du changement de mot de passe'); } }; // Settings functions const updateSetting = async (key, value) => { try { await axios.post(`${API_URL}/settings`, { key, value: value.toString() }); setConfig(prev => ({ ...prev, [key]: value })); toast.success('Paramètre mis à jour'); } catch (error) { toast.error('Erreur lors de la mise à jour'); } }; const fetchOpenRouterModels = async () => { setOpenrouterLoading(true); try { const params = new URLSearchParams(); if (config.ai_api_key) params.append('api_key', config.ai_api_key); if (openrouterCategory && openrouterCategory !== 'all') params.append('category', openrouterCategory); const response = await axios.get(`${API_URL}/openrouter/models?${params.toString()}`); setOpenrouterModels(response.data.models || []); } catch (error) { console.error('Erreur chargement modèles OpenRouter:', error); } finally { setOpenrouterLoading(false); } }; const formatPrice = (pricePerToken) => { if (!pricePerToken || pricePerToken === 0) return 'Gratuit'; const pricePerMillion = pricePerToken * 1000000; if (pricePerMillion < 0.01) return `$${pricePerMillion.toFixed(4)}/1M`; if (pricePerMillion < 1) return `$${pricePerMillion.toFixed(3)}/1M`; return `$${pricePerMillion.toFixed(2)}/1M`; }; const updateJobConfig = async () => { try { await axios.post(`${API_URL}/jobs/config`, { key: 'refresh_interval_minutes', value: jobConfig.refresh_interval_minutes.toString() }); toast.success('Configuration du job mise à jour'); fetchAll(); } catch (error) { toast.error('Erreur lors de la mise à jour'); } }; // Channel functions const handleChannelSubmit = async (e) => { e.preventDefault(); try { let configuration = newChannel.configuration; // Construct configuration based on type if (newChannel.type === 'email') { configuration = JSON.stringify({ user: newChannel.email_user, password: newChannel.email_pass, host: newChannel.email_host, port: newChannel.email_port }); } else if (newChannel.type === 'discord') { configuration = JSON.stringify({ webhook_url: newChannel.discord_webhook }); } else if (newChannel.type === 'mattermost') { configuration = JSON.stringify({ webhook_url: newChannel.mattermost_webhook }); } const channelData = { name: newChannel.name, type: newChannel.type, configuration: configuration, is_active: newChannel.is_active }; if (editingChannelId) { await axios.put(`${API_URL}/notifications/channels/${editingChannelId}`, channelData); toast.success('Channel updated'); } else { await axios.post(`${API_URL}/notifications/channels`, channelData); toast.success('Channel created'); } resetChannelForm(); fetchAll(); } catch (error) { toast.error(editingChannelId ? 'Failed to update channel' : 'Failed to create channel'); } }; const resetChannelForm = () => { setNewChannel({ name: '', type: 'email', configuration: '', email_user: '', email_pass: '', email_host: '', email_port: '', discord_webhook: '', mattermost_webhook: '', is_active: true }); setEditingChannelId(null); }; const editChannel = (channel) => { let email_user = ''; let email_pass = ''; let email_host = ''; let email_port = ''; let discord_webhook = ''; let mattermost_webhook = ''; try { const config = JSON.parse(channel.configuration); if (channel.type === 'email') { email_user = config.user || ''; email_pass = config.password || ''; email_host = config.host || ''; email_port = config.port || ''; } else if (channel.type === 'discord') { discord_webhook = config.webhook_url || ''; } else if (channel.type === 'mattermost') { mattermost_webhook = config.webhook_url || ''; } } catch (e) { // If config is not JSON, maybe it's a raw URL } setNewChannel({ name: channel.name, type: channel.type, configuration: channel.configuration, email_user, email_pass, email_host, email_port, discord_webhook, mattermost_webhook, is_active: channel.is_active }); setEditingChannelId(channel.id); }; const deleteChannel = async (id) => { if (confirm('Are you sure you want to delete this channel?')) { try { await axios.delete(`${API_URL}/notifications/channels/${id}`); toast.success('Channel deleted'); if (editingChannelId === id) { resetChannelForm(); } fetchAll(); } catch (error) { toast.error('Failed to delete channel'); } } }; const testChannel = async (id) => { try { await axios.post(`${API_URL}/notifications/channels/${id}/test`); toast.success('Test notification queued'); } catch (error) { toast.error('Failed to send test notification'); } }; // Site functions const toggleSiteActive = async (site) => { try { await axios.put(`${API_URL}/search-sites/${site.id}`, { is_active: !site.is_active }); fetchAll(); } catch (error) { toast.error('Erreur lors de la mise à jour'); } }; const resetSitesToDefaults = async () => { if (!confirm('Êtes-vous sûr de vouloir réinitialiser tous les sites ?')) return; setResettingSites(true); try { const response = await axios.post(`${API_URL}/search-sites/reset`); toast.success(response.data.message); fetchAll(); } catch (error) { toast.error('Erreur lors de la réinitialisation'); } finally { setResettingSites(false); } }; const tabs = [ { id: 'users', label: 'Utilisateurs', icon: Users }, { id: 'ai', label: 'Configuration IA', icon: Cpu }, { id: 'scraper', label: 'Scraper', icon: SettingsIcon }, { id: 'sites', label: 'Sites', icon: Globe }, { id: 'jobs', label: 'Jobs', icon: Clock }, { id: 'notifications', label: 'Notifications', icon: Bell }, ]; return (
Gérez les utilisateurs et la configuration de l'application.
{u.username}
{u.is_admin ? 'Administrateur' : 'Utilisateur'} • {u.is_active ? ' Actif' : ' Inactif'} {u.last_login && ` • Dernière connexion: ${new Date(u.last_login).toLocaleDateString()}`}
Scroll pour charger le contenu lazy.
Envoyer le texte de la page à l'IA.
{site.domain}
Prochain lancement: {jobConfig.next_run ? new Date(jobConfig.next_run).toLocaleString() : 'Non planifié'}
Statut: {jobConfig.running ? 'En cours' : 'Inactif'}
No channels created yet.
) : ({channel.type}