import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { Search, RotateCcw, CheckCircle, XCircle, Server, ServerOff, ClipboardList, Calculator, X, MoreVertical, Play, Pause, Power, Trash2, Unlink, QrCode, RotateCw, Edit3, Plus } from 'lucide-react'; import { auditWapServer, createWapServer, deleteWapBot, deleteWapServer, detachWapOrganizationServer, fetchWapServers, getWapBotQr, recalculateWapServerCount, restartWapBot, startWapBot, stopWapBot, updateWapServer, type WapContainerDto, type WapServerAuditResult, type WapServerDeleteResult, type WapServerQrResult, type WapServerRecalculateCountResult } from '../api/sysadmin'; const getErrorMessage = (error: unknown, fallback: string): string => { const responseData = (error as any)?.response?.data; if (typeof responseData === 'string') { return responseData; } const responseError = responseData?.error; return responseData?.desc || responseData?.message || responseError?.desc || responseError?.message || responseError || (error as Error)?.message || fallback; }; export const WapServersPage = () => { const [servers, setServers] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [loading, setLoading] = useState(false); const [auditByServer, setAuditByServer] = useState>({}); const [selectedAuditServerId, setSelectedAuditServerId] = useState(null); const [auditTab, setAuditTab] = useState<'assigned' | 'ghosts' | 'missing'>('assigned'); const [recalculateResult, setRecalculateResult] = useState(null); const [qrResult, setQrResult] = useState(null); const [actionMessage, setActionMessage] = useState(null); const [actionServerId, setActionServerId] = useState(null); const [openActionsServerId, setOpenActionsServerId] = useState(null); const [openAuditActionKey, setOpenAuditActionKey] = useState(null); const [editingServer, setEditingServer] = useState(null); const [deleteServerTarget, setDeleteServerTarget] = useState(null); const [forceDeleteServer, setForceDeleteServer] = useState(false); const [serverFormMode, setServerFormMode] = useState<'create' | 'edit'>('edit'); const [editForm, setEditForm] = useState({ name: '', ipv4: '', port: '', maxBots: '', active: true }); // Filters const [filters, setFilters] = useState({ name: '', description: '', active: 'all', // all | true | false countBotsFrom: '', countBotsTo: '' }); const loadServers = async () => { setLoading(true); try { const payload: any = { page, limit: 10 }; if (filters.name) payload.name = filters.name; if (filters.description) payload.description = filters.description; if (filters.active !== 'all') payload.active = filters.active === 'true'; if (filters.countBotsFrom) payload.countBotsFrom = parseInt(filters.countBotsFrom, 10); if (filters.countBotsTo) payload.countBotsTo = parseInt(filters.countBotsTo, 10); const res = await fetchWapServers(payload); setServers(res.data); setTotalPages(res.pages); } catch (e) { console.error(e); alert('Error cargando servidores WAP'); } setLoading(false); }; useEffect(() => { loadServers(); }, [page, filters.active]); useEffect(() => { if (!openActionsServerId && !openAuditActionKey) return; const closeActionsMenu = () => { setOpenActionsServerId(null); setOpenAuditActionKey(null); }; document.addEventListener('click', closeActionsMenu); return () => document.removeEventListener('click', closeActionsMenu); }, [openActionsServerId, openAuditActionKey]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setPage(1); loadServers(); }; const handleResetFilters = () => { setFilters({ name: '', description: '', active: 'all', countBotsFrom: '', countBotsTo: '' }); setPage(1); }; const getServerId = (server: any) => server.id || server._id; const runRowAction = (action: () => void) => { setOpenActionsServerId(null); setOpenAuditActionKey(null); action(); }; const openEditServer = (server: any) => { setServerFormMode('edit'); setEditingServer(server); setEditForm({ name: server.name || '', ipv4: server.ipv4 || '', port: server.port === undefined || server.port === null ? '' : String(server.port), maxBots: server.maxBots === undefined || server.maxBots === null ? '' : String(server.maxBots), active: Boolean(server.active), }); }; const openCreateServer = () => { setServerFormMode('create'); setEditingServer({}); setEditForm({ name: '', ipv4: '', port: '', maxBots: '', active: true }); }; const handleSaveServer = async (event: React.FormEvent) => { event.preventDefault(); if (!editingServer) return; const serverId = serverFormMode === 'edit' ? getServerId(editingServer) : 'create'; const port = Number(editForm.port); const maxBots = Number(editForm.maxBots); if (!Number.isFinite(port) || port < 0 || !Number.isFinite(maxBots) || maxBots < 0) { alert('Puerto y cantidad máxima de bots deben ser números no negativos.'); return; } setActionServerId(serverId); try { if (serverFormMode === 'create') { await createWapServer({ name: editForm.name, ipv4: editForm.ipv4, port, maxBots, active: editForm.active, }); setActionMessage('Servidor WAP creado.'); } else { await updateWapServer({ serverId, name: editForm.name, ipv4: editForm.ipv4, port, maxBots, active: editForm.active, }); setActionMessage('Servidor WAP actualizado.'); } setEditingServer(null); await loadServers(); } catch (e) { console.error(e); alert(getErrorMessage(e, 'Error actualizando servidor WAP')); } setActionServerId(null); }; const refreshSelectedAudit = async (serverId: string) => { const audit = await auditWapServer(serverId); setAuditByServer(prev => ({ ...prev, [serverId]: audit })); }; const handleAudit = async (serverId: string) => { setActionServerId(serverId); try { const audit = await auditWapServer(serverId); setAuditByServer(prev => ({ ...prev, [serverId]: audit })); setAuditTab('assigned'); setSelectedAuditServerId(serverId); } catch (e) { console.error(e); alert(getErrorMessage(e, 'Error auditando servidor WAP')); } setActionServerId(null); }; const handleRecalculateCount = async (serverId: string) => { setActionServerId(serverId); try { const result = await recalculateWapServerCount(serverId); setRecalculateResult(result); setSelectedAuditServerId(null); await loadServers(); } catch (e) { console.error(e); alert(getErrorMessage(e, 'Error recalculando contador WAP')); } setActionServerId(null); }; const handleDeleteServer = async (server: any) => { setForceDeleteServer(false); setDeleteServerTarget(server); }; const confirmDeleteServer = async () => { if (!deleteServerTarget) return; const server = deleteServerTarget; const serverId = getServerId(server); const serverLabel = server.name || serverId; setActionServerId(serverId); try { const result: WapServerDeleteResult = await deleteWapServer({ serverId, force: forceDeleteServer || undefined }); const forceSummary = result.force ? ` Modo forzado: organizaciones desasociadas ${result.detachedOrganizations ?? 0}, bots runtime eliminados ${result.deletedRuntimeBots ?? 0}, bots omitidos ${result.skippedRuntimeBots ?? 0}${result.auditFailed ? `. Auditoría no disponible: ${result.auditMessage || 'sin detalle'}` : ''}.` : ''; setActionMessage(`Servidor WAP eliminado: ${serverLabel}.${forceSummary}`); setSelectedAuditServerId(null); setDeleteServerTarget(null); setForceDeleteServer(false); await loadServers(); } catch (e) { console.error(e); alert(getErrorMessage(e, 'Error eliminando servidor WAP')); } setActionServerId(null); }; const handleBotAction = async ( serverId: string, organizationId: string, action: 'start' | 'pause' | 'restart' | 'qr' | 'poweroff' | 'delete' | 'detach' ) => { const labels = { start: 'iniciar el bot', pause: 'pausar el bot', restart: 'reiniciar el bot', qr: 'obtener QR', poweroff: 'apagar el bot', delete: 'eliminar el bot', detach: 'desasociar el servidor' }; const destructive = action === 'poweroff' || action === 'delete' || action === 'detach'; if (destructive && !window.confirm(`Confirmá ${labels[action]} para la organización ${organizationId}.`)) { return; } setActionServerId(serverId); try { const payload = { serverId, organizationId }; if (action === 'start') await startWapBot(payload); if (action === 'pause' || action === 'poweroff') await stopWapBot(payload); if (action === 'restart') await restartWapBot(payload); if (action === 'qr') setQrResult(await getWapBotQr(payload)); if (action === 'delete') await deleteWapBot(payload); if (action === 'detach') { const result = await detachWapOrganizationServer(payload); const botMessage = result.botAlreadyMissing ? 'El bot ya no existía en el servidor.' : 'Bot eliminado del servidor.'; setActionMessage(`Servidor desasociado. ${botMessage} Contador: ${result.before} -> ${result.after}.`); await loadServers(); } if (action !== 'qr' && action !== 'detach') { setActionMessage(`Acción completada: ${labels[action]}.`); } await refreshSelectedAudit(serverId); if (action === 'delete') await loadServers(); } catch (e) { console.error(e); alert(getErrorMessage(e, `Error al ${labels[action]}`)); } setActionServerId(null); }; const renderAuditActionsMenu = (key: string, serverId: string, organizationId: string, includeBotActions: boolean, includeDetach: boolean) => (
e.stopPropagation()}> {openAuditActionKey === key && (
{includeBotActions && } {includeBotActions && } {includeBotActions && } {includeBotActions && } {includeBotActions && } {includeBotActions && } {includeDetach && }
)}
); useEffect(() => { if (filters.name === '' && filters.description === '' && filters.active === 'all' && filters.countBotsFrom === '' && filters.countBotsTo === '') { loadServers(); } }, [filters]); const selectedAudit = selectedAuditServerId ? auditByServer[selectedAuditServerId] : null; const selectedAuditServer = selectedAuditServerId ? servers.find(s => getServerId(s) === selectedAuditServerId) : null; const recalculateServer = recalculateResult ? servers.find(s => getServerId(s) === recalculateResult.serverId) : null; return (

Servidores WAP

{/* Filters Bar */}
setFilters({...filters, name: e.target.value})} />
setFilters({...filters, description: e.target.value})} />
setFilters({...filters, countBotsFrom: e.target.value})} />
setFilters({...filters, countBotsTo: e.target.value})} />
{/* Table */}
{loading ? ( ) : servers.length === 0 ? ( ) : ( servers.map(s => { const serverId = getServerId(s); const isRunningAction = actionServerId === serverId; return ( );}) )}
Servidor Dirección IP Capacidad Estado Diagnóstico
Cargando servidores...
No se encontraron servidores WAP
{s.active ? : } {s.name}
{s.description || 'Sin descripción'}
IPv4: {s.ipv4 || '-'}
IPv6: {s.ipv6 || '-'}
= s.maxBots ? 'var(--warning)' : 'var(--text-main)' }}> {s.countBots} / {s.maxBots} bots
{s.active ? : } {s.active ? 'Activo' : 'Inactivo'}
e.stopPropagation()}> {openActionsServerId === serverId && (
)}
{/* Pagination */}
Página {page} de {totalPages || 1}
{selectedAudit && (
setSelectedAuditServerId(null)}>
event.stopPropagation()}>

Diagnóstico WAP

{selectedAuditServer?.name || selectedAudit.serverId}

Stored: {selectedAudit.storedCountBots} Expected: {selectedAudit.expectedCountBots} Runtime: {selectedAudit.runtimeCountBots} {selectedAudit.countMismatch ? 'Mismatch' : 'OK'}
{auditTab === 'assigned' && ( selectedAudit.assignedOrganizations.length === 0 ?

Sin organizaciones asignadas.

: {selectedAudit.assignedOrganizations.map(org => { const bot = selectedAudit.validBots.find((item: WapContainerDto) => item.organizationId === org.id); return ( ); })}
Organización ID Estado del bot Acciones
{org.name || 'Sin nombre'} {org.id} {bot?.status || bot?.state || 'Sin bot detectado'} {renderAuditActionsMenu(`assigned-${org.id}`, selectedAudit.serverId, org.id, true, true)}
)} {auditTab === 'ghosts' && ( selectedAudit.ghostBots.length === 0 ?

Sin bots fantasma.

: {selectedAudit.ghostBots.map((bot: WapContainerDto) => ( ))}
Bot Organización Estado Acciones
{bot.name || bot.id || 'Sin nombre'} {bot.organizationId || 'organizationId no detectado'} {bot.status || bot.state || '-'} {bot.organizationId ? renderAuditActionsMenu(`ghost-${bot.organizationId}-${bot.id || bot.name}`, selectedAudit.serverId, bot.organizationId, true, false) : '-'}
)} {auditTab === 'missing' && ( selectedAudit.missingBots.length === 0 ?

Sin organizaciones sin bot.

: {selectedAudit.missingBots.map(org => ( ))}
Organización ID Acciones
{org.name || 'Sin nombre'} {org.id} {renderAuditActionsMenu(`missing-${org.id}`, selectedAudit.serverId, org.id, false, true)}
)}
)} {editingServer && (
setEditingServer(null)}>
event.stopPropagation()}>

{serverFormMode === 'create' ? 'Nuevo servidor' : 'Editar servidor'}

{serverFormMode === 'create' ? 'Crear servidor WAP' : editingServer.name || getServerId(editingServer)}

)} {deleteServerTarget && (
setDeleteServerTarget(null)}>
event.stopPropagation()}>

Eliminar servidor WAP

{deleteServerTarget.name || getServerId(deleteServerTarget)}

Esta acción elimina el servidor WAP.

Sin forzar, el servidor debe estar inactivo y no tener bots asociados.

Si activás borrado forzado, se omiten esas validaciones: las organizaciones asignadas se desasocian y los bots/contenedores runtime detectados pueden eliminarse u omitirse si no se pueden identificar con seguridad.

)} {recalculateResult && (
setRecalculateResult(null)}>
event.stopPropagation()}>

Contador recalculado

{recalculateServer?.name || recalculateResult.serverId}

Antes: {recalculateResult.before} Después: {recalculateResult.after}
)} {qrResult && (
setQrResult(null)}>
event.stopPropagation()}>

QR del bot

{qrResult.organizationId}

{`QR