/* ═══════════════════════════════════════════════════════════ * page-logs.js — 系統事件紀錄 * 將伺服器紀錄轉成營運可讀的事件摘要,避免前台暴露工程細節。 * ═══════════════════════════════════════════════════════════ */ let autoRefreshEnabled = true; let autoScrollEnabled = true; let currentFilter = 'all'; let searchKeyword = ''; let refreshInterval = null; let eventLogText = ''; document.addEventListener('DOMContentLoaded', function () { if (typeof showLoading === 'function') showLoading('正在載入系統事件...', '請稍候'); fetchLogs(); startAutoRefresh(); }); function updateLastUpdateTime() { const now = new Date(); document.getElementById('last-update').textContent = `最後更新: ${now.toLocaleTimeString('zh-TW')}`; } function fetchLogs() { fetch('/api/logs') .then(response => response.json()) .then(data => { eventLogText = normalizeLogText(data.logs); updateStats(eventLogText); displayLogs(); updateLastUpdateTime(); document.getElementById('connection-status').textContent = '資料連線正常'; if (typeof hideLoading === 'function') hideLoading(); }) .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'); const rows = getVisibleEvents(); if (!eventLogText.trim()) { container.innerHTML = '

暫無事件紀錄

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

沒有符合的事件

'; if (autoScrollEnabled) container.scrollTop = container.scrollHeight; } 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: '背景排程需使用已完成授權的憑證,請確認自動匯入授權檔是否已掛載並可續期。', }; } 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) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeRegExp(value) { return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function updateStats(logs) { 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 = document.querySelector('.btn-control--refresh'); if (btn) { btn.classList.add('spinning'); btn.disabled = true; } fetchLogs(); setTimeout(() => { if (btn) { btn.classList.remove('spinning'); btn.disabled = false; } if (typeof showToast === 'function') showToast('事件紀錄已更新', 'success'); }, 600); } function toggleAutoRefresh() { autoRefreshEnabled = !autoRefreshEnabled; const btn = document.getElementById('pause-btn'); if (autoRefreshEnabled) { btn.className = 'btn-control btn-control--pause'; btn.innerHTML = '暫停自動刷新'; startAutoRefresh(); if (typeof showToast === 'function') showToast('已啟用自動刷新', 'success'); } else { btn.className = 'btn-control btn-control--resume'; btn.innerHTML = '繼續自動刷新'; stopAutoRefresh(); if (typeof showToast === 'function') showToast('已暫停自動刷新', 'info'); } } function startAutoRefresh() { if (refreshInterval) clearInterval(refreshInterval); refreshInterval = setInterval(fetchLogs, 5000); } function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; } } function clearLogs() { if (!confirm('確定要清除畫面上的事件紀錄嗎?(不會刪除正式紀錄)')) return; eventLogText = ''; updateStats(''); displayLogs(); if (typeof showToast === 'function') showToast('已清除畫面事件', 'success'); } function downloadLogs() { 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_events_${ts}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); 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(button => { button.classList.toggle('is-active', button.dataset.level === level); }); displayLogs(); } function searchLogs() { const input = document.getElementById('search-input'); const box = document.getElementById('search-box'); searchKeyword = input.value.trim(); box.classList.toggle('has-text', !!searchKeyword); displayLogs(); } function clearSearch() { const input = document.getElementById('search-input'); input.value = ''; searchKeyword = ''; document.getElementById('search-box').classList.remove('has-text'); displayLogs(); input.focus(); } function changeFontSize(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 container = document.getElementById('log-container'); container.scrollTop = container.scrollHeight; } }