diff --git a/config.py b/config.py index 067f862..256b5f3 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.722" +SYSTEM_VERSION = "V10.723" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index a8fcd56..4484fcc 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-06-27 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯;PChome 後台業績匯入韌性已補強;產品定位正名為「PChome 業績成長自動化作戰系統」;外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口、高可見頁面繁中化守門、比價/作戰 UI 工作台化、跨平台來源治理與商品身份 UI 契約已建立,GCP embedding 熔斷延後處理、110 proxy rescue 與 direct host health skip 已建立 -> **適用版本**: V10.722 +> **適用版本**: V10.723 --- @@ -807,3 +807,4 @@ POSTGRES_HOST=momo-db | 2026-06-27 | 設定頁監控來源不得直接外露 crawler 命名 | V10.720 起 `/settings` 的商品監控來源由 API 邊界轉為營運名稱與說明,前端卡片再以 `escapeHtml(monitorText(...))` 顯示;啟停與頻率更新訊息統一使用「監控來源」,不得回傳「爬蟲 XXX 已啟用」這類工程主語。 | | 2026-06-27 | 監控來源 API 不回傳前台不需要的內部設定欄位 | V10.721 起 `/api/crawlers` 的設定頁 response 會移除 `function`、`page_type`、`status`、`paused_date`,只保留前台需要的名稱、說明、啟用狀態、頻率、活動代碼與營運提示。 | | 2026-06-27 | 服務更新監控頁不得以內部工具名當主語 | V10.722 起 `/cicd` 可見文字使用「測試站、正式站、監控圖表、自動化流程、今日更新」;前台模板不得用 `issue.error_log` 判斷顯示診斷資料,也不得顯示 `UAT 狀態`、`PROD 狀態`、`Grafana`、`n8n` 作為主按鈕文字。 | +| 2026-06-27 | 系統事件頁不得顯示或下載原始工程紀錄 | V10.723 起 `/logs` 改為「系統事件紀錄」,前台只顯示事件等級、營運判讀與建議處置;不得以 raw log 變數、舊下載檔名或「系統日誌/下載日誌」作為使用者可見介面。 | diff --git a/templates/components/_navbar.html b/templates/components/_navbar.html index 992738f..471f5c8 100755 --- a/templates/components/_navbar.html +++ b/templates/components/_navbar.html @@ -346,7 +346,7 @@
  • - 系統日誌 + 系統事件
  • diff --git a/templates/logs.html b/templates/logs.html index 6416658..7ea94cd 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -1,6 +1,6 @@ {% extends 'ewoooc_base.html' %} -{% block title %}系統日誌 · EwoooC{% endblock %} +{% block title %}系統事件紀錄 · EwoooC{% endblock %} {% block page_accent %}observability{% endblock %} {% block extra_css %} @@ -10,13 +10,12 @@ {% block content %}
    - -
    {% set stats = [ - ('total', 'fa-list', 'total-lines', '總行數'), - ('error', 'fa-times-circle', 'error-count', '錯誤'), - ('warning', 'fa-exclamation-triangle','warning-count', '注意'), - ('info', 'fa-info-circle', 'info-count', '資訊') + ('total', 'fa-list', 'total-lines', '事件數'), + ('error', 'fa-exclamation-circle', 'error-count', '需處理'), + ('warning', 'fa-exclamation-triangle','warning-count', '需追蹤'), + ('info', 'fa-info-circle', 'info-count', '一般') ] %} {% for kind, icon, vid, label in stats %}
    @@ -44,10 +42,9 @@ {% endfor %}
    -
    -
    主要操作
    +
    事件操作
    - -
    -
    過濾與搜尋
    +
    事件篩選
    {% for level, icon, label in [ ('all', 'fa-list', '全部'), - ('error', 'fa-times-circle', '錯誤'), - ('warning', 'fa-exclamation-triangle','注意'), - ('info', 'fa-info-circle', '資訊') + ('error', 'fa-exclamation-circle', '需處理'), + ('warning', 'fa-exclamation-triangle','需追蹤'), + ('info', 'fa-info-circle', '一般') ] %}
    -
    顯示選項
    +
    閱讀設定
    字體大小: @@ -106,19 +103,18 @@ - 自動滾動 + 自動跟隨最新事件
    -
    - -

    正在載入日誌...

    + +

    正在載入事件紀錄...

    diff --git a/templates/pchome_crawler.html b/templates/pchome_crawler.html index 26dfaea..1f02e34 100644 --- a/templates/pchome_crawler.html +++ b/templates/pchome_crawler.html @@ -152,7 +152,7 @@

    PChome 24h 商品監控

    -

    補齊 PChome 商品、售價、庫存與賣場連結,支援同款確認、價差與促銷監控。

    +

    補齊 PChome 商品資料、售價、庫存與賣場連結,支援同款確認、價差與促銷監控。

    diff --git a/templates/vendor_stockout_import_v2.html b/templates/vendor_stockout_import_v2.html index b663d59..09ff052 100644 --- a/templates/vendor_stockout_import_v2.html +++ b/templates/vendor_stockout_import_v2.html @@ -18,7 +18,7 @@

    Excel 匯入

    - 補齊缺貨清單,先保住主推商品供貨。 + 補齊供貨風險資料,先保住主推商品供貨。

    diff --git a/templates/vendor_stockout_vendor_management_v2.html b/templates/vendor_stockout_vendor_management_v2.html index e2b8d89..65575da 100644 --- a/templates/vendor_stockout_vendor_management_v2.html +++ b/templates/vendor_stockout_vendor_management_v2.html @@ -10,7 +10,7 @@
    供貨風險

    供應商窗口

    -

    維護正確窗口,讓缺貨補救能快速送達。

    +

    維護正確窗口;維護供應商與收件人,讓缺貨補救能快速送達。

    總覽 diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index deb9524..9f8aa0d 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -731,6 +731,9 @@ def test_utility_pages_keep_operator_copy_professional(): observability_labels = (ROOT / "templates/admin/_observability_labels.html").read_text(encoding="utf-8") host_health = (ROOT / "templates/admin/host_health.html").read_text(encoding="utf-8") cicd_dashboard = (ROOT / "templates/cicd_dashboard.html").read_text(encoding="utf-8") + logs_page = (ROOT / "templates/logs.html").read_text(encoding="utf-8") + logs_js = (ROOT / "web/static/js/page-logs.js").read_text(encoding="utf-8") + navbar = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8") observability_js = (ROOT / "web/static/js/observability-charts.js").read_text(encoding="utf-8") budget = (ROOT / "templates/admin/budget.html").read_text(encoding="utf-8") agent_orchestration = (ROOT / "templates/admin/agent_orchestration.html").read_text(encoding="utf-8") @@ -754,6 +757,9 @@ def test_utility_pages_keep_operator_copy_professional(): observability_labels, host_health, cicd_dashboard, + logs_page, + logs_js, + navbar, observability_js, budget, agent_orchestration, @@ -806,6 +812,19 @@ def test_utility_pages_keep_operator_copy_professional(): assert "PROD 狀態" not in cicd_dashboard assert ">Grafana<" not in cicd_dashboard assert ">n8n<" not in cicd_dashboard + assert "系統事件紀錄" in logs_page + assert "事件數" in logs_page + assert "需處理" in logs_page + assert "需追蹤" in logs_page + assert "下載事件摘要" in logs_page + assert "系統事件" in navbar + assert "toEventSummary" in logs_js + assert "buildEventSummaryText" in logs_js + assert "rawLogs" not in logs_js + assert "momo_system_logs_" not in logs_js + assert "系統日誌" not in logs_page + assert "下載日誌" not in logs_page + assert "系統日誌" not in navbar assert "供貨風險" in stockout_index assert "無服務資料 / 未連線" in host_health assert "部署檢查已排入背景處理" in observability_js @@ -861,6 +880,9 @@ def test_utility_pages_keep_operator_copy_professional(): "Commit:", "變更檔案:", "查看系統日誌", + "下載日誌", + "rawLogs", + "momo_system_logs_", "could not locate runnable browser", "daily_sales_snapshot", "realtime_sales_monthly", diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index 71150ba..59ad78b 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -641,7 +641,7 @@ def test_primary_pages_use_growth_outcome_copy_instead_of_feature_explaining(): "templates/edm_dashboard_v2.html": "用活動價格異動找主推、補貨與曝光機會", "templates/sales_analysis.html": "用分類、品牌與毛利找出主推、守價與補資料順序", "templates/growth_analysis.html": "用月趨勢評估成長缺口、價差壓力與毛利品質", - "templates/vendor_stockout_import_v2.html": "補齊缺貨資料,先保住主推商品供貨", + "templates/vendor_stockout_import_v2.html": "補齊供貨風險資料,先保住主推商品供貨", "templates/vendor_stockout_list_v2.html": "先看待發送與失敗,避免主推商品斷貨拖累業績", "templates/vendor_stockout_send_email_v2.html": "先處理失敗通知,讓補貨協調不中斷", "templates/vendor_stockout_vendor_management_v2.html": "維護供應商與收件人,讓缺貨補救能快速送達", @@ -816,7 +816,7 @@ def test_governance_and_low_frequency_pages_avoid_engineering_status_copy(): "templates/vendor_stockout_index_v2.html": ["處理缺貨", "主推商品供貨風險"], "templates/vendor_stockout_vendor_management_v2.html": ["正確窗口"], "templates/login_history.html": ["避免未授權操作影響業績流程", "裝置資訊"], - "templates/logs.html": ["錯誤", "注意", "資訊"], + "templates/logs.html": ["事件數", "需處理", "需追蹤", "一般"], "templates/ai_automation_smoke.html": ["AI 閉環守門", "支援業績流程"], "templates/external_tool_status.html": ["共用預覽"], "templates/market_intel/disabled.html": ["市場情報", "操作入口"], diff --git a/web/static/css/page-logs.css b/web/static/css/page-logs.css index 9b4f8b8..6307997 100644 --- a/web/static/css/page-logs.css +++ b/web/static/css/page-logs.css @@ -1,14 +1,12 @@ /* ═══════════════════════════════════════════════════════════ - * page-logs.css — 系統日誌 - * 從原 logs.html L7-L425 抽出(共 419 行 inline) - * 全部硬編碼色 → token;藍紫漸層 → page-accent;綠紅黃 → tag-* + * page-logs.css — 系統事件紀錄 * ═══════════════════════════════════════════════════════════ */ .page-logs { --logs-control-bg: var(--momo-paper); - --logs-terminal-bg: #1e1e1e; /* 終端機沿用深色(語意需要)*/ - --logs-terminal-text: #d4d4d4; - --logs-terminal-track: #2d2d2d; + --logs-event-bg: var(--momo-paper); + --logs-event-text: var(--momo-ink); + --logs-event-track: var(--momo-shell-border); padding: 24px 0; } @@ -317,7 +315,7 @@ font-weight: 500; } -/* ---------- Log Terminal ---------- */ +/* ---------- Event Summary ---------- */ .page-logs .log-container-wrapper { background: var(--logs-control-bg); border-radius: 8px; @@ -325,14 +323,12 @@ overflow: hidden; } .page-logs #log-container { - background: var(--logs-terminal-bg); - color: var(--logs-terminal-text); - font-family: var(--momo-font-mono, 'Consolas', 'Monaco', 'Courier New', monospace); + background: var(--logs-event-bg); + color: var(--logs-event-text); + font-family: var(--momo-font-sans); padding: 20px; height: 65vh; overflow: auto; - white-space: pre-wrap; - word-wrap: break-word; font-size: 13px; line-height: 1.6; } @@ -341,20 +337,50 @@ .page-logs #log-container.font-large { font-size: 15px; } .page-logs #log-container::-webkit-scrollbar { width: 12px; height: 12px; } -.page-logs #log-container::-webkit-scrollbar-track { background: var(--logs-terminal-track); border-radius: 6px; } +.page-logs #log-container::-webkit-scrollbar-track { background: var(--logs-event-track); border-radius: 6px; } .page-logs #log-container::-webkit-scrollbar-thumb { background: var(--momo-page-accent); border-radius: 6px; } -/* Log lines */ -.page-logs .log-line { padding: 2px 0; border-radius: 3px; } -.page-logs .log-line.error { background: rgba(193, 96, 67, 0.15); border-left: 3px solid var(--momo-tag-clay); padding-left: 8px; margin: 2px 0; } -.page-logs .log-line.warning { background: rgba(201, 162, 89, 0.15); border-left: 3px solid var(--momo-tag-honey); padding-left: 8px; margin: 2px 0; } -.page-logs .log-line.info { border-left: 3px solid var(--momo-tag-sand); padding-left: 8px; margin: 2px 0; } - -.page-logs .log-timestamp { color: #9ec59c; font-weight: 600; } -.page-logs .log-error { color: #e89c8a; font-weight: 600; } -.page-logs .log-warning { color: #e6c98a; font-weight: 600; } -.page-logs .log-info { color: #b5c8d4; font-weight: 600; } -.page-logs .highlight { background: rgba(230, 201, 138, 0.4); padding: 2px 4px; border-radius: 2px; } +.page-logs .log-line { + display: grid; + grid-template-columns: minmax(140px, 0.18fr) minmax(180px, 0.28fr) minmax(0, 1fr); + gap: 14px; + align-items: start; + padding: 14px 16px; + border: 1px solid var(--momo-rule); + border-left-width: 4px; + border-radius: 8px; + margin-bottom: 10px; + background: var(--momo-paper); +} +.page-logs .log-line.error { + border-left-color: var(--momo-tag-clay); + background: color-mix(in srgb, var(--momo-tag-clay-soft) 50%, var(--momo-paper)); +} +.page-logs .log-line.warning { + border-left-color: var(--momo-tag-honey); + background: color-mix(in srgb, var(--momo-tag-honey-soft) 50%, var(--momo-paper)); +} +.page-logs .log-line.info { + border-left-color: var(--momo-tag-sand); +} +.page-logs .log-timestamp { + color: var(--momo-ink-soft); + font-weight: 700; + white-space: nowrap; +} +.page-logs .log-event-title { + color: var(--momo-ink); + font-weight: 700; +} +.page-logs .log-event-action { + color: var(--momo-ink-soft); + font-weight: 500; +} +.page-logs .highlight { + background: var(--momo-tag-honey-soft); + padding: 2px 4px; + border-radius: 2px; +} .page-logs .log-empty { display: flex; @@ -522,6 +548,16 @@ line-height: 1.55; } + .page-logs .log-line { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + padding: 12px; + } + + .page-logs .log-timestamp { + white-space: normal; + } + .page-logs .log-empty i { font-size: 42px; margin-bottom: 12px; diff --git a/web/static/js/page-logs.js b/web/static/js/page-logs.js index 6e49cde..c35f948 100644 --- a/web/static/js/page-logs.js +++ b/web/static/js/page-logs.js @@ -1,7 +1,6 @@ /* ═══════════════════════════════════════════════════════════ - * page-logs.js — 系統日誌 - * 從原 logs.html L645-L865 抽出,邏輯不動 - * 僅將 active class 名稱統一為 is-active(隨 CSS 對齊) + * page-logs.js — 系統事件紀錄 + * 將伺服器紀錄轉成營運可讀的事件摘要,避免前台暴露工程細節。 * ═══════════════════════════════════════════════════════════ */ let autoRefreshEnabled = true; @@ -9,10 +8,10 @@ let autoScrollEnabled = true; let currentFilter = 'all'; let searchKeyword = ''; let refreshInterval = null; -let rawLogs = ''; +let eventLogText = ''; document.addEventListener('DOMContentLoaded', function () { - if (typeof showLoading === 'function') showLoading('正在載入系統日誌...', '請稍候'); + if (typeof showLoading === 'function') showLoading('正在載入系統事件...', '請稍候'); fetchLogs(); startAutoRefresh(); }); @@ -24,61 +23,191 @@ function updateLastUpdateTime() { function fetchLogs() { fetch('/api/logs') - .then(r => r.json()) + .then(response => response.json()) .then(data => { - rawLogs = data.logs || ''; - updateStats(rawLogs); + eventLogText = normalizeLogText(data.logs); + updateStats(eventLogText); displayLogs(); updateLastUpdateTime(); - document.getElementById('connection-status').textContent = '連接正常'; + document.getElementById('connection-status').textContent = '資料連線正常'; if (typeof hideLoading === 'function') hideLoading(); }) - .catch(e => { - console.error('Error fetching logs:', e); - document.getElementById('connection-status').textContent = '連接失敗'; - if (typeof showToast === 'function') showToast('載入日誌失敗', 'error'); + .catch(error => { + console.error('載入事件紀錄失敗:', error); + document.getElementById('connection-status').textContent = '資料連線異常'; + if (typeof showToast === 'function') showToast('載入事件紀錄失敗', 'error'); if (typeof hideLoading === 'function') hideLoading(); }); } +function normalizeLogText(value) { + if (Array.isArray(value)) return value.join('\n'); + return String(value || ''); +} + function displayLogs() { const container = document.getElementById('log-container'); - if (!rawLogs || !rawLogs.trim()) { - container.innerHTML = '

    暫無日誌資料

    '; + const rows = getVisibleEvents(); + + if (!eventLogText.trim()) { + container.innerHTML = '

    暫無事件紀錄

    '; return; } - let lines = rawLogs.split('\n'); - if (currentFilter !== 'all') { - lines = lines.filter(line => { - const u = line.toUpperCase(); - if (currentFilter === 'error') return u.includes('ERROR'); - if (currentFilter === 'warning') return u.includes('WARNING'); - if (currentFilter === 'info') return u.includes('INFO'); - return true; - }); - } - if (searchKeyword) { - lines = lines.filter(line => line.toLowerCase().includes(searchKeyword.toLowerCase())); - } - const html = lines.map(formatLogLine).join('\n'); - container.innerHTML = html || '

    沒有符合的日誌

    '; + + container.innerHTML = rows.length + ? rows.map(renderEventRow).join('') + : '

    沒有符合的事件

    '; + if (autoScrollEnabled) container.scrollTop = container.scrollHeight; } -function formatLogLine(line) { - if (!line.trim()) return ''; - let cls = ''; - let f = escapeLogText(line); - const u = line.toUpperCase(); - if (u.includes('ERROR')) { cls = 'error'; f = f.replace(/ERROR/gi, 'ERROR'); } - else if (u.includes('WARNING')) { cls = 'warning'; f = f.replace(/WARNING/gi, 'WARNING'); } - else if (u.includes('INFO')) { cls = 'info'; f = f.replace(/INFO/gi, 'INFO'); } - f = f.replace(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/g, '$1'); - if (searchKeyword) { - const re = new RegExp(`(${searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); - f = f.replace(re, '$1'); +function getVisibleEvents() { + return eventLogText + .split('\n') + .map(toEventSummary) + .filter(Boolean) + .filter(eventItem => currentFilter === 'all' || eventItem.level === currentFilter) + .filter(eventItem => { + if (!searchKeyword) return true; + return eventItem.searchText.includes(searchKeyword.toLowerCase()); + }); +} + +function toEventSummary(line) { + const source = String(line || '').trim(); + if (!source) return null; + + const level = detectEventLevel(source); + const eventInfo = classifyEvent(source, level); + const timestamp = extractTimestamp(source); + + return { + level, + timestamp, + title: eventInfo.title, + action: eventInfo.action, + searchText: `${timestamp} ${eventInfo.title} ${eventInfo.action} ${source}`.toLowerCase(), + }; +} + +function classifyEvent(source, level) { + const lower = source.toLowerCase(); + + if (lower.includes('runnable') && lower.includes('browser')) { + return { + title: '雲端資料授權流程需確認', + action: '背景排程需使用已完成授權的憑證,請確認自動匯入授權檔是否已掛載並可續期。', + }; } - return `
    ${f}
    `; + + if (lower.includes('google') || lower.includes('drive') || lower.includes('授權') || lower.includes('credential') || lower.includes('token')) { + return { + title: '雲端資料連線需確認', + action: '請確認 Google Drive 授權、來源資料夾與當日業績檔是否可由排程服務讀取。', + }; + } + + if (lower.includes('database') || lower.includes('資料庫') || lower.includes('postgres') || lower.includes('sqlalchemy') || lower.includes('psycopg')) { + return { + title: '資料服務連線需確認', + action: '請先確認資料服務健康狀態,再重試匯入、分析或商品比價流程。', + }; + } + + if (lower.includes('import') || lower.includes('匯入') || lower.includes('excel') || lower.includes('csv') || lower.includes('業績')) { + return { + title: '業績資料匯入事件', + action: '請確認來源檔案、欄位內容與最近一次匯入結果,避免今日分析使用過期資料。', + }; + } + + if (lower.includes('crawler') || lower.includes('監控') || lower.includes('pchome') || lower.includes('momo') || lower.includes('price') || lower.includes('商品')) { + return { + title: '商品監控資料事件', + action: '請確認商品連結、價格資訊與候選比對資料是否已更新,必要時重新整理監控來源。', + }; + } + + if (lower.includes('scheduler') || lower.includes('排程') || lower.includes('job')) { + return { + title: '排程任務事件', + action: '請確認下一輪排程是否正常執行;若連續失敗,優先檢查資料來源與服務健康。', + }; + } + + if (lower.includes('telegram') || lower.includes('通知')) { + return { + title: '通知服務事件', + action: '請確認通知是否成功送達,必要時改用平台內事件紀錄追蹤處理狀態。', + }; + } + + if (level === 'error') { + return { + title: '系統流程需要處理', + action: '請先確認最近匯入、排程與服務健康狀態,必要時安排人工補救。', + }; + } + + if (level === 'warning') { + return { + title: '系統提醒需追蹤', + action: '請觀察下一輪是否恢復;若重複出現,加入今日營運處理清單。', + }; + } + + return { + title: '系統事件已記錄', + action: '目前不需立即處理,保留作為營運流程追蹤依據。', + }; +} + +function detectEventLevel(line) { + const upper = line.toUpperCase(); + const lower = line.toLowerCase(); + if ( + upper.includes('ERROR') || + upper.includes('CRITICAL') || + lower.includes('失敗') || + lower.includes('異常') || + lower.includes('exception') || + lower.includes('traceback') + ) { + return 'error'; + } + if ( + upper.includes('WARNING') || + upper.includes('WARN') || + lower.includes('注意') || + lower.includes('待確認') || + lower.includes('retry') + ) { + return 'warning'; + } + return 'info'; +} + +function extractTimestamp(line) { + const match = String(line || '').match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})(?:,\d+)?/); + return match ? match[1] : '最新事件'; +} + +function renderEventRow(eventItem) { + return ` +
    + ${highlightText(eventItem.timestamp)} + ${highlightText(eventItem.title)} + ${highlightText(eventItem.action)} +
    + `; +} + +function highlightText(value) { + const safe = escapeLogText(value); + if (!searchKeyword) return safe; + const safeKeyword = escapeLogText(searchKeyword); + const re = new RegExp(`(${escapeRegExp(safeKeyword)})`, 'gi'); + return safe.replace(re, '$1'); } function escapeLogText(value) { @@ -90,23 +219,34 @@ function escapeLogText(value) { .replace(/'/g, '''); } +function escapeRegExp(value) { + return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + function updateStats(logs) { - const lines = logs.split('\n').filter(l => l.trim()); - document.getElementById('total-lines').textContent = lines.length; - document.getElementById('error-count').textContent = lines.filter(l => l.toUpperCase().includes('ERROR')).length; - document.getElementById('warning-count').textContent = lines.filter(l => l.toUpperCase().includes('WARNING')).length; - document.getElementById('info-count').textContent = lines.filter(l => l.toUpperCase().includes('INFO')).length; + const events = normalizeLogText(logs) + .split('\n') + .map(line => line.trim()) + .filter(Boolean); + document.getElementById('total-lines').textContent = events.length; + document.getElementById('error-count').textContent = events.filter(line => detectEventLevel(line) === 'error').length; + document.getElementById('warning-count').textContent = events.filter(line => detectEventLevel(line) === 'warning').length; + document.getElementById('info-count').textContent = events.filter(line => detectEventLevel(line) === 'info').length; } function refreshLogs() { - const btn = event.target.closest('.btn-control'); - btn.classList.add('spinning'); - btn.disabled = true; + const btn = document.querySelector('.btn-control--refresh'); + if (btn) { + btn.classList.add('spinning'); + btn.disabled = true; + } fetchLogs(); setTimeout(() => { - btn.classList.remove('spinning'); - btn.disabled = false; - if (typeof showToast === 'function') showToast('日誌已刷新', 'success'); + if (btn) { + btn.classList.remove('spinning'); + btn.disabled = false; + } + if (typeof showToast === 'function') showToast('事件紀錄已更新', 'success'); }, 600); } @@ -130,37 +270,57 @@ function startAutoRefresh() { if (refreshInterval) clearInterval(refreshInterval); refreshInterval = setInterval(fetchLogs, 5000); } + function stopAutoRefresh() { - if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; } + if (refreshInterval) { + clearInterval(refreshInterval); + refreshInterval = null; + } } function clearLogs() { - if (!confirm('確定要清除日誌顯示嗎?(不會刪除實際日誌檔案)')) return; - rawLogs = ''; + if (!confirm('確定要清除畫面上的事件紀錄嗎?(不會刪除正式紀錄)')) return; + eventLogText = ''; updateStats(''); displayLogs(); - if (typeof showToast === 'function') showToast('已清除日誌顯示', 'success'); + if (typeof showToast === 'function') showToast('已清除畫面事件', 'success'); } function downloadLogs() { - if (!rawLogs) { if (typeof showToast === 'function') showToast('沒有可下載的日誌', 'error'); return; } - const blob = new Blob([rawLogs], { type: 'text/plain' }); + const summary = buildEventSummaryText(); + if (!summary) { + if (typeof showToast === 'function') showToast('沒有可下載的事件摘要', 'error'); + return; + } + const blob = new Blob([summary], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); - a.download = `momo_system_logs_${ts}.txt`; + a.download = `momo_system_events_${ts}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - if (typeof showToast === 'function') showToast('日誌已下載', 'success'); + if (typeof showToast === 'function') showToast('事件摘要已下載', 'success'); +} + +function buildEventSummaryText() { + const rows = eventLogText + .split('\n') + .map(toEventSummary) + .filter(Boolean); + if (!rows.length) return ''; + return rows.map(eventItem => { + const label = { error: '需處理', warning: '需追蹤', info: '一般' }[eventItem.level] || '事件'; + return `[${label}] ${eventItem.timestamp} ${eventItem.title} - ${eventItem.action}`; + }).join('\n'); } function filterByLevel(level) { currentFilter = level; - document.querySelectorAll('.btn-filter').forEach(b => { - b.classList.toggle('is-active', b.dataset.level === level); + document.querySelectorAll('.btn-filter').forEach(button => { + button.classList.toggle('is-active', button.dataset.level === level); }); displayLogs(); } @@ -183,18 +343,18 @@ function clearSearch() { } function changeFontSize(size) { - const c = document.getElementById('log-container'); - c.classList.remove('font-small', 'font-medium', 'font-large'); - c.classList.add(`font-${size}`); - document.querySelectorAll('.btn-font-size').forEach(b => { - b.classList.toggle('is-active', b.dataset.size === size); + const container = document.getElementById('log-container'); + container.classList.remove('font-small', 'font-medium', 'font-large'); + container.classList.add(`font-${size}`); + document.querySelectorAll('.btn-font-size').forEach(button => { + button.classList.toggle('is-active', button.dataset.size === size); }); } function toggleAutoScroll() { autoScrollEnabled = document.getElementById('auto-scroll-toggle').checked; if (autoScrollEnabled) { - const c = document.getElementById('log-container'); - c.scrollTop = c.scrollHeight; + const container = document.getElementById('log-container'); + container.scrollTop = container.scrollHeight; } }