mirror of
https://github.com/KeymonSoft/jRDC-MultiDB-Hikari.git
synced 2026-04-18 05:09:29 +00:00
393 lines
13 KiB
HTML
393 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>jRDC2-Multi - Panel de Administración</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
Helvetica, Arial, sans-serif;
|
|
background-color: #f8f9fa;
|
|
color: #212529;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.admin-menu {
|
|
background-color: #e9ecef;
|
|
padding: 1.5em 2em;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
.admin-menu h2 {
|
|
margin: 0 0 0.2em 0;
|
|
color: #343a40;
|
|
}
|
|
.admin-menu p {
|
|
margin: 0 0 1em 0;
|
|
color: #495057;
|
|
}
|
|
.admin-menu a {
|
|
color: #007bff;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
margin-right: 0.5em;
|
|
padding-right: 0.5em;
|
|
border-right: 1px solid #ccc;
|
|
cursor: pointer;
|
|
}
|
|
.admin-menu a:last-child {
|
|
border-right: none;
|
|
}
|
|
.admin-menu a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.main-content {
|
|
padding: 2em;
|
|
}
|
|
#output-container {
|
|
background: #fff;
|
|
padding: 1em;
|
|
border: 1px solid #eee;
|
|
border-radius: 8px;
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
min-height: 200px;
|
|
overflow-x: auto;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 1.5em;
|
|
font-family: sans-serif;
|
|
font-size: 0.9em;
|
|
}
|
|
th,
|
|
td {
|
|
padding: 10px 12px;
|
|
text-align: center;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
thead {
|
|
background-color: #007bff;
|
|
color: #fff;
|
|
}
|
|
th {
|
|
font-weight: 600;
|
|
cursor: help;
|
|
}
|
|
tbody td:first-child {
|
|
font-weight: bold;
|
|
color: #0056b3;
|
|
text-align: left;
|
|
}
|
|
|
|
/* --- ESTILOS PARA EL INDICADOR SSE --- */
|
|
.sse-status-container {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-top: 1em;
|
|
font-size: 0.8em;
|
|
}
|
|
.sse-status {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
margin-left: 10px;
|
|
min-width: 80px;
|
|
text-align: center;
|
|
}
|
|
.sse-connected {
|
|
background-color: #28a745;
|
|
color: white;
|
|
}
|
|
.sse-disconnected {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
.sse-connecting {
|
|
background-color: #ffc107;
|
|
color: #212529;
|
|
}
|
|
.cell-update {
|
|
animation: flash 0.7s ease-out;
|
|
}
|
|
@keyframes flash {
|
|
0% {
|
|
background-color: #ffc107;
|
|
}
|
|
100% {
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="admin-menu">
|
|
<h2>Panel de Administración jRDC</h2>
|
|
<p>Bienvenido, <strong>admin</strong></p>
|
|
<nav id="main-nav">
|
|
<a data-command="test">Test</a>
|
|
<a data-command="ping">Ping</a>
|
|
<a data-command="reload">Reload</a>
|
|
<a data-command="slowqueries">Queries Lentas</a>
|
|
<a data-command="getstats">Estadísticas Pool</a>
|
|
<a data-command="rpm2">Reiniciar (pm2)</a>
|
|
<a data-command="reviveBow">Revive Bow</a>
|
|
<a data-command="getconfiginfo">Info</a>
|
|
</nav>
|
|
<div class="sse-status-container">
|
|
<span>Estado de Estadísticas en Tiempo Real:</span>
|
|
<span id="sse-status" class="sse-status sse-disconnected"
|
|
>Desconectado</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<h1 id="content-title">Bienvenido</h1>
|
|
<div id="output-container">
|
|
<p style="font-family: sans-serif">
|
|
Selecciona una opción del menú para comenzar.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const outputContainer = document.getElementById("output-container");
|
|
const contentTitle = document.getElementById("content-title");
|
|
const sseStatus = document.getElementById("sse-status");
|
|
let sseConnection = null;
|
|
|
|
// --- CONFIGURACIÓN PARA LA TABLA DE ESTADÍSTICAS ---
|
|
const COLUMN_ORDER = [
|
|
"InitialPoolSize",
|
|
"MinPoolSize",
|
|
"MaxPoolSize",
|
|
"AcquireIncrement",
|
|
"TotalConnections",
|
|
"BusyConnections",
|
|
"IdleConnections",
|
|
"CheckoutTimeout",
|
|
"MaxIdleTime",
|
|
"MaxConnectionAge",
|
|
];
|
|
const HEADER_TOOLTIPS = {
|
|
InitialPoolSize:
|
|
"Número de conexiones que el pool intenta adquirir al arrancar.",
|
|
MinPoolSize: "Número mínimo de conexiones que el pool mantendrá.",
|
|
MaxPoolSize: "Número máximo de conexiones que el pool puede mantener.",
|
|
AcquireIncrement:
|
|
"Número de conexiones a adquirir cuando el pool se queda sin conexiones.",
|
|
TotalConnections: "Número total de conexiones (ocupadas + libres).",
|
|
BusyConnections: "Número de conexiones activamente en uso.",
|
|
IdleConnections: "Número de conexiones disponibles en el pool.",
|
|
CheckoutTimeout: "Tiempo máximo de espera por una conexión (ms).",
|
|
MaxIdleTime:
|
|
"Tiempo máximo que una conexión puede estar inactiva (segundos).",
|
|
MaxConnectionAge: "Tiempo máximo de vida de una conexión (segundos).",
|
|
};
|
|
|
|
// --- MANEJO DE LA CONEXIÓN SSE ---
|
|
|
|
function connectSSE() {
|
|
if (sseConnection && sseConnection.readyState !== EventSource.CLOSED) {
|
|
return; // Ya está conectado o conectando
|
|
}
|
|
|
|
outputContainer.innerHTML =
|
|
'<p style="font-family: sans-serif;">Esperando datos del pool de conexiones...</p>';
|
|
updateSSEStatus("connecting");
|
|
|
|
// La ruta debe coincidir con la que registraste en srvr.AddHandler
|
|
const SSE_ENDPOINT = "/stats-stream";
|
|
sseConnection = new EventSource(SSE_ENDPOINT);
|
|
|
|
sseConnection.onopen = () => {
|
|
console.log("Conexión SSE establecida.");
|
|
updateSSEStatus("connected");
|
|
};
|
|
|
|
// Escucha el evento específico "stats_update"
|
|
sseConnection.addEventListener("stats_update", (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
renderOrUpdateStatsTable(data);
|
|
} catch (e) {
|
|
console.error("Error al parsear datos SSE:", e);
|
|
}
|
|
});
|
|
|
|
sseConnection.onerror = () => {
|
|
console.error("Error en la conexión SSE. Reintentando...");
|
|
updateSSEStatus("disconnected");
|
|
sseConnection.close();
|
|
// El navegador reintentará automáticamente la conexión
|
|
};
|
|
}
|
|
|
|
function disconnectSSE() {
|
|
if (sseConnection) {
|
|
sseConnection.close();
|
|
sseConnection = null;
|
|
console.log("Conexión SSE cerrada.");
|
|
updateSSEStatus("disconnected");
|
|
}
|
|
}
|
|
|
|
function updateSSEStatus(status) {
|
|
switch (status) {
|
|
case "connected":
|
|
sseStatus.textContent = "Conectado";
|
|
sseStatus.className = "sse-status sse-connected";
|
|
break;
|
|
case "connecting":
|
|
sseStatus.textContent = "Conectando";
|
|
sseStatus.className = "sse-status sse-connecting";
|
|
break;
|
|
case "disconnected":
|
|
sseStatus.textContent = "Desconectado";
|
|
sseStatus.className = "sse-status sse-disconnected";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// --- RENDERIZADO Y ACTUALIZACIÓN DE LA TABLA ---
|
|
|
|
function renderOrUpdateStatsTable(data) {
|
|
const table = document.getElementById("stats-table");
|
|
// Si la tabla no existe, la crea
|
|
if (!table) {
|
|
outputContainer.innerHTML = createStatsTableHTML(data);
|
|
return;
|
|
}
|
|
|
|
// Si la tabla ya existe, solo actualiza las celdas
|
|
for (const dbKey in data) {
|
|
const poolData = data[dbKey];
|
|
COLUMN_ORDER.forEach((metric) => {
|
|
const cell = document.getElementById(`${dbKey}_${metric}`);
|
|
if (cell && cell.textContent != poolData[metric]) {
|
|
cell.textContent = poolData[metric];
|
|
cell.classList.add("cell-update");
|
|
setTimeout(() => cell.classList.remove("cell-update"), 700);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function createStatsTableHTML(data) {
|
|
let tableHtml = `<table id="stats-table"><thead><tr><th title="Nombre de la Base de Datos">DB Key</th>`;
|
|
COLUMN_ORDER.forEach((key) => {
|
|
tableHtml += `<th title="${HEADER_TOOLTIPS[key] || ""}">${key}</th>`;
|
|
});
|
|
tableHtml += `</tr></thead><tbody>`;
|
|
for (const dbKey in data) {
|
|
const poolData = data[dbKey];
|
|
tableHtml += `<tr><td>${dbKey}</td>`;
|
|
COLUMN_ORDER.forEach((metric) => {
|
|
// Se añade un ID único a cada celda: "DB1_TotalConnections"
|
|
tableHtml += `<td id="${dbKey}_${metric}">${
|
|
poolData[metric] ?? "N/A"
|
|
}</td>`;
|
|
});
|
|
tableHtml += `</tr>`;
|
|
}
|
|
tableHtml += `</tbody></table>`;
|
|
return tableHtml;
|
|
}
|
|
|
|
// --- MANEJO DE COMANDOS ESTÁTICOS (SIN CAMBIOS) ---
|
|
|
|
async function loadStaticContent(command) {
|
|
contentTitle.textContent = `Resultado del Comando: '${command}'`;
|
|
outputContainer.innerHTML = "Cargando...";
|
|
try {
|
|
const response = await fetch(`/manager?command=${command}`);
|
|
const responseText = await response.text();
|
|
if (!response.ok)
|
|
throw new Error(
|
|
`Error del servidor (${response.status}): ${responseText}`
|
|
);
|
|
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType && contentType.includes("application/json")) {
|
|
const data = JSON.parse(responseText);
|
|
if (command === "slowqueries") {
|
|
outputContainer.innerHTML = data.message
|
|
? `<p>${data.message}</p>`
|
|
: createTableFromJSON(data.data);
|
|
} else {
|
|
outputContainer.textContent = JSON.stringify(data, null, 2);
|
|
}
|
|
} else {
|
|
outputContainer.textContent = responseText;
|
|
}
|
|
} catch (error) {
|
|
outputContainer.textContent = `Error al procesar la respuesta:\n${error.message}`;
|
|
}
|
|
}
|
|
|
|
function createTableFromJSON(data) {
|
|
if (!data || data.length === 0)
|
|
return "<p>No se encontraron queries lentas.</p>";
|
|
const headers = Object.keys(data[0]);
|
|
let table = "<table><thead><tr>";
|
|
headers.forEach((h) => (table += `<th>${h.replace(/_/g, " ")}</th>`));
|
|
table += "</tr></thead><tbody>";
|
|
data.forEach((row) => {
|
|
table += "<tr>";
|
|
headers.forEach((h) => (table += `<td>${row[h]}</td>`));
|
|
table += "</tr>";
|
|
});
|
|
table += "</tbody></table>";
|
|
return table;
|
|
}
|
|
|
|
// --- EVENT LISTENER PRINCIPAL ---
|
|
|
|
document.getElementById("main-nav").addEventListener("click", (event) => {
|
|
if (event.target.tagName === "A") {
|
|
const command = event.target.dataset.command;
|
|
if (!command) return;
|
|
|
|
if (command === "reload") {
|
|
// Pedimos al usuario la DB Key. Si la deja vacía, se asume recarga total.
|
|
const dbKey = prompt(
|
|
"Ingrese la llave de la DB a recargar (ej: DB2, DB3). Deje vacío para recargar TODAS:",
|
|
""
|
|
);
|
|
if (dbKey === null) {
|
|
// El usuario presionó Cancelar o cerró la ventana. NO HACER NADA.
|
|
outputContainer.textContent = "Recarga cancelada por el usuario.";
|
|
contentTitle.textContent = "Administración";
|
|
return;
|
|
}
|
|
let finalCommand = "reload";
|
|
if (dbKey && dbKey.trim() !== "") {
|
|
// Si el usuario especificó una DB (ej. DB2), construimos el comando con el parámetro 'db'.
|
|
const key = dbKey.toUpperCase().trim();
|
|
finalCommand = `reload&db=${key}`;
|
|
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>${key}</b> (Hot-Swap)...</p>`;
|
|
} else {
|
|
// Recarga total.
|
|
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>TODAS</b> (Hot-Swap)...</p>`;
|
|
}
|
|
disconnectSSE();
|
|
// Llamamos a la función loadStaticContent con el comando completo (ej: 'reload&db=DB2' o 'reload')
|
|
loadStaticContent(finalCommand);
|
|
} else if (command === "getstats") {
|
|
contentTitle.textContent = `Estadísticas del Pool en Tiempo Real`;
|
|
connectSSE();
|
|
} else {
|
|
// Si se selecciona cualquier otro comando, se desconecta del SSE
|
|
disconnectSSE();
|
|
loadStaticContent(command);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|