From 85547612fe5faa8220ea0c6fa129389dc576717f Mon Sep 17 00:00:00 2001 From: bergeouss Date: Mon, 11 May 2026 15:40:49 +0000 Subject: [PATCH] fix(logs): clipboard fallback + severity filter for Logs panel (#2081) - replace navigator.clipboard.writeText with _copyText (has textarea fallback) - add severity filter dropdown (All / Errors / Warnings+) - add _severityForLine and _filteredLogsLines helpers - add logsSeverityFilter HTML element + CSS class hooks - add 5 new i18n keys across all 8 locales - update test_logs_ui_static.py to match new implementation Closes #2081 --- static/i18n.js | 40 +++++++++++++++++++++++++++++ static/index.html | 6 +++++ static/panels.js | 49 ++++++++++++++++++++++++++++++------ tests/test_logs_ui_static.py | 4 ++- 4 files changed, 90 insertions(+), 9 deletions(-) diff --git a/static/i18n.js b/static/i18n.js index 20820631..207418f2 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -645,6 +645,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', logs_copied: 'Logs copied', + logs_severity: 'Severity', + logs_severity_all: 'All', + logs_severity_errors: 'Errors', + logs_severity_warnings: 'Warnings+', + logs_filter_active: 'shown (filter active)', // Insights insights_title: 'Usage Analytics', @@ -1735,6 +1740,11 @@ const LOCALES = { logs_no_mtime: '未書き込み', logs_truncated_hint: '大きなログファイルの末尾を表示しています。メモリ使用量を抑えるため、古いデータは省略されました。', logs_copied: 'ログをコピーしました', + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate // Insights insights_title: '使用状況分析', @@ -2639,6 +2649,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate new_conversation: 'Новая беседа', filter_conversations: 'Фильтр бесед...', session_time_unknown: 'Неизвестно', @@ -3663,6 +3678,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate new_conversation: 'Nueva conversación', filter_conversations: 'Filtrar conversaciones...', session_time_unknown: 'Desconocido', @@ -4670,6 +4690,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate new_conversation: 'Neuer Chat', filter_conversations: 'Chats filtern...', scheduled_jobs: 'Geplante Aufgaben', @@ -5710,6 +5735,11 @@ const LOCALES = { logs_no_mtime: '尚未写入', logs_truncated_hint: '此处显示的是日志文件的末尾内容。为节省内存,已省略较早的数据。', logs_copied: '日志已复制', + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate new_conversation: '新建对话', filter_conversations: '筛选对话…', session_time_unknown: '未知', @@ -7902,6 +7932,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate new_conversation: 'Nova conversa', filter_conversations: 'Filtrar conversas...', session_time_unknown: 'Desconhecido', @@ -8890,6 +8925,11 @@ const LOCALES = { logs_no_mtime: 'not written yet', // TODO: translate logs_truncated_hint: 'Showing the tail of a large log file; older bytes were skipped to keep memory bounded.', // TODO: translate logs_copied: 'Logs copied', // TODO: translate + logs_severity: 'Severity', // TODO: translate + logs_severity_all: 'All', // TODO: translate + logs_severity_errors: 'Errors', // TODO: translate + logs_severity_warnings: 'Warnings+', // TODO: translate + logs_filter_active: 'shown (filter active)', // TODO: translate new_conversation: '새 대화', filter_conversations: '대화 필터…', session_time_unknown: 'Unknown', diff --git a/static/index.html b/static/index.html index 2fd68911..714f8564 100644 --- a/static/index.html +++ b/static/index.html @@ -257,6 +257,12 @@ + + diff --git a/static/panels.js b/static/panels.js index d2517582..88d0b0b4 100644 --- a/static/panels.js +++ b/static/panels.js @@ -32,6 +32,7 @@ let _profilePreFormDetail = null; let _pendingSettingsTargetPanel = null; // destination selected while settings had unsaved changes let _logsAutoRefreshTimer = null; let _lastLogsLines = []; +let _logsSeverityFilter = 'all'; // Map of panel names → i18n keys for the app titlebar label. const APP_TITLEBAR_KEYS = { @@ -2663,6 +2664,32 @@ function _selectedLogsTail() { return [100,200,500,1000].includes(value) ? value : 200; } +function _severityForLine(line) { + const text = String(line || '').toUpperCase(); + if (/\b(ERROR|CRITICAL|TRACEBACK)\b/.test(text)) return 'error'; + if (/\b(WARNING|WARN)\b/.test(text)) return 'warning'; + if (/\b(DEBUG)\b/.test(text)) return 'debug'; + if (/\b(INFO)\b/.test(text)) return 'info'; + return 'other'; +} + +function _filteredLogsLines() { + if (_logsSeverityFilter === 'all') return _lastLogsLines; + return _lastLogsLines.filter(line => { + const sev = _severityForLine(line); + if (_logsSeverityFilter === 'errors') return sev === 'error'; + if (_logsSeverityFilter === 'warnings') return sev === 'warning' || sev === 'error'; + return true; + }); +} + +function _applyLogsSeverityFilter() { + const el = $('logsSeverityFilter'); + _logsSeverityFilter = (el && el.value) || 'all'; + // Re-render from cached lines without re-fetching + _renderLogs({ lines: _lastLogsLines, hint: '', truncated: false, _fromFilter: true }); +} + function _logLineSeverityClass(line) { const text = String(line || '').toUpperCase(); if (/\b(WARNING|WARN)\b/.test(text)) return 'log-line-warning'; @@ -2710,14 +2737,19 @@ function _renderLogs(data) { const box = $('logsOutput'); const status = $('logsStatus'); if (!box) return; - const lines = Array.isArray(data && data.lines) ? data.lines : []; - _lastLogsLines = lines.slice(); + const rawLines = Array.isArray(data && data.lines) ? data.lines : []; + // Only update cache when loading fresh data (not when re-rendering from filter) + if (data && !data._fromFilter) _lastLogsLines = rawLines.slice(); + const displayLines = _filteredLogsLines(); const hint = data && data.hint ? `
${esc(data.hint)}
` : ''; const truncated = data && data.truncated ? `
${esc(t('logs_truncated_hint'))}
` : ''; - if (!lines.length) { - box.innerHTML = `${hint}${truncated}
${esc(t('logs_empty'))}
`; + const filterNote = _logsSeverityFilter !== 'all' + ? `
${esc(displayLines.length + ' / ' + _lastLogsLines.length + ' ' + t('logs_filter_active'))}
` + : ''; + if (!displayLines.length) { + box.innerHTML = `${hint}${truncated}${filterNote}
${esc(t('logs_empty'))}
`; } else { - box.innerHTML = `${hint}${truncated}` + lines.map(line => { + box.innerHTML = `${hint}${truncated}${filterNote}` + displayLines.map(line => { const cls = _logLineSeverityClass(line); return `
${esc(line)}
`; }).join(''); @@ -2726,7 +2758,7 @@ function _renderLogs(data) { if (status) { const bytes = data && Number(data.total_bytes || 0); const when = data && data.mtime ? new Date(data.mtime * 1000).toLocaleString() : t('logs_no_mtime'); - status.textContent = `${lines.length} / ${data.tail || _selectedLogsTail()} lines · ${bytes.toLocaleString()} bytes · ${when}`; + status.textContent = `${rawLines.length} / ${data.tail || _selectedLogsTail()} lines · ${bytes.toLocaleString()} bytes · ${when}`; } } @@ -2754,9 +2786,10 @@ function _syncLogsAutoRefresh() { } async function copyLogsAll() { - const text = _lastLogsLines.join('\n'); + const lines = _filteredLogsLines(); + const text = lines.join('\n'); try { - await navigator.clipboard.writeText(text); + await _copyText(text); showToast(t('logs_copied')); } catch(e) { showToast(t('copy_failed'), 'error'); diff --git a/tests/test_logs_ui_static.py b/tests/test_logs_ui_static.py index 1cb00791..00e28953 100644 --- a/tests/test_logs_ui_static.py +++ b/tests/test_logs_ui_static.py @@ -93,7 +93,9 @@ def test_logs_panel_fetches_allowlisted_api_and_exposes_controls(): assert "logsWrap" in INDEX assert "logsCopyAll" in INDEX assert "logsAutoRefresh" in INDEX - assert "navigator.clipboard.writeText" in PANELS + assert "logsSeverityFilter" in INDEX + copy_fn = _function_body(PANELS, "copyLogsAll") + assert "_copyText" in copy_fn assert "logs-copy" in INDEX