style: fix black/isort formatting
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 41s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m6s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 54s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 1m57s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m34s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m47s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m29s
AI Code Review / AI Code Review (pull_request) Successful in 4m7s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m55s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 13m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 39m46s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 49s

This commit is contained in:
2026-07-22 13:50:12 +08:00
parent 66bdff866c
commit 7206bcbe50
+217 -184
View File
@@ -11,6 +11,7 @@ CI 可观测性看板 - 从 Gitea Actions API 拉取数据并生成 Markdown/HTM
GITEA_REPO 仓库 (默认 xiaoxia/xiaoxia-saas)
GITEA_TOKEN API Token (优先) 或 GITEA_USERNAME + GITEA_PASSWORD
"""
import argparse
import base64
import json
@@ -29,6 +30,7 @@ DEFAULT_REPO = "xiaoxia/xiaoxia-saas"
DEFAULT_DAYS = 7
PAGE_LIMIT = 50 # 每页数量,最大50
# ── API 封装 ─────────────────────────────────────────
class GiteaActions:
def __init__(self, base_url, repo, token=None, username=None, password=None):
@@ -310,8 +312,20 @@ def analyze_runs(runs):
# 基础设施关键词(与 ci_health_check.py 保持一致的分类逻辑)
infra_job_keywords = ["checkout", "build", "deploy", "cleanup", "setup", "cache", "install", "docker"]
business_job_keywords = [
"unit test", "pytest", "vitest", "jest", "lint", "eslint", "prettier",
"integration", "e2e", "validate", "code quality", "mypy", "ruff", "flake8",
"unit test",
"pytest",
"vitest",
"jest",
"lint",
"eslint",
"prettier",
"integration",
"e2e",
"validate",
"code quality",
"mypy",
"ruff",
"flake8",
]
for r in runs:
@@ -581,12 +595,14 @@ def generate_html(stats, start_date, end_date, repo):
for name, s in stats["job_success_stats"].items():
if s["total"] >= 3: # 至少有3次才统计
rate = (s["success"] / s["total"] * 100) if s["total"] > 0 else 0
job_stats_list.append({
"name": name,
"rate": round(rate, 1),
"total": s["total"],
"success": s["success"],
})
job_stats_list.append(
{
"name": name,
"rate": round(rate, 1),
"total": s["total"],
"success": s["success"],
}
)
job_stats_list.sort(key=lambda x: x["rate"])
job_stats_list = job_stats_list[:15] # 取成功率最低的15个
job_names = [j["name"] for j in job_stats_list]
@@ -601,277 +617,294 @@ def generate_html(stats, start_date, end_date, repo):
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 序列化数据为 JSON(供 JS 使用)
data_json = json.dumps({
"daily_dates": daily_dates,
"daily_success_rates": daily_success_rates,
"daily_run_counts": daily_run_counts,
"wf_names": wf_names,
"wf_avg_durations": wf_avg_durations,
"failure_pie_data": failure_pie_data,
"job_names": job_names,
"job_rates": job_rates,
}, ensure_ascii=False)
data_json = json.dumps(
{
"daily_dates": daily_dates,
"daily_success_rates": daily_success_rates,
"daily_run_counts": daily_run_counts,
"wf_names": wf_names,
"wf_avg_durations": wf_avg_durations,
"failure_pie_data": failure_pie_data,
"job_names": job_names,
"job_rates": job_rates,
},
ensure_ascii=False,
)
# HTML 模板(注意:不使用 f-string,避免与 CSS/JS 的大括号冲突)
html_parts = []
html_parts.append('<!DOCTYPE html>')
html_parts.append("<!DOCTYPE html>")
html_parts.append('<html lang="zh-CN">')
html_parts.append('<head>')
html_parts.append("<head>")
html_parts.append(' <meta charset="UTF-8">')
html_parts.append(' <meta name="viewport" content="width=device-width, initial-scale=1.0">')
html_parts.append(f' <title>CI 健康度看板 - {repo}</title>')
html_parts.append(f" <title>CI 健康度看板 - {repo}</title>")
html_parts.append(' <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>')
html_parts.append(' <style>')
html_parts.append(' * { margin: 0; padding: 0; box-sizing: border-box; }')
html_parts.append(' body {')
html_parts.append(' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB",')
html_parts.append(" <style>")
html_parts.append(" * { margin: 0; padding: 0; box-sizing: border-box; }")
html_parts.append(" body {")
html_parts.append(
' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB",'
)
html_parts.append(' "Microsoft YaHei", sans-serif;')
html_parts.append(' background: #f0f2f5;')
html_parts.append(' color: #333;')
html_parts.append(' padding: 20px;')
html_parts.append(' }')
html_parts.append(' .container { max-width: 1400px; margin: 0 auto; }')
html_parts.append(' .header {')
html_parts.append(' background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);')
html_parts.append(' color: white;')
html_parts.append(' padding: 24px 32px;')
html_parts.append(' border-radius: 12px;')
html_parts.append(' margin-bottom: 20px;')
html_parts.append(' }')
html_parts.append(' .header h1 { font-size: 24px; margin-bottom: 8px; }')
html_parts.append(' .header .subtitle { font-size: 14px; opacity: 0.9; }')
html_parts.append(' .header .meta { font-size: 12px; opacity: 0.8; margin-top: 8px; }')
html_parts.append(' .metrics-row {')
html_parts.append(' display: grid;')
html_parts.append(' grid-template-columns: repeat(4, 1fr);')
html_parts.append(' gap: 16px;')
html_parts.append(' margin-bottom: 20px;')
html_parts.append(' }')
html_parts.append(' .metric-card {')
html_parts.append(' background: white;')
html_parts.append(' border-radius: 12px;')
html_parts.append(' padding: 20px;')
html_parts.append(' box-shadow: 0 2px 8px rgba(0,0,0,0.06);')
html_parts.append(' transition: transform 0.2s;')
html_parts.append(' }')
html_parts.append(' .metric-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }')
html_parts.append(' .metric-card .label { font-size: 13px; color: #8c8c8c; margin-bottom: 8px; }')
html_parts.append(' .metric-card .value { font-size: 28px; font-weight: 600; }')
html_parts.append(' .metric-card .unit { font-size: 14px; color: #8c8c8c; margin-left: 4px; }')
html_parts.append(' .metric-card.success .value { color: #52c41a; }')
html_parts.append(' .metric-card.warning .value { color: #faad14; }')
html_parts.append(' .metric-card.danger .value { color: #ff4d4f; }')
html_parts.append(' .metric-card.info .value { color: #1890ff; }')
html_parts.append(' .charts-grid {')
html_parts.append(' display: grid;')
html_parts.append(' grid-template-columns: 1fr 1fr;')
html_parts.append(' gap: 16px;')
html_parts.append(' margin-bottom: 20px;')
html_parts.append(' }')
html_parts.append(' .chart-card {')
html_parts.append(' background: white;')
html_parts.append(' border-radius: 12px;')
html_parts.append(' padding: 20px;')
html_parts.append(' box-shadow: 0 2px 8px rgba(0,0,0,0.06);')
html_parts.append(' }')
html_parts.append(' .chart-card.full-width { grid-column: 1 / -1; }')
html_parts.append(' .chart-card h3 {')
html_parts.append(' font-size: 16px;')
html_parts.append(' margin-bottom: 12px;')
html_parts.append(' color: #262626;')
html_parts.append(' font-weight: 600;')
html_parts.append(' }')
html_parts.append(' .chart-container { width: 100%; height: 320px; }')
html_parts.append(' .chart-container.tall { height: 400px; }')
html_parts.append(' .footer {')
html_parts.append(' text-align: center;')
html_parts.append(' color: #8c8c8c;')
html_parts.append(' font-size: 12px;')
html_parts.append(' padding: 16px 0;')
html_parts.append(' }')
html_parts.append(' @media (max-width: 900px) {')
html_parts.append(' .metrics-row { grid-template-columns: repeat(2, 1fr); }')
html_parts.append(' .charts-grid { grid-template-columns: 1fr; }')
html_parts.append(' }')
html_parts.append(' @media (max-width: 600px) {')
html_parts.append(' .metrics-row { grid-template-columns: 1fr; }')
html_parts.append(' body { padding: 12px; }')
html_parts.append(' }')
html_parts.append(' </style>')
html_parts.append('</head>')
html_parts.append('<body>')
html_parts.append(" background: #f0f2f5;")
html_parts.append(" color: #333;")
html_parts.append(" padding: 20px;")
html_parts.append(" }")
html_parts.append(" .container { max-width: 1400px; margin: 0 auto; }")
html_parts.append(" .header {")
html_parts.append(" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);")
html_parts.append(" color: white;")
html_parts.append(" padding: 24px 32px;")
html_parts.append(" border-radius: 12px;")
html_parts.append(" margin-bottom: 20px;")
html_parts.append(" }")
html_parts.append(" .header h1 { font-size: 24px; margin-bottom: 8px; }")
html_parts.append(" .header .subtitle { font-size: 14px; opacity: 0.9; }")
html_parts.append(" .header .meta { font-size: 12px; opacity: 0.8; margin-top: 8px; }")
html_parts.append(" .metrics-row {")
html_parts.append(" display: grid;")
html_parts.append(" grid-template-columns: repeat(4, 1fr);")
html_parts.append(" gap: 16px;")
html_parts.append(" margin-bottom: 20px;")
html_parts.append(" }")
html_parts.append(" .metric-card {")
html_parts.append(" background: white;")
html_parts.append(" border-radius: 12px;")
html_parts.append(" padding: 20px;")
html_parts.append(" box-shadow: 0 2px 8px rgba(0,0,0,0.06);")
html_parts.append(" transition: transform 0.2s;")
html_parts.append(" }")
html_parts.append(
" .metric-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }"
)
html_parts.append(" .metric-card .label { font-size: 13px; color: #8c8c8c; margin-bottom: 8px; }")
html_parts.append(" .metric-card .value { font-size: 28px; font-weight: 600; }")
html_parts.append(" .metric-card .unit { font-size: 14px; color: #8c8c8c; margin-left: 4px; }")
html_parts.append(" .metric-card.success .value { color: #52c41a; }")
html_parts.append(" .metric-card.warning .value { color: #faad14; }")
html_parts.append(" .metric-card.danger .value { color: #ff4d4f; }")
html_parts.append(" .metric-card.info .value { color: #1890ff; }")
html_parts.append(" .charts-grid {")
html_parts.append(" display: grid;")
html_parts.append(" grid-template-columns: 1fr 1fr;")
html_parts.append(" gap: 16px;")
html_parts.append(" margin-bottom: 20px;")
html_parts.append(" }")
html_parts.append(" .chart-card {")
html_parts.append(" background: white;")
html_parts.append(" border-radius: 12px;")
html_parts.append(" padding: 20px;")
html_parts.append(" box-shadow: 0 2px 8px rgba(0,0,0,0.06);")
html_parts.append(" }")
html_parts.append(" .chart-card.full-width { grid-column: 1 / -1; }")
html_parts.append(" .chart-card h3 {")
html_parts.append(" font-size: 16px;")
html_parts.append(" margin-bottom: 12px;")
html_parts.append(" color: #262626;")
html_parts.append(" font-weight: 600;")
html_parts.append(" }")
html_parts.append(" .chart-container { width: 100%; height: 320px; }")
html_parts.append(" .chart-container.tall { height: 400px; }")
html_parts.append(" .footer {")
html_parts.append(" text-align: center;")
html_parts.append(" color: #8c8c8c;")
html_parts.append(" font-size: 12px;")
html_parts.append(" padding: 16px 0;")
html_parts.append(" }")
html_parts.append(" @media (max-width: 900px) {")
html_parts.append(" .metrics-row { grid-template-columns: repeat(2, 1fr); }")
html_parts.append(" .charts-grid { grid-template-columns: 1fr; }")
html_parts.append(" }")
html_parts.append(" @media (max-width: 600px) {")
html_parts.append(" .metrics-row { grid-template-columns: 1fr; }")
html_parts.append(" body { padding: 12px; }")
html_parts.append(" }")
html_parts.append(" </style>")
html_parts.append("</head>")
html_parts.append("<body>")
html_parts.append(' <div class="container">')
html_parts.append(' <div class="header">')
html_parts.append(' <h1>📊 CI 健康度看板</h1>')
html_parts.append(" <h1>📊 CI 健康度看板</h1>")
html_parts.append(f' <div class="subtitle">仓库: {repo}</div>')
html_parts.append(f' <div class="meta">统计周期: {start_date} ~ {end_date} | 生成时间: {now_str}</div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(' <div class="metrics-row">')
html_parts.append(' <div class="metric-card success">')
html_parts.append(' <div class="label">总成功率</div>')
html_parts.append(f' <div class="value">{success_rate}<span class="unit">%</span></div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(' <div class="metric-card info">')
html_parts.append(' <div class="label">总 Run 数</div>')
html_parts.append(f' <div class="value">{total_runs}<span class="unit">次</span></div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(' <div class="metric-card warning">')
html_parts.append(' <div class="label">平均耗时</div>')
html_parts.append(f' <div class="value">{avg_dur_min}<span class="unit">分钟</span></div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(' <div class="metric-card danger">')
html_parts.append(' <div class="label">基础设施故障率</div>')
html_parts.append(f' <div class="value">{infra_fail_rate}<span class="unit">%</span></div>')
html_parts.append(' </div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(" </div>")
html_parts.append(' <div class="charts-grid">')
html_parts.append(' <div class="chart-card full-width">')
html_parts.append(' <h3>📈 CI 成功率趋势</h3>')
html_parts.append(" <h3>📈 CI 成功率趋势</h3>")
html_parts.append(' <div id="chart-success-rate" class="chart-container"></div>')
html_parts.append(' </div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(" </div>")
html_parts.append(' <div class="charts-grid">')
html_parts.append(' <div class="chart-card">')
html_parts.append(' <h3>⏱️ 各 Workflow 平均耗时</h3>')
html_parts.append(" <h3>⏱️ 各 Workflow 平均耗时</h3>")
html_parts.append(' <div id="chart-wf-duration" class="chart-container"></div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(' <div class="chart-card">')
html_parts.append(' <h3>❌ 失败原因分布</h3>')
html_parts.append(" <h3>❌ 失败原因分布</h3>")
html_parts.append(' <div id="chart-failure-pie" class="chart-container"></div>')
html_parts.append(' </div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(" </div>")
html_parts.append(' <div class="charts-grid">')
html_parts.append(' <div class="chart-card full-width">')
html_parts.append(' <h3>📋 各 Job 成功率排行(最低 15 名)</h3>')
html_parts.append(" <h3>📋 各 Job 成功率排行(最低 15 名)</h3>")
html_parts.append(' <div id="chart-job-success" class="chart-container tall"></div>')
html_parts.append(' </div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(" </div>")
html_parts.append(' <div class="charts-grid">')
html_parts.append(' <div class="chart-card full-width">')
html_parts.append(' <h3>📊 每日 Run 数量趋势</h3>')
html_parts.append(" <h3>📊 每日 Run 数量趋势</h3>")
html_parts.append(' <div id="chart-run-count" class="chart-container"></div>')
html_parts.append(' </div>')
html_parts.append(' </div>')
html_parts.append(" </div>")
html_parts.append(" </div>")
html_parts.append(' <div class="footer">')
html_parts.append(' 由 ci_dashboard.py 自动生成 | ECharts 可视化')
html_parts.append(' </div>')
html_parts.append(' </div>')
html_parts.append(' <script>')
html_parts.append(f' const DATA = {data_json};')
html_parts.append('')
html_parts.append(" 由 ci_dashboard.py 自动生成 | ECharts 可视化")
html_parts.append(" </div>")
html_parts.append(" </div>")
html_parts.append(" <script>")
html_parts.append(f" const DATA = {data_json};")
html_parts.append("")
# 图表 1: 成功率趋势
html_parts.append(' (function() {')
html_parts.append(" (function() {")
html_parts.append(' const chart = echarts.init(document.getElementById("chart-success-rate"));')
html_parts.append(' chart.setOption({')
html_parts.append(" chart.setOption({")
html_parts.append(' tooltip: { trigger: "axis", formatter: function(params) {')
html_parts.append(' const p = params[0]; return p.name + "<br/>成功率: <b>" + p.value + "%</b>";')
html_parts.append(' }},')
html_parts.append(" }},")
html_parts.append(' grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },')
html_parts.append(' xAxis: { type: "category", boundaryGap: false, data: DATA.daily_dates,')
html_parts.append(' axisLabel: { rotate: 30, fontSize: 11 } },')
html_parts.append(" axisLabel: { rotate: 30, fontSize: 11 } },")
html_parts.append(' yAxis: { type: "value", min: 0, max: 100, axisLabel: { formatter: "{value}%" } },')
html_parts.append(' series: [{ name: "成功率", type: "line", smooth: true, data: DATA.daily_success_rates,')
html_parts.append(
' series: [{ name: "成功率", type: "line", smooth: true, data: DATA.daily_success_rates,'
)
html_parts.append(' itemStyle: { color: "#52c41a" },')
html_parts.append(' areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [')
html_parts.append(" areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [")
html_parts.append(' { offset: 0, color: "rgba(82, 196, 26, 0.3)" },')
html_parts.append(' { offset: 1, color: "rgba(82, 196, 26, 0.05)" }')
html_parts.append(' ])},')
html_parts.append(" ])},")
html_parts.append(' markLine: { silent: true, data: [{ type: "average", name: "平均值",')
html_parts.append(' label: { formatter: "均值 {c}%" } }] }')
html_parts.append(' }]')
html_parts.append(' });')
html_parts.append(" }]")
html_parts.append(" });")
html_parts.append(' window.addEventListener("resize", () => chart.resize());')
html_parts.append(' })();')
html_parts.append('')
html_parts.append(" })();")
html_parts.append("")
# 图表 2: Workflow 耗时对比
html_parts.append(' (function() {')
html_parts.append(" (function() {")
html_parts.append(' const chart = echarts.init(document.getElementById("chart-wf-duration"));')
html_parts.append(' chart.setOption({')
html_parts.append(" chart.setOption({")
html_parts.append(' tooltip: { trigger: "axis", formatter: function(params) {')
html_parts.append(' const p = params[0]; return p.name + "<br/>平均耗时: <b>" + p.value + " 分钟</b>";')
html_parts.append(' }},')
html_parts.append(
' const p = params[0]; return p.name + "<br/>平均耗时: <b>" + p.value + " 分钟</b>";'
)
html_parts.append(" }},")
html_parts.append(' grid: { left: "3%", right: "4%", bottom: "15%", containLabel: true },')
html_parts.append(' xAxis: { type: "category", data: DATA.wf_names,')
html_parts.append(' axisLabel: { rotate: 30, fontSize: 10, interval: 0 } },')
html_parts.append(" axisLabel: { rotate: 30, fontSize: 10, interval: 0 } },")
html_parts.append(' yAxis: { type: "value", name: "分钟", axisLabel: { formatter: "{value} min" } },')
html_parts.append(' series: [{ name: "平均耗时", type: "bar", data: DATA.wf_avg_durations,')
html_parts.append(' itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [')
html_parts.append(" itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [")
html_parts.append(' { offset: 0, color: "#1890ff" },')
html_parts.append(' { offset: 1, color: "#096dd9" }')
html_parts.append(' ]), borderRadius: [4, 4, 0, 0] },')
html_parts.append(' barMaxWidth: 40')
html_parts.append(' }]')
html_parts.append(' });')
html_parts.append(" ]), borderRadius: [4, 4, 0, 0] },")
html_parts.append(" barMaxWidth: 40")
html_parts.append(" }]")
html_parts.append(" });")
html_parts.append(' window.addEventListener("resize", () => chart.resize());')
html_parts.append(' })();')
html_parts.append('')
html_parts.append(" })();")
html_parts.append("")
# 图表 3: 失败原因饼图
html_parts.append(' (function() {')
html_parts.append(" (function() {")
html_parts.append(' const chart = echarts.init(document.getElementById("chart-failure-pie"));')
html_parts.append(' chart.setOption({')
html_parts.append(" chart.setOption({")
html_parts.append(' tooltip: { trigger: "item", formatter: "{b}: {c} 次 ({d}%)" },')
html_parts.append(' legend: { orient: "vertical", right: "5%", top: "center" },')
html_parts.append(' series: [{ name: "失败原因", type: "pie", radius: ["40%", "70%"], center: ["35%", "50%"],')
html_parts.append(' avoidLabelOverlap: false,')
html_parts.append(
' series: [{ name: "失败原因", type: "pie", radius: ["40%", "70%"], center: ["35%", "50%"],'
)
html_parts.append(" avoidLabelOverlap: false,")
html_parts.append(' itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },')
html_parts.append(' label: { show: false, position: "center" },')
html_parts.append(' emphasis: { label: { show: true, fontSize: 16, fontWeight: "bold" } },')
html_parts.append(' labelLine: { show: false },')
html_parts.append(' data: DATA.failure_pie_data,')
html_parts.append(" labelLine: { show: false },")
html_parts.append(" data: DATA.failure_pie_data,")
html_parts.append(' color: ["#ff4d4f", "#faad14", "#8c8c8c"]')
html_parts.append(' }]')
html_parts.append(' });')
html_parts.append(" }]")
html_parts.append(" });")
html_parts.append(' window.addEventListener("resize", () => chart.resize());')
html_parts.append(' })();')
html_parts.append('')
html_parts.append(" })();")
html_parts.append("")
# 图表 4: Job 成功率排行(横向柱状图)
html_parts.append(' (function() {')
html_parts.append(" (function() {")
html_parts.append(' const chart = echarts.init(document.getElementById("chart-job-success"));')
html_parts.append(' const barData = DATA.job_rates.map(function(rate, i) {')
html_parts.append(' return { value: rate, itemStyle: {')
html_parts.append(" const barData = DATA.job_rates.map(function(rate, i) {")
html_parts.append(" return { value: rate, itemStyle: {")
html_parts.append(' color: rate >= 90 ? "#52c41a" : (rate >= 70 ? "#faad14" : "#ff4d4f")')
html_parts.append(' }};')
html_parts.append(' });')
html_parts.append(' chart.setOption({')
html_parts.append(" }};")
html_parts.append(" });")
html_parts.append(" chart.setOption({")
html_parts.append(' tooltip: { trigger: "axis", formatter: function(params) {')
html_parts.append(' const p = params[0]; return p.name + "<br/>成功率: <b>" + p.value + "%</b>";')
html_parts.append(' }},')
html_parts.append(" }},")
html_parts.append(' grid: { left: "3%", right: "8%", bottom: "3%", top: "3%", containLabel: true },')
html_parts.append(' xAxis: { type: "value", min: 0, max: 100, axisLabel: { formatter: "{value}%" } },')
html_parts.append(' yAxis: { type: "category", data: DATA.job_names, axisLabel: { fontSize: 11 } },')
html_parts.append(' series: [{ name: "成功率", type: "bar", data: barData, barWidth: "60%",')
html_parts.append(' label: { show: true, position: "right", formatter: "{c}%", fontSize: 11 }')
html_parts.append(' }]')
html_parts.append(' });')
html_parts.append(" }]")
html_parts.append(" });")
html_parts.append(' window.addEventListener("resize", () => chart.resize());')
html_parts.append(' })();')
html_parts.append('')
html_parts.append(" })();")
html_parts.append("")
# 图表 5: 每日 Run 数量趋势(面积图)
html_parts.append(' (function() {')
html_parts.append(" (function() {")
html_parts.append(' const chart = echarts.init(document.getElementById("chart-run-count"));')
html_parts.append(' chart.setOption({')
html_parts.append(" chart.setOption({")
html_parts.append(' tooltip: { trigger: "axis", formatter: function(params) {')
html_parts.append(' const p = params[0]; return p.name + "<br/>Run 数量: <b>" + p.value + " 次</b>";')
html_parts.append(' }},')
html_parts.append(
' const p = params[0]; return p.name + "<br/>Run 数量: <b>" + p.value + " 次</b>";'
)
html_parts.append(" }},")
html_parts.append(' grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },')
html_parts.append(' xAxis: { type: "category", boundaryGap: false, data: DATA.daily_dates,')
html_parts.append(' axisLabel: { rotate: 30, fontSize: 11 } },')
html_parts.append(" axisLabel: { rotate: 30, fontSize: 11 } },")
html_parts.append(' yAxis: { type: "value", name: "次数" },')
html_parts.append(' series: [{ name: "Run 数量", type: "line", smooth: true, data: DATA.daily_run_counts,')
html_parts.append(
' series: [{ name: "Run 数量", type: "line", smooth: true, data: DATA.daily_run_counts,'
)
html_parts.append(' itemStyle: { color: "#722ed1" },')
html_parts.append(' areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [')
html_parts.append(" areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [")
html_parts.append(' { offset: 0, color: "rgba(114, 46, 209, 0.3)" },')
html_parts.append(' { offset: 1, color: "rgba(114, 46, 209, 0.05)" }')
html_parts.append(' ])},')
html_parts.append(" ])},")
html_parts.append(' markLine: { silent: true, data: [{ type: "average", name: "平均值",')
html_parts.append(' label: { formatter: "均值 {c}" } }] }')
html_parts.append(' }]')
html_parts.append(' });')
html_parts.append(" }]")
html_parts.append(" });")
html_parts.append(' window.addEventListener("resize", () => chart.resize());')
html_parts.append(' })();')
html_parts.append(' </script>')
html_parts.append('</body>')
html_parts.append('</html>')
html_parts.append(" })();")
html_parts.append(" </script>")
html_parts.append("</body>")
html_parts.append("</html>")
return "\n".join(html_parts)