Enhance logging functionality and UI improvements
- Updated ConfigurationManager to allow reading a limited number of log lines. - Modified Logger to read the log file with an option to limit the number of lines returned. - Adjusted JavaScript to fetch only the last 1000 lines of logs for performance. - Added a new function to open a full log viewer in a new tab. - Implemented a log viewer template with search functionality, download option, and refresh capability. - Improved log display in the main interface with a message indicating the number of lines shown. - Added functionality to manage log lines in memory to prevent overflow.
This commit is contained in:
parent
fab8c95038
commit
70bbc8d6f9
38
app.py
38
app.py
|
@ -368,12 +368,48 @@ def open_file():
|
|||
@app.route("/api/logs", methods=["GET", "DELETE"])
|
||||
def handle_logs():
|
||||
if request.method == "GET":
|
||||
return jsonify({"logs": config_manager.read_log()})
|
||||
# Obtener parámetro opcional para limitar líneas
|
||||
lines_limit = request.args.get("limit", type=int)
|
||||
if lines_limit is None:
|
||||
# Por defecto, mostrar solo las últimas 1000 líneas
|
||||
lines_limit = 1000
|
||||
return jsonify({"logs": config_manager.read_log(lines_limit)})
|
||||
else: # DELETE
|
||||
success = config_manager.clear_log()
|
||||
return jsonify({"status": "success" if success else "error"})
|
||||
|
||||
|
||||
@app.route("/api/open-log", methods=["POST"])
|
||||
def open_log_file():
|
||||
"""Redirigir a la página del log completo"""
|
||||
return jsonify({"status": "success", "redirect_url": "/log-viewer"})
|
||||
|
||||
|
||||
@app.route("/log-viewer")
|
||||
def log_viewer():
|
||||
"""Página para mostrar el log completo"""
|
||||
try:
|
||||
# Leer todo el log sin límite
|
||||
full_log = config_manager.read_log(lines_limit=None)
|
||||
return render_template("log_viewer.html", log_content=full_log)
|
||||
except Exception as e:
|
||||
error_msg = f"Error cargando el log: {str(e)}"
|
||||
print(error_msg)
|
||||
return render_template("log_viewer.html", log_content=f"Error: {error_msg}")
|
||||
|
||||
|
||||
@app.route("/api/full-log")
|
||||
def get_full_log():
|
||||
"""API para obtener el log completo en formato JSON"""
|
||||
try:
|
||||
full_log = config_manager.read_log(lines_limit=None)
|
||||
return jsonify({"logs": full_log})
|
||||
except Exception as e:
|
||||
error_msg = f"Error leyendo el log: {str(e)}"
|
||||
print(error_msg)
|
||||
return jsonify({"error": error_msg}), 500
|
||||
|
||||
|
||||
@app.route("/api/group-description/<group>", methods=["GET", "POST"])
|
||||
def handle_group_description(group):
|
||||
if request.method == "GET":
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
{
|
||||
"history": [
|
||||
{
|
||||
"id": "896ccf92",
|
||||
"group_id": "2",
|
||||
"script_name": "main.py",
|
||||
"executed_date": "2025-08-29T11:18:38.765024Z",
|
||||
"arguments": [],
|
||||
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||
"python_env": "tia_scripting",
|
||||
"executable_type": "pythonw.exe",
|
||||
"status": "running",
|
||||
"pid": 13388,
|
||||
"execution_time": null
|
||||
},
|
||||
{
|
||||
"id": "66baa92e",
|
||||
"group_id": "2",
|
||||
|
|
54535
data/log.txt
54535
data/log.txt
File diff suppressed because it is too large
Load Diff
|
@ -105,8 +105,8 @@ class ConfigurationManager:
|
|||
# but the core logging is handled by the Logger instance.
|
||||
self.logger.append_log(message)
|
||||
|
||||
def read_log(self) -> str:
|
||||
return self.logger.read_log()
|
||||
def read_log(self, lines_limit: int = None) -> str:
|
||||
return self.logger.read_log(lines_limit)
|
||||
|
||||
def clear_log(self) -> bool:
|
||||
return self.logger.clear_log()
|
||||
|
|
|
@ -34,11 +34,19 @@ class Logger:
|
|||
except Exception as e:
|
||||
print(f"Error writing to log file {self.log_file}: {e}")
|
||||
|
||||
def read_log(self) -> str:
|
||||
"""Read the entire log file"""
|
||||
def read_log(self, lines_limit: int = None) -> str:
|
||||
"""Read the log file, optionally limiting to the last N lines"""
|
||||
try:
|
||||
with open(self.log_file, "r", encoding="utf-8") as f:
|
||||
if lines_limit is None:
|
||||
return f.read()
|
||||
else:
|
||||
# Read all lines and get the last N lines
|
||||
all_lines = f.readlines()
|
||||
if len(all_lines) <= lines_limit:
|
||||
return "".join(all_lines)
|
||||
else:
|
||||
return "".join(all_lines[-lines_limit:])
|
||||
except Exception as e:
|
||||
print(f"Error reading log file {self.log_file}: {e}")
|
||||
return ""
|
||||
|
|
|
@ -1131,13 +1131,35 @@ async function clearLogs() {
|
|||
}
|
||||
|
||||
async function loadStoredLogs() {
|
||||
const response = await fetch('/api/logs');
|
||||
// Cargar solo las últimas 1000 líneas para mejorar rendimiento
|
||||
const response = await fetch('/api/logs?limit=1000');
|
||||
const result = await response.json();
|
||||
const logArea = document.getElementById('log-area');
|
||||
logArea.innerHTML = result.logs;
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
async function openFullLog() {
|
||||
try {
|
||||
const response = await fetch('/api/open-log', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
// Abrir el visor de log en una nueva pestaña
|
||||
window.open('/log-viewer', '_blank');
|
||||
showToast ? showToast('Visor de log abierto en nueva pestaña', 'success') :
|
||||
console.log('Log viewer opened successfully');
|
||||
} else {
|
||||
showToast ? showToast('Error al abrir el visor de log: ' + result.message, 'error') :
|
||||
alert('Error al abrir el visor de log: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening log viewer:', error);
|
||||
showToast ? showToast('Error de red al abrir el visor de log', 'error') :
|
||||
alert('Error de red al abrir el visor de log');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
async function initializeApp() {
|
||||
try {
|
||||
|
@ -1238,7 +1260,13 @@ function getTimestamp() {
|
|||
});
|
||||
}
|
||||
|
||||
// Función para agregar línea al log con timestamp (optimizada)
|
||||
// Configuración para el manejo del log
|
||||
const LOG_CONFIG = {
|
||||
MAX_LINES_IN_MEMORY: 1000, // Máximo número de líneas a mantener en memoria
|
||||
TRIM_TO_LINES: 800 // Cuando se alcanza el máximo, recortar a este número
|
||||
};
|
||||
|
||||
// Función para agregar línea al log con timestamp (optimizada con límite de líneas)
|
||||
function addLogLine(message) {
|
||||
const logArea = document.getElementById('log-area');
|
||||
if (!logArea) return;
|
||||
|
@ -1249,6 +1277,15 @@ function addLogLine(message) {
|
|||
if (cleanMessage) {
|
||||
// Usar textContent + newline es más eficiente que innerHTML concatenation
|
||||
logArea.textContent += cleanMessage + '\n';
|
||||
|
||||
// Controlar el número de líneas para evitar sobrecarga de memoria
|
||||
const lines = logArea.textContent.split('\n');
|
||||
if (lines.length > LOG_CONFIG.MAX_LINES_IN_MEMORY) {
|
||||
// Mantener solo las últimas líneas especificadas
|
||||
const trimmedLines = lines.slice(-LOG_CONFIG.TRIM_TO_LINES);
|
||||
logArea.textContent = trimmedLines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
|
||||
// Detectar finalización de scripts
|
||||
|
|
|
@ -804,9 +804,17 @@
|
|||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Logs</h2>
|
||||
<button class="bg-red-500 text-white px-4 py-2 rounded" onclick="clearLogs()">
|
||||
Limpiar
|
||||
<div class="flex gap-2">
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" onclick="openFullLog()">
|
||||
🔍 Ver Log Completo
|
||||
</button>
|
||||
<button class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600" onclick="clearLogs()">
|
||||
🗑️ Limpiar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-gray-600">Mostrando las últimas 1000 líneas del log. Use "Ver Log Completo" para ver todo el historial.</small>
|
||||
</div>
|
||||
<div id="log-area"
|
||||
class="bg-gray-100 p-4 rounded h-64 overflow-y-auto font-mono text-sm whitespace-pre-wrap">
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Visor de Log Completo - Script Parameter Manager</title>
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='icon.png') }}">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
.log-content {
|
||||
font-family: 'Courier New', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.search-highlight {
|
||||
background-color: yellow;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div class="container mx-auto p-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white rounded-lg shadow mb-6 p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-2xl font-bold text-gray-800">Visor de Log Completo</h1>
|
||||
<div class="flex gap-3">
|
||||
<button onclick="downloadLog()"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
||||
📥 Descargar
|
||||
</button>
|
||||
<button onclick="refreshLog()"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
||||
🔄 Actualizar
|
||||
</button>
|
||||
<button onclick="window.close()"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
|
||||
✖️ Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-4">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="searchInput" placeholder="Buscar en el log..."
|
||||
class="flex-1 p-2 border border-gray-300 rounded focus:border-blue-500 focus:outline-none">
|
||||
<button onclick="searchInLog()"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
||||
🔍 Buscar
|
||||
</button>
|
||||
<button onclick="clearSearch()"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
|
||||
Limpiar
|
||||
</button>
|
||||
</div>
|
||||
<div id="searchStats" class="text-sm text-gray-600 mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Content -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="p-4 border-b bg-gray-50">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold">Contenido del Log</h2>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span id="lineCount">Calculando líneas...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div id="logContent" class="log-content bg-gray-100 p-4 rounded border max-h-[70vh] overflow-auto text-sm">
|
||||
{{ log_content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let originalLogContent = '';
|
||||
|
||||
// Inicializar al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const logContentElement = document.getElementById('logContent');
|
||||
originalLogContent = logContentElement.textContent;
|
||||
updateLineCount();
|
||||
|
||||
// Scroll al final del log
|
||||
logContentElement.scrollTop = logContentElement.scrollHeight;
|
||||
|
||||
// Configurar búsqueda con Enter
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchInLog();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function updateLineCount() {
|
||||
const lines = originalLogContent.split('\n').length;
|
||||
document.getElementById('lineCount').textContent = `${lines.toLocaleString()} líneas`;
|
||||
}
|
||||
|
||||
function searchInLog() {
|
||||
const searchTerm = document.getElementById('searchInput').value.trim();
|
||||
const logContentElement = document.getElementById('logContent');
|
||||
const searchStatsElement = document.getElementById('searchStats');
|
||||
|
||||
if (!searchTerm) {
|
||||
clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escapar caracteres especiales para regex
|
||||
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(escapedTerm, 'gi');
|
||||
|
||||
// Contar coincidencias
|
||||
const matches = (originalLogContent.match(regex) || []).length;
|
||||
|
||||
if (matches > 0) {
|
||||
// Resaltar coincidencias
|
||||
const highlightedContent = originalLogContent.replace(regex,
|
||||
'<span class="search-highlight">$&</span>');
|
||||
logContentElement.innerHTML = highlightedContent;
|
||||
|
||||
searchStatsElement.textContent = `Encontradas ${matches} coincidencias de "${searchTerm}"`;
|
||||
searchStatsElement.className = 'text-sm text-green-600 mt-2';
|
||||
|
||||
// Scroll a la primera coincidencia
|
||||
const firstHighlight = logContentElement.querySelector('.search-highlight');
|
||||
if (firstHighlight) {
|
||||
firstHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
} else {
|
||||
searchStatsElement.textContent = `No se encontraron coincidencias de "${searchTerm}"`;
|
||||
searchStatsElement.className = 'text-sm text-red-600 mt-2';
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
const logContentElement = document.getElementById('logContent');
|
||||
const searchStatsElement = document.getElementById('searchStats');
|
||||
|
||||
// Restaurar contenido original
|
||||
logContentElement.textContent = originalLogContent;
|
||||
document.getElementById('searchInput').value = '';
|
||||
searchStatsElement.textContent = '';
|
||||
}
|
||||
|
||||
async function refreshLog() {
|
||||
try {
|
||||
const response = await fetch('/api/full-log');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.logs !== undefined) {
|
||||
originalLogContent = result.logs;
|
||||
document.getElementById('logContent').textContent = originalLogContent;
|
||||
updateLineCount();
|
||||
clearSearch();
|
||||
|
||||
// Scroll al final
|
||||
const logContentElement = document.getElementById('logContent');
|
||||
logContentElement.scrollTop = logContentElement.scrollHeight;
|
||||
} else {
|
||||
alert('Error al actualizar el log: ' + (result.error || 'Error desconocido'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing log:', error);
|
||||
alert('Error de red al actualizar el log');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadLog() {
|
||||
const blob = new Blob([originalLogContent], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||
|
||||
a.href = url;
|
||||
a.download = `log_${timestamp}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue