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