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"])
|
@app.route("/api/logs", methods=["GET", "DELETE"])
|
||||||
def handle_logs():
|
def handle_logs():
|
||||||
if request.method == "GET":
|
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
|
else: # DELETE
|
||||||
success = config_manager.clear_log()
|
success = config_manager.clear_log()
|
||||||
return jsonify({"status": "success" if success else "error"})
|
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"])
|
@app.route("/api/group-description/<group>", methods=["GET", "POST"])
|
||||||
def handle_group_description(group):
|
def handle_group_description(group):
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
{
|
{
|
||||||
"history": [
|
"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",
|
"id": "66baa92e",
|
||||||
"group_id": "2",
|
"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.
|
# but the core logging is handled by the Logger instance.
|
||||||
self.logger.append_log(message)
|
self.logger.append_log(message)
|
||||||
|
|
||||||
def read_log(self) -> str:
|
def read_log(self, lines_limit: int = None) -> str:
|
||||||
return self.logger.read_log()
|
return self.logger.read_log(lines_limit)
|
||||||
|
|
||||||
def clear_log(self) -> bool:
|
def clear_log(self) -> bool:
|
||||||
return self.logger.clear_log()
|
return self.logger.clear_log()
|
||||||
|
|
|
@ -34,11 +34,19 @@ class Logger:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error writing to log file {self.log_file}: {e}")
|
print(f"Error writing to log file {self.log_file}: {e}")
|
||||||
|
|
||||||
def read_log(self) -> str:
|
def read_log(self, lines_limit: int = None) -> str:
|
||||||
"""Read the entire log file"""
|
"""Read the log file, optionally limiting to the last N lines"""
|
||||||
try:
|
try:
|
||||||
with open(self.log_file, "r", encoding="utf-8") as f:
|
with open(self.log_file, "r", encoding="utf-8") as f:
|
||||||
|
if lines_limit is None:
|
||||||
return f.read()
|
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:
|
except Exception as e:
|
||||||
print(f"Error reading log file {self.log_file}: {e}")
|
print(f"Error reading log file {self.log_file}: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -1131,13 +1131,35 @@ async function clearLogs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStoredLogs() {
|
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 result = await response.json();
|
||||||
const logArea = document.getElementById('log-area');
|
const logArea = document.getElementById('log-area');
|
||||||
logArea.innerHTML = result.logs;
|
logArea.innerHTML = result.logs;
|
||||||
logArea.scrollTop = logArea.scrollHeight;
|
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
|
// Initialize on page load
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
try {
|
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) {
|
function addLogLine(message) {
|
||||||
const logArea = document.getElementById('log-area');
|
const logArea = document.getElementById('log-area');
|
||||||
if (!logArea) return;
|
if (!logArea) return;
|
||||||
|
@ -1249,6 +1277,15 @@ function addLogLine(message) {
|
||||||
if (cleanMessage) {
|
if (cleanMessage) {
|
||||||
// Usar textContent + newline es más eficiente que innerHTML concatenation
|
// Usar textContent + newline es más eficiente que innerHTML concatenation
|
||||||
logArea.textContent += cleanMessage + '\n';
|
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;
|
logArea.scrollTop = logArea.scrollHeight;
|
||||||
|
|
||||||
// Detectar finalización de scripts
|
// Detectar finalización de scripts
|
||||||
|
|
|
@ -804,9 +804,17 @@
|
||||||
<div class="bg-white p-6 rounded-lg shadow">
|
<div class="bg-white p-6 rounded-lg shadow">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="text-xl font-bold">Logs</h2>
|
<h2 class="text-xl font-bold">Logs</h2>
|
||||||
<button class="bg-red-500 text-white px-4 py-2 rounded" onclick="clearLogs()">
|
<div class="flex gap-2">
|
||||||
Limpiar
|
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" onclick="openFullLog()">
|
||||||
|
🔍 Ver Log Completo
|
||||||
</button>
|
</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>
|
||||||
<div id="log-area"
|
<div id="log-area"
|
||||||
class="bg-gray-100 p-4 rounded h-64 overflow-y-auto font-mono text-sm whitespace-pre-wrap">
|
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