- db: email_logs (sent/failed) + system_logs (info/warn/error) tables, auto-migrated for existing installs - backend/logger.js: logEmail/logSystem with try/catch (never breaks flow) - mailer: log send result (SMTP unconfigured/sent/failed + error msg) - webhook: log blocked-internal, non-2xx response, send failure - server: error middleware records 500 errors to system_logs - routes/logs.js: GET /logs/emails + /logs/system (admin/owner, filters) - UI: 系统日志 page (owner/admin) with system/email tabs + filters - API docs updated
87 lines
4.2 KiB
JavaScript
87 lines
4.2 KiB
JavaScript
const LogsPage = {
|
|
tab: 'system',
|
|
|
|
async render() { return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
|
|
|
|
async mount() {
|
|
document.getElementById('page-title').textContent = '系统日志';
|
|
document.getElementById('page-actions').innerHTML = `<button class="btn btn-o btn-sm" onclick="LogsPage.refresh()"><i class="fas fa-rotate"></i> 刷新</button>`;
|
|
const ct = document.getElementById('page-content');
|
|
ct.innerHTML = `
|
|
<div class="tab-nav" id="logs-tabs">
|
|
<button data-tab="system" class="active"><i class="fas fa-server"></i> 系统日志</button>
|
|
<button data-tab="email"><i class="fas fa-envelope"></i> 邮件日志</button>
|
|
</div>
|
|
<div class="filter-bar">
|
|
<select id="lg-level" onchange="LogsPage.applyFilter()">
|
|
<option value="">全部级别</option><option value="info">信息</option><option value="warn">警告</option><option value="error">错误</option>
|
|
</select>
|
|
<input id="lg-source" placeholder="来源关键字" onkeyup="if(event.key==='Enter')LogsPage.applyFilter()">
|
|
<button class="btn btn-o btn-sm" onclick="LogsPage.applyFilter()"><i class="fas fa-filter"></i></button>
|
|
</div>
|
|
<div id="logs-list"></div>
|
|
`;
|
|
document.querySelectorAll('#logs-tabs button').forEach(b => {
|
|
b.onclick = () => {
|
|
document.querySelectorAll('#logs-tabs button').forEach(x => x.classList.remove('active'));
|
|
b.classList.add('active');
|
|
this.tab = b.dataset.tab;
|
|
this.load();
|
|
};
|
|
});
|
|
await this.load();
|
|
},
|
|
|
|
refresh() { this.load(); },
|
|
|
|
async applyFilter() {
|
|
this.load();
|
|
},
|
|
|
|
async load() {
|
|
const list = document.getElementById('logs-list');
|
|
U.loading(list);
|
|
const level = document.getElementById('lg-level')?.value || '';
|
|
const src = document.getElementById('lg-source')?.value.trim() || '';
|
|
const isEmail = this.tab === 'email';
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (level) params.set('level', level);
|
|
if (src) params.set(isEmail ? 'email' : 'source', src);
|
|
const qs = params.toString();
|
|
const rows = await API.get('/logs/' + (isEmail ? 'emails' : 'system') + (qs ? '?' + qs : ''));
|
|
this.renderList(list, rows, isEmail);
|
|
} catch (e) { list.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
|
|
},
|
|
|
|
renderList(ct, rows, isEmail) {
|
|
if (isEmail) {
|
|
ct.innerHTML = rows.length === 0
|
|
? '<div class="empty"><i class="fas fa-envelope-open"></i><p>暂无邮件日志</p></div>'
|
|
: `<div class="card table-wrap"><table><thead><tr><th>#</th><th>收件人</th><th>模板</th><th>主题</th><th>状态</th><th>错误</th><th>时间</th></tr></thead><tbody>${rows.map(r => `
|
|
<tr>
|
|
<td>${r.id}</td>
|
|
<td>${U.esc(r.to_email)}</td>
|
|
<td><span class="badge bg-admin">${U.esc(r.template_code||'-')}</span></td>
|
|
<td class="ts">${U.esc(r.subject||'')}</td>
|
|
<td>${r.status==='sent'?'<span class="badge bg-resolved">成功</span>':'<span class="badge bg-rejected">失败</span>'}</td>
|
|
<td class="ts" style="max-width:260px;word-break:break-all">${U.esc(r.error||'-')}</td>
|
|
<td class="ts">${U.date(r.created_at)}</td>
|
|
</tr>`).join('')}</tbody></table></div>`;
|
|
} else {
|
|
const lv = { info:'<span class="badge bg-resolved">信息</span>', warn:'<span class="badge bg-medium">警告</span>', error:'<span class="badge bg-rejected">错误</span>' };
|
|
ct.innerHTML = rows.length === 0
|
|
? '<div class="empty"><i class="fas fa-clipboard"></i><p>暂无系统日志</p></div>'
|
|
: `<div class="card table-wrap"><table><thead><tr><th>#</th><th>级别</th><th>来源</th><th>消息</th><th>详情</th><th>时间</th></tr></thead><tbody>${rows.map(r => `
|
|
<tr>
|
|
<td>${r.id}</td>
|
|
<td>${lv[r.level]||r.level}</td>
|
|
<td><span class="badge bg-admin">${U.esc(r.source)}</span></td>
|
|
<td>${U.esc(r.message)}</td>
|
|
<td class="ts" style="max-width:280px;word-break:break-all">${U.esc(r.details||'-')}</td>
|
|
<td class="ts">${U.date(r.created_at)}</td>
|
|
</tr>`).join('')}</tbody></table></div>`;
|
|
}
|
|
}
|
|
};
|