feat: system logs - email log + system log with admin UI
- 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
This commit is contained in:
@@ -74,6 +74,7 @@
|
||||
<script src="js/pages/polls.js"></script>
|
||||
<script src="js/pages/servers.js"></script>
|
||||
<script src="js/pages/bans.js"></script>
|
||||
<script src="js/pages/logs.js"></script>
|
||||
<script src="js/pages/features.js"></script>
|
||||
<script src="js/pages/feature-detail.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
@@ -11,6 +11,7 @@ const App = {
|
||||
{ id: 'notifications', label: '通知配置', icon: 'fa-bell', roles: ['owner'] },
|
||||
{ id: 'templates', label: '邮件模板', icon: 'fa-envelope', roles: ['owner'] },
|
||||
{ id: 'settings', label: '系统设置', icon: 'fa-cog', roles: ['owner'] },
|
||||
{ id: 'logs', label: '系统日志', icon: 'fa-clipboard-list', roles: ['owner','admin'] },
|
||||
{ id: 'export', label: '数据导出', icon: 'fa-download', roles: ['owner'] },
|
||||
],
|
||||
|
||||
@@ -99,6 +100,7 @@ const App = {
|
||||
case 'feature': this.renderMain('更新详情', FeatureDetail, param); break;
|
||||
case 'servers': this.renderMain('服务器管理', ServersPage, param); break;
|
||||
case 'bans': this.renderMain('封禁列表', BansPage, param); break;
|
||||
case 'logs': this.renderMain('系统日志', LogsPage, param); break;
|
||||
default: this.renderMain('控制台', Dashboard, param);
|
||||
}
|
||||
},
|
||||
@@ -178,6 +180,7 @@ window.FeaturesPage = FeaturesPage;
|
||||
window.FeatureDetail = FeatureDetail;
|
||||
window.ServersPage = ServersPage;
|
||||
window.BansPage = BansPage;
|
||||
window.LogsPage = LogsPage;
|
||||
window.LoginPage = LoginPage;
|
||||
window.RegisterPage = RegisterPage;
|
||||
window.HomePage = HomePage;
|
||||
|
||||
86
public/js/pages/logs.js
Normal file
86
public/js/pages/logs.js
Normal file
@@ -0,0 +1,86 @@
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user