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
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:
+217
-184
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user