From 3404812a1ea5bf5c6f76e01e64505236af2d61ff Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Sat, 18 Apr 2026 23:13:51 -0700
Subject: [PATCH] =?UTF-8?q?Improve=20live=20log=20viewer=20=E2=80=94=20fix?=
=?UTF-8?q?=20level=20filters,=20faster=20updates,=20add=20search?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix level filter showing nothing: now uses heuristic classification
for print() output (error/traceback/failed→ERROR, warn→WARNING, etc.)
in addition to exact logger format matching
- Speed up WebSocket updates from 2s to 0.5s polling
- Add search box with 300ms debounce — filters both initial load and live
- Use DocumentFragment for batch DOM appends (performance)
- Increase line cap from 1000 to 2000
- Backend search parameter support in /api/logs/tail
---
web_server.py | 25 ++++++++++++---
webui/index.html | 1 +
webui/static/script.js | 69 +++++++++++++++++++++++++++++++-----------
webui/static/style.css | 12 ++++++++
4 files changed, 85 insertions(+), 22 deletions(-)
diff --git a/web_server.py b/web_server.py
index 57f0c54e..a961aa4d 100644
--- a/web_server.py
+++ b/web_server.py
@@ -6282,21 +6282,38 @@ def get_log_tail():
}
log_path = log_map.get(log_source, log_map['app'])
+ search = request.args.get('search', '').lower()
+
+ def _classify_log_level(line):
+ """Classify a log line's level. Returns DEBUG/INFO/WARNING/ERROR or empty for unclassified."""
+ if ' - DEBUG - ' in line: return 'DEBUG'
+ if ' - INFO - ' in line: return 'INFO'
+ if ' - WARNING - ' in line: return 'WARNING'
+ if ' - ERROR - ' in line or ' - CRITICAL - ' in line: return 'ERROR'
+ # Heuristic for print() output and non-logger lines
+ ll = line.lower()
+ if 'error' in ll or 'traceback' in ll or 'exception' in ll or 'failed' in ll: return 'ERROR'
+ if 'warning' in ll or 'warn' in ll: return 'WARNING'
+ if 'debug' in ll: return 'DEBUG'
+ return 'INFO' # Default unclassified lines to INFO
+
result_lines = []
if os.path.exists(log_path):
try:
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
all_lines = f.readlines()
# Read more lines than requested so filtering has enough to work with
- tail = all_lines[-(lines * 3):] if level_filter else all_lines[-lines:]
+ pool_size = lines * 5 if (level_filter or search) else lines
+ tail = all_lines[-pool_size:]
for line in tail:
stripped = line.rstrip()
if not stripped:
continue
if level_filter and level_filter in ('DEBUG', 'INFO', 'WARNING', 'ERROR'):
- # Match lines like "2026-04-18 12:00:00 - name - INFO - message"
- if f' - {level_filter} - ' not in stripped:
+ if _classify_log_level(stripped) != level_filter:
continue
+ if search and search not in stripped.lower():
+ continue
result_lines.append(stripped)
# Trim to requested count after filtering
result_lines = result_lines[-lines:]
@@ -54470,7 +54487,7 @@ def _emit_live_log_loop():
'source_reuse': os.path.join('logs', 'source_reuse.log'),
}
while not globals().get('IS_SHUTTING_DOWN', False):
- socketio.sleep(2)
+ socketio.sleep(0.5)
try:
# Read which source clients want (stored by subscribe handler)
source = getattr(_emit_live_log_loop, '_source', 'app')
diff --git a/webui/index.html b/webui/index.html
index 605c8b9c..e8b268e5 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -5867,6 +5867,7 @@