Files
MeshSate/app/templates/diagnostics.html
zhaolei 16f134e328 feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交
核心功能:
- 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制
- FastAPI + PostgreSQL + Redis + Docker Compose 架构
- 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性
- 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页)
- 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min)

并发与稳定性修复:
- 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行)
- JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写
- 移除 SQLite fallback, 统一 PostgreSQL
- API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
2026-10-28 19:07:11 +08:00

416 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<link rel="icon" href="/static/logo.png">
<title>MeshSafe · 诊断面板</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
margin: 0; background: #f4f6fa; color: #1e293b; }
{% include "_nav_style.html" %}
/* 诊断面板专用样式 */
.diag-wrap { padding: 28px 40px; max-width: 1180px; margin: 0 auto; }
.diag-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 24px;
}
.diag-header h1 { font-size: 20px; font-weight: 700; color: #0f172a; }
.diag-header .meta { font-size: 12.5px; color: #64748b; }
/* 统计卡 */
.diag-stats {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
margin-bottom: 24px;
}
.diag-stat {
background: white; border: 1px solid #e2e8f0; border-radius: 12px;
padding: 18px 20px;
}
.diag-stat .label { font-size: 12px; color: #64748b; margin-bottom: 4px; }
.diag-stat .val { font-size: 26px; font-weight: 700; color: #0f172a; }
.diag-stat .val.crit { color: #b91c1c; }
.diag-stat .val.warn { color: #b45309; }
.diag-stat .val.ok { color: #15803d; }
/* 区块 */
.diag-block {
background: white; border: 1px solid #e2e8f0; border-radius: 12px;
margin-bottom: 20px;
}
.diag-block h2 {
font-size: 14px; font-weight: 600; color: #0f172a;
padding: 14px 20px; border-bottom: 1px solid #f1f5f9;
}
/* 规则列表 */
.rule-item {
padding: 12px 20px; border-bottom: 1px solid #f8fafc;
display: flex; align-items: flex-start; gap: 12px;
}
.rule-item:last-child { border-bottom: none; }
.rule-item .rule-id {
font-family: monospace; font-size: 12px; color: #94a3b8;
min-width: 44px; flex-shrink: 0;
}
.rule-item .rule-body { flex: 1; min-width: 0; }
.rule-item .rule-msg {
font-size: 13.5px; color: #0f172a; margin-bottom: 2px;
}
.rule-item .rule-hint {
font-size: 12px; color: #64748b; line-height: 1.5;
white-space: pre-line;
}
.rule-item .rule-evidence {
font-size: 11.5px; color: #94a3b8; font-family: monospace;
margin-top: 4px; padding: 6px 10px; background: #f8fafc; border-radius: 6px;
max-height: 120px; overflow-y: auto; white-space: pre-wrap;
}
.rule-item .rule-time {
font-size: 11px; color: #94a3b8; flex-shrink: 0;
}
.rule-item.skipped .rule-msg { color: #94a3b8; }
/* 容器/引擎表 */
.diag-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.diag-table th {
text-align: left; padding: 10px 20px; font-weight: 500;
color: #64748b; font-size: 12px; border-bottom: 1px solid #f1f5f9;
}
.diag-table td { padding: 10px 20px; border-bottom: 1px solid #f8fafc; color: #0f172a; }
.diag-table tr:last-child td { border-bottom: none; }
.engine-name, .container-name { font-weight: 500; }
.status-running { color: #15803d; font-weight: 500; }
.status-exited { color: #b91c1c; font-weight: 500; }
.status-paused { color: #b45309; font-weight: 500; }
/* 未启用横幅 */
.diag-disabled {
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
border: 1px solid #cbd5e1; border-radius: 12px;
padding: 28px 32px; text-align: center; margin-bottom: 24px;
}
.diag-disabled h2 { font-size: 18px; font-weight: 600; color: #475569; margin-bottom: 10px; }
.diag-disabled p { font-size: 13px; color: #64748b; line-height: 1.7; }
/* 空状态 */
.diag-empty {
padding: 20px; text-align: center; color: #94a3b8; font-size: 13px;
}
/* 按钮 */
.btn-export {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 22px;
background: linear-gradient(135deg, #1e3a5f, #3b82f6);
color: white; border: none; border-radius: 8px;
font-size: 13.5px; font-weight: 500; cursor: pointer;
text-decoration: none; transition: all .2s;
}
.btn-export:hover { box-shadow: 0 4px 12px rgba(59,130,246,.3); transform: translateY(-1px); }
.diag-footer {
display: flex; justify-content: flex-end; align-items: center;
gap: 16px; margin-top: 20px;
}
/* 刷新按钮 */
.refresh-btn {
display: inline-flex; align-items: center; gap: 4px;
background: transparent; border: none; cursor: pointer;
color: #94a3b8; font-size: 12px; font-weight: 400;
margin-left: 8px; padding: 2px 6px; border-radius: 4px;
transition: color .2s, background .2s;
vertical-align: middle;
}
.refresh-btn:hover { color: #3b82f6; background: rgba(59,130,246,.08); }
.refresh-btn svg { transition: transform .6s cubic-bezier(.4,0,.2,1); }
.refresh-btn.spinning svg { animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.refresh-time { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
{% include "_nav.html" %}
<div class="diag-wrap">
<!-- 顶部 -->
<div class="diag-header">
<h1>🔧 系统诊断</h1>
<div class="meta">
🕐 刷新时间: {{ refresh_time }}
&nbsp;·&nbsp; {{ docker_status_msg }}
{% if started_at %}&nbsp;·&nbsp; 诊断启用: {{ started_at[:19].replace('T', ' ') }}{% endif %}
</div>
</div>
<!-- 未启用横幅 -->
{% if not diag_enabled %}
<div class="diag-disabled">
<h2>⛔ 诊断套件未启用</h2>
<p>diagnostics.yaml: diagnostics.enabled = false<br>
启用后将开启后台定时检测(三个定时器,CPU 占用可忽略)。<br>
当前仅保留手动导出功能,自动规则引擎已停止。</p>
<p style="margin-top:14px;"><a href="/api/v1/telemetry/dump" class="btn-export">📦 手动导出诊断包</a></p>
</div>
{% else %}
<!-- 统计卡 -->
<div class="diag-stats">
<div class="diag-stat">
<div class="label">规则总数</div>
<div class="val">{{ rule_count }}</div>
</div>
<div class="diag-stat">
<div class="label">严重</div>
<div class="val crit">{{ crit_count }}</div>
</div>
<div class="diag-stat">
<div class="label">警告</div>
<div class="val warn">{{ warn_count }}</div>
</div>
<div class="diag-stat">
<div class="label">通过</div>
<div class="val ok">{{ ok_count }}</div>
</div>
</div>
<!-- 异常检测 -->
<div class="diag-block">
<h2>⚠️ 规则检测</h2>
{% if all_rules %}
{% for r in all_rules %}
<div class="rule-item {% if r.skipped %}skipped{% endif %}">
<span class="rule-id">{{ r.id }}</span>
<div class="rule-body">
<div class="rule-msg">
<span class="{{ badge_for(r.severity) }}">{{ {
'high': '严重', 'medium': '警告', 'low': '提示',
'info': '信息', 'ok': '通过'
}.get(r.severity, '未知') }}</span>
&nbsp;{{ r.message }}
</div>
{% if r.hint %}<div class="rule-hint">💡 {{ r.hint }}</div>{% endif %}
{% if r.evidence %}
<div class="rule-evidence">{% for e in r.evidence %}{{ e }}{% if not loop.last %}\n{% endif %}{% endfor %}</div>
{% endif %}
{% if r.skipped and r.skip_reason %}
<div class="rule-hint" style="color:#94a3b8;">⏭️ 跳过原因: {{ r.skip_reason }}</div>
{% endif %}
</div>
<span class="rule-time">
{% if r.run_at %}{{ r.run_at }}{% endif %}
{% if r.run_by %}<br><span style="color:#cbd5e1;font-size:10.5px;">{{ r.run_by }}</span>{% endif %}
</span>
</div>
{% endfor %}
{% else %}
<div class="diag-empty">暂无规则结果(定时器还没跑起来?)</div>
{% endif %}
</div>
<!-- 引擎健康 -->
<div class="diag-block" id="engine-health-wrap">
<h2>🧪 引擎状态
<button id="engine-refresh-btn" class="refresh-btn" title="立即刷新" aria-label="刷新">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
<span id="engine-refresh-time" class="refresh-time">—</span>
</button>
</h2>
<div id="engine-health-body">
{% if engine_health %}
<table class="diag-table">
<thead><tr><th>引擎</th><th>状态</th><th>版本 / 详情</th></tr></thead>
<tbody>
{% for name, info in engine_health.items() %}
<tr>
<td class="engine-name">{{ name }}</td>
<td>
{% if info.get('ok') or info.get('available') %}
<span class="tag ok">在线</span>
{% else %}
<span class="tag bad">离线</span>
{% endif %}
</td>
<td style="color:#64748b;font-size:12px;">
{% if info.get('version') %}版本: {{ info.version }}{% endif %}
{% if info.get('signature_date') %}· 病毒库: {{ info.signature_date }}{% endif %}
{% if info.get('rules_loaded') is not none %}· 规则: {{ info.rules_loaded }}{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="diag-empty">引擎健康信息不可用</div>
{% endif %}
</div>
</div>
<!-- 容器状态(Docker 可用时显示)-->
{% if docker_status == 'full' %}
<div class="diag-block">
<h2>🐳 容器运行状态</h2>
{% if docker_containers %}
<table class="diag-table">
<thead><tr><th>容器</th><th>状态</th><th>健康</th><th>Docker重启</th><th>历史重启</th><th>运行时长</th></tr></thead>
<tbody>
{% for c in docker_containers %}
{% if 'meshsafe' in c.name %}
<tr>
<td class="container-name">{{ c.name }}</td>
<td class="status-{{ c.status }}">
{% if c.oom_killed %}<span class="tag bad">OOM</span> {% endif %}
{{ c.status }}
</td>
<td>
{% if c.health %}
{% if c.health == 'healthy' %}<span class="tag ok">✓ healthy</span>
{% elif c.health == 'unhealthy' %}<span class="tag bad">✗ unhealthy</span>
{% elif c.health == 'starting' %}<span class="tag warn">… starting</span>
{% else %}{{ c.health }}{% endif %}
{% else %}—{% endif %}
</td>
<td>
{% if c.restart_count and c.restart_count > 3 %}
<span class="tag bad">{{ c.restart_count }}</span>
{% else %}
{{ c.restart_count if c.restart_count is not none else '-' }}
{% endif %}
</td>
<td>
{% if c.historical_restart_count and c.historical_restart_count > 0 %}
<span class="tag warn">{{ c.historical_restart_count }}</span>
{% else %}
0
{% endif %}
</td>
<td>{{ c.uptime or '—' }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<div class="diag-empty" style="font-size:11.5px;color:#94a3b8;margin-top:6px;padding:0;">
说明:Docker重启 = RestartCount(仅自动崩溃被拉起计数);
历史重启 = R015 检测到的 StartedAt/restart_count 变化(含手动 docker restart)
</div>
{% else %}
<div class="diag-empty">未发现 meshsafe 容器</div>
{% endif %}
</div>
{% elif docker_status == 'partial' %}
<div class="diag-block">
<h2>🐳 容器运行状态 <span class="tag warn">Docker 访问受限</span></h2>
<div class="diag-empty">部分容器信息不可用 — {{ docker_info.get('message', '') }}</div>
</div>
{% elif docker_status == 'none' %}
<div class="diag-block">
<h2>🐳 容器运行状态 <span class="tag muted">未启用</span></h2>
<div class="diag-empty">
Docker 日志检测未启用 — 如需启用,在 docker-compose.yml 的 api 服务中加<br>
<code>volumes: /var/run/docker.sock:/var/run/docker.sock:ro</code><br>
并确保 <code>group_add: ["999"]</code>
</div>
</div>
{% endif %}
<!-- 底部导出 -->
<div class="diag-footer">
<a href="/api/v1/telemetry/dump" class="btn-export" onclick="return confirm('将导出最近 500 条扫描记录、引擎状态、容器日志。\n不含客户内部文件名、IP、用户名等敏感信息。\n确定导出?')">📦 一键导出诊断包</a>
</div>
{% endif %} <!-- diag_enabled endif -->
</div>
<script>
(function(){
var REFRESH_INTERVAL = 10000; // 10 秒
var btn = document.getElementById('engine-refresh-btn');
var timeEl = document.getElementById('engine-refresh-time');
var bodyEl = document.getElementById('engine-health-body');
if (!btn || !bodyEl) return;
var timer = null;
function fmtTime(d){
var p = n => n.toString().padStart(2,'0');
return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds());
}
function buildHealthHTML(health){
if (!health || Object.keys(health).length === 0){
return '<div class="diag-empty">引擎健康信息不可用</div>';
}
var rows = Object.entries(health).map(function([name, info]){
var online = info.ok || info.available;
var tag = online
? '<span class="tag ok">在线</span>'
: '<span class="tag bad">离线</span>';
var parts = [];
if (info.version) parts.push('版本: ' + info.version);
if (info.signature_date) parts.push('· 病毒库: ' + info.signature_date);
if (info.rules_loaded != null) parts.push('· 规则: ' + info.rules_loaded);
var detail = parts.length
? '<td style="color:#64748b;font-size:12px;">' + parts.join('') + '</td>'
: '<td style="color:#64748b;font-size:12px;">—</td>';
return '<tr>' +
'<td class="engine-name">' + name + '</td>' +
'<td>' + tag + '</td>' +
detail +
'</tr>';
}).join('');
return '<table class="diag-table">' +
'<thead><tr><th>引擎</th><th>状态</th><th>版本 / 详情</th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>';
}
function refresh(){
btn.classList.add('spinning');
fetch('/api/v1/telemetry/summary', {credentials: 'same-origin'})
.then(function(r){ return r.json(); })
.then(function(data){
if (data.engine_health){
bodyEl.innerHTML = buildHealthHTML(data.engine_health);
}
if (timeEl) timeEl.textContent = fmtTime(new Date());
})
.catch(function(){
if (timeEl) timeEl.textContent = '失败';
})
.finally(function(){
btn.classList.remove('spinning');
});
}
btn.addEventListener('click', function(e){
e.preventDefault();
refresh();
});
// 10 秒自动刷新
timer = setInterval(refresh, REFRESH_INTERVAL);
// 页面隐藏时暂停(省请求)
document.addEventListener('visibilitychange', function(){
if (document.hidden){
if (timer){ clearInterval(timer); timer = null; }
} else {
if (!timer){ timer = setInterval(refresh, REFRESH_INTERVAL); refresh(); }
}
});
// 首次记录时间
if (timeEl) timeEl.textContent = fmtTime(new Date());
})();
</script>
</body>
</html>