You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
function fmtUSD(n){return "$"+(Number(n)||0).toFixed(2)}
154
156
function fmtPct(n){return ((Number(n)||0)*100).toFixed(0)+"%"}
155
157
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"}
158
160
function clear(el){el.replaceChildren()}
159
161
function el(tag,className,text){const n=document.createElement(tag);if(className)n.className=className;if(text!==undefined)n.textContent=String(text);return n}
160
162
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
168
170
function badge(kind,text){return el("span","badge badge-"+kind,text)}
169
171
function loading(title,text){return '<div class="loading"><div><div class="spinner"></div><strong>'+title+'</strong><span>'+text+'</span></div></div>'}
170
172
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(";"))}}
173
175
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")}
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
+
175
180
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}
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=
195
200
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)}
196
201
function safeAvatarUrl(value){try{const u=new URL(value);return u.protocol==="https:"?u.toString():""}catch(e){return ""}}
197
202
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){}}
0 commit comments