Files
ewoooc/web/static/js/page-logs.js
ogt 9425e8f05a
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
fix: present system logs as event summaries
2026-06-27 20:08:43 +08:00

361 lines
12 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════
* 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 = '<div class="log-empty"><i class="fas fa-clipboard-list"></i><p>暫無事件紀錄</p></div>';
return;
}
container.innerHTML = rows.length
? rows.map(renderEventRow).join('')
: '<div class="log-empty"><i class="fas fa-search"></i><p>沒有符合的事件</p></div>';
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 `
<div class="log-line ${eventItem.level}">
<span class="log-timestamp">${highlightText(eventItem.timestamp)}</span>
<strong class="log-event-title">${highlightText(eventItem.title)}</strong>
<span class="log-event-action">${highlightText(eventItem.action)}</span>
</div>
`;
}
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, '<span class="highlight">$1</span>');
}
function escapeLogText(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 = '<i class="fas fa-pause"></i><span>暫停自動刷新</span>';
startAutoRefresh();
if (typeof showToast === 'function') showToast('已啟用自動刷新', 'success');
} else {
btn.className = 'btn-control btn-control--resume';
btn.innerHTML = '<i class="fas fa-play"></i><span>繼續自動刷新</span>';
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;
}
}