diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 1918a8e..b0aec90 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -41,7 +41,8 @@ export default function Admin() { const [adminPasswordChange, setAdminPasswordChange] = useState({ userId: null, newPassword: '' }); // Settings state (from Settings.jsx) - const [profiles, setProfiles] = useState([]); + // 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', @@ -59,16 +60,20 @@ export default function Admin() { text_context_length: 5000, scraper_timeout: 90000 }); - const [newProfile, setNewProfile] = useState({ + const [newChannel, setNewChannel] = useState({ name: '', - apprise_url: '', - check_interval_minutes: 60, - notify_on_price_drop: true, - notify_on_target_price: true, - price_drop_threshold_percent: 10, - notify_on_stock_change: true + type: 'email', + configuration: '', + // Helper fields for UI + email_user: '', + email_pass: '', + email_host: '', + email_port: '', + discord_webhook: '', + mattermost_webhook: '', + is_active: true }); - const [editingProfileId, setEditingProfileId] = useState(null); + const [editingChannelId, setEditingChannelId] = useState(null); const [showAdvancedAI, setShowAdvancedAI] = useState(false); const [searchSites, setSearchSites] = useState([]); const [resettingSites, setResettingSites] = useState(false); @@ -101,16 +106,16 @@ export default function Admin() { const fetchAll = async () => { try { const headers = getAuthHeaders(); - const [usersRes, profilesRes, settingsRes, jobRes, sitesRes] = await Promise.all([ + const [usersRes, channelsRes, settingsRes, jobRes, sitesRes] = await Promise.all([ axios.get(`${API_URL}/auth/users`, { headers }), - axios.get(`${API_URL}/notification-profiles`), + 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); - setProfiles(profilesRes.data); + setChannels(channelsRes.data); setJobConfig(jobRes.data); setSearchSites(sitesRes.data); @@ -254,63 +259,131 @@ export default function Admin() { } }; - // Profile functions - const handleProfileSubmit = async (e) => { + // Channel functions + const handleChannelSubmit = async (e) => { e.preventDefault(); try { - if (editingProfileId) { - await axios.put(`${API_URL}/notification-profiles/${editingProfileId}`, newProfile); - toast.success('Profil mis à jour'); - } else { - await axios.post(`${API_URL}/notification-profiles`, newProfile); - toast.success('Profil créé'); + 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 + }); } - setNewProfile({ - name: '', apprise_url: '', check_interval_minutes: 60, - notify_on_price_drop: true, notify_on_target_price: true, - price_drop_threshold_percent: 10, notify_on_stock_change: true - }); - setEditingProfileId(null); + + 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('Erreur lors de l\'enregistrement'); + toast.error(editingChannelId ? 'Failed to update channel' : 'Failed to create channel'); } }; - const editProfile = (profile) => { - setNewProfile({ - name: profile.name, apprise_url: profile.apprise_url, - check_interval_minutes: profile.check_interval_minutes, - notify_on_price_drop: profile.notify_on_price_drop, - notify_on_target_price: profile.notify_on_target_price, - price_drop_threshold_percent: profile.price_drop_threshold_percent, - notify_on_stock_change: profile.notify_on_stock_change + const resetChannelForm = () => { + setNewChannel({ + name: '', + type: 'email', + configuration: '', + email_user: '', + email_pass: '', + email_host: '', + email_port: '', + discord_webhook: '', + mattermost_webhook: '', + is_active: true }); - setEditingProfileId(profile.id); + setEditingChannelId(null); }; - const cancelEdit = () => { - setNewProfile({ - name: '', apprise_url: '', check_interval_minutes: 60, - notify_on_price_drop: true, notify_on_target_price: true, - price_drop_threshold_percent: 10, notify_on_stock_change: true + 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 }); - setEditingProfileId(null); + setEditingChannelId(channel.id); }; - const deleteProfile = async (id) => { - if (confirm('Êtes-vous sûr de vouloir supprimer ce profil ?')) { + const deleteChannel = async (id) => { + if (confirm('Are you sure you want to delete this channel?')) { try { - await axios.delete(`${API_URL}/notification-profiles/${id}`); - toast.success('Profil supprimé'); - if (editingProfileId === id) cancelEdit(); + await axios.delete(`${API_URL}/notifications/channels/${id}`); + toast.success('Channel deleted'); + if (editingChannelId === id) { + resetChannelForm(); + } fetchAll(); } catch (error) { - toast.error('Erreur lors de la suppression'); + 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 { @@ -774,77 +847,117 @@ export default function Admin() { {activeTab === 'notifications' && ( - Profils de notification + Notification Channels + Configure where you want to receive alerts (Email, Discord, Mattermost). -
+
-

{editingProfileId ? 'Modifier le profil' : 'Nouveau profil'}

- {editingProfileId && } +

{editingChannelId ? 'Edit Channel' : 'Create New Channel'}

+ {editingChannelId && ( + + )}
- - setNewProfile({ ...newProfile, name: e.target.value })} /> + + setNewChannel({ ...newChannel, name: e.target.value })} placeholder="e.g., My Discord" />
- - setNewProfile({ ...newProfile, apprise_url: e.target.value })} /> + +
-
-
- - setNewProfile({ ...newProfile, check_interval_minutes: parseInt(e.target.value) })} /> -
-
- - setNewProfile({ ...newProfile, price_drop_threshold_percent: parseFloat(e.target.value) })} /> -
+ + {/* Configuration Fields based on Type */} +
+ {newChannel.type === 'email' && ( +
+
+ + setNewChannel({ ...newChannel, email_user: e.target.value })} placeholder="user@example.com" /> +
+
+ + setNewChannel({ ...newChannel, email_pass: e.target.value })} /> +
+
+ + setNewChannel({ ...newChannel, email_host: e.target.value })} placeholder="smtp.gmail.com" /> +
+
+ + setNewChannel({ ...newChannel, email_port: e.target.value })} placeholder="587" /> +
+
+ )} + + {newChannel.type === 'discord' && ( +
+ + setNewChannel({ ...newChannel, discord_webhook: e.target.value })} placeholder="https://discord.com/api/webhooks/..." /> +
+ )} + + {newChannel.type === 'mattermost' && ( +
+ + setNewChannel({ ...newChannel, mattermost_webhook: e.target.value })} placeholder="https://mattermost.example.com/hooks/..." /> +
+ )}
-
-
- setNewProfile({ ...newProfile, notify_on_price_drop: checked })} /> - -
-
- setNewProfile({ ...newProfile, notify_on_target_price: checked })} /> - -
-
- setNewProfile({ ...newProfile, notify_on_stock_change: checked })} /> - -
+ +
+ setNewChannel({ ...newChannel, is_active: checked })} /> +
- + +
-

Profils existants

- {profiles.length === 0 ? ( -

Aucun profil créé.

+

Existing Channels

+ {channels.length === 0 ? ( +

No channels created yet.

) : (
- {profiles.map(profile => ( -
-
-
-

{profile.name}

-

{profile.apprise_url}

+ {channels.map(channel => ( +
+
+
+
+

+ {channel.name} + {!channel.is_active && (Inactive)} +

+

{channel.type}

+
+
+ + + +
-
- - -
-
-
-
{profile.check_interval_minutes}m
-
{profile.price_drop_threshold_percent}%
-
-
- {profile.notify_on_price_drop && Baisse} - {profile.notify_on_target_price && Cible} - {profile.notify_on_stock_change && Stock}
))} @@ -854,6 +967,6 @@ export default function Admin() { )} -
+
); } diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx deleted file mode 100644 index 33f3401..0000000 --- a/frontend/src/pages/Settings.jsx +++ /dev/null @@ -1,809 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import { toast } from 'sonner'; -import { Sun, Clock, Cpu, Settings as SettingsIcon, Bell, Trash2, ChevronDown, ChevronUp, Edit2, CheckCircle2, AlertCircle, TrendingDown, DollarSign, Package, RefreshCw, Filter, Eye, Code, Brain, Sparkles, Globe, RotateCcw } 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'; - -const API_URL = '/api'; - -// Icônes pour les catégories OpenRouter -const categoryIcons = { - chat: Sparkles, - vision: Eye, - code: Code, - reasoning: Brain, - free: DollarSign -}; - -export default function Settings() { - 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); - - // Search Sites state - const [searchSites, setSearchSites] = useState([]); - const [resettingSites, setResettingSites] = useState(false); - - // OpenRouter state - 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(() => { - fetchAll(); - }, []); - - // Charger les modèles OpenRouter quand le provider change ou la catégorie - useEffect(() => { - if (config.ai_provider === 'openrouter') { - fetchOpenRouterModels(); - } - }, [config.ai_provider, openrouterCategory]); - - 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); - toast.error('Erreur lors du chargement des modèles OpenRouter'); - } 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 fetchAll = async () => { - try { - const [channelsRes, settingsRes, jobRes, sitesRes] = await Promise.all([ - axios.get(`${API_URL}/notifications/channels`), - axios.get(`${API_URL}/settings`), - axios.get(`${API_URL}/jobs/config`), - axios.get(`${API_URL}/search-sites`) - ]); - - 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) { - toast.error('Failed to fetch settings'); - } - }; - - const updateSetting = async (key, value) => { - try { - await axios.post(`${API_URL}/settings`, { key, value: value.toString() }); - setConfig(prev => ({ ...prev, [key]: value })); - toast.success('Setting updated'); - } catch (error) { - toast.error('Failed to update setting'); - } - }; - - const updateJobConfig = async () => { - try { - await axios.post(`${API_URL}/jobs/config`, { - key: 'refresh_interval_minutes', - value: jobConfig.refresh_interval_minutes.toString() - }); - toast.success('Job configuration updated'); - fetchAll(); - } catch (error) { - toast.error('Failed to update job config'); - } - }; - - 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); - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - }; - - 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'); - } - }; - - // Search Sites 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 ? Cette action va remettre les valeurs par défaut.')) { - 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); - } - }; - - return ( -
-
-

Settings

-

Configure application preferences and notifications.

-
- - {/* AI Configuration */} - - - AI Configuration - Configure the AI model used for analyzing product pages. - - -
-
- - -
- {config.ai_provider !== 'openrouter' ? ( -
- - updateSetting('ai_model', e.target.value)} placeholder="ex: moondream" /> -
- ) : ( -
- - -
- )} -
- -
- - updateSetting('ai_api_key', e.target.value)} - placeholder={config.ai_provider === 'openrouter' ? 'sk-or-...' : 'Optionnel pour les modèles locaux'} - /> -
- - {/* OpenRouter Model Selector */} - {config.ai_provider === 'openrouter' && ( -
-
- - -
- - {/* Filtres par catégorie - badges cliquables */} -
- {openrouterCategories.map(cat => { - const Icon = categoryIcons[cat.id] || Filter; - return ( - - ); - })} -
- - {/* Liste des modèles */} -
- - {openrouterLoading ? ( -
- Chargement des modèles... -
- ) : ( - - )} -
- - {/* Afficher les détails du modèle sélectionné */} - {config.ai_model && openrouterModels.length > 0 && ( -
- {(() => { - const selectedModel = openrouterModels.find(m => m.id === config.ai_model); - if (!selectedModel) return Modèle: {config.ai_model}; - return ( -
-
{selectedModel.name}
-
{selectedModel.description}
-
- - Contexte: {selectedModel.context_length?.toLocaleString()} tokens - - - Entrée: {formatPrice(selectedModel.pricing?.prompt)} - - - Sortie: {formatPrice(selectedModel.pricing?.completion)} - - {selectedModel.categories?.map(cat => ( - - {cat} - - ))} -
-
- ); - })()} -
- )} -
- )} - -
- -
- - {showAdvancedAI && ( -
-
- - updateSetting('ai_api_base', e.target.value)} /> -
- -
-
-
-
- - {config.ai_temperature} -
- updateSetting('ai_temperature', parseFloat(e.target.value))} - /> -
-
-
- - {config.confidence_threshold_price} -
- updateSetting('confidence_threshold_price', parseFloat(e.target.value))} - /> -
-
-
- - {config.confidence_threshold_stock} -
- updateSetting('confidence_threshold_stock', parseFloat(e.target.value))} - /> -
-
- -
-
- - updateSetting('ai_max_tokens', parseInt(e.target.value))} /> -
-
- - updateSetting('enable_json_repair', checked)} /> -
-
-
-
- )} -
-
- - {/* Scraper Configuration */} - - - Scraper Configuration - Configure how the scraper interacts with web pages. - - -
-
- -

Scroll down to trigger lazy loading.

-
- updateSetting('smart_scroll_enabled', checked)} /> -
- {config.smart_scroll_enabled && ( -
- - updateSetting('smart_scroll_pixels', parseInt(e.target.value))} /> -
- )} -
-
- -

Send page text to AI along with screenshot.

-
- updateSetting('text_context_enabled', checked)} /> -
- {config.text_context_enabled && ( -
- - updateSetting('text_context_length', parseInt(e.target.value))} /> -
- )} -
- - updateSetting('scraper_timeout', parseInt(e.target.value))} /> -
-
-
- - {/* Job Configuration */} - - - Automated Refresh Job - Configure the background job that checks for price updates. - - -
- - setJobConfig({ ...jobConfig, refresh_interval_minutes: e.target.value })} /> -
-
-

Next run: {jobConfig.next_run ? new Date(jobConfig.next_run).toLocaleString() : 'Not scheduled'}

-

Status: {jobConfig.running ? 'Running' : 'Idle'}

-
- -
-
- - {/* Search Sites Configuration */} - - -
-
- Sites de recherche - Sites e-commerce français configurés pour la recherche de produits. -
- -
-
- -
- {searchSites.filter(s => s.is_active).length} site(s) actif(s) sur {searchSites.length} -
- - {searchSites.length === 0 ? ( -

Aucun site configuré. Cliquez sur "Réinitialiser" pour charger les sites par défaut.

- ) : ( -
- {searchSites.map(site => ( -
-
-
-
-

{site.name}

-

{site.domain}

-
- toggleSiteActive(site)} - className="ml-2" - /> -
-
- {site.category && ( - - {site.category} - - )} -
-
-
- ))} -
- )} -
-
- - {/* Notification Profiles */} - {/* Notification Channels */} - - - Notification Channels - Configure where you want to receive alerts (Email, Discord, Mattermost). - - -
-
-

{editingChannelId ? 'Edit Channel' : 'Create New Channel'}

- {editingChannelId && ( - - )} -
-
-
- - setNewChannel({ ...newChannel, name: e.target.value })} placeholder="e.g., My Discord" /> -
-
- - -
-
- - {/* Configuration Fields based on Type */} -
- {newChannel.type === 'email' && ( -
-
- - setNewChannel({ ...newChannel, email_user: e.target.value })} placeholder="user@example.com" /> -
-
- - setNewChannel({ ...newChannel, email_pass: e.target.value })} /> -
-
- - setNewChannel({ ...newChannel, email_host: e.target.value })} placeholder="smtp.gmail.com" /> -
-
- - setNewChannel({ ...newChannel, email_port: e.target.value })} placeholder="587" /> -
-
- )} - - {newChannel.type === 'discord' && ( -
- - setNewChannel({ ...newChannel, discord_webhook: e.target.value })} placeholder="https://discord.com/api/webhooks/..." /> -
- )} - - {newChannel.type === 'mattermost' && ( -
- - setNewChannel({ ...newChannel, mattermost_webhook: e.target.value })} placeholder="https://mattermost.example.com/hooks/..." /> -
- )} -
- -
- setNewChannel({ ...newChannel, is_active: checked })} /> - -
- - -
- -
-

Existing Channels

- {channels.length === 0 ? ( -

No channels created yet.

- ) : ( -
- {channels.map(channel => ( -
-
-
-
-

- {channel.name} - {!channel.is_active && (Inactive)} -

-

{channel.type}

-
-
- - - -
-
-
-
- ))} -
- )} -
-
-
- -
- ); -}