B4J=true Group=Default Group ModulesStructureVersion=1 Type=Class Version=10.3 @EndOfDesignText@ ' Módulo de clase: Manager ' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi. ' Permite monitorear el estado del servidor, recargar configuraciones de bases de datos, ' ver estadísticas de rendimiento, reiniciar servicios externos, y gestionar la autenticación de usuarios. Sub Class_Globals ' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible. Dim j As JSONGenerator ' La clase BCrypt no se usa directamente en este módulo, pero se mantiene si hubiera planes futuros. ' Private bc As BCrypt End Sub ' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase. Public Sub Initialize ' No se requiere inicialización específica para esta clase en este momento. End Sub ' Método principal que maneja las peticiones HTTP para el panel de administración. ' req: El objeto ServletRequest que contiene la información de la petición entrante. ' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente. ' Módulo de clase: Manager ' ... (tu código de Class_Globals e Initialize se queda igual) ... ' Método principal que maneja las peticiones HTTP para el panel de administración. ' Refactorizado para funcionar como una API con un frontend estático. Sub Handle(req As ServletRequest, resp As ServletResponse) ' --- 1. Bloque de Seguridad (sin cambios) --- If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then resp.SendRedirect("/login") Return End If Dim Command As String = req.GetParameter("command") ' --- 2. Servidor de la Página Principal --- ' Si NO se especifica un comando, servimos la página principal del manager desde la carpeta 'www'. If Command = "" Then Try resp.ContentType = "text/html; charset=utf-8" resp.Write(File.ReadString(File.DirApp, "www/manager.html")) Catch resp.SendError(500, "Error: No se pudo encontrar el archivo principal del panel (www/manager.html). " & LastException.Message) End Try Return End If ' --- 3. Manejo de Comandos como API --- ' La variable 'j' (JSONGenerator) está en Class_Globals Select Command.ToLowerCase ' --- Comandos que devuelven JSON --- Case "getstats" resp.ContentType = "application/json; charset=utf-8" Dim allPoolStats As Map allPoolStats.Initialize For Each dbKey As String In Main.listaDeCP Dim connector As RDCConnector = Main.Connectors.Get(dbKey) If connector.IsInitialized Then allPoolStats.Put(dbKey, connector.GetPoolStats) Else allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado")) End If Next j.Initialize(allPoolStats) resp.Write(j.ToString) Return Case "slowqueries" resp.ContentType = "application/json; charset=utf-8" Dim results As List results.Initialize Try ' Verificamos si la tabla de logs existe antes de consultarla Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null If tableExists = False Then ' Si la tabla no existe, devolvemos un JSON con un mensaje claro y terminamos. j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilita 'enableSQLiteLogs=1' en la configuración.")) resp.Write(j.ToString) Return End If ' La tabla existe, procedemos con la consulta original Dim oneHourAgoMs As Long = DateTime.Now - 3600000 Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$) Do While rs.NextRow Dim row As Map row.Initialize row.Put("Query", rs.GetString("query_name")) row.Put("Duracion_ms", rs.GetLong("duration_ms")) row.Put("Fecha_Hora", rs.GetString("timestamp_local")) row.Put("DB_Key", rs.GetString("db_key")) row.Put("Cliente_IP", rs.GetString("client_ip")) row.Put("Conexiones_Ocupadas", rs.GetInt("busy_connections")) row.Put("Peticiones_Activas", rs.GetInt("handler_active_requests")) results.Add(row) Loop rs.Close ' 1. Creamos un mapa "raíz" para contener nuestra lista. Dim root As Map root.Initialize root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común. ' 2. Ahora sí, inicializamos el generador con el mapa raíz. j.Initialize(root) resp.Write(j.ToString) Catch Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message) ' <<< CORRECCIÓN AQUÍ >>> ' Se utiliza la propiedad .Status para asignar el código de error resp.Status = 500 ' Internal Server Error ' 1. Creamos un mapa "raíz" para contener nuestra lista. Dim root As Map root.Initialize root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común. ' 2. Ahora sí, inicializamos el generador con el mapa raíz. j.Initialize(root) resp.Write(j.ToString) End Try Return Case "logs", "totalrequests", "totalblocked" resp.ContentType = "application/json; charset=utf-8" Dim mp As Map If Command = "logs" And GlobalParameters.mpLogs.IsInitialized Then mp = GlobalParameters.mpLogs If Command = "totalrequests" And GlobalParameters.mpTotalRequests.IsInitialized Then mp = GlobalParameters.mpTotalRequests If Command = "totalblocked" And GlobalParameters.mpBlockConnection.IsInitialized Then mp = GlobalParameters.mpBlockConnection If mp.IsInitialized Then j.Initialize(mp) resp.Write(j.ToString) Else resp.Write("{}") End If Return ' --- Comandos que devuelven TEXTO PLANO --- Case "ping" resp.ContentType = "text/plain" resp.Write($"Pong ($DateTime{DateTime.Now})"$) Return Case "reload" resp.ContentType = "text/plain; charset=utf-8" Dim sbTemp As StringBuilder sbTemp.Initialize ' <<< LÓGICA ORIGINAL: Se mantiene intacta toda la lógica de recarga >>> ' (Copiada y pegada directamente de tu código anterior) sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF) Dim oldTimerState As Boolean = Main.timerLogs.Enabled If oldTimerState Then Main.timerLogs.Enabled = False sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF) End If Dim newConnectors As Map newConnectors.Initialize Dim oldConnectors As Map Dim reloadSuccessful As Boolean = True Main.MainConnectorsLock.RunMethod("lock", Null) oldConnectors = Main.Connectors Main.MainConnectorsLock.RunMethod("unlock", Null) For Each dbKey As String In Main.listaDeCP Try Dim newRDC As RDCConnector newRDC.Initialize(dbKey) Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0) Dim isEnabled As Boolean = (enableLogsSetting = 1) newConnectors.Put(dbKey & "_LOG_STATE", isEnabled) sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF) newConnectors.Put(dbKey, newRDC) Dim newPoolStats As Map = newRDC.GetPoolStats sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF) Catch sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF) reloadSuccessful = False Exit End Try Next If reloadSuccessful Then Main.MainConnectorsLock.RunMethod("lock", Null) Main.Connectors = newConnectors Main.MainConnectorsLock.RunMethod("unlock", Null) Main.SQLiteLoggingStatusByDB.Clear Dim isAnyEnabled As Boolean = False For Each dbKey As String In Main.listaDeCP Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE") Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled) If isEnabled Then isAnyEnabled = True Next Main.IsAnySQLiteLoggingEnabled = isAnyEnabled If Main.IsAnySQLiteLoggingEnabled Then Main.timerLogs.Enabled = True sbTemp.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF) Else Main.timerLogs.Enabled = False sbTemp.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF) End If sbTemp.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF) Else If oldTimerState Then Main.timerLogs.Enabled = True sbTemp.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF) End If sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF) End If ' <<< CAMBIO: Se devuelve el contenido del StringBuilder como texto plano >>> resp.Write(sbTemp.ToString) Return Case "test" resp.ContentType = "text/plain; charset=utf-8" Dim sb As StringBuilder sb.Initialize Try Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") sb.Append("Connection successful." & CRLF & CRLF) Dim estaDB As String = "" Log(Main.listaDeCP) For i = 0 To Main.listaDeCP.Size - 1 If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i) sb.Append($"Using config${estaDB}.properties"$ & CRLF) Next con.Close resp.Write(sb.ToString) Catch resp.Write("Error fetching connection: " & LastException.Message) End Try Return Case "rsx", "rpm2", "revivebow", "restartserver" resp.ContentType = "text/plain; charset=utf-8" Dim batFile As String Select Command Case "rsx": batFile = "start.bat" Case "rpm2": batFile = "reiniciaProcesoPM2.bat" Case "reviveBow": batFile = "reiniciaProcesoBow.bat" Case "restartserver": batFile = "restarServer.bat" End Select Log($"Ejecutando ${File.DirApp}\${batFile}"$) Try Dim shl As Shell shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) resp.Write($"Comando '${Command}' ejecutado. Script invocado: ${batFile}"$) Catch resp.Write($"Error al ejecutar el script para '${Command}': ${LastException.Message}"$) End Try Return Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) Public shl As Shell shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) Case "paused", "continue" resp.ContentType = "text/plain; charset=utf-8" If Command = "paused" Then GlobalParameters.IsPaused = 1 resp.Write("Servidor pausado.") Else GlobalParameters.IsPaused = 0 resp.Write("Servidor reanudado.") End If Return Case "block", "unblock" resp.ContentType = "text/plain; charset=utf-8" Dim ip As String = req.GetParameter("IP") If ip = "" Then resp.Write("Error: El parámetro IP es requerido.") Return End If If GlobalParameters.mpBlockConnection.IsInitialized Then If Command = "block" Then GlobalParameters.mpBlockConnection.Put(ip, ip) resp.Write($"IP bloqueada: ${ip}"$) Else GlobalParameters.mpBlockConnection.Remove(ip) resp.Write($"IP desbloqueada: ${ip}"$) End If Else resp.Write("Error: El mapa de bloqueo no está inicializado.") End If Return Case Else resp.ContentType = "text/plain; charset=utf-8" resp.SendError(404, $"Comando desconocido: '{Command}'"$) Return End Select End Sub