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:
Miguel 2025-08-29 12:47:29 +02:00
parent fab8c95038
commit 70bbc8d6f9
8 changed files with 2679 additions and 52171 deletions

38
app.py
View File

@ -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":

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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 ""

View File

@ -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

View File

@ -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">

191
templates/log_viewer.html Normal file
View File

@ -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>