Skip to content

Commit 15d58c2

Browse files
authored
feat: add org health dashboard
Harden dashboard/org-health behavior and central Codex PR review workflow to avoid repeated run failures.
1 parent 4fe8e56 commit 15d58c2

9 files changed

Lines changed: 1338 additions & 21 deletions

File tree

.github/actionlint.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
self-hosted-runner:
22
labels:
33
- codex-vps
4+
paths:
5+
.github/workflows/codex_pr_review.yml:
6+
ignore:
7+
- 'workflow_repository'
8+
- 'workflow_sha'

.github/workflows/codex_pr_review.yml

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@ name: Codex PR Review
55
# uses: QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@main
66
# secrets: inherit
77
on:
8-
pull_request:
8+
pull_request_target:
99
types: [opened, synchronize, reopened]
1010
workflow_call:
11+
inputs:
12+
caller_concurrency_key:
13+
description: "Stable caller-side key used to cancel stale review jobs for the same PR."
14+
required: false
15+
type: string
16+
secrets:
17+
CODEX_AUDIT_REUSABLE_WORKFLOW_TOKEN:
18+
description: "Token that can read QuantStrategyLab/AIAuditBridge when this workflow is called from another private repo."
19+
required: false
20+
ANTHROPIC_API_KEY:
21+
required: false
22+
OPENAI_API_KEY:
23+
required: false
24+
CODEX_AUDIT_SERVICE_URL:
25+
required: false
1126

1227
permissions:
1328
contents: read
@@ -16,19 +31,33 @@ permissions:
1631
pull-requests: write
1732

1833
concurrency:
19-
group: codex-pr-review-${{ github.event.pull_request.number }}
34+
group: codex-pr-review-${{ github.repository }}-${{ inputs.caller_concurrency_key || github.event.pull_request.number || github.run_id }}
2035
cancel-in-progress: true
2136

2237
jobs:
2338
review:
39+
if: github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository
2440
runs-on: ubuntu-latest
2541
timeout-minutes: 30
2642
env:
2743
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
2844

2945
steps:
30-
- name: Checkout
46+
- name: Checkout review target
3147
uses: actions/checkout@v6
48+
with:
49+
path: source
50+
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
51+
persist-credentials: false
52+
53+
- name: Checkout bridge review scripts
54+
uses: actions/checkout@v6
55+
with:
56+
repository: ${{ github.repository == 'QuantStrategyLab/AIAuditBridge' && 'QuantStrategyLab/AIAuditBridge' || job.workflow_repository }}
57+
ref: ${{ github.repository == 'QuantStrategyLab/AIAuditBridge' && github.event.pull_request.base.sha || job.workflow_sha }}
58+
path: bridge
59+
token: ${{ secrets.CODEX_AUDIT_REUSABLE_WORKFLOW_TOKEN || github.token }}
60+
persist-credentials: false
3261

3362
- name: Set up Python
3463
uses: actions/setup-python@v6
@@ -45,12 +74,23 @@ jobs:
4574
OPENAI_MODEL: ${{ vars.OPENAI_MODEL || 'gpt-5.4-mini' }}
4675
CODEX_AUDIT_SERVICE_URL: ${{ secrets.CODEX_AUDIT_SERVICE_URL }}
4776
CODEX_AUDIT_SERVICE_AUDIENCE: ${{ vars.CODEX_AUDIT_SERVICE_AUDIENCE || 'quant-codex-audit' }}
48-
run: python scripts/run_codex_pr_review.py
77+
CODEX_PR_REVIEW_REPO_ROOT: ${{ github.workspace }}/source
78+
working-directory: source
79+
run: |
80+
set -euo pipefail
81+
bridge_script="${GITHUB_WORKSPACE}/bridge/scripts/run_codex_pr_review.py"
82+
if [ -f "${bridge_script}" ]; then
83+
script_path="${bridge_script}"
84+
else
85+
echo "::error::Trusted Codex review script not found. Ensure the bridge checkout can read QuantStrategyLab/AIAuditBridge." >&2
86+
exit 1
87+
fi
88+
python "${script_path}"
4989
5090
- name: Upload review diagnostics
5191
if: always()
5292
uses: actions/upload-artifact@v7
5393
with:
54-
name: codex-pr-review-${{ github.event.pull_request.number }}-${{ github.run_id }}
55-
path: data/output/codex_pr_review/
94+
name: codex-pr-review-${{ github.event.pull_request.number || github.run_id }}-${{ github.run_id }}
95+
path: source/data/output/codex_pr_review/
5696
if-no-files-found: warn

cloudflare/ai-gateway-dash/src/index.mjs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const COOKIE_MAX_AGE = 86400; // 24h
2020
const SESSION_SECRET_LENGTH = 32;
2121
const DASHBOARD_API_ROUTES = new Set([
2222
"/v1/ai/health",
23+
"/v1/ai/org-health",
2324
"/v1/ai/quota",
2425
"/v1/ai/changes",
2526
"/v1/ai/changes/effectiveness",
@@ -79,7 +80,7 @@ const LOGIN_HTML = `<!DOCTYPE html>
7980
<div class="capabilities">
8081
<h2>登录后,您可以查看</h2>
8182
<ul>
82-
<li>服务整体健康状态与关键接口指标摘要</li>
83+
<li>服务整体健康状态、组织仓库 Actions 与关键接口指标摘要</li>
8384
<li>各仓库 / 团队的用量与额度概览</li>
8485
<li>变更有效性评估(90 天)</li>
8586
<li>影子审计分歧统计与裁决结果</li>
@@ -137,6 +138,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
137138
<main>
138139
<section class="grid">
139140
<article class="card span-6"><div class="card-body"><div class="card-head"><h2>服务健康</h2><span class="updated" id="health-updated">更新: —</span></div><div id="health"></div></div></article>
141+
<article class="card span-6"><div class="card-body"><div class="card-head"><h2>组织健康</h2><span class="updated" id="org-health-updated">更新: —</span></div><div id="org-health"></div></div></article>
140142
<article class="card span-6"><div class="card-body"><div class="card-head"><h2>用量与额度</h2><span class="updated" id="quota-updated">更新: —</span></div><div id="quota"></div></div></article>
141143
<article class="card span-4"><div class="card-body"><div class="card-head"><h2>有效性 · 90 天</h2><span class="updated" id="effectiveness-updated">更新: —</span></div><div id="effectiveness"></div></div></article>
142144
<article class="card span-4"><div class="card-body"><div class="card-head"><h2>影子审计分歧</h2><span class="updated" id="shadow-updated">更新: —</span></div><div id="shadow"></div></div></article>
@@ -153,8 +155,8 @@ const API="/api";let refreshTimer;
153155
function fmtUSD(n){return "$"+(Number(n)||0).toFixed(2)}
154156
function fmtPct(n){return ((Number(n)||0)*100).toFixed(0)+"%"}
155157
function fmtNum(n){return (Number(n)||0).toLocaleString()}
156-
function clsStatus(st){return st==="healthy"?"ok":st==="degraded"?"warn":"err"}
157-
function statusText(st){return st==="healthy"?"健康":st==="degraded"?"部分异常":st==="unhealthy"?"异常":st||"unknown"}
158+
function clsStatus(st){return st==="healthy"?"ok":st==="degraded"||st==="unknown"?"warn":"err"}
159+
function statusText(st){return st==="healthy"?"健康":st==="degraded"?"部分异常":st==="unknown"?"未知":st==="unhealthy"?"异常":st||"unknown"}
158160
function clear(el){el.replaceChildren()}
159161
function el(tag,className,text){const n=document.createElement(tag);if(className)n.className=className;if(text!==undefined)n.textContent=String(text);return n}
160162
function append(parent){for(let i=1;i<arguments.length;i++)parent.appendChild(arguments[i]);return parent}
@@ -168,10 +170,13 @@ function statRow(label,value,valueClass){const row=el("div","stat-row");append(r
168170
function badge(kind,text){return el("span","badge badge-"+kind,text)}
169171
function loading(title,text){return '<div class="loading"><div><div class="spinner"></div><strong>'+title+'</strong><span>'+text+'</span></div></div>'}
170172
function progressClass(p){return p>80?"err":p>50?"warn":"ok"}
171-
async function fetchJSON(path){const r=await fetch(API+path);let data=null;try{data=await r.json()}catch(e){}if(!r.ok||data&&data.status==="error"){const detail=data&&data.error?": "+data.error:"";throw new Error(path+": "+r.status+detail)}if(!data)throw new Error(path+": invalid JSON");return data}
172-
async function refresh(){const requests=[["/v1/ai/health","health","服务健康加载失败",renderHealth],["/v1/ai/quota","quota","配额消耗加载失败",d=>renderQuota(d.quota||d)],["/v1/ai/changes/effectiveness?days=90","effectiveness","有效性报告加载失败",d=>renderEffectiveness(d.report||d)],["/v1/ai/feedback/shadow","shadow","影子审计分歧加载失败",d=>renderShadow(d.disagreements||[])],["/v1/ai/changes?days=7","changes","最近变更加载失败",d=>renderChangePanels(d.changes||[])]],results=await Promise.allSettled(requests.map(r=>fetchJSON(r[0]))),failures=[];results.forEach((result,i)=>{const item=requests[i];try{if(result.status==="fulfilled")item[3](result.value);else throw result.reason}catch(e){failures.push(publicErrorMessage(e));if(item[1]==="changes"){panelError("autonomy","自治决策加载失败",e);panelError("human-audit","人工审计队列加载失败",e)}panelError(item[1],item[2],e)}});document.getElementById("last-refresh").textContent=new Date().toLocaleTimeString();if(failures.length){setStatus("error");showToast("API: "+failures.slice(0,2).join(";"))}}
173+
async function fetchJSON(path){const r=await fetch(API+path);let data=null;try{data=await r.json()}catch(e){}if(!r.ok||data&&data.status==="error"){if(path==="/v1/ai/org-health"&&r.status===503&&data&&data.status==="unavailable")return data;const detail=data&&data.error?": "+data.error:"";throw new Error(path+": "+r.status+detail)}if(!data)throw new Error(path+": invalid JSON");return data}
174+
async function refresh(){const requests=[["/v1/ai/health","health","服务健康加载失败",renderHealth],["/v1/ai/org-health","org-health","组织健康加载失败",renderOrgHealth],["/v1/ai/quota","quota","配额消耗加载失败",d=>renderQuota(d.quota||d)],["/v1/ai/changes/effectiveness?days=90","effectiveness","有效性报告加载失败",d=>renderEffectiveness(d.report||d)],["/v1/ai/feedback/shadow","shadow","影子审计分歧加载失败",d=>renderShadow(d.disagreements||[])],["/v1/ai/changes?days=7","changes","最近变更加载失败",d=>renderChangePanels(d.changes||[])]],results=await Promise.allSettled(requests.map(r=>fetchJSON(r[0]))),failures=[];results.forEach((result,i)=>{const item=requests[i];try{if(result.status==="fulfilled")item[3](result.value);else throw result.reason}catch(e){failures.push(publicErrorMessage(e));if(item[1]==="changes"){panelError("autonomy","自治决策加载失败",e);panelError("human-audit","人工审计队列加载失败",e)}panelError(item[1],item[2],e)}});document.getElementById("last-refresh").textContent=new Date().toLocaleTimeString();if(failures.length){setStatus("error");showToast("API: "+failures.slice(0,2).join(";"))}}
173175
function healthReasonText(r){const path=r.path||"endpoint";if(r.reason==="error_rate")return path+" 错误率 "+fmtPct(r.value||0)+" ≥ "+fmtPct(r.threshold||0);if(r.reason==="p95_latency_ms")return path+" P95 "+fmtNum(r.value||0)+" ms ≥ "+fmtNum(r.threshold||0)+" ms";return path+" "+(r.reason||"degraded")}
174176
function renderHealth(h){setStatus(h.status||"unknown");setUpdated("health-updated");const target=document.getElementById("health"),endpoints=h.endpoints||[],reasons=h.degradation_reasons||[],statusEndpoints=endpoints.filter(i=>i.latency_profile!=="background_job"),backgroundEndpoints=endpoints.filter(i=>i.latency_profile==="background_job"),total=endpoints.reduce((s,i)=>s+(Number(i.total)||0),0),errors=endpoints.reduce((s,i)=>s+(Number(i.errors)||0),0),p95=Math.max(0,...statusEndpoints.map(i=>Number(i.p95_ms)||0)),backgroundP95=Math.max(0,...backgroundEndpoints.map(i=>Number(i.p95_ms)||0)),details=el("div","");append(details,statRow("接口数量",endpoints.length,"info"),statRow("总请求",fmtNum(total),""),statRow("错误",fmtNum(errors),errors?"err":"ok"),statRow("在线接口 P95",fmtNum(p95)+" ms",p95>30000?"err":p95>10000?"warn":"ok"));if(backgroundP95)append(details,statRow("后台任务 P95",fmtNum(backgroundP95)+" ms","info"));if(reasons.length){reasons.slice(0,3).forEach((reason,i)=>append(details,statRow(i?"降级原因 "+(i+1):"降级原因",healthReasonText(reason),clsStatus(reason.severity||h.status))));if(reasons.length>3)append(details,statRow("更多原因","+"+fmtNum(reasons.length-3),"warn"))}else if(h.last_error&&h.last_error.message){append(details,statRow("最近错误",String(h.last_error.message).slice(0,80),"warn"))}clear(target);append(target,append(el("div","metric-line"),el("div","big ",statusText(h.status||"unknown")),el("span","delta "+clsStatus(h.status||"unknown"),fmtNum(Math.round((h.uptime_seconds||0)/3600))+" h")),details)}
177+
function orgReasonText(reason){return reason==="latest_run_in_progress"?"最新 workflow 运行中":reason==="no_workflow_runs"?"暂无 workflow run":reason==="github_api_error"?"GitHub API 读取失败":String(reason||"需要关注").replaceAll("_"," ")}
178+
function renderOrgHealth(h){setUpdated("org-health-updated");const target=document.getElementById("org-health"),provider=h.provider||{},summary=h.summary||{},repos=h.repositories||[];clear(target);if(provider.status==="unavailable"){empty(target,"组织健康未连接","配置 CODEX_AUDIT_SERVICE_GITHUB_TOKEN 后,会显示组织仓库 Actions 健康。",);return}const unhealthy=Number(summary.unhealthy_repositories)||0,unknown=Number(summary.unknown_repositories)||0,degraded=Number(summary.degraded_repositories)||0,failed=Number(summary.failed_workflow_runs)||0,running=Number(summary.in_progress_workflow_runs)||0,total=Number(summary.total_repositories)||repos.length,problemRepos=repos.filter(r=>r.status!=="healthy").slice(0,5),statusClass=unhealthy?"err":degraded||unknown?"warn":"ok";append(target,append(el("div","metric-line"),el("div","big ",unhealthy?"异常":degraded?"关注":unknown?"未知":"健康"),el("span","delta "+statusClass,fmtNum(total)+" repos")));const details=el("div","");append(details,statRow("异常仓库",unhealthy,unhealthy?"err":"ok"),statRow("未知仓库",unknown,unknown?"warn":"ok"),statRow("需关注仓库",degraded,degraded?"warn":"ok"),statRow("失败 workflow",failed,failed?"err":"ok"),statRow("运行中",running,running?"info":"ok"));append(target,details);if(problemRepos.length){const list=el("div","");for(const r of problemRepos){const latest=r.latest_run||{},problem=r.problem_run||{},repoName=String(r.repo||"").split("/").pop()||"—",label=(problem.name||latest.name||orgReasonText((r.reasons||[])[0])),runUrl=problem.url||latest.url;const row=el("div","stat-row"),value=el("span","stat-value "+clsStatus(r.status));if(runUrl){const a=el("a","",label);a.href=runUrl;a.target="_blank";a.rel="noreferrer";a.style.color="inherit";a.style.textDecoration="none";value.appendChild(a)}else value.textContent=label;append(row,el("span","stat-label",repoName),value);list.appendChild(row)}target.appendChild(quotaSection("需要处理",list))}}
179+
175180
function quotaTotalRow(label,d){const row=el("div","stat-row"),used=Number(d&&d.total_cost_usd)||0;append(row,el("span","stat-label",label),el("span","stat-value info",fmtUSD(used)));return row}
176181
function codexAccountRow(d){const row=el("div","stat-row"),r=d&&d.rate_limits||{},p=r.primary||{},s=r.secondary||{},credits=r.credits||{},parts=[];if(p.used_percent!==null&&p.used_percent!==undefined)parts.push((p.window_duration_mins?Math.round(p.window_duration_mins/60)+"h":"主窗口")+" "+p.used_percent+"%");if(s.used_percent!==null&&s.used_percent!==undefined)parts.push((s.window_duration_mins?Math.round(s.window_duration_mins/1440)+"d":"次窗口")+" "+s.used_percent+"%");if(r.plan_type)parts.push(String(r.plan_type));if(credits.balance!==null&&credits.balance!==undefined)parts.push("credits "+credits.balance);append(row,el("span","stat-label","Codex 账户"),el("span","stat-value "+(Number(p.used_percent)>80?"err":Number(p.used_percent)>50?"warn":"ok"),parts.join(" · ")||"实时可用"));return row}
177182
function providerUsageBreakdown(u){const input=Number(u&&u.input_tokens)||0,output=Number(u&&u.output_tokens)||0,uncached=Number(u&&u.uncached_input_tokens)||0,cached=Number(u&&u.input_cached_tokens)||0,parts=[];if(input||output||uncached)parts.push("in "+fmtNum(input||uncached)+" / out "+fmtNum(output));if(cached)parts.push("cached "+fmtNum(cached));return parts.join(" · ")}
@@ -195,7 +200,7 @@ function renderHumanAudit(items){setUpdated("human-audit-updated");const target=
195200
function renderChanges(items){setUpdated("changes-updated");const target=document.getElementById("changes");clear(target);if(!items.length){empty(target,"暂无变更记录","最近 7 天没有登记的 AI 变更;后续通过反馈接口登记后会在这里形成报告。");return}const table=el("table",""),thead=el("thead",""),tr=el("tr","");for(const h of ["仓库","操作","风险","效果","置信度","时间"])tr.appendChild(el("th","",h));thead.appendChild(tr);const tbody=el("tbody","");for(const c of items){const effect=c.effect||"pending",effectClass=effect==="improved"?"ok":effect==="degraded"?"err":"info",actionClass=c.action==="auto_merge"?"ok":c.action==="auto_pr"?"info":c.action?"violet":"warn",dt=c.created_at?new Date(c.created_at*1000).toLocaleDateString():"—",row=el("tr",""),repoName=String(c.repo||"").split("/").pop()||"—",repoCell=el("td","mono");if(c.external_url){const a=el("a","",repoName);a.href=c.external_url;a.target="_blank";a.rel="noreferrer";a.style.color="#93c5fd";a.style.textDecoration="none";repoCell.appendChild(a)}else repoCell.textContent=repoName;append(row,repoCell,append(el("td",""),badge(actionClass,actionText(c.action))),append(el("td",""),badge(riskClass(c.risk),riskText(c.risk))),el("td","stat-value "+effectClass,effectText(effect)),el("td","mono",fmtPct(c.confidence||0)),el("td","mono",dt));tbody.appendChild(row)}append(table,thead,tbody);target.appendChild(table)}
196201
function safeAvatarUrl(value){try{const u=new URL(value);return u.protocol==="https:"?u.toString():""}catch(e){return ""}}
197202
async function loadUser(){try{const r=await fetch("/api/user");if(r.ok){const u=await r.json(),img=document.getElementById("avatar"),avatar=safeAvatarUrl(u.avatar_url);document.getElementById("username").textContent=u.login||"";if(avatar){img.src=avatar;img.style.display="block"}else{img.removeAttribute("src");img.style.display="none"}}}catch(e){}}
198-
document.getElementById("health").innerHTML=loading("加载中…","正在获取服务健康数据");document.getElementById("quota").innerHTML=loading("加载中…","正在获取配额消耗数据");document.getElementById("effectiveness").innerHTML=loading("加载中…","正在获取有效性报告");document.getElementById("shadow").innerHTML=loading("加载中…","正在获取影子审计分歧数据");document.getElementById("autonomy").innerHTML=loading("加载中…","正在获取自治决策数据");document.getElementById("human-audit").innerHTML=loading("加载中…","正在获取人工审计队列");document.getElementById("changes").innerHTML=loading("加载中…","正在获取最近变更数据");loadUser();refresh();refreshTimer=setInterval(refresh,30000);
203+
document.getElementById("health").innerHTML=loading("加载中…","正在获取服务健康数据");document.getElementById("org-health").innerHTML=loading("加载中…","正在获取组织健康数据");document.getElementById("quota").innerHTML=loading("加载中…","正在获取配额消耗数据");document.getElementById("effectiveness").innerHTML=loading("加载中…","正在获取有效性报告");document.getElementById("shadow").innerHTML=loading("加载中…","正在获取影子审计分歧数据");document.getElementById("autonomy").innerHTML=loading("加载中…","正在获取自治决策数据");document.getElementById("human-audit").innerHTML=loading("加载中…","正在获取人工审计队列");document.getElementById("changes").innerHTML=loading("加载中…","正在获取最近变更数据");loadUser();refresh();refreshTimer=setInterval(refresh,30000);
199204
</script>
200205
</body></html>`;
201206

cloudflare/ai-gateway-dash/tests/index.test.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,10 @@ test("buildDashboardApiUrl rejects insecure origins", () => {
3737
/HTTPS/,
3838
);
3939
});
40+
41+
test("buildDashboardApiUrl allows org health route", () => {
42+
assert.equal(
43+
buildDashboardApiUrl("https://origin.example", "/v1/ai/org-health", ""),
44+
"https://origin.example/v1/ai/org-health",
45+
);
46+
});

0 commit comments

Comments
 (0)