192 lines
7.2 KiB
JavaScript
192 lines
7.2 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════
|
||
* page-logs.js — 系統日誌
|
||
* 從原 logs.html L645-L865 抽出,邏輯不動
|
||
* 僅將 active class 名稱統一為 is-active(隨 CSS 對齊)
|
||
* ═══════════════════════════════════════════════════════════ */
|
||
|
||
let autoRefreshEnabled = true;
|
||
let autoScrollEnabled = true;
|
||
let currentFilter = 'all';
|
||
let searchKeyword = '';
|
||
let refreshInterval = null;
|
||
let rawLogs = '';
|
||
|
||
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(r => r.json())
|
||
.then(data => {
|
||
rawLogs = data.logs || '';
|
||
updateStats(rawLogs);
|
||
displayLogs();
|
||
updateLastUpdateTime();
|
||
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');
|
||
if (typeof hideLoading === 'function') hideLoading();
|
||
});
|
||
}
|
||
|
||
function displayLogs() {
|
||
const container = document.getElementById('log-container');
|
||
if (!rawLogs || !rawLogs.trim()) {
|
||
container.innerHTML = '<div class="log-empty"><i class="fas fa-file-alt"></i><p>暫無日誌資料</p></div>';
|
||
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 || '<div class="log-empty"><i class="fas fa-search"></i><p>沒有符合的日誌</p></div>';
|
||
if (autoScrollEnabled) container.scrollTop = container.scrollHeight;
|
||
}
|
||
|
||
function formatLogLine(line) {
|
||
if (!line.trim()) return '';
|
||
let cls = '';
|
||
let f = line;
|
||
const u = line.toUpperCase();
|
||
if (u.includes('ERROR')) { cls = 'error'; f = line.replace(/ERROR/gi, '<span class="log-error">ERROR</span>'); }
|
||
else if (u.includes('WARNING')) { cls = 'warning'; f = line.replace(/WARNING/gi, '<span class="log-warning">WARNING</span>'); }
|
||
else if (u.includes('INFO')) { cls = 'info'; f = line.replace(/INFO/gi, '<span class="log-info">INFO</span>'); }
|
||
f = f.replace(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/g, '<span class="log-timestamp">$1</span>');
|
||
if (searchKeyword) {
|
||
const re = new RegExp(`(${searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||
f = f.replace(re, '<span class="highlight">$1</span>');
|
||
}
|
||
return `<div class="log-line ${cls}">${f}</div>`;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
function refreshLogs() {
|
||
const btn = event.target.closest('.btn-control');
|
||
btn.classList.add('spinning');
|
||
btn.disabled = true;
|
||
fetchLogs();
|
||
setTimeout(() => {
|
||
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;
|
||
rawLogs = '';
|
||
updateStats('');
|
||
displayLogs();
|
||
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 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`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
if (typeof showToast === 'function') showToast('日誌已下載', 'success');
|
||
}
|
||
|
||
function filterByLevel(level) {
|
||
currentFilter = level;
|
||
document.querySelectorAll('.btn-filter').forEach(b => {
|
||
b.classList.toggle('is-active', b.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 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);
|
||
});
|
||
}
|
||
|
||
function toggleAutoScroll() {
|
||
autoScrollEnabled = document.getElementById('auto-scroll-toggle').checked;
|
||
if (autoScrollEnabled) {
|
||
const c = document.getElementById('log-container');
|
||
c.scrollTop = c.scrollHeight;
|
||
}
|
||
}
|