Files
MeshSate/app/templates/tasks.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

233 lines
9.5 KiB
HTML
Raw 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" %}
main { padding: 24px; max-width: 1500px; margin: 0 auto; }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 18px; }
.page-head h1 { margin: 0 0 6px; font-size: 21px; font-weight: 700; }
.page-head .sub { font-size: 13px; color: #64748b; }
.page-head .count-chip {
background: #fff; border: 1px solid #e2e8f0; border-radius: 8px;
padding: 8px 14px; font-size: 12.5px; color: #475569;
}
.page-head .count-chip b { color: #0f172a; }
.panel { background: #fff; border-radius: 12px; padding: 18px 20px;
box-shadow: 0 1px 3px rgba(15,23,42,.07), 0 6px 20px rgba(15,23,42,.04);
border: 1px solid #eef2f7; }
table { width: 100%; border-collapse: collapse; }
th { background: #f8fafc; padding: 10px 12px; text-align: left; font-size: 11.5px;
color: #64748b; font-weight: 600; border-bottom: 1px solid #e8edf4; white-space: nowrap; }
td { padding: 10px 12px; font-size: 12.5px; border-bottom: 1px solid #f4f7fb; color: #334155; }
tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #fafbfd; }
td.mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11.5px; color: #64748b; }
td.fname { font-weight: 500; color: #1e293b; max-width: 230px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.verdict-cell { display: flex; align-items: center; gap: 6px; }
.mode-badge { display: inline-block; padding: 2px 8px; border-radius: 5px; font-size: 11px; }
.mode-sync { background: #e0f2fe; color: #0369a1; }
.mode-async { background: #f3e5f5; color: #7b1fa2; }
details > summary { cursor: pointer; color: #2563eb; user-select: none; font-size: 12px; }
details > summary:hover { text-decoration: underline; }
/* 详情列:限制宽度,内层内容横向滚动,不撑开外层表格 */
tbody td:last-child { min-width: 220px; max-width: 360px; vertical-align: top; }
details { display: block; width: 100%; }
details .findings { padding: 12px; font-size: 12px; background: #f8fafc;
border-radius: 8px; margin-top: 6px;
max-width: 100%; overflow-x: auto; overflow-y: visible; display: block; }
details .findings table { border: 1px solid #e8edf4; border-radius: 6px; overflow: hidden; min-width: 600px; }
details .findings th { font-size: 11px; padding: 6px 10px; white-space: nowrap; }
details .findings td { font-size: 11.5px; padding: 6px 10px; white-space: nowrap; }
.empty { text-align: center; padding: 50px; color: #94a3b8; font-size: 13px; }
/* ---------- 分页 ---------- */
.pager { display: flex; align-items: center; justify-content: space-between; margin-top: 16px;
padding-top: 14px; border-top: 1px solid #eef2f7; }
.pager .p-info { font-size: 12.5px; color: #64748b; }
.pager .p-info b { color: #0f172a; }
.pager .p-nav { display: flex; gap: 5px; align-items: center; }
.pager .p-nav a, .pager .p-nav span {
min-width: 32px; height: 30px; padding: 0 10px;
display: inline-flex; align-items: center; justify-content: center;
border-radius: 7px; font-size: 12.5px; text-decoration: none;
border: 1px solid #e2e8f0; color: #475569; background: #fff;
transition: all .15s;
}
.pager .p-nav a:hover { border-color: #93c5fd; color: #2563eb; background: #eff6ff; }
.pager .p-nav .cur { background: #1e3a5f; border-color: #1e3a5f; color: #fff; font-weight: 600; }
.pager .p-nav .disabled { opacity: .4; pointer-events: none; }
.pager .p-nav .dots { border: none; background: none; min-width: 20px; padding: 0; color: #94a3b8; }
/* ---------- 风险体系说明 ---------- */
.risk-legend { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; padding-top: 14px;
border-top: 1px dashed #e8edf4; }
.risk-legend .rl { display: flex; align-items: flex-start; gap: 7px; font-size: 11.5px; color: #94a3b8; }
.risk-legend .rl .rl-name { color: #334155; font-weight: 600; font-size: 12px; white-space: nowrap; }
</style>
</head>
<body>
{% set nav_active = 'tasks' %}
{% include "_nav.html" %}
<main>
<div class="page-head">
<div>
<h1>任务历史</h1>
<div class="sub">全部扫描任务记录 · 每页 {{ page_size }} 条</div>
</div>
<div class="count-chip">
共 <b>{{ total }}</b> 条记录 · 第 <b>{{ page }}</b> / {{ total_pages }} 页
</div>
</div>
<div class="panel">
{% if tasks %}
<table>
<thead>
<tr>
<th>时间</th>
<th>任务 ID</th>
<th>文件名</th>
<th>来源渠道</th>
<th>类型</th>
<th>大小</th>
<th>模式</th>
<th>检测结论</th>
<th>处置动作</th>
<th>风险分</th>
<th>耗时</th>
<th></th>
</tr>
</thead>
<tbody>
{% for t in tasks %}
<tr id="task-{{ t.id }}">
<td class="mono">{{ t.created_at|bj_time("%m-%d %H:%M:%S") if t.created_at else '' }}</td>
<td class="mono" title="{{ t.id }}">{{ t.id[0:10] }}...</td>
<td class="fname" title="{{ t.filename }}">{{ t.filename }}</td>
<td class="mono" title="{{ t.client_id }}">{{ t.channel|cn_channel }}</td>
<td class="mono" title="{{ t.real_type }}">{{ t.real_type[:16] if t.real_type else t.declared_ext }}</td>
<td class="mono">
{% if t.size > 1048576 %}{{ (t.size / 1048576)|round(1) }} MB
{% elif t.size > 1024 %}{{ (t.size / 1024)|round(1) }} KB
{% else %}{{ t.size }} B{% endif %}
</td>
<td><span class="mode-badge mode-{{ t.mode }}">{{ t.mode|cn_mode }}</span></td>
<td>
<div class="verdict-cell">
<span class="tag {{ t.verdict|cn_verdict_badge }}">{{ t.verdict|cn_verdict }}</span>
</div>
</td>
<td><span class="tag plain">{{ t.action|cn_action }}</span></td>
<td class="mono">{{ "%.0f"|format(t.score) }}</td>
<td class="mono">{{ t.duration_ms }} ms</td>
<td>
<details>
<summary>详情 ({{ t.findings|length }})</summary>
<div class="findings">
{% if t.findings %}
<table>
<thead>
<tr><th>检测引擎</th><th>严重度</th><th>命中规则</th><th>权重</th><th>细节</th></tr>
</thead>
<tbody>
{% for f in t.findings %}
<tr>
<td><span class="tag plain">{{ f.engine|cn_engine }}</span></td>
<td><span class="tag {{ f.severity|cn_severity_badge }}">{{ f.severity|cn_severity }}</span></td>
<td class="mono">{{ f.rule }}</td>
<td class="mono">{{ f.weight }}</td>
<td>{{ f.detail }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div style="color:#94a3b8;font-size:12px;">未检出任何风险信号</div>
{% endif %}
{% if t.sha256 %}
<div style="margin-top:10px;font-size:11px;color:#94a3b8;font-family:monospace;line-height:1.8;">
SHA256:{{ t.sha256 }}
{% if t.md5 %}<br>MD5:{{ t.md5 }}{% endif %}
<br>策略版本:{{ t.policy_version }}
{% if t.reason %}<br>裁决依据:{{ t.reason }}{% endif %}
</div>
{% endif %}
</div>
</details>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 风险体系说明 -->
<div class="risk-legend">
{% for r in RISK_MATRIX %}
<div class="rl">
<span class="tag risk-{{ r.key }}">{{ r.label }}</span>
<span>{{ r.desc }}</span>
</div>
{% endfor %}
</div>
<!-- 分页 -->
<div class="pager">
<div class="p-info">
显示第 <b>{{ (page - 1) * page_size + 1 }}</b> - <b>{{ [page * page_size, total]|min }}</b> 条,共 <b>{{ total }}</b> 条
</div>
<div class="p-nav">
{% if has_prev %}
<a href="/console/tasks?page={{ prev_page }}">上一页</a>
{% else %}
<span class="disabled">上一页</span>
{% endif %}
{% if page_nums and page_nums[0] > 1 %}
<a href="/console/tasks?page=1">1</a>
{% if page_nums[0] > 2 %}<span class="dots">···</span>{% endif %}
{% endif %}
{% for p in page_nums %}
{% if p == page %}
<span class="cur">{{ p }}</span>
{% else %}
<a href="/console/tasks?page={{ p }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page_nums and page_nums[-1] < total_pages %}
{% if page_nums[-1] < total_pages - 1 %}<span class="dots">···</span>{% endif %}
<a href="/console/tasks?page={{ total_pages }}">{{ total_pages }}</a>
{% endif %}
{% if has_next %}
<a href="/console/tasks?page={{ next_page }}">下一页</a>
{% else %}
<span class="disabled">下一页</span>
{% endif %}
</div>
</div>
{% else %}
<div class="empty">
<div style="font-size:28px;margin-bottom:12px;">📭</div>
暂无扫描记录。调用 <code>/api/v1/scans</code> 提交文件后,结果会自动出现在这里。
</div>
{% endif %}
</div>
</main>
</body>
</html>