// BotsManagement.tsx "use client"; import React, { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { API_BASE_URL } from '@/constants/api'; import { DiscordId, System, ErrorResponse } from '@/types'; import { authAwareFetch } from '@/utils/AuthAwareFetch'; interface BotsManagementProps { token: string | null; // Allow token to be null initially logoutUser: () => void; } const BotsManagement: React.FC = ({ token, logoutUser }) => { const [bots, setBots] = useState([]); const [discordIds, setDiscordIds] = useState([]); const [systems, setSystems] = useState([]); const [loading, setLoading] = useState(true); // Set to true initially const [error, setError] = useState(''); const [isAddIdDialogOpen, setIsAddIdDialogOpen] = useState(false); const [newIdData, setNewIdData] = useState & { guild_ids: string }>({ discord_id: '', name: '', token: '', active: false, guild_ids: '', }); const [editingId, setEditingId] = useState(null); const [isAssignDismissDialogOpen, setIsAssignDismissDialogOpen] = useState(false); const [selectedBotClientId, setSelectedBotClientId] = useState(''); const [selectedSystemId, setSelectedSystemId] = useState(''); const [assignDismissAction, setAssignDismissAction] = useState<'assign' | 'dismiss'>('assign'); const fetchData = async (): Promise => { // Only attempt to fetch if a token is present if (!token) { setLoading(false); // Stop loading if no token setError('Authentication token is missing. Please log in.'); return; } setLoading(true); setError(''); let accumulatedError = ''; try { const [nodesRes, discordIdsRes, systemsRes] = await Promise.all([ authAwareFetch('/nodes/', { token }, logoutUser), authAwareFetch('/bots/tokens/', { token }, logoutUser), authAwareFetch('/systems/', { token }, logoutUser), ]); if (nodesRes.ok) { const botsData: any = await nodesRes.json(); setBots(botsData); } else { let errorMsg = `Failed to fetch bots (${nodesRes.status})`; try { const errorData: ErrorResponse = await nodesRes.json(); errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || nodesRes.statusText}`; } catch (e) { errorMsg += `: ${nodesRes.statusText}`; } accumulatedError += errorMsg + '\n'; } if (discordIdsRes.ok) { const discordIdsData: DiscordId[] = await discordIdsRes.json(); setDiscordIds(discordIdsData); } else { let errorMsg = `Failed to fetch Discord IDs (${discordIdsRes.status})`; try { const errorData: ErrorResponse = await discordIdsRes.json(); errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || discordIdsRes.statusText}`; } catch (e) { errorMsg += `: ${discordIdsRes.statusText}`; } accumulatedError += errorMsg + '\n'; } if (systemsRes.ok) { const systemsData: System[] = await systemsRes.json(); setSystems(systemsData); } else { let errorMsg = `Failed to fetch systems (${systemsRes.status})`; try { const errorData: ErrorResponse = await systemsRes.json(); errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || systemsRes.statusText}`; } catch (e) { errorMsg += `: ${systemsRes.statusText}`; } accumulatedError += errorMsg + '\n'; } if (accumulatedError) { setError(accumulatedError.trim()); } } catch (err: any) { // Catch specific unauthorized errors and let authAwareFetch handle logout if (err.message === 'Unauthorized: Session expired or invalid token.') { setLoading(false); // already handled by authAwareFetch return; } setError('Failed to fetch data. Check server connection or console.'); console.error(err); } finally { setLoading(false); } }; useEffect(() => { // This effect runs whenever 'token' changes. // If token becomes available, it triggers fetchData. if (token) { fetchData(); } else { // If token is explicitly null (e.g., after logout or initial load without a token) setLoading(false); // No need to set error "Please log in" here, as AppContent handles the redirect // This is a component internal message if it somehow renders without a token when it shouldn't setError('Authentication required to load data.'); setBots([]); // Clear data if token is gone setDiscordIds([]); setSystems([]); } }, [token]); // Dependency on token ensures fetchData is called when token becomes available const handleAddId = async (): Promise => { setError(''); if (!token) { setError('Authentication token is missing. Please log in.'); return; } try { const payload: Omit = { ...newIdData, guild_ids: newIdData.guild_ids.split(',').map(id => id.trim()).filter(id => id), }; const method = editingId ? 'PUT' : 'POST'; const url = editingId ? `/bots/token/${editingId._id}` : `/bots/token`; const response = await authAwareFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), token, }, logoutUser); if (response.ok) { fetchData(); setIsAddIdDialogOpen(false); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setEditingId(null); } else { let errorMsg = `Failed to ${editingId ? 'update' : 'add'} Discord ID (${response.status})`; try { const errorData: ErrorResponse = await response.json(); errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`; } catch (e) { errorMsg += `: ${response.statusText}`; } setError(errorMsg); } } catch (err: any) { if (err.message === 'Unauthorized: Session expired or invalid token.') { setLoading(false); // already handled by authAwareFetch return; } setError(`Network error during ${editingId ? 'update' : 'add'} Discord ID.`); console.error(err); } }; const handleDeleteId = async (id: string): Promise => { if (!window.confirm('Are you sure you want to delete this Discord ID?')) return; setError(''); if (!token) { setError('Authentication token is missing. Please log in.'); return; } try { const response = await authAwareFetch(`/bots/token/${id}`, { method: 'DELETE', token }, logoutUser); if (response.ok) { fetchData(); } else { let errorMsg = `Failed to delete Discord ID (${response.status})`; try { const errorData: ErrorResponse = await response.json(); errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`; } catch (e) { errorMsg += `: ${response.statusText}`; } setError(errorMsg); } } catch (err: any) { if (err.message === 'Unauthorized: Session expired or invalid token.') { setLoading(false); // already handled by authAwareFetch return; } setError('Network error during delete Discord ID.'); console.error(err); } }; const handleAssignDismiss = async (): Promise => { setError(''); if (!selectedBotClientId || !selectedSystemId) { setError("Bot Client ID and System must be selected."); return; } if (!token) { setError('Authentication token is missing. Please log in.'); return; } try { const endpoint = assignDismissAction === 'assign' ? 'assign' : 'dismiss'; const response = await authAwareFetch(`/systems/${selectedSystemId}/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: selectedBotClientId }), token, }, logoutUser); if (response.ok) { fetchData(); setIsAssignDismissDialogOpen(false); setSelectedBotClientId(''); setSelectedSystemId(''); } else { let errorMsg = `Failed to ${assignDismissAction} bot (${response.status})`; try { const errorData: ErrorResponse = await response.json(); errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`; } catch (e) { errorMsg += `: ${response.statusText}`; } setError(errorMsg); } } catch (err: any) { if (err.message === 'Unauthorized: Session expired or invalid token.') { setLoading(false); // already handled by authAwareFetch return; } setError(`Network error during ${assignDismissAction} bot.`); console.error(err); } }; // Only show loading if actively fetching or waiting for token if (loading && token) return

Loading bots and Discord IDs...

; // Show error if there's an error if (error) return

{error}

; // If not loading and no token, it implies no action can be taken or a login is needed if (!loading && !token) return

Please log in to view and manage bots.

; return ( Bots Management {error &&

{error}

}

Online Bots (Clients)

Client ID Assigned Discord ID (Name) {bots.length > 0 ? ( bots.map((botClientId) => { const assignedSystem = systems.find(sys => sys.avail_on_nodes.includes(botClientId)); const assignedDiscordBot = discordIds.find(did => did.name === assignedSystem?.name); return ( {botClientId} {assignedSystem ? `${assignedSystem.name} (System)` : "N/A or requires different mapping"} ); }) ) : ( No bots currently online or reported by /bots. )}

Manage Discord IDs

Name Discord ID Token (Partial) Active Guilds Actions {discordIds.length > 0 ? ( discordIds.map((dId) => ( {dId.name} {dId.discord_id} {dId.token ? dId.token.substring(0, 8) + '...' : 'N/A'} {dId.active ? 'Yes' : 'No'} {dId.guild_ids?.join(', ')} )) ) : ( No Discord IDs found. )}
{editingId ? 'Edit Discord ID' : 'Add New Discord ID'}
setNewIdData({ ...newIdData, name: e.target.value })} className="col-span-3" />
setNewIdData({ ...newIdData, discord_id: e.target.value })} className="col-span-3" />
setNewIdData({ ...newIdData, token: e.target.value })} className="col-span-3" placeholder={editingId ? "Token hidden for security, re-enter to change" : ""} />
setNewIdData({ ...newIdData, guild_ids: e.target.value })} className="col-span-3" />
setNewIdData({ ...newIdData, active: checked === true })} className="col-span-3" />
Assign/Dismiss Bot to System
); }; export default BotsManagement;