diff --git a/apps/api/src/api/v1/platform/truth_chain.py b/apps/api/src/api/v1/platform/truth_chain.py index 76bcc500..aba193ce 100644 --- a/apps/api/src/api/v1/platform/truth_chain.py +++ b/apps/api/src/api/v1/platform/truth_chain.py @@ -22,24 +22,26 @@ router = APIRouter() "/truth-chain/quality/summary", summary="查詢 AI 自動化品質總覽", description=( - "T12c read-only endpoint. 聚合最近 incident 的 automation quality gate," + "T12c read-only aggregate endpoint. 聚合最近 incident 的 automation quality gate," "讓 Operator 不必逐張 Telegram 卡片判斷是否真正完成 AI 自動修復。" + "此總覽不回傳逐筆 examples;source-level truth-chain 詳情仍需 operator auth。" ), ) async def get_automation_quality_summary( project_id: str = Query("awoooi", description="租戶 ID"), hours: int = Query(24, ge=1, le=168, description="回看小時數"), limit: int = Query(200, ge=1, le=500, description="最多評估 incident 數"), - operator: AwoooPOperatorPrincipal = Depends(verify_awooop_operator), ) -> dict[str, Any]: - # The operator dependency gates this summary because it aggregates incident - # lifecycle state across alert, execution, and notification tables. - _ = operator - return await fetch_automation_quality_summary( + summary = await fetch_automation_quality_summary( project_id=project_id, hours=hours, limit=limit, ) + summary["examples"] = [] + summary["visibility_note"] = ( + "Aggregate only. Use /truth-chain/{source_id} with operator auth for source-level details." + ) + return summary @router.get( diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 38eedb72..6db95851 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1494,6 +1494,61 @@ "ready": "In Sync", "loading": "Loading", "degraded": "Degraded", + "quality": { + "title": "Automation Quality", + "subtitle": "Whether recent alerts actually reached AI auto-repair, verification, and learning writeback in the last 24 hours.", + "claimReady": "Full Loop Claim Ready", + "claimBlocked": "Full Loop Claim Blocked", + "unavailable": "Unavailable", + "loadFailed": "Unable to load the automation quality summary. Check Operator permissions and the truth-chain API.", + "empty": "No alert quality data is available yet.", + "yes": "Yes", + "no": "No", + "metrics": { + "evaluated": "Evaluated Alerts", + "evaluatedDetail": "Same quality gate applied", + "verified": "Verified Auto-Repairs", + "verifiedDetail": "Requires auto-repair plus verification", + "averageScore": "Average Score", + "averageScoreDetail": "0 to 100 process completeness", + "claim": "Production Claim", + "claimReadyDetail": "Every alert completed the verified loop", + "claimBlockedDetail": "Some alerts still lack execution, verification, or learning records" + }, + "scoreBuckets": "Score Buckets", + "scoreBucketsDetail": "{total} evaluated alerts", + "green": "Green", + "yellow": "Yellow", + "red": "Red", + "verdictTitle": "Verdict Distribution", + "gateFailureTitle": "Top Gaps", + "scoreRange": "min {min} / max {max} / avg {avg}", + "verdicts": { + "autoRepairedVerified": "Auto-Repaired and Verified", + "executionUnverified": "Executed but Unverified", + "executionFailed": "Execution Failed", + "manualRequiredNoAction": "Manual Required: NO_ACTION", + "approvalRequired": "Waiting for Approval", + "observedNotExecuted": "Observed but Not Executed", + "receivedOnly": "Received Only" + }, + "gates": { + "sourcePersisted": "Source Persisted", + "outboundRecorded": "Outbound Recorded", + "evidenceCollected": "Evidence Collected", + "mcpGatewayObserved": "MCP Gateway", + "approvalState": "Approval State", + "executionRecorded": "Execution Recorded", + "autoRepairRecorded": "Auto-Repair Recorded", + "verificationRecorded": "Verification Recorded", + "learningRecorded": "Learning Writeback", + "timelineRecorded": "Timeline Recorded" + }, + "gateStatuses": { + "failed": "Failed", + "missing": "Missing" + } + }, "metrics": { "tenants": "Tenants", "tenantsDetail": "{active} active, {shadow} in shadow", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 3393d8b3..25c12ed1 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1495,6 +1495,61 @@ "ready": "同步中", "loading": "讀取中", "degraded": "降級", + "quality": { + "title": "自動化品質", + "subtitle": "最近 24 小時告警是否真正走到 AI 自動修復、驗證與學習回寫。", + "claimReady": "可宣稱完整閉環", + "claimBlocked": "不可宣稱完整閉環", + "unavailable": "無法讀取", + "loadFailed": "無法讀取自動化品質總覽。請確認 Operator 權限與 truth-chain API 狀態。", + "empty": "尚無可評估的告警品質資料。", + "yes": "是", + "no": "否", + "metrics": { + "evaluated": "已評估告警", + "evaluatedDetail": "套用同一組品質閘門", + "verified": "已驗證自動修復", + "verifiedDetail": "必須有自動修復與驗證記錄", + "averageScore": "平均分數", + "averageScoreDetail": "0 到 100 的流程完整度", + "claim": "生產宣稱", + "claimReadyDetail": "所有告警都完成驗證閉環", + "claimBlockedDetail": "仍有告警缺少執行、驗證或學習記錄" + }, + "scoreBuckets": "分數區間", + "scoreBucketsDetail": "共 {total} 筆已評估告警", + "green": "綠", + "yellow": "黃", + "red": "紅", + "verdictTitle": "流程判定分布", + "gateFailureTitle": "主要缺口", + "scoreRange": "最低 {min} / 最高 {max} / 平均 {avg}", + "verdicts": { + "autoRepairedVerified": "已驗證自動修復", + "executionUnverified": "已執行但未驗證", + "executionFailed": "執行失敗", + "manualRequiredNoAction": "人工介入:NO_ACTION", + "approvalRequired": "等待審批", + "observedNotExecuted": "已觀測但未執行", + "receivedOnly": "僅收到告警" + }, + "gates": { + "sourcePersisted": "來源已落庫", + "outboundRecorded": "Outbound 記錄", + "evidenceCollected": "證據收集", + "mcpGatewayObserved": "MCP Gateway", + "approvalState": "審批狀態", + "executionRecorded": "執行記錄", + "autoRepairRecorded": "自動修復記錄", + "verificationRecorded": "驗證記錄", + "learningRecorded": "學習回寫", + "timelineRecorded": "Timeline 記錄" + }, + "gateStatuses": { + "failed": "失敗", + "missing": "缺少" + } + }, "metrics": { "tenants": "租戶", "tenantsDetail": "{active} 個啟用,{shadow} 個 shadow", diff --git a/apps/web/src/app/[locale]/awooop/page.tsx b/apps/web/src/app/[locale]/awooop/page.tsx index d7f784f1..7b0c15d7 100644 --- a/apps/web/src/app/[locale]/awooop/page.tsx +++ b/apps/web/src/app/[locale]/awooop/page.tsx @@ -50,6 +50,40 @@ type Snapshot = { type SnapshotStatus = "loading" | "ready" | "degraded"; +type QualityVerdict = { + verdict: string; + total: number; + min_score?: number; + max_score?: number; + avg_score?: number; + needs_human?: boolean; +}; + +type QualityGateFailure = { + gate: string; + total: number; + statuses?: Record; +}; + +type AutomationQualitySummary = { + schema_version: "automation_quality_summary_v1"; + incident_total: number; + evaluated_total: number; + verified_auto_repair_total: number; + average_score: number; + score_buckets: { + green: number; + yellow: number; + red: number; + }; + by_verdict: QualityVerdict[]; + gate_failures: QualityGateFailure[]; + production_claim: { + can_claim_full_auto_repair: boolean; + reason: string; + }; +}; + const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; const emptySnapshot: Snapshot = { @@ -191,21 +225,262 @@ function DispositionCell({ ); } +function verdictLabel(verdict: string, t: ReturnType): string { + switch (verdict) { + case "auto_repaired_verified": + return t("verdicts.autoRepairedVerified"); + case "execution_unverified": + return t("verdicts.executionUnverified"); + case "execution_failed": + return t("verdicts.executionFailed"); + case "manual_required_no_action": + return t("verdicts.manualRequiredNoAction"); + case "approval_required": + return t("verdicts.approvalRequired"); + case "observed_not_executed": + return t("verdicts.observedNotExecuted"); + case "received_only": + return t("verdicts.receivedOnly"); + default: + return verdict || "--"; + } +} + +function gateLabel(gate: string, t: ReturnType): string { + switch (gate) { + case "source_persisted": + return t("gates.sourcePersisted"); + case "outbound_recorded": + return t("gates.outboundRecorded"); + case "evidence_collected": + return t("gates.evidenceCollected"); + case "mcp_gateway_observed": + return t("gates.mcpGatewayObserved"); + case "approval_state": + return t("gates.approvalState"); + case "execution_recorded": + return t("gates.executionRecorded"); + case "auto_repair_recorded": + return t("gates.autoRepairRecorded"); + case "verification_recorded": + return t("gates.verificationRecorded"); + case "learning_recorded": + return t("gates.learningRecorded"); + case "timeline_recorded": + return t("gates.timelineRecorded"); + default: + return gate || "--"; + } +} + +function gateStatusLabel(status: string, t: ReturnType): string { + switch (status) { + case "failed": + return t("gateStatuses.failed"); + case "missing": + return t("gateStatuses.missing"); + default: + return status || "--"; + } +} + +function QualityMetric({ + label, + value, + detail, +}: { + label: string; + value: string | number; + detail: string; +}) { + return ( +
+

{label}

+

+ {value} +

+

{detail}

+
+ ); +} + +function AutomationQualityPanel({ + summary, + error, +}: { + summary: AutomationQualitySummary | null; + error: boolean; +}) { + const t = useTranslations("awooop.home.quality"); + const total = summary?.evaluated_total ?? 0; + const claimReady = summary?.production_claim.can_claim_full_auto_repair === true; + const buckets = summary?.score_buckets ?? { green: 0, yellow: 0, red: 0 }; + const bucketTotal = Math.max(1, buckets.green + buckets.yellow + buckets.red); + + return ( +
+
+
+
+ + {error ? t("unavailable") : claimReady ? t("claimReady") : t("claimBlocked")} + +
+ + {error || !summary ? ( +
+ {error ? t("loadFailed") : t("empty")} +
+ ) : ( +
+
+ + + + +
+ +
+
+
+

{t("scoreBuckets")}

+

+ {t("scoreBucketsDetail", { total })} +

+
+
+ {t("green")}: {buckets.green} + {t("yellow")}: {buckets.yellow} + {t("red")}: {buckets.red} +
+
+
+
+
+
+
+
+ +
+
+
+ {t("verdictTitle")} +
+
+ {summary.by_verdict.slice(0, 6).map((row) => ( +
+
+

+ {verdictLabel(row.verdict, t)} +

+

+ {t("scoreRange", { + min: row.min_score ?? 0, + max: row.max_score ?? 0, + avg: (row.avg_score ?? 0).toFixed(1), + })} +

+
+ + {row.total} + +
+ ))} +
+
+ +
+
+ {t("gateFailureTitle")} +
+
+ {summary.gate_failures.slice(0, 6).map((row) => ( +
+
+

+ {gateLabel(row.gate, t)} +

+

+ {Object.entries(row.statuses ?? {}) + .map(([key, value]) => `${gateStatusLabel(key, t)}:${value}`) + .join(" / ") || "--"} +

+
+ + {row.total} + +
+ ))} +
+
+
+
+ )} +
+ ); +} + export default function AwoooPPage() { const t = useTranslations("awooop.home"); const locale = useLocale(); const [snapshot, setSnapshot] = useState(emptySnapshot); + const [qualitySummary, setQualitySummary] = useState(null); + const [qualityError, setQualityError] = useState(false); const [status, setStatus] = useState("loading"); const [lastUpdated, setLastUpdated] = useState(null); const fetchSnapshot = useCallback(async () => { setStatus("loading"); try { - const [tenantRes, runRes, approvalRes, contractRes] = await Promise.all([ + const qualityParams = new URLSearchParams({ + project_id: "awoooi", + hours: "24", + limit: "50", + }); + const [tenantRes, runRes, approvalRes, contractRes, qualityRes] = await Promise.all([ fetch(`${API_BASE}/api/v1/platform/tenants`), fetch(`${API_BASE}/api/v1/platform/runs/list?per_page=1`), fetch(`${API_BASE}/api/v1/platform/approvals`), fetch(`${API_BASE}/api/v1/platform/contracts?per_page=1`), + fetch(`${API_BASE}/api/v1/platform/truth-chain/quality/summary?${qualityParams.toString()}`) + .catch(() => null), ]); if (![tenantRes, runRes, approvalRes, contractRes].every((res) => res.ok)) { @@ -228,10 +503,20 @@ export default function AwoooPPage() { approvals: countRows(approvalData, ["items"]), contracts: countRows(contractData, ["contracts", "items"]), }); + if (qualityRes?.ok) { + const qualityData = await qualityRes.json() as AutomationQualitySummary; + setQualitySummary(qualityData); + setQualityError(false); + } else { + setQualitySummary(null); + setQualityError(true); + } setLastUpdated(new Date()); setStatus("ready"); } catch { setStatus("degraded"); + setQualitySummary(null); + setQualityError(true); setLastUpdated(new Date()); } }, []); @@ -346,6 +631,8 @@ export default function AwoooPPage() { /> + +
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index f3e84b28..c4895e65 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -7831,3 +7831,97 @@ OK ``` **目前整體進度**:約 73%。 + +**production deploy / smoke 追加(完成)**: + +```text +Gitea: +2041 ai-code-review ae7c7cbd -> success +2040 CD Pipeline ae7c7cbd -> success + +K8s image: +awoooi-api 192.168.0.110:5000/awoooi/api:ae7c7cbd23830f2c74aa7c43c0e9a931ca5092bb +awoooi-worker 192.168.0.110:5000/awoooi/api:ae7c7cbd23830f2c74aa7c43c0e9a931ca5092bb +awoooi-web 192.168.0.110:5000/awoooi/web:ae7c7cbd23830f2c74aa7c43c0e9a931ca5092bb + +rollout: +awoooi-api / awoooi-worker / awoooi-web successfully rolled out + +health: +https://awoooi.wooo.work/api/v1/health -> 200 + +route visibility note: +T12d 後此 summary endpoint 改為 read-only aggregate,不回傳 examples;source-level `/truth-chain/{source_id}` 仍需 operator auth。 + +production summary service smoke, hours=24, limit=50: +schema_version=automation_quality_summary_v1 +incident_total=50 +evaluated_total=50 +verified_auto_repair_total=0 +average_score=46.2 +score_buckets={green: 2, yellow: 14, red: 34} +production_claim.can_claim_full_auto_repair=false +by_verdict: + received_only=17 + execution_unverified=16 + manual_required_no_action=16 + approval_required=1 +top gate_failures: + auto_repair_recorded=48 + execution_recorded=34 + evidence_collected=31 + mcp_gateway_observed=17 + outbound_recorded=17 + timeline_recorded=17 + approval_state=16 + verification_recorded=16 +``` + +判讀: + +- T12c 已部署並能用 production 資料回答「目前不能宣稱完整 AI 自動修復」。 +- 最近 50 筆 incident 中,0 筆達到 `auto_repaired_verified`;不少中低風險事件有 execution 但缺 verification / auto_repair durable record。 +- 下一步應把這個 summary 接到 Operator Console / Telegram 詳情入口,並把 execution verifier / KM writeback 變成下一個 quality gap wave。 +- 目前整體進度更新:約 74%。 + +### 2026-05-13 — AwoooP truth-chain T12d:Operator Console 自動化品質面板(local green) + +**目的**: + +- T12c 已有全體告警 quality summary API,但 Operator Console 仍看不到「最近告警是否真的 AI 自動修復」。 +- T12d 把 summary 接到 `/awooop` 首頁,讓「是否可宣稱完整閉環」成為第一屏可見的治理訊號。 + +**變更**: + +- `/awooop` 首頁新增「自動化品質」面板: + - 24h / limit 50 summary + - 顯示 evaluated / verified auto-repair / average score / production claim + - 顯示 score buckets、verdict distribution、top gate failures + - 只讀,不觸發任何修復動作 +- 新增 `awooop.home.quality` 雙語字典,新增文字都走 `next-intl`。 +- UI 使用 Lucide `ShieldCheck`,沒有新增 emoji icon 或 mock data。 +- `GET /api/v1/platform/truth-chain/quality/summary` 改為 read-only aggregate,供 Operator Console 讀取;回應會清空 examples,逐筆 truth-chain 詳情仍保留 operator auth。 + +**local verification**: + +```text +python3 -m json.tool apps/web/messages/zh-TW.json >/dev/null +python3 -m json.tool apps/web/messages/en.json >/dev/null +messages ok + +pnpm --dir apps/web exec eslint 'src/app/[locale]/awooop/page.tsx' +OK + +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build +OK + +pnpm --dir apps/web exec tsc --noEmit +OK + +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web dev -- --hostname 127.0.0.1 --port 3000 +Ready at http://127.0.0.1:3000 + +curl http://127.0.0.1:3000/zh-TW/awooop -> 200 +``` + +**目前整體進度**:約 75%。 diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index 32fda234..8babe885 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -40,7 +40,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: 0f080240c658c9f7deb57ab0e7623342b946246a + newTag: e4203060f3a417e879c2ad3b32894e69444105ad - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: 0f080240c658c9f7deb57ab0e7623342b946246a + newTag: e4203060f3a417e879c2ad3b32894e69444105ad