wip: 2026-08-11 工作树快照(docs/memory/h5.py/scripts 等 447 项未评审改动,安全提交到 feat 分支)
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
build_inspection_report.py — 巡检报告 HTML 生成脚本(2026-08-06 落地)
|
||||
========================================================================
|
||||
说明:从对话/看板/Markdown 抽取的巡检数据,按统一模板生成单页 HTML 报告。
|
||||
套用 2026-08-05 早班报告样式 + 4 维分类筛选交互,零 CDN 依赖。
|
||||
|
||||
用法:
|
||||
python scripts/build_inspection_report.py <report_date> <shift>
|
||||
# 例:python scripts/build_inspection_report.py 2026-08-06 早班
|
||||
|
||||
输入参数:
|
||||
- date YYYY-MM-DD
|
||||
- shift 早班/午班/晚班
|
||||
|
||||
输出:
|
||||
docs/07-项目管理/巡检报告/YYYY-MM/YYYY-MM-DD-{shift}.html
|
||||
========================================================================
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_DIR = SCRIPT_DIR.parent
|
||||
DEFAULT_OUT_DIR = PROJECT_DIR / "docs" / "07-项目管理" / "巡检报告"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 巡检数据(2026-08-06 06:00 早班)
|
||||
# ============================================================
|
||||
DATA = {
|
||||
"date": "2026-08-06",
|
||||
"shift": "早班",
|
||||
"time_str": "2026-08-06 06:00",
|
||||
"kanban_version": "v1.9.0-DRAFT",
|
||||
"filter_counts": {
|
||||
"all": 11,
|
||||
"p0": 5,
|
||||
"decide": 2,
|
||||
"doing": 1,
|
||||
"server": 8,
|
||||
"code": 5,
|
||||
"security": 4,
|
||||
"history": 5,
|
||||
},
|
||||
"p0_rows": [
|
||||
{
|
||||
"id": "P0-1",
|
||||
"name": "<code>/itportal/</code> 入口 500 错误",
|
||||
"category": "♻️ 历史遗留",
|
||||
"desc": "2026-08-06 06:00 jumpserver-V2 实测仍 500,nginx error: <code>rewrite or internal redirection cycle while internally redirecting to "/itportal/index.html"</code>。根因:容器内 <code>/usr/share/nginx/html/itportal/</code> 空目录 + nginx <code>location /itportal/</code> 仍配置 <code>try_files $uri /itportal/index.html</code>。建议:<code>docker compose up -d --force-recreate nginx</code>(前提:commit 21830d5 中 nginx.conf 已删除该 location 块)",
|
||||
},
|
||||
{
|
||||
"id": "P0-3",
|
||||
"name": "<code>closing_service.py:467</code> datetime 时区错位",
|
||||
"category": "♻️ 历史遗留",
|
||||
"desc": "<code>datetime.now() - close_time</code> 报 <code>TypeError: can't subtract offset-naive and offset-aware datetimes</code>。影响 <code>POST /h5/conversations/current/reopen</code>。同文件 18 处 <code>datetime.now()</code> 全是 naive,且 <code>from datetime import datetime, timedelta</code> 未 import timezone。24h 内未触发,但用户使用 H5 重开会话即会爆",
|
||||
},
|
||||
{
|
||||
"id": "P0-4",
|
||||
"name": "<code>employee_profile_service.py:236</code> SessionLocal NoneType 🔥",
|
||||
"category": "🆕 位置修正",
|
||||
"desc": "<strong>看板 v1.8 行号描述错误</strong>:实际错误位置是 <code>app/services/employee_profile_service.py:236</code> 而非 <code>h5_ai_task.py:1198</code>。<code>db = SessionLocal()</code> 返回 None → <code>TypeError: 'NoneType' object is not callable</code>。2026-08-05 24h 内累计 10+ 次(01:33 / 03:25 / 04:07 / 04:11 多波次),影响 H5 IT 资产推荐异步推送。<strong>同根因 P0-5</strong>",
|
||||
},
|
||||
{
|
||||
"id": "P0-5",
|
||||
"name": "容器 <code>app/constants/</code> 打包错误 🔥",
|
||||
"category": "🆕 新增",
|
||||
"desc": "容器内 <code>/app/app/constants/__init__.py</code> 11635 bytes 内容是 automation.py(两个文件互换了),<code>/app/app/constants/automation.py</code> <strong>不存在</strong>。直接后果:连续 10+ 次 <code>ModuleNotFoundError: No module named 'app.constants.ai_reply_mode'</code> + 同根因连锁导致 P0-4(SessionLocal 绑定失败)。建议:① 临时 <code>docker cp</code> 修补 → ② 长期排查 backend 镜像构建流程",
|
||||
},
|
||||
{
|
||||
"id": "P0-6",
|
||||
"name": "后端 OAuth 40029 抖动",
|
||||
"category": "ℹ️ 信息类",
|
||||
"desc": "218.75.34.87 等外部 IP 持续试探 <code>/api/auth/validate-sso</code> + 偶发 OAuth code 失效。属 WAF 扫描背景噪声,已 fail2ban 监控。无需主动处理",
|
||||
},
|
||||
],
|
||||
"decide_rows": [
|
||||
{
|
||||
"id": "BLK-A",
|
||||
"name": "企微会议室 Secret",
|
||||
"duration": "⚠️ <strong>26 天</strong>(自 2026-07-11)",
|
||||
"owner": "需企业微信管理后台申请。Owner: 平台组",
|
||||
},
|
||||
{
|
||||
"id": "BLK-B",
|
||||
"name": "ITSM API 授权",
|
||||
"duration": "⚠️ <strong>26 天</strong>(自 2026-07-11)",
|
||||
"owner": "需向 ITSM 平台方申请 app_id/app_secret。Owner: 平台组",
|
||||
},
|
||||
],
|
||||
"doing_rows": [
|
||||
{
|
||||
"id": "#81",
|
||||
"name": "敏感词检测 + 语气优化",
|
||||
"status": "v1.2 待排期",
|
||||
"desc": "v1.1 阶段 1 已于 2026-07-28 完成(词库入库 + 后台管理 UI + 12 配套 API)。剩余 "语气优化、灰度策略细化" 待 v1.2 排期",
|
||||
},
|
||||
],
|
||||
"server_rows": [
|
||||
{"name": "Docker 容器", "status": "ok", "label": "正常", "desc": "5/5 healthy:nginx (Up 12h) / backend (Up 17h) / redis (Up 2w) / postgres (Up 3w) / neo4j (Up 3w)。无 Exited/Restarting/Unhealthy"},
|
||||
{"name": "后端 health", "status": "ok", "label": "正常", "desc": "<code>/health</code> → HTTP 200(容器内 3.9ms);外部 <code>curl http://localhost:8000</code> 000(端口未映射给宿主,nginx 反代访问正常)"},
|
||||
{"name": "后端 5xx 错误", "status": "fail", "label": "异常", "desc": "24h 内仍见 2 类 Traceback:P0-5 ModuleNotFoundError(10+ 次)+ P0-4 SessionLocal NoneType(10+ 次)"},
|
||||
{"name": "PostgreSQL", "status": "ok", "label": "正常", "desc": "<code>pg_isready -U postgres</code> → <code>accepting connections</code>"},
|
||||
{"name": "Redis", "status": "ok", "label": "正常", "desc": "<code>redis_version:7.4.9</code> / 独立模式 + 密码认证(NOAUTH 正常);已配密码 <code>WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76</code>"},
|
||||
{"name": "Nginx 配置", "status": "ok", "label": "正常", "desc": "<code>nginx -t</code> → syntax OK;80/443 已 bind"},
|
||||
{"name": "前端入口", "status": "fail", "label": "异常", "desc": "<code>/itportal/</code> <strong>500</strong>(rewrite cycle);<code>/h5/</code>、<code>/itdesk/</code> 200 OK;<code>/</code> 200(612 bytes root index.html)"},
|
||||
{"name": "磁盘 / 内存", "status": "ok", "label": "正常", "desc": "130G 可用(13% 用)/ 3.4G used / 11G available / 负载 0.99 / 已运行 54 天"},
|
||||
],
|
||||
"code_rows": [
|
||||
{"name": "🟢 鉴权补漏", "desc": "sensitive_words.py 13 端点已正确加 <code>dependencies=[Depends(require_admin)]</code> (line 47-51)。看板描述脱节,v1.9 已迁移至已完成"},
|
||||
{"name": "🔴 容器/源码不一致", "desc": "容器内 <code>app/constants/__init__.py</code> = automation.py 内容。排查镜像构建流程,重打 backend 镜像"},
|
||||
{"name": "🟡 死代码 / 残留", "desc": "console.log 在 <code>src/frontend-*/src</code> 已搜不到。看板描述脱节,v1.9 已迁移至已完成"},
|
||||
{"name": "🟢 时区隐患", "desc": "closing_service.py 全部 18 处 <code>datetime.now()</code> 全是 naive,无 import timezone。建议全文件改造为 <code>datetime.now(tz=timezone.utc)</code>"},
|
||||
{"name": "🟢 未使用导入 / 硬编码", "desc": "未发现明显问题(快速扫描未命中);源码层无硬编码(生产 env 路径已隔离)"},
|
||||
],
|
||||
"security_rows": [
|
||||
{"name": "🟡 API 鉴权", "desc": "sensitive_words.py 13 端点已 require_admin;其余 admin API 状态需逐项复核(本次未做全量扫描)"},
|
||||
{"name": "🟢 敏感数据处理", "desc": "数据库连接串、Redis 密码均在 env 注入,未硬编码到源码"},
|
||||
{"name": "🟡 依赖包", "desc": "<code>cryptography==42.0.8</code> / <code>pydantic==2.7.4</code>(2026-08-05 巡检已记录,仍待升级)"},
|
||||
{"name": "🟡 日志敏感信息", "desc": "生产 logs 记录了用户 OAuth code(如 <code>code=OFVMVR7IB2dgFVWX...</code>),属企微临时凭据,建议在 <code>wecom_service.py:1411</code> 打印前 mask"},
|
||||
],
|
||||
"history_rows": [
|
||||
{"name": "P0-2 sensitive_words.py 13 端点无鉴权", "category": "♻️→✅", "desc": "看板 v1.8 标\"待治理\",2026-08-06 06:00 巡检发现<strong>代码已修复</strong>(router 加 <code>dependencies=[Depends(require_admin)]</code> line 47-51),v1.9 看板已迁移至已完成区"},
|
||||
{"name": "P1-1 前端 console.log 残留 208 行", "category": "♻️→✅", "desc": "看板 v1.8 标\"待清理\",2026-08-06 06:00 巡检发现 <code>rg/find</code> 搜不到任何 console.log,v1.9 看板已迁移至已完成区"},
|
||||
{"name": "P0-4 行号描述修正", "category": "🔄 修正", "desc": "看板 v1.8 写 <code>h5_ai_task.py:1198</code>,实际是 <code>employee_profile_service.py:236</code>。<strong>同根因 P0-5</strong>,建议合并排查"},
|
||||
{"name": "BLK-A/B 阻塞时长刷新", "category": "📅 +1d", "desc": "自 2026-07-11 起,2026-08-06 已 <strong>26 天</strong>(较昨日 +1 自然日),远超 3 天阈值"},
|
||||
{"name": "/itportal/ 500 持续 29 天", "category": "🔥 持续", "desc": "2026-07-08 起至今,今日 06:00 jumpserver-V2 实测仍 500。需 PM 派单部署 commit 21830d5 或清理 nginx location 块"},
|
||||
],
|
||||
"pm_alert": [
|
||||
"🔴 P0-NEW1(constants 包打包错误)必须当日修:直接导致 H5 异步任务全部崩溃(资产推荐推送 NoneType)。修复方向:① 临时 <code>docker cp</code> 修补 → ② 长期重打 backend 镜像",
|
||||
"🔴 /itportal 500 已持续 29 天:根因明确(容器内空 dist + nginx location 块残留)。建议执行 <code>docker compose up -d --force-recreate nginx</code>",
|
||||
"🟡 看板 v1.8.0-FROZEN 2 项描述脱节:P0-2 sensitive_words / P1-1 console.log 实际已修复 / 已清理,v1.9 已修正",
|
||||
"⚠️ BLK-A/B 阻塞已 26 天(+23 天超过 3 天阈值):建议 PM 升级催办或重新评估优先级",
|
||||
"🟡 closing_service.py:467 时区隐患:24h 内未触发,修复成本低(<code>from datetime import timezone</code> + 18 处替换)",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 样式(复用 2026-08-05 早班报告色板,零 CDN)
|
||||
# ============================================================
|
||||
CSS = r"""
|
||||
:root {
|
||||
--accent: #07C160;
|
||||
--accent-soft: #E7F8EF;
|
||||
--warn: #FA9D3B;
|
||||
--danger: #FA5151;
|
||||
--info: #10AEFF;
|
||||
--text: #1F2329;
|
||||
--text-soft: #646A73;
|
||||
--bg: #FFFFFF;
|
||||
--bg-soft: #F7F8FA;
|
||||
--border: #DEE0E3;
|
||||
--shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
--radius: 8px;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg-soft);
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
.container { max-width: 1080px; margin: 0 auto; padding: 32px 24px 64px; }
|
||||
header {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #06AD56 100%);
|
||||
color: #fff;
|
||||
padding: 32px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
header h1 { font-size: 26px; font-weight: 600; margin-bottom: 8px; }
|
||||
header .meta { font-size: 13px; opacity: 0.92; }
|
||||
header .meta span { margin-right: 18px; }
|
||||
header .meta strong { font-weight: 600; }
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 32px 0 14px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.summary-card {
|
||||
background: #fff;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
}
|
||||
.summary-card .num { font-size: 24px; font-weight: 700; color: var(--accent); }
|
||||
.summary-card .label { font-size: 12px; color: var(--text-soft); margin-top: 4px; }
|
||||
.summary-card.warn .num { color: var(--warn); }
|
||||
.summary-card.danger .num { color: var(--danger); }
|
||||
.summary-card.info .num { color: var(--info); }
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; margin-bottom: 24px; }
|
||||
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
th { background: var(--bg-soft); font-size: 12px; font-weight: 600; color: var(--text-soft); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
||||
.badge.p0 { background: #FEE; color: var(--danger); border: 1px solid var(--danger); }
|
||||
.badge.p1 { background: #FFF3E0; color: var(--warn); border: 1px solid var(--warn); }
|
||||
.badge.decide { background: #E6F4FF; color: var(--info); border: 1px solid var(--info); }
|
||||
.badge.doing { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent); }
|
||||
.badge.ok { background: var(--accent-soft); color: var(--accent); }
|
||||
.badge.fail { background: #FEE; color: var(--danger); }
|
||||
code { background: var(--bg-soft); padding: 2px 6px; border-radius: 3px; font-family: "Cascadia Code", Consolas, monospace; font-size: 12px; }
|
||||
.alert { background: #FFF8E6; border-left: 4px solid var(--warn); padding: 12px 16px; margin: 16px 0; border-radius: 4px; }
|
||||
.alert.danger { background: #FEE; border-left-color: var(--danger); }
|
||||
.alert strong { display: block; margin-bottom: 4px; }
|
||||
.legend { color: var(--text-soft); font-size: 12px; margin-top: 4px; margin-bottom: 16px; }
|
||||
footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); color: var(--text-soft); font-size: 12px; text-align: center; }
|
||||
ul.condensed { list-style: none; padding: 0; }
|
||||
ul.condensed li { padding: 8px 12px; border-bottom: 1px dashed var(--border); background: #fff; }
|
||||
ul.condensed li:last-child { border-bottom: none; }
|
||||
ul.condensed li:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
||||
ul.condensed li:last-child { border-radius: 0 0 var(--radius) var(--radius); box-shadow: var(--shadow); }
|
||||
|
||||
.filter-bar { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 8px; padding: 12px 14px; background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); align-items: center; }
|
||||
.filter-bar .filter-label { font-size: 12px; color: var(--text-soft); font-weight: 600; margin-right: 4px; }
|
||||
.filter-btn { cursor: pointer; border: 1px solid var(--border); background: #fff; color: var(--text-soft); padding: 6px 12px; border-radius: 16px; font-size: 12px; font-weight: 600; transition: all 0.15s ease; user-select: none; }
|
||||
.filter-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
|
||||
.filter-btn.active { color: #fff; border-color: transparent; }
|
||||
.filter-btn[data-cat="all"].active { background: var(--text); }
|
||||
.filter-btn[data-cat="p0"].active { background: var(--danger); }
|
||||
.filter-btn[data-cat="decide"].active { background: var(--info); }
|
||||
.filter-btn[data-cat="doing"].active { background: var(--accent); }
|
||||
.filter-btn[data-cat="server"].active { background: var(--danger); }
|
||||
.filter-btn[data-cat="code"].active { background: #8B5CF6; }
|
||||
.filter-btn[data-cat="security"].active { background: #DC2626; }
|
||||
.filter-btn[data-cat="history"].active { background: #64748B; }
|
||||
.filter-btn .badge-count { display: inline-block; margin-left: 6px; padding: 1px 6px; background: rgba(0,0,0,0.08); border-radius: 8px; font-size: 10px; }
|
||||
.filter-btn.active .badge-count { background: rgba(255,255,255,0.25); }
|
||||
.hidden { display: none !important; }
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================
|
||||
# HTML 渲染
|
||||
# ============================================================
|
||||
def render_html(data: dict) -> str:
|
||||
fc = data["filter_counts"]
|
||||
|
||||
# P0 表格
|
||||
p0_rows_html = ""
|
||||
for r in data["p0_rows"]:
|
||||
p0_rows_html += f""" <tr data-cat="p0">
|
||||
<td><span class="badge p0">{r['id']}</span></td>
|
||||
<td>{r['name']}</td>
|
||||
<td>{r['category']}</td>
|
||||
<td>{r['desc']}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
# 等决策表格
|
||||
decide_rows_html = ""
|
||||
for r in data["decide_rows"]:
|
||||
decide_rows_html += f""" <tr data-cat="decide">
|
||||
<td><span class="badge decide">{r['id']}</span></td>
|
||||
<td>{r['name']}</td>
|
||||
<td>{r['duration']}</td>
|
||||
<td>{r['owner']}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
# 进行中表格
|
||||
doing_rows_html = ""
|
||||
for r in data["doing_rows"]:
|
||||
doing_rows_html += f""" <tr data-cat="doing">
|
||||
<td><span class="badge doing">{r['id']}</span></td>
|
||||
<td>{r['name']}</td>
|
||||
<td>{r['status']}</td>
|
||||
<td>{r['desc']}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
# 服务器状态
|
||||
server_rows_html = ""
|
||||
for r in data["server_rows"]:
|
||||
server_rows_html += f""" <tr data-cat="server">
|
||||
<td>{r['name']}</td>
|
||||
<td><span class="badge {r['status']}">{r['label']}</span></td>
|
||||
<td>{r['desc']}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
# 代码质量
|
||||
code_items_html = "\n".join(
|
||||
f' <li data-cat="code"><strong>{r["name"]}</strong>:{r["desc"]}</li>'
|
||||
for r in data["code_rows"]
|
||||
)
|
||||
|
||||
# 安全
|
||||
security_items_html = "\n".join(
|
||||
f' <li data-cat="security"><strong>{r["name"]}</strong>:{r["desc"]}</li>'
|
||||
for r in data["security_rows"]
|
||||
)
|
||||
|
||||
# 对比/关注
|
||||
history_rows_html = ""
|
||||
for r in data["history_rows"]:
|
||||
history_rows_html += f""" <tr data-cat="history">
|
||||
<td>{r['name']}</td>
|
||||
<td>{r['category']}</td>
|
||||
<td>{r['desc']}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
# PM 立即关注
|
||||
pm_items_html = "\n".join(
|
||||
f' <li data-cat="all">{item}</li>' for item in data["pm_alert"]
|
||||
)
|
||||
|
||||
# 摘要卡片
|
||||
summary_html = f"""
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card danger"><div class="num">{len(data['p0_rows'])}</div><div class="label">🔴 P0 必做</div></div>
|
||||
<div class="summary-card info"><div class="num">{len(data['decide_rows'])}</div><div class="label">🟢 等决策</div></div>
|
||||
<div class="summary-card"><div class="num">{len(data['doing_rows'])}</div><div class="label">⏳ 进行中</div></div>
|
||||
<div class="summary-card {'danger' if any(r['status']=='fail' for r in data['server_rows']) else ''}"><div class="num">{sum(1 for r in data['server_rows'] if r['status']=='fail')}</div><div class="label">🖥️ 服务器异常项</div></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='14' font-size='14'>📋</text></svg>">
|
||||
<title>早班巡检报告 · {data['date']} 06:00 · v3.0</title>
|
||||
<style>{CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<h1>📋 早班巡检报告 · IT 智能服务台</h1>
|
||||
<div class="meta">
|
||||
<span>📅 <strong>{data['time_str']}</strong>(GMT+8)</span>
|
||||
<span>🛰️ 服务器 <strong>10.90.5.110</strong></span>
|
||||
<span>📄 看板 <strong>{data['kanban_version']}</strong></span>
|
||||
<span>🤖 执行 <strong>Duckula</strong></span>
|
||||
<span>📦 看板源 <code>docs/07-项目管理/项目状态看板.md</code></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{summary_html}
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<div class="filter-bar">
|
||||
<span class="filter-label">🔍 分类筛选</span>
|
||||
<button class="filter-btn active" data-cat="all">全部<span class="badge-count">{fc['all']}</span></button>
|
||||
<button class="filter-btn" data-cat="p0">🔴 P0<span class="badge-count">{fc['p0']}</span></button>
|
||||
<button class="filter-btn" data-cat="decide">🟢 等决策<span class="badge-count">{fc['decide']}</span></button>
|
||||
<button class="filter-btn" data-cat="doing">⏳ 进行中<span class="badge-count">{fc['doing']}</span></button>
|
||||
<button class="filter-btn" data-cat="server">🖥️ 服务器<span class="badge-count">{fc['server']}</span></button>
|
||||
<button class="filter-btn" data-cat="code">🔍 代码<span class="badge-count">{fc['code']}</span></button>
|
||||
<button class="filter-btn" data-cat="security">🔐 安全<span class="badge-count">{fc['security']}</span></button>
|
||||
<button class="filter-btn" data-cat="history">🔄 对比/关注<span class="badge-count">{fc['history']}</span></button>
|
||||
</div>
|
||||
|
||||
<!-- P0 必做 -->
|
||||
<h2 data-section="p0">🔴 P0 必做(高优先级)</h2>
|
||||
<p class="legend">本次巡检最高优先级阻塞 / 待修复项</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>编号</th><th>任务</th><th>类别</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{p0_rows_html} </tbody>
|
||||
</table>
|
||||
|
||||
<!-- 等用户决策 -->
|
||||
<h2 data-section="decide">🟢 等用户决策(阻塞项)</h2>
|
||||
<p class="legend">均超 3 天阈值,需 PM 升级催办</p>
|
||||
<table>
|
||||
<thead><tr><th>编号</th><th>任务</th><th>阻塞时长</th><th>卡点 / Owner</th></tr></thead>
|
||||
<tbody>
|
||||
{decide_rows_html} </tbody>
|
||||
</table>
|
||||
|
||||
<!-- 进行中 -->
|
||||
<h2 data-section="doing">⏳ 进行中 (In Progress)</h2>
|
||||
<table>
|
||||
<thead><tr><th>编号</th><th>任务</th><th>状态</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
{doing_rows_html} </tbody>
|
||||
</table>
|
||||
|
||||
<!-- 服务器状态 -->
|
||||
<h2 data-section="server">🖥️ 服务器运行状态</h2>
|
||||
<table>
|
||||
<thead><tr><th>组件</th><th>状态</th><th>实测数据</th></tr></thead>
|
||||
<tbody>
|
||||
{server_rows_html} </tbody>
|
||||
</table>
|
||||
|
||||
<!-- 代码规范与质量 -->
|
||||
<h2 data-section="code">🔍 代码规范与质量检查</h2>
|
||||
<ul class="condensed">
|
||||
{code_items_html}
|
||||
</ul>
|
||||
|
||||
<!-- 安全检查 -->
|
||||
<h2 data-section="security">🔐 安全检查</h2>
|
||||
<ul class="condensed">
|
||||
{security_items_html}
|
||||
</ul>
|
||||
|
||||
<!-- 对比/关注 -->
|
||||
<h2 data-section="history">🔄 与昨日对比 / PM 关注项</h2>
|
||||
<table>
|
||||
<thead><tr><th>条目</th><th>变化</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
{history_rows_html} </tbody>
|
||||
</table>
|
||||
|
||||
<!-- PM 立即关注 -->
|
||||
<h2 data-section="all">⚠️ PM 立即关注</h2>
|
||||
<ul class="condensed">
|
||||
{pm_items_html}
|
||||
</ul>
|
||||
|
||||
<footer>
|
||||
📋 报告生成时间:{data['time_str']} · 看板版本 {data['kanban_version']} · Duckula(达寇拉)自动化生成<br>
|
||||
📎 数据源:jumpserver-V2 实测 + 后端 24h 日志审计 + 源码扫描 + 容器内文件核对<br>
|
||||
🔗 服务地址:<code>https://itsupport.servyou.com.cn/docs/inspection/2026-08/2026-08-06-早班.html</code>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {{
|
||||
// 简易分类筛选
|
||||
const all = {{ all: true }};
|
||||
document.querySelectorAll('.filter-btn').forEach((btn) => {{
|
||||
btn.addEventListener('click', () => filter(btn.dataset.cat));
|
||||
}});
|
||||
function filter(cat) {{
|
||||
document.querySelectorAll('.filter-btn').forEach((b) => b.classList.remove('active'));
|
||||
document.querySelector('.filter-btn[data-cat="' + cat + '"]').classList.add('active');
|
||||
document.querySelectorAll('tr[data-cat], li[data-cat]').forEach((el) => {{
|
||||
if (cat === 'all') {{ el.classList.remove('hidden'); }}
|
||||
else if (el.dataset.cat === cat) {{ el.classList.remove('hidden'); }}
|
||||
else {{ el.classList.add('hidden'); }}
|
||||
}});
|
||||
document.querySelectorAll('h2[data-section]').forEach((h) => {{
|
||||
if (cat === 'all' || h.dataset.section === cat) {{ h.classList.remove('hidden'); }}
|
||||
else {{ h.classList.add('hidden'); }}
|
||||
}});
|
||||
history.replaceState(null, '', '#' + cat);
|
||||
}}
|
||||
const initCat = (location.hash || '#all').slice(1);
|
||||
if (['all','p0','decide','doing','server','code','security','history'].includes(initCat)) {{
|
||||
filter(initCat);
|
||||
}}
|
||||
const keys = ['1','2','3','4','5','6','7','8','0'];
|
||||
const map = ['p0','decide','doing','server','code','security','history','overview','all'];
|
||||
document.addEventListener('keydown', (e) => {{
|
||||
if (['INPUT','TEXTAREA'].includes(e.target.tagName)) return;
|
||||
const idx = keys.indexOf(e.key);
|
||||
if (idx >= 0 && map[idx]) filter(map[idx]);
|
||||
}});
|
||||
}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Build inspection report HTML")
|
||||
parser.add_argument("date", help="Report date (YYYY-MM-DD)")
|
||||
parser.add_argument("shift", help="Shift (早班/午班/晚班)")
|
||||
args = parser.parse_args()
|
||||
|
||||
year_month = args.date[:7] # YYYY-MM
|
||||
out_dir = DEFAULT_OUT_DIR / year_month
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_file = out_dir / f"{args.date}-{args.shift}.html"
|
||||
|
||||
DATA["date"] = args.date
|
||||
DATA["shift"] = args.shift
|
||||
DATA["time_str"] = f"{args.date} 06:00" if args.shift == "早班" else f"{args.date} 14:00"
|
||||
|
||||
html = render_html(DATA)
|
||||
out_file.write_text(html, encoding="utf-8")
|
||||
print(f"[OK] Generated: {out_file} ({len(html)} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user