From 344e11f4b2f228252f7280b44ee335fed0a53d6f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 02:35:17 -0500 Subject: [PATCH] Replace GoAccess with built-in log viewer, clickable error links New /logs page with 5 tabs: - App Log (journalctl for llm-team-ui service) - Run History (all completed runs with errors inline) - Nginx Errors (with red highlighting) - Nginx Access (with color-coded status codes) - Security Log (fail2ban/exploit detection) Features: - Live text filter (grep-style) - Configurable line limit (50-500) - Auto-refresh every 10s - Run history shows mode, user, duration, response count, errors - Error lines highlighted red, warnings amber - Status codes color-coded (2xx green, 3xx blue, 4xx amber, 5xx red) Error linking: - Stream errors in main UI link to /admin/monitor - Error response cards have "View error details" link - Error cards styled with red border and monospace body Co-Authored-By: Claude Opus 4.6 (1M context) --- llm_team_ui.py | 240 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 228 insertions(+), 12 deletions(-) diff --git a/llm_team_ui.py b/llm_team_ui.py index 38b25bb..a3e6d6f 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -480,14 +480,223 @@ def demo_set_allowlist(): @app.route("/logs") +@admin_required def logs_page(): - if not is_admin(): - return redirect("/login") + return LOGS_HTML + +@app.route("/api/admin/logs") +@admin_required +def admin_logs(): + source = request.args.get("source", "app") + limit = min(int(request.args.get("limit", 100)), 500) + lines = [] try: - with open("/var/www/html/report.html") as f: - return f.read() - except Exception: - return "GoAccess report not found. Run: goaccess /var/log/nginx/access.log -o /var/www/html/report.html --log-format=COMBINED", 404 + if source == "nginx_access": + with open("/var/log/nginx/access.log") as f: + lines = f.readlines()[-limit:] + elif source == "nginx_error": + with open("/var/log/nginx/error.log") as f: + lines = f.readlines()[-limit:] + elif source == "security": + with open("/var/log/llm-team-security.log") as f: + lines = f.readlines()[-limit:] + elif source == "runs": + return jsonify({"lines": [], "runs": list(reversed(_run_log[-limit:]))}) + else: + # App log — get from journalctl + import subprocess + result = subprocess.run( + ["journalctl", "-u", "llm-team-ui", "--no-pager", "-n", str(limit), "--output=short-iso"], + capture_output=True, text=True, timeout=5 + ) + lines = result.stdout.strip().split("\n") if result.stdout else [] + except Exception as e: + lines = [f"Error reading log: {e}"] + return jsonify({"lines": [l.rstrip() for l in lines]}) + + +LOGS_HTML = r""" + + +LLM Team — Logs + + + + +
+
+
+

Logs // System View

+ Monitor + Admin + ← Team +
+
+
App Log
+
Run History
+
Nginx Errors
+
Nginx Access
+
Security
+
+
+ + + + + +
+
Loading...
+
+ +""" CONFIG_PATH = "/root/llm_team_config.json" @@ -640,6 +849,11 @@ HTML = r""" .synthesis-card { border-color: var(--accent); } .synthesis-card .card-header { background: var(--glow); } .synthesis-card::before { content: ''; position: absolute; top: -1px; left: 0; right: 0; height: 1px; background: var(--accent); opacity: 0.3; } + .error-card { border-color: var(--red); } + .error-card .card-header { background: rgba(224,82,82,0.08); } + .error-card .card-body { color: var(--red); font-family: 'JetBrains Mono', monospace; font-size: 12px; } + .error-card .error-link { display: block; padding: 6px 14px 10px; font-family: 'JetBrains Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: var(--red); opacity: 0.6; text-decoration: none; } + .error-card .error-link:hover { opacity: 1; text-decoration: underline; } .crazy-card { border-color: #a855f7; } .crazy-card .card-header { background: rgba(168,85,247,0.08); } .status-bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px; background: rgba(0,0,0,0.3); border: 2px solid var(--border); border-radius: 2px; font-size: 11px; color: var(--text2); font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.5px; } @@ -1459,11 +1673,11 @@ async function runTeam() { } } } catch(e) { - const errDiv = document.createElement('div'); + var errDiv = document.createElement('a'); errDiv.className = 'status-bar'; - errDiv.style.color = 'var(--red)'; - errDiv.style.borderColor = 'var(--red)'; - errDiv.textContent = 'Error: ' + e.message + ' (after ' + formatElapsed(Date.now() - _runStartTime) + ')'; + errDiv.href = '/admin/monitor'; + errDiv.style.cssText = 'color:var(--red);border-color:var(--red);text-decoration:none;cursor:pointer;display:flex'; + errDiv.textContent = 'Error: ' + e.message + ' (after ' + formatElapsed(Date.now() - _runStartTime) + ') — click to view logs'; output.appendChild(errDiv); } clearInterval(_runTimer); @@ -1554,13 +1768,15 @@ function handleEvent(evt) { const mi = availableModels.findIndex(m => m.name === evt.model); const color = COLORS[(mi >= 0 ? mi : 0) % COLORS.length]; const displayName = mi >= 0 ? (availableModels[mi].display_name || evt.model) : evt.model; + const isError = evt.role === 'error'; const hl = ['synthesis','judge','verdict','final','consensus','patcher','assembler','analyzer','survivor','mesh-360'].includes(evt.role); const isCrazy = evt.role && (evt.role.includes('catastrophe') || evt.role.includes('chaos') || evt.role === 'survivor'); const card = document.createElement('div'); - card.className = 'output-card' + (hl ? ' synthesis-card' : '') + (isCrazy ? ' crazy-card' : ''); + card.className = 'output-card' + (isError ? ' error-card' : '') + (hl ? ' synthesis-card' : '') + (isCrazy ? ' crazy-card' : ''); const roleTag = evt.role ? `${evt.role}` : ''; const uid = 'resp-' + Date.now() + '-' + Math.random().toString(36).substr(2,4); - card.innerHTML = `
${displayName}${roleTag}
${escapeHtml(evt.text)}
`; + const errorLink = isError ? `View error details in monitor →` : ''; + card.innerHTML = `
${displayName}${roleTag}
${escapeHtml(evt.text)}
${errorLink}
`; card.dataset.model = evt.model; card.dataset.role = evt.role || ''; card.dataset.displayName = displayName;