From 3d8911c093f07e453b44e7ba8753841e2ccabdb4 Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Tue, 28 Jul 2026 22:45:26 -0300 Subject: [PATCH] feat: implement full CRUD and administrative management endpoints for WAP servers in the sysadmin API --- .../WapServer/WapServer.Adapter.Mongoose.ts | 4 + .../Models/WapServer/WapServer.Interface.ts | 119 +++ server/src/Models/WapServer/WapServer.ts | 527 +++++++++++- .../SysAdminWapServer.Controller.ts | 245 ++++++ .../WapServers/SysAdminWapServer.Service.ts | 67 ++ sysadmin-cli/src/api/sysadmin.ts | 124 +++ sysadmin-cli/src/pages/WapServersPage.tsx | 756 +++++++++++++++++- sysadmin/src/controllers/wap.controller.ts | 134 +++- sysadmin/src/models/WapServers.Model.ts | 70 ++ sysadmin/src/routes/wap.routes.ts | 10 + sysadmin/src/services/wap.service.ts | 98 ++- 11 files changed, 2144 insertions(+), 10 deletions(-) diff --git a/server/src/Models/WapServer/WapServer.Adapter.Mongoose.ts b/server/src/Models/WapServer/WapServer.Adapter.Mongoose.ts index fef7b4a..65943c3 100644 --- a/server/src/Models/WapServer/WapServer.Adapter.Mongoose.ts +++ b/server/src/Models/WapServer/WapServer.Adapter.Mongoose.ts @@ -36,6 +36,10 @@ export class WapServerAdapterMongoose implements IWapServerAdapter { return this.serverList.findOne(data).exec(); } + public async delete(id: string): Promise { + await this.serverList.deleteOne({ _id: id }); + } + private buildSearchCriteria(filters: FindWapServerParams): FilterQuery { const searchCriteria: FilterQuery = {}; diff --git a/server/src/Models/WapServer/WapServer.Interface.ts b/server/src/Models/WapServer/WapServer.Interface.ts index 20cf202..5876612 100644 --- a/server/src/Models/WapServer/WapServer.Interface.ts +++ b/server/src/Models/WapServer/WapServer.Interface.ts @@ -7,6 +7,7 @@ export type FindWapServerParams = { description?: string; countBots?: number; maxBots?: number; + port?: number; active?: boolean; countBotsFrom?: number; countBotsTo?: number; @@ -100,9 +101,19 @@ export interface IWapServer { ipv6: string; countBots: number; maxBots: number; + port: number; active: boolean; } +export type UpdateWapServerParams = { + serverId: string; + name?: string; + ipv4?: string; + port?: number; + maxBots?: number; + active?: boolean; +}; + export type PaginateWapServerParams = FindWapServerParams & { page: number; limit: number; @@ -114,13 +125,111 @@ export type PaginateWapServerResults = { pages: number; }; +export type SysAdminWapServerByIdParams = { + serverId: string; +}; + +export type SysAdminWapServerAuditParams = SysAdminWapServerByIdParams; + +export type SysAdminWapServerRecalculateCountParams = SysAdminWapServerByIdParams; + +export type SysAdminDeleteWapServerParams = SysAdminWapServerByIdParams; + +export type SysAdminDeleteWapServerResult = { + serverId: string; + deleted: true; +}; + +export type SysAdminWapServerOrganizationActionParams = SysAdminWapServerByIdParams & { + organizationId: string; +}; + +export type SysAdminWapServerQrResult = { + serverId: string; + organizationId: string; + qr: string; +}; + +export type SysAdminWapServerDetachResult = { + serverId: string; + organizationId: string; + botDeleted: boolean; + botAlreadyMissing: boolean; + before: number; + after: number; +}; + +export type SysAdminWapServerDeleteBotResult = { + serverId: string; + organizationId: string; + assignmentRemains: true; + before: number; + after: number; +}; + +export type SysAdminWapAssignedOrganization = { + id: string; + name?: string; +}; + +export type SysAdminWapContainerDto = { + id?: string; + name?: string; + organizationId?: string; + image?: string; + state?: string; + status?: string; + ports?: unknown[]; + created?: number; +}; + +export type SysAdminWapServerAuditResult = { + serverId: string; + assignedOrganizations: SysAdminWapAssignedOrganization[]; + detectedBots: SysAdminWapContainerDto[]; + validBots: SysAdminWapContainerDto[]; + ghostBots: SysAdminWapContainerDto[]; + missingBots: SysAdminWapAssignedOrganization[]; + expectedCountBots: number; + storedCountBots: number; + runtimeCountBots: number; + countMismatch: boolean; +}; + +export type SysAdminWapServerRecalculateCountResult = { + serverId: string; + before: number; + after: number; +}; + export type SysAdminGetWapServerParams = PaginateWapServerParams & { payload: IPayload; }; +export type SysAdminUpdateWapServerParams = UpdateWapServerParams & { + payload: IPayload; +}; + +export type SysAdminAuditWapServerParams = SysAdminWapServerAuditParams & { + payload: IPayload; +}; + +export type SysAdminRecalculateWapServerCountParams = SysAdminWapServerRecalculateCountParams & { + payload: IPayload; +}; + +export type SysAdminDeleteWapServerRequest = SysAdminDeleteWapServerParams & { + payload: IPayload; +}; + +export type SysAdminWapServerOrganizationActionRequest = SysAdminWapServerOrganizationActionParams & { + payload: IPayload; +}; + export interface IWapServerAdapter { find(data: FindWapServerParams): Promise; findOne(data: FindWapServerParams): Promise; + delete(id: string): Promise; paginate(filters: PaginateWapServerParams): Promise; } @@ -140,4 +249,14 @@ export interface IWapServerManager { getQr(data: BotEventParams): Promise; sendMessage(data: SendBotMessageParams): Promise; sysAdminGetWapServers(data: PaginateWapServerParams): Promise; + sysAdminUpdateWapServer(data: UpdateWapServerParams): Promise; + sysAdminAuditWapServer(data: SysAdminWapServerAuditParams): Promise; + sysAdminRecalculateWapServerCount(data: SysAdminWapServerRecalculateCountParams): Promise; + sysAdminDeleteWapServer(data: SysAdminDeleteWapServerParams): Promise; + sysAdminStartBot(data: SysAdminWapServerOrganizationActionParams): Promise; + sysAdminStopBot(data: SysAdminWapServerOrganizationActionParams): Promise; + sysAdminRestartBot(data: SysAdminWapServerOrganizationActionParams): Promise; + sysAdminGetQr(data: SysAdminWapServerOrganizationActionParams): Promise; + sysAdminDeleteBot(data: SysAdminWapServerOrganizationActionParams): Promise; + sysAdminDetachOrganizationServer(data: SysAdminWapServerOrganizationActionParams): Promise; } diff --git a/server/src/Models/WapServer/WapServer.ts b/server/src/Models/WapServer/WapServer.ts index 155d1fd..3b451be 100644 --- a/server/src/Models/WapServer/WapServer.ts +++ b/server/src/Models/WapServer/WapServer.ts @@ -13,6 +13,17 @@ import { BotView, IWapServer, IWapServerManager, + SysAdminDeleteWapServerParams, + SysAdminDeleteWapServerResult, + SysAdminWapContainerDto, + SysAdminWapServerAuditParams, + SysAdminWapServerAuditResult, + SysAdminWapServerRecalculateCountParams, + SysAdminWapServerRecalculateCountResult, + SysAdminWapServerDetachResult, + SysAdminWapServerDeleteBotResult, + SysAdminWapServerOrganizationActionParams, + SysAdminWapServerQrResult, SendBotMessageParams, ValidateBotEventStatus, BOT_STATE, @@ -21,12 +32,19 @@ import { VALIDATE_BOT_ENVENT_ERRORS, PaginateWapServerResults, PaginateWapServerParams, + UpdateWapServerParams, } from "./WapServer.Interface"; import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons"; import { PlanFeatures } from "../Plans/Plans.interface"; import axios, { AxiosError } from "axios"; import { isNull } from "../../helpers/IsNull"; +const BOT_ADMIN_ACTION_TIMEOUT_MS = 10000; +const BOT_ADMIN_RESTART_STATUS_RETRIES = 5; +const BOT_ADMIN_RESTART_STATUS_DELAY_MS = 1000; +const MONGO_OBJECT_ID_REGEX = /^[a-fA-F0-9]{24}$/; +const WAP_SERVER_METADATA_HOSTS = new Set(["169.254.169.254", "metadata.google.internal"]); + export interface CreateBotResult { organizationId: string; port: number; @@ -142,10 +160,259 @@ class WapServerManager implements IWapServerManager { return servidores; } + private normalizeBotAdminApiUrl(url: string): string { + const rawUrl = String(url).trim(); + + if (!rawUrl) { + throw new Error("URL is required"); + } + + if (/\s|[\x00-\x1F\x7F]/.test(rawUrl)) { + throw new Error("URL must not contain whitespace or control characters"); + } + + const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(rawUrl); + const normalizedInput = hasProtocol ? rawUrl : `http://${rawUrl}`; + let parsedUrl: URL; + + try { + parsedUrl = new URL(normalizedInput); + } catch (error) { + throw new Error("URL must be a valid HTTP(S) origin"); + } + + if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { + throw new Error("URL protocol must be http or https"); + } + + if (!parsedUrl.hostname) { + throw new Error("URL host is required"); + } + + if (parsedUrl.username || parsedUrl.password) { + throw new Error("URL must not include credentials"); + } + + const path = decodeURIComponent(parsedUrl.pathname); + if ((parsedUrl.pathname && parsedUrl.pathname !== "/") || path.includes("..")) { + throw new Error("URL path is not allowed; use only the origin"); + } + + if (parsedUrl.search || parsedUrl.hash) { + throw new Error("URL must not include query string or hash"); + } + + if (WAP_SERVER_METADATA_HOSTS.has(parsedUrl.hostname.toLowerCase())) { + throw new Error("URL host is not allowed"); + } + + return `${parsedUrl.origin}/`; + } + private getBotAdminApiUrl(url: string): string { //return `https://${url}:${process.env.DEFAULT_BOT_ADMIN_PORT}/`; //return `https://${url}/`; - return url; + return this.normalizeBotAdminApiUrl(url); + } + + private formatResponseBody(data: unknown): string { + if (data === undefined || data === null) { + return "empty body"; + } + + const body = typeof data === "string" ? data : JSON.stringify(data); + + return body.length > 1000 ? `${body.slice(0, 1000)}...` : body; + } + + private createBotAdminContainersError(error: unknown, targetUrl: string): Error { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + const body = this.formatResponseBody(error.response?.data); + const statusText = status ? `status ${status}` : error.code || "no response"; + + return new Error(`Bot-admin containers request failed for ${targetUrl}: ${statusText}; body: ${body}`); + } + + if (error instanceof Error && error.message) { + return new Error(`Bot-admin containers request failed for ${targetUrl}: ${error.message}`); + } + + return new Error(`Bot-admin containers request failed for ${targetUrl}: unknown error`); + } + + private createBotAdminActionError(error: unknown, targetUrl: string): Error { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + const body = this.formatResponseBody(error.response?.data); + const statusText = status ? `status ${status}` : error.code || "no response"; + + return new Error(`Bot-admin request failed for ${targetUrl}: ${statusText}; body: ${body}`); + } + + if (error instanceof Error && error.message) { + return new Error(`Bot-admin request failed for ${targetUrl}: ${error.message}`); + } + + return new Error(`Bot-admin request failed for ${targetUrl}: unknown error`); + } + + private isBotAdminMissingContainerError(error: unknown): boolean { + if (!axios.isAxiosError(error)) { + return false; + } + + const status = error.response?.status; + const body = this.formatResponseBody(error.response?.data).toLowerCase(); + + // Be conservative: only allow detach to continue for explicit missing-container signals. + return status === 404 || body.includes("not found") || body.includes("no such container"); + } + + private async getSysAdminServerActionTarget(data: SysAdminWapServerOrganizationActionParams): Promise<{ + server: any; + baseUrl: string; + encodedOrganizationId: string; + }> { + if (!data.serverId) { + throw new Error("Server id is required"); + } + + if (!data.organizationId) { + throw new Error("Organization id is required"); + } + + if (!MONGO_OBJECT_ID_REGEX.test(data.organizationId)) { + throw new Error("Organization id must be a valid Mongo ObjectId"); + } + + const server = await this.servers.serverList.findOne({ _id: data.serverId }); + + if (!server) { + throw new Error("WAP server not found"); + } + + return { server, baseUrl: this.getBotAdminApiUrl(server.ipv4), encodedOrganizationId: encodeURIComponent(data.organizationId) }; + } + + private async postSysAdminBotAction( + data: SysAdminWapServerOrganizationActionParams, + action: "start" | "stop", + ): Promise { + const { baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); + const targetUrl = `${baseUrl}containers/${encodedOrganizationId}/${action}`; + + try { + const response = await axios.post(targetUrl, {}, { + headers: { "Content-Type": "application/json" }, + timeout: BOT_ADMIN_ACTION_TIMEOUT_MS, + }); + const result: CreateBotResult = response.data; + + if (result.error) { + throw new Error(result.error); + } + } catch (error) { + throw this.createBotAdminActionError(error, targetUrl); + } + } + + private async deleteSysAdminRuntimeBot( + baseUrl: string, + encodedOrganizationId: string, + allowMissingContainer = false, + ): Promise<{ botDeleted: boolean; botAlreadyMissing: boolean }> { + const targetUrl = `${baseUrl}containers/${encodedOrganizationId}`; + + try { + const response = await axios.delete(targetUrl, { timeout: BOT_ADMIN_ACTION_TIMEOUT_MS }); + const result: CreateBotResult = response.data; + + if (result.error) { + throw new Error(result.error); + } + + return { botDeleted: true, botAlreadyMissing: false }; + } catch (error) { + if (allowMissingContainer && this.isBotAdminMissingContainerError(error)) { + return { botDeleted: false, botAlreadyMissing: true }; + } + + throw this.createBotAdminActionError(error, targetUrl); + } + } + + private async waitForSysAdminBotStopped(data: SysAdminWapServerOrganizationActionParams): Promise { + const { baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); + const targetUrl = `${baseUrl}containers/${encodedOrganizationId}/status`; + + for (let attempt = 0; attempt < BOT_ADMIN_RESTART_STATUS_RETRIES; attempt += 1) { + try { + const response = await axios.post(targetUrl, {}, { timeout: BOT_ADMIN_ACTION_TIMEOUT_MS }); + const status = String(response.data?.status || "").toLowerCase(); + + if (!status.includes("up")) { + return; + } + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 500) { + return; + } + + throw this.createBotAdminActionError(error, targetUrl); + } + + await new Promise((resolve) => setTimeout(resolve, BOT_ADMIN_RESTART_STATUS_DELAY_MS)); + } + + throw new Error("Bot did not stop before restart timeout; start was not attempted."); + } + + private extractOrganizationIdFromContainer(container: any): string | undefined { + const names = Array.isArray(container?.Names) ? container.Names : []; + + for (const rawName of names) { + const name = String(rawName || "").replace(/^\//, ""); + const match = name.match(/^(.+)_bot_\d+$/); + if (match && match[1]) { + return match[1]; + } + } + + return undefined; + } + + private sanitizeContainer(container: any): SysAdminWapContainerDto { + const names = Array.isArray(container?.Names) ? container.Names : []; + const firstName = names.length > 0 ? String(names[0]).replace(/^\//, "") : undefined; + + return { + id: container?.Id, + name: firstName, + organizationId: this.extractOrganizationIdFromContainer(container), + image: container?.Image, + state: container?.State, + status: container?.Status, + ports: Array.isArray(container?.Ports) ? container.Ports : [], + created: container?.Created, + }; + } + + private async getSysAdminRuntimeContainers(server: any): Promise { + const containersUrl = this.getBotAdminApiUrl(server.ipv4) + "containers"; + + try { + const response = await axios.get(containersUrl, { + headers: { "Content-Type": "application/json" }, + timeout: BOT_ADMIN_ACTION_TIMEOUT_MS, + }); + + return Array.isArray(response.data) + ? response.data.map((container: any) => this.sanitizeContainer(container)) + : []; + } catch (error) { + throw this.createBotAdminContainersError(error, containersUrl); + } } public async getAvailableServer(): Promise { @@ -713,6 +980,264 @@ class WapServerManager implements IWapServerManager { return servers; } + + public async sysAdminUpdateWapServer(data: UpdateWapServerParams): Promise { + if (!data.serverId) { + throw new Error("Server id is required"); + } + + if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { + throw new Error("Server id must be a valid Mongo ObjectId"); + } + + const server = await this.servers.serverList.findOne({ _id: data.serverId }); + + if (!server) { + throw new Error("WAP server not found"); + } + + if (data.name !== undefined) { + server.name = String(data.name).trim(); + } + + if (data.ipv4 !== undefined) { + server.ipv4 = this.normalizeBotAdminApiUrl(data.ipv4); + } + + if (data.port !== undefined) { + if (!Number.isFinite(data.port) || data.port < 0) { + throw new Error("Port must be a finite non-negative number"); + } + + server.port = data.port; + } + + if (data.maxBots !== undefined) { + if (!Number.isFinite(data.maxBots) || data.maxBots < 0) { + throw new Error("Max bots must be a finite non-negative number"); + } + + server.maxBots = data.maxBots; + } + + if (data.active !== undefined) { + server.active = Boolean(data.active); + } + + await server.save(); + + return server; + } + + public async sysAdminAuditWapServer(data: SysAdminWapServerAuditParams): Promise { + if (!data.serverId) { + throw new Error("Server id is required"); + } + + if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { + throw new Error("Server id must be a valid Mongo ObjectId"); + } + + const server = await this.servers.serverList.findOne({ _id: data.serverId }); + + if (!server) { + throw new Error("WAP server not found"); + } + + const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); + const assignedOrganizations = assignedCompanies.map((company: any) => ({ + id: String(company.id || company._id), + name: company.name, + })); + const assignedIds = new Set(assignedOrganizations.map((organization) => organization.id)); + + const detectedBots = await this.getSysAdminRuntimeContainers(server); + const runtimeOrganizationIds = new Set( + detectedBots + .map((container) => container.organizationId) + .filter((organizationId): organizationId is string => Boolean(organizationId)), + ); + + const validBots = detectedBots.filter( + (container) => container.organizationId && assignedIds.has(container.organizationId), + ); + const ghostBots = detectedBots.filter( + (container) => !container.organizationId || !assignedIds.has(container.organizationId), + ); + const missingBots = assignedOrganizations.filter( + (organization) => !runtimeOrganizationIds.has(organization.id), + ); + const expectedCountBots = assignedOrganizations.length; + const storedCountBots = server.countBots ?? 0; + const runtimeCountBots = detectedBots.length; + + return { + serverId: String(server.id || server._id), + assignedOrganizations, + detectedBots, + validBots, + ghostBots, + missingBots, + expectedCountBots, + storedCountBots, + runtimeCountBots, + countMismatch: storedCountBots !== expectedCountBots || runtimeCountBots !== expectedCountBots, + }; + } + + public async sysAdminRecalculateWapServerCount( + data: SysAdminWapServerRecalculateCountParams, + ): Promise { + if (!data.serverId) { + throw new Error("Server id is required"); + } + + if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { + throw new Error("Server id must be a valid Mongo ObjectId"); + } + + const server = await this.servers.serverList.findOne({ _id: data.serverId }); + + if (!server) { + throw new Error("WAP server not found"); + } + + const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); + const before = server.countBots; + const after = assignedCompanies.length; + + server.countBots = after; + await server.save(); + + return { serverId: String(server.id || server._id), before, after }; + } + + public async sysAdminDeleteWapServer(data: SysAdminDeleteWapServerParams): Promise { + if (!data.serverId) { + throw new Error("Server id is required"); + } + + if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { + throw new Error("Server id must be a valid Mongo ObjectId"); + } + + const server = await this.servers.serverList.findOne({ _id: data.serverId }); + + if (!server) { + throw new Error("WAP server not found"); + } + + if (server.active) { + throw new Error("WAP server must be inactive before deletion"); + } + + const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); + + if (assignedCompanies.length > 0) { + throw new Error(`WAP server cannot be deleted because it has ${assignedCompanies.length} associated bot(s) in DB`); + } + + let detectedBots: SysAdminWapContainerDto[]; + + try { + detectedBots = await this.getSysAdminRuntimeContainers(server); + } catch (error) { + const message = error instanceof Error && error.message ? error.message : "unknown error"; + + throw new Error(`WAP server cannot be deleted because bot-admin could not be audited. Verify runtime bots manually before deleting. ${message}`); + } + + if (detectedBots.length > 0) { + throw new Error(`WAP server cannot be deleted because bot-admin detected ${detectedBots.length} runtime bot(s)`); + } + + await this.servers.delete(data.serverId); + + return { serverId: data.serverId, deleted: true }; + } + + public async sysAdminStartBot(data: SysAdminWapServerOrganizationActionParams): Promise { + await this.postSysAdminBotAction(data, "start"); + } + + public async sysAdminStopBot(data: SysAdminWapServerOrganizationActionParams): Promise { + await this.postSysAdminBotAction(data, "stop"); + } + + public async sysAdminRestartBot(data: SysAdminWapServerOrganizationActionParams): Promise { + await this.postSysAdminBotAction(data, "stop"); + await this.waitForSysAdminBotStopped(data); + + try { + await this.postSysAdminBotAction(data, "start"); + } catch (error) { + const message = error instanceof Error && error.message ? error.message : "unknown error"; + + throw new Error(`Bot restart stopped the bot, but start failed. The bot may be stopped. ${message}`); + } + } + + public async sysAdminGetQr(data: SysAdminWapServerOrganizationActionParams): Promise { + const { baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); + const targetUrl = `${baseUrl}containers/${encodedOrganizationId}/qr`; + + try { + const response = await axios.get(targetUrl, { + responseType: "arraybuffer", + timeout: BOT_ADMIN_ACTION_TIMEOUT_MS, + }); + const qr = Buffer.from(response.data, "binary").toString("base64"); + + return { serverId: data.serverId, organizationId: data.organizationId, qr }; + } catch (error) { + throw this.createBotAdminActionError(error, targetUrl); + } + } + + public async sysAdminDeleteBot( + data: SysAdminWapServerOrganizationActionParams, + ): Promise { + const { server, baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); + const before = server.countBots ?? 0; + + await this.deleteSysAdminRuntimeBot(baseUrl, encodedOrganizationId); + + // Deleting the runtime bot intentionally keeps the DB assignment; detach is a separate sysadmin action. + const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); + const after = assignedCompanies.length; + server.countBots = after; + await server.save(); + + return { serverId: data.serverId, organizationId: data.organizationId, assignmentRemains: true, before, after }; + } + + public async sysAdminDetachOrganizationServer( + data: SysAdminWapServerOrganizationActionParams, + ): Promise { + const { server, baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); + const company = await CompaniesManager.companies.findOne({ _id: data.organizationId }); + + if (!company) { + throw new Error("Organization not found"); + } + + if (String(company.wapServerId || "") !== data.serverId) { + throw new Error("Organization is not assigned to this WAP server"); + } + + const before = server.countBots ?? 0; + const deleteResult = await this.deleteSysAdminRuntimeBot(baseUrl, encodedOrganizationId, true); + + company.wapServerId = undefined; + await company.save(); + + const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); + const after = assignedCompanies.length; + server.countBots = after; + await server.save(); + + return { serverId: data.serverId, organizationId: data.organizationId, ...deleteResult, before, after }; + } } const WapServerList = new WapServerManager(); diff --git a/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Controller.ts b/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Controller.ts index 8e5ca77..e363d21 100644 --- a/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Controller.ts +++ b/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Controller.ts @@ -1,12 +1,60 @@ import { PaginateWapServerResults, + SysAdminDeleteWapServerRequest, + SysAdminDeleteWapServerResult, + SysAdminAuditWapServerParams, SysAdminGetWapServerParams, + SysAdminRecalculateWapServerCountParams, + SysAdminWapServerDeleteBotResult, + SysAdminWapServerDetachResult, + SysAdminWapServerAuditResult, + SysAdminWapServerOrganizationActionRequest, + SysAdminWapServerQrResult, + SysAdminWapServerRecalculateCountResult, + SysAdminUpdateWapServerParams, + IWapServer, } from "../../../Models/WapServer/WapServer.Interface"; import { ApiValidationError } from "../../../Models/Server.Error.model"; +import { ApiVoidResult } from "../../../Models/Api.VoidResult.type"; +import axios from "axios"; import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa"; import { SA_WapServerService } from "./SysAdminWapServer.Service"; import { sysAdminAuthMiddleware } from "../../../middleware/sysadminAuth"; +const stringifyErrorBody = (data: unknown): string => { + if (data === undefined || data === null) { + return ""; + } + + if (typeof data === "string") { + return data; + } + + const body = JSON.stringify(data); + + return body.length > 1000 ? `${body.slice(0, 1000)}...` : body; +}; + +const getErrorDesc = (error: unknown): string => { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data as any; + const responseMessage = + responseData?.desc || responseData?.message || responseData?.error || stringifyErrorBody(responseData); + const requestUrl = error.config?.url ? ` url ${error.config.url}` : ""; + const status = error.response?.status ? `status ${error.response.status}` : error.code || "no response"; + + return responseMessage || `Upstream request failed:${requestUrl} ${status}`.trim(); + } + + if (error instanceof Error && error.message) { + return error.message; + } + + const fallback = stringifyErrorBody(error); + + return fallback || "Unknown error while auditing WAP server"; +}; + @Route("sysadmin/wapserver/get") export class SysAdminWapServerGetController extends Controller { @Response(500, "Ha ocurrido un error") @@ -20,6 +68,69 @@ export class SysAdminWapServerGetController extends Controller { const result = await new SA_WapServerService().sysAdminGetWapServers(requestBody); this.setStatus(200); return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } +} + +@Route("sysadmin/wapserver/update") +export class SysAdminWapServerUpdateController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminUpdateWapServer( + @Body() requestBody: SysAdminUpdateWapServerParams, + ): Promise { + try { + const { payload, ...cleanRequestBody } = requestBody as any; + const result = await new SA_WapServerService().sysAdminUpdateWapServer(cleanRequestBody); + this.setStatus(200); + return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } +} + +@Route("sysadmin/wapserver/audit") +export class SysAdminWapServerAuditController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminAuditWapServer( + @Body() requestBody: SysAdminAuditWapServerParams, + ): Promise { + try { + const { payload, ...cleanRequestBody } = requestBody as any; + const result = await new SA_WapServerService().sysAdminAuditWapServer(cleanRequestBody); + this.setStatus(200); + return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } +} + +@Route("sysadmin/wapserver/recalculate-count") +export class SysAdminWapServerRecalculateCountController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminRecalculateWapServerCount( + @Body() requestBody: SysAdminRecalculateWapServerCountParams, + ): Promise { + try { + const { payload, ...cleanRequestBody } = requestBody as any; + const result = await new SA_WapServerService().sysAdminRecalculateWapServerCount(cleanRequestBody); + this.setStatus(200); + return result; } catch (e) { const errorOccurred: Error = e as Error; this.setStatus(500); @@ -27,3 +138,137 @@ export class SysAdminWapServerGetController extends Controller { } } } + +@Route("sysadmin/wapserver/delete") +export class SysAdminWapServerDeleteController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminDeleteWapServer( + @Body() requestBody: SysAdminDeleteWapServerRequest, + ): Promise { + try { + const { payload, ...cleanRequestBody } = requestBody as any; + const result = await new SA_WapServerService().sysAdminDeleteWapServer(cleanRequestBody); + this.setStatus(200); + return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } +} + +@Route("sysadmin/wapserver/bot") +export class SysAdminWapServerBotController extends Controller { + private cleanRequestBody(requestBody: SysAdminWapServerOrganizationActionRequest) { + const { payload, ...cleanRequestBody } = requestBody as any; + + return cleanRequestBody; + } + + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post("start") + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminStartBot( + @Body() requestBody: SysAdminWapServerOrganizationActionRequest, + ): Promise { + try { + await new SA_WapServerService().sysAdminStartBot(this.cleanRequestBody(requestBody)); + this.setStatus(200); + return { success: true, message: "ok" }; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } + + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post("stop") + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminStopBot( + @Body() requestBody: SysAdminWapServerOrganizationActionRequest, + ): Promise { + try { + await new SA_WapServerService().sysAdminStopBot(this.cleanRequestBody(requestBody)); + this.setStatus(200); + return { success: true, message: "ok" }; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } + + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post("restart") + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminRestartBot( + @Body() requestBody: SysAdminWapServerOrganizationActionRequest, + ): Promise { + try { + await new SA_WapServerService().sysAdminRestartBot(this.cleanRequestBody(requestBody)); + this.setStatus(200); + return { success: true, message: "ok" }; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } + + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post("qr") + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminGetQr( + @Body() requestBody: SysAdminWapServerOrganizationActionRequest, + ): Promise { + try { + const result = await new SA_WapServerService().sysAdminGetQr(this.cleanRequestBody(requestBody)); + this.setStatus(200); + return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } + + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post("delete") + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminDeleteBot( + @Body() requestBody: SysAdminWapServerOrganizationActionRequest, + ): Promise { + try { + const result = await new SA_WapServerService().sysAdminDeleteBot(this.cleanRequestBody(requestBody)); + this.setStatus(200); + return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } + + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post("detach") + @Middlewares(sysAdminAuthMiddleware) + public async sysAdminDetachOrganizationServer( + @Body() requestBody: SysAdminWapServerOrganizationActionRequest, + ): Promise { + try { + const result = await new SA_WapServerService().sysAdminDetachOrganizationServer( + this.cleanRequestBody(requestBody), + ); + this.setStatus(200); + return result; + } catch (e) { + this.setStatus(500); + return new ApiValidationError(500, getErrorDesc(e)); + } + } +} diff --git a/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Service.ts b/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Service.ts index cfa9beb..369a41e 100644 --- a/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Service.ts +++ b/server/src/api/SysAdmin/WapServers/SysAdminWapServer.Service.ts @@ -3,6 +3,17 @@ import { connect } from "mongoose"; import { PaginateWapServerParams, PaginateWapServerResults, + SysAdminDeleteWapServerParams, + SysAdminDeleteWapServerResult, + SysAdminWapServerAuditParams, + SysAdminWapServerAuditResult, + SysAdminWapServerDeleteBotResult, + SysAdminWapServerDetachResult, + SysAdminWapServerOrganizationActionParams, + SysAdminWapServerQrResult, + SysAdminWapServerRecalculateCountParams, + SysAdminWapServerRecalculateCountResult, + UpdateWapServerParams, } from "../../../Models/WapServer/WapServer.Interface"; import WapServerList from "../../../Models/WapServer/WapServer"; @@ -11,4 +22,60 @@ export class SA_WapServerService { await connect(`${process.env.DATABASE_CONNECTION}`); return await WapServerList.sysAdminGetWapServers(data); } + + public async sysAdminUpdateWapServer(data: UpdateWapServerParams) { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminUpdateWapServer(data); + } + + public async sysAdminAuditWapServer(data: SysAdminWapServerAuditParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminAuditWapServer(data); + } + + public async sysAdminRecalculateWapServerCount( + data: SysAdminWapServerRecalculateCountParams, + ): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminRecalculateWapServerCount(data); + } + + public async sysAdminDeleteWapServer(data: SysAdminDeleteWapServerParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminDeleteWapServer(data); + } + + public async sysAdminStartBot(data: SysAdminWapServerOrganizationActionParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + await WapServerList.sysAdminStartBot(data); + } + + public async sysAdminStopBot(data: SysAdminWapServerOrganizationActionParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + await WapServerList.sysAdminStopBot(data); + } + + public async sysAdminRestartBot(data: SysAdminWapServerOrganizationActionParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + await WapServerList.sysAdminRestartBot(data); + } + + public async sysAdminGetQr(data: SysAdminWapServerOrganizationActionParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminGetQr(data); + } + + public async sysAdminDeleteBot( + data: SysAdminWapServerOrganizationActionParams, + ): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminDeleteBot(data); + } + + public async sysAdminDetachOrganizationServer( + data: SysAdminWapServerOrganizationActionParams, + ): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + return await WapServerList.sysAdminDetachOrganizationServer(data); + } } diff --git a/sysadmin-cli/src/api/sysadmin.ts b/sysadmin-cli/src/api/sysadmin.ts index 56b1fe9..8b51a13 100644 --- a/sysadmin-cli/src/api/sysadmin.ts +++ b/sysadmin-cli/src/api/sysadmin.ts @@ -174,6 +174,130 @@ export const fetchWapServers = async (filters: any) => { return res.data; }; +export type WapServer = { + id?: string; + _id?: string; + name?: string; + description?: string; + ipv4: string; + ipv6?: string; + countBots: number; + maxBots: number; + port: number; + active: boolean; +}; + +export type UpdateWapServerPayload = { + serverId: string; + name?: string; + ipv4?: string; + port?: number; + maxBots?: number; + active?: boolean; +}; + +export const updateWapServer = async (data: UpdateWapServerPayload): Promise => { + const res = await sysadminApi.post('/wap/update', data); + return res.data; +}; + +export type WapAssignedOrganization = { + id: string; + name?: string; +}; + +export type WapContainerDto = { + id?: string; + name?: string; + organizationId?: string; + image?: string; + state?: string; + status?: string; + ports?: unknown[]; + created?: number; +}; + +export type WapServerAuditResult = { + serverId: string; + assignedOrganizations: WapAssignedOrganization[]; + detectedBots: WapContainerDto[]; + validBots: WapContainerDto[]; + ghostBots: WapContainerDto[]; + missingBots: WapAssignedOrganization[]; + expectedCountBots: number; + storedCountBots: number; + runtimeCountBots: number; + countMismatch: boolean; +}; + +export type WapServerRecalculateCountResult = { + serverId: string; + before: number; + after: number; +}; + +export type WapServerDeleteResult = { + serverId: string; + deleted: true; +}; + +export type WapServerOrganizationActionPayload = { + serverId: string; + organizationId: string; +}; + +export type WapServerQrResult = WapServerOrganizationActionPayload & { + qr: string; +}; + +export type WapServerDetachResult = WapServerOrganizationActionPayload & { + botDeleted: boolean; + botAlreadyMissing: boolean; + before: number; + after: number; +}; + +export const auditWapServer = async (serverId: string): Promise => { + const res = await sysadminApi.post('/wap/audit', { serverId }); + return res.data; +}; + +export const recalculateWapServerCount = async (serverId: string): Promise => { + const res = await sysadminApi.post('/wap/recalculate-count', { serverId }); + return res.data; +}; + +export const deleteWapServer = async (serverId: string): Promise => { + const res = await sysadminApi.post('/wap/delete', { serverId }); + return res.data; +}; + +export const startWapBot = async (data: WapServerOrganizationActionPayload): Promise => { + await sysadminApi.post('/wap/bot/start', data); +}; + +export const stopWapBot = async (data: WapServerOrganizationActionPayload): Promise => { + await sysadminApi.post('/wap/bot/stop', data); +}; + +export const restartWapBot = async (data: WapServerOrganizationActionPayload): Promise => { + await sysadminApi.post('/wap/bot/restart', data); +}; + +export const getWapBotQr = async (data: WapServerOrganizationActionPayload): Promise => { + const res = await sysadminApi.post('/wap/bot/qr', data); + return res.data; +}; + +export const deleteWapBot = async (data: WapServerOrganizationActionPayload): Promise => { + await sysadminApi.post('/wap/bot/delete', data); +}; + +export const detachWapOrganizationServer = async (data: WapServerOrganizationActionPayload): Promise => { + const res = await sysadminApi.post('/wap/bot/detach', data); + return res.data; +}; + // Companies API Calls export const fetchCompanies = async (filters: any) => { const res = await sysadminApi.post('/companies/paginate', filters); diff --git a/sysadmin-cli/src/pages/WapServersPage.tsx b/sysadmin-cli/src/pages/WapServersPage.tsx index 4febc7b..d9210d8 100644 --- a/sysadmin-cli/src/pages/WapServersPage.tsx +++ b/sysadmin-cli/src/pages/WapServersPage.tsx @@ -1,13 +1,36 @@ import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { Search, RotateCcw, CheckCircle, XCircle, Server, ServerOff } from 'lucide-react'; -import { fetchWapServers } from '../api/sysadmin'; +import { Search, RotateCcw, CheckCircle, XCircle, Server, ServerOff, ClipboardList, Calculator, X, MoreVertical, Play, Pause, Power, Trash2, Unlink, QrCode, RotateCw, Edit3 } from 'lucide-react'; +import { auditWapServer, deleteWapBot, deleteWapServer, detachWapOrganizationServer, fetchWapServers, getWapBotQr, recalculateWapServerCount, restartWapBot, startWapBot, stopWapBot, updateWapServer, type WapContainerDto, type WapServerAuditResult, 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 [editForm, setEditForm] = useState({ name: '', ipv4: '', port: '', maxBots: '', active: true }); // Filters const [filters, setFilters] = useState({ @@ -42,6 +65,18 @@ export const WapServersPage = () => { 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); @@ -59,12 +94,190 @@ export const WapServersPage = () => { setPage(1); }; + const getServerId = (server: any) => server.id || server._id; + + const runRowAction = (action: () => void) => { + setOpenActionsServerId(null); + setOpenAuditActionKey(null); + action(); + }; + + const openEditServer = (server: any) => { + 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 handleSaveServer = async (event: React.FormEvent) => { + event.preventDefault(); + if (!editingServer) return; + + const serverId = getServerId(editingServer); + 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 { + 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) => { + const serverId = getServerId(server); + const serverLabel = server.name || serverId; + + if (!window.confirm(`Confirmá eliminar el servidor WAP ${serverLabel}. Debe estar inactivo y sin bots asociados.`)) { + return; + } + + setActionServerId(serverId); + try { + await deleteWapServer(serverId); + setActionMessage(`Servidor WAP eliminado: ${serverLabel}.`); + setSelectedAuditServerId(null); + 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 (
@@ -147,16 +360,22 @@ export const WapServersPage = () => { Dirección IP Capacidad Estado + Diagnóstico {loading ? ( - Cargando servidores... + Cargando servidores... ) : servers.length === 0 ? ( - No se encontraron servidores WAP + No se encontraron servidores WAP ) : ( - servers.map(s => ( - + servers.map(s => { + const serverId = getServerId(s); + const isRunningAction = actionServerId === serverId; + + return ( + +
{s.active ? : } @@ -179,8 +398,40 @@ export const WapServersPage = () => { {s.active ? 'Activo' : 'Inactivo'}
+ +
e.stopPropagation()}> + + {openActionsServerId === serverId && ( +
+ + + + +
+ )} +
+ - )) +
+ );}) )} @@ -196,6 +447,223 @@ export const WapServersPage = () => {
+ {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ónIDEstado del botAcciones
{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) => ( + + + + + + + ))} + +
BotOrganizaciónEstadoAcciones
{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ónIDAcciones
{org.name || 'Sin nombre'}{org.id}{renderAuditActionsMenu(`missing-${org.id}`, selectedAudit.serverId, org.id, false, true)}
+ )} +
+
+
+
+ )} + + {editingServer && ( +
setEditingServer(null)}> +
event.stopPropagation()}> +
+
+

Editar servidor

+

{editingServer.name || getServerId(editingServer)}

+
+ +
+ +
+ + + + +
+ + + +
+ + +
+
+
+ )} + + {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 +