Files
jRDC-MultiDB-Hikari/Files/www/manager.html
jaguerrau 9c9e2975e9 - VERSION 5.10.27
- feat(arquitectura): Consolidación de estabilidad y diagnóstico.
- refactor: Arquitectura de base de datos local y políticas de logs.
- arch(sqlite): Aislamiento total de las conexiones SQLite en SQL_Auth y SQL_Logs. Esto protege las operaciones de autenticación críticas de la alta carga de I/O generada por el subsistema de logs.
- feat(logs): Implementación de modo de almacenamiento flexible para logs (disco o en memoria), mejorando la capacidad de testing.
- refactor(logs): Se estandariza el límite de retención de registros a 10,000 para todas las tablas de logs, y se renombra la subrutina de limpieza a borraArribaDe10000Logs.
2025-10-29 05:25:56 -06:00

663 lines
25 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; /* Mantenido para otros comandos */
word-wrap: break-word;
min-height: 200px;
overflow-x: auto;
}
/* --- CORRECCIÓN DE ESPACIO --- */
#slow-queries-controls-wrapper,
#slow-queries-table-wrapper {
white-space: normal;
font-family: sans-serif;
}
#slow-queries-table-wrapper {
opacity: 1;
transition: opacity 0.2s ease;
/* --- NUEVO PARA SCROLL DE TABLA --- */
max-height: 60vh; /* Altura máxima (60% de la ventana) */
overflow-y: auto; /* Scroll vertical SÓLO para la tabla */
/* --- FIN DE NUEVO --- */
}
/* --- FIN DE CORRECCIÓN --- */
table {
width: 100%;
border-collapse: collapse;
font-family: sans-serif;
font-size: 0.9em;
}
#slow-queries-table-wrapper table {
margin-top: 0.1em; /* Reducido de 0.25em */
}
#slow-queries-controls-wrapper {
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: -24px; /* Tu valor */
margin-top: -2em; /* Tu valor */
}
th,
td {
padding: 10px 12px;
text-align: center;
border-bottom: 1px solid #dee2e6;
}
thead {
background-color: #23007bff;
color: #fff;
/* --- NUEVO PARA STICKY HEADER --- */
position: sticky; /* Fija el encabezado */
top: 0; /* Lo pega en la parte superior del wrapper */
z-index: 10; /* Se asegura de que esté por encima del tbody */
/* --- FIN DE NUEVO --- */
}
th {
font-weight: 600;
cursor: help;
}
th.sortable-header:hover {
background-color: #0069d9;
}
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="togglelogs">Logs (On/Off)</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;
// --- ESTADO GLOBAL PARA SLOW QUERIES ---
let currentSortColumn = "duration_ms";
let currentSortOrder = "DESC";
let currentLimit = 20; // Límite de registros (default 20)
let currentMinutes = 60; // Filtro de tiempo (default 60)
const SLOW_QUERY_COLUMN_MAP = {
"Query": "query_name",
"Duracion_ms": "duration_ms",
"Fecha_Hora": "timestamp",
"DB_Key": "db_key",
"Cliente_IP": "client_ip",
"Conexiones_Ocupadas": "busy_connections",
"Peticiones_Activas": "handler_active_requests"
};
// 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 minimo 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;
}
outputContainer.innerHTML =
'<p style="font-family: sans-serif;">Esperando datos del pool de conexiones...</p>';
updateSSEStatus("connecting");
const SSE_ENDPOINT = "/stats-stream";
sseConnection = new EventSource(SSE_ENDPOINT);
sseConnection.onopen = () => {
console.log("Conexión SSE establecida.");
updateSSEStatus("connected");
};
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();
};
}
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 DE ESTADÍSTICAS
function renderOrUpdateStatsTable(data) {
const table = document.getElementById("stats-table");
if (!table) {
outputContainer.innerHTML = createStatsTableHTML(data);
return;
}
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) => {
tableHtml += `<td id="${dbKey}-${metric}">${
poolData[metric] ?? "N/A"
}</td>`;
});
tableHtml += "</tr>";
}
tableHtml += "</tbody></table>";
return tableHtml;
}
// --- MANEJO DE COMANDOS ESTÁTICOS (MODIFICADO) ---
function initSlowQueriesView() {
outputContainer.innerHTML = `
<div id="slow-queries-controls-wrapper">
<!-- Los controles se cargarán aquí -->
</div>
<div id="slow-queries-table-wrapper" style="opacity: 0.5;">
Cargando...
</div>
`;
}
async function loadStaticContent(command) {
const commandName = command.split('&')[0];
contentTitle.textContent = `Resultado del Comando: '${commandName}'`;
let isSlowQuery = command.startsWith("slowqueries");
if (!isSlowQuery) {
outputContainer.innerHTML = "Cargando...";
} else {
let controlsWrapper = document.getElementById("slow-queries-controls-wrapper");
let tableWrapper = document.getElementById("slow-queries-table-wrapper");
if (!controlsWrapper || !tableWrapper) {
// Si no existen (primera carga), los crea y pone "Cargando".
initSlowQueriesView();
controlsWrapper = document.getElementById("slow-queries-controls-wrapper");
tableWrapper = document.getElementById("slow-queries-table-wrapper");
} else {
// --- CORRECCIÓN DE PARPADEO ---
// Si YA existen, solo baja la opacidad. NO TOCAR innerHTML.
tableWrapper.style.opacity = "0.5";
}
// Siempre actualiza los controles (para 'selected')
controlsWrapper.innerHTML = createSlowQueriesControlsHTML();
// tableWrapper.innerHTML = "<p style='font-family: sans-serif;'>Cargando datos...</p>"; // <-- ELIMINADO
tableWrapper.style.transition = "opacity 0.2s ease";
}
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 (isSlowQuery) {
renderSlowQueriesTable(data);
if(data.meta) {
console.log("Meta de Slow Queries:", data.meta);
}
} else {
outputContainer.innerHTML = "";
outputContainer.textContent = JSON.stringify(data, null, 2);
}
} else {
outputContainer.innerHTML = "";
outputContainer.textContent = responseText;
}
} catch (error) {
if (isSlowQuery) {
const tableWrapper = document.getElementById("slow-queries-table-wrapper");
if(tableWrapper) {
tableWrapper.innerHTML = `<p style="color: red;">Error al procesar la respuesta:\n${error.message}</p>`;
tableWrapper.style.opacity = "1";
} else {
outputContainer.innerHTML = `<p style="color: red;">Error al procesar la respuesta:\n${error.message}</p>`;
}
} else {
outputContainer.innerHTML = `<p style="color: red;">Error al procesar la respuesta:\n${error.message}</p>`;
}
}
}
// --- NUEVAS FUNCIONES PARA RENDERIZADO DE TABLA 'SLOW QUERIES' ---
function createSlowQueriesControlsHTML() {
const limitOptions = [20, 25, 50, 75, 100];
let limitOptionsHtml = "";
limitOptions.forEach(opt => {
limitOptionsHtml += `<option value="${opt}" ${opt === currentLimit ? 'selected' : ''}>${opt}</option>`;
});
const minuteOptions = [
{ value: 30, text: "30 min" },
{ value: 60, text: "1 hora" },
{ value: 90, text: "90 min" },
{ value: 120, text: "2 horas" },
{ value: 150, text: "150 min" },
{ value: 180, text: "3 horas" }
];
let minuteOptionsHtml = "";
minuteOptions.forEach(opt => {
minuteOptionsHtml += `<option value="${opt.value}" ${opt.value === currentMinutes ? 'selected' : ''}>${opt.text}</option>`;
});
return `
<label for="minutes-select" style="margin-right: 0.5em; font-size: 0.9em;">Período:</label>
<select id="minutes-select" style="font-size: 0.9em; padding: 4px; border-radius: 4px; border: 1px solid #ccc;">
${minuteOptionsHtml}
</select>
<label for="limit-select" style="margin-left: 1em; margin-right: 0.5em; font-size: 0.9em;">Registros:</label>
<select id="limit-select" style="font-size: 0.9em; padding: 4px; border-radius: 4px; border: 1px solid #ccc;">
${limitOptionsHtml}
</select>
`;
}
function createSlowQueriesTableHeaders(headers) {
let headerHtml = "<tr>";
headers.forEach((h) => {
const headerText = h.replace(/_/g, " ");
const sortKey = SLOW_QUERY_COLUMN_MAP[h];
if (sortKey) {
const isSorted = (sortKey === currentSortColumn);
const sortIcon = isSorted ? (currentSortOrder === 'DESC' ? ' ▼' : ' ▲') : '';
headerHtml += `<th class="sortable-header" data-sortkey="${sortKey}" style="cursor: pointer; user-select: none;">${headerText}${sortIcon}</th>`;
} else {
headerHtml += `<th>${headerText}</th>`;
}
});
headerHtml += "</tr>";
return headerHtml;
}
function createSlowQueriesTableBody(data, headers) {
let bodyHtml = "";
data.forEach((row) => {
bodyHtml += "<tr>";
headers.forEach((h) => (bodyHtml += `<td>${row[h]}</td>`));
bodyHtml += "</tr>";
});
return bodyHtml;
}
function renderSlowQueriesTable(data) {
const controlsWrapper = document.getElementById("slow-queries-controls-wrapper");
const tableWrapper = document.getElementById("slow-queries-table-wrapper");
if (!controlsWrapper || !tableWrapper) {
console.error("No se encontraron los wrappers de slow queries.");
return;
}
controlsWrapper.innerHTML = createSlowQueriesControlsHTML();
if (data.message) {
tableWrapper.innerHTML = `<p>${data.message}</p>`;
} else if (!data.data || data.data.length === 0) {
tableWrapper.innerHTML = "<p>No se encontraron queries lentas.</p>";
} else {
const headers = Object.keys(data.data[0]);
const headersHtml = createSlowQueriesTableHeaders(headers);
const bodyHtml = createSlowQueriesTableBody(data.data, headers);
tableWrapper.innerHTML = `
<table id="slow-queries-table">
<thead>${headersHtml}</thead>
<tbody>${bodyHtml}</tbody>
</table>`;
}
tableWrapper.style.opacity = "1";
}
// --- FIN DE NUEVAS FUNCIONES ---
// EVENT LISTENER PRINCIPAL (MODIFICADO)
document.getElementById("main-nav").addEventListener("click", (event) => {
if (event.target.tagName === "A") {
const command = event.target.dataset.command;
if (!command) return;
if (command === "reload") {
const dbKey = prompt(
"Ingrese la llave de la DB a recargar (ej: DB2, DB3). Deje vacio para recargar TODAS:",
""
);
if (dbKey === null) {
outputContainer.textContent = "Recarga cancelada por el usuario.";
contentTitle.textContent = "Administración";
return;
}
let finalCommand = "reload";
if (dbKey && dbKey.trim() !== "") {
const key = dbKey.toUpperCase().trim();
finalCommand = `reload&db=${key}`;
outputContainer.innerHTML = `<p>Intentando recargar: <b>${key}</b> (Hot-Swap)...</p>`;
} else {
outputContainer.innerHTML = `<p>Intentando recargar: <b>TODAS</b> (Hot-Swap)...</p>`;
}
disconnectSSE();
loadStaticContent(finalCommand);
} else if (command === "slowqueries") {
disconnectSSE();
currentSortColumn = "duration_ms";
currentSortOrder = "DESC";
currentLimit = 20;
currentMinutes = 60;
initSlowQueriesView();
const fullCommand = `slowqueries&sortby=${currentSortColumn}&sortorder=${currentSortOrder}&limit=${currentLimit}&minutes=${currentMinutes}`;
loadStaticContent(fullCommand);
} else if (command === "togglelogs") {
disconnectSSE();
contentTitle.textContent = "Control de Logs en Caliente (SQLite)";
const dbKey = prompt(
"Ingrese la DB Key a modificar (ej: DB2):",
"DB1"
);
if (dbKey === null) {
outputContainer.textContent = "Operación cancelada por el usuario.";
return;
}
const status = prompt(
"Ingrese el nuevo estado (1 para HABILITAR, 0 para DESHABILITAR):",
"0"
);
if (status === null || (status !== "0" && status !== "1")) {
outputContainer.textContent =
"Estado inválido o cancelado. (Debe ser 1 o 0).";
return;
}
const key = dbKey.toUpperCase().trim();
const finalCommand = `setlogstatus&db=${key}&status=${status}`;
outputContainer.innerHTML = `<p>Intentando cambiar Logs de <b>${key}</b> a estado <b>${status}</b>...</p>`;
loadStaticContent(finalCommand);
} else if (command === "getstats") {
contentTitle.textContent = 'Estadisticas del Pool en Tiempo Real';
connectSSE();
} else {
disconnectSSE();
loadStaticContent(command);
}
}
});
outputContainer.addEventListener('click', (e) => {
if (e.target && e.target.classList.contains('sortable-header')) {
const newSortKey = e.target.dataset.sortkey;
if (!newSortKey) return;
if (newSortKey === currentSortColumn) {
currentSortOrder = (currentSortOrder === 'DESC' ? 'ASC' : 'DESC');
} else {
currentSortColumn = newSortKey;
currentSortOrder = 'DESC';
}
const fullCommand = `slowqueries&sortby=${currentSortColumn}&sortorder=${currentSortOrder}&limit=${currentLimit}&minutes=${currentMinutes}`;
loadStaticContent(fullCommand);
}
});
outputContainer.addEventListener('change', (e) => {
let needsReload = false;
if (e.target && e.target.id === 'limit-select') {
const newLimit = parseInt(e.target.value, 10);
if (newLimit) {
currentLimit = newLimit;
needsReload = true;
}
} else if (e.target && e.target.id === 'minutes-select') {
const newMinutes = parseInt(e.target.value, 10);
if (newMinutes) {
currentMinutes = newMinutes;
needsReload = true;
}
}
if(needsReload) {
const fullCommand = `slowqueries&sortby=${currentSortColumn}&sortorder=${currentSortOrder}&limit=${currentLimit}&minutes=${currentMinutes}`;
loadStaticContent(fullCommand);
}
});
</script>
</body>
</html>