Merge remote-tracking branch 'gitea/main' into codex/security-supply-chain-contracts-20260512
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, number>;
|
||||
};
|
||||
|
||||
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<typeof useTranslations>): 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<typeof useTranslations>): 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<typeof useTranslations>): 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 (
|
||||
<div className="min-h-[96px] border border-[#e0ddd4] bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{label}</p>
|
||||
<p className="mt-2 font-mono text-2xl font-semibold leading-none text-[#141413]">
|
||||
{value}
|
||||
</p>
|
||||
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">{detail}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<ShieldCheck className="mt-0.5 h-4 w-4 text-[#d97757]" aria-hidden="true" />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex border px-2 py-0.5 text-xs font-semibold",
|
||||
claimReady && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||
!claimReady && !error && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
error && "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
|
||||
)}
|
||||
>
|
||||
{error ? t("unavailable") : claimReady ? t("claimReady") : t("claimBlocked")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{error || !summary ? (
|
||||
<div className="px-4 py-5 text-sm leading-6 text-[#5f5b52]">
|
||||
{error ? t("loadFailed") : t("empty")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<QualityMetric
|
||||
label={t("metrics.evaluated")}
|
||||
value={`${summary.evaluated_total}/${summary.incident_total}`}
|
||||
detail={t("metrics.evaluatedDetail")}
|
||||
/>
|
||||
<QualityMetric
|
||||
label={t("metrics.verified")}
|
||||
value={summary.verified_auto_repair_total}
|
||||
detail={t("metrics.verifiedDetail")}
|
||||
/>
|
||||
<QualityMetric
|
||||
label={t("metrics.averageScore")}
|
||||
value={summary.average_score.toFixed(1)}
|
||||
detail={t("metrics.averageScoreDetail")}
|
||||
/>
|
||||
<QualityMetric
|
||||
label={t("metrics.claim")}
|
||||
value={claimReady ? t("yes") : t("no")}
|
||||
detail={claimReady ? t("claimReadyDetail") : t("claimBlockedDetail")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("scoreBuckets")}</p>
|
||||
<p className="mt-1 text-xs text-[#5f5b52]">
|
||||
{t("scoreBucketsDetail", { total })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 font-mono text-xs text-[#141413]">
|
||||
<span>{t("green")}: {buckets.green}</span>
|
||||
<span>{t("yellow")}: {buckets.yellow}</span>
|
||||
<span>{t("red")}: {buckets.red}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex h-2 overflow-hidden border border-[#d8d3c7] bg-white">
|
||||
<div
|
||||
className="bg-[#6aa879]"
|
||||
style={{ width: `${(buckets.green / bucketTotal) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-[#d9b36f]"
|
||||
style={{ width: `${(buckets.yellow / bucketTotal) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-[#d76f61]"
|
||||
style={{ width: `${(buckets.red / bucketTotal) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<div className="border border-[#e0ddd4] bg-white">
|
||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3 text-xs font-semibold text-[#141413]">
|
||||
{t("verdictTitle")}
|
||||
</div>
|
||||
<div className="divide-y divide-[#eee9dd]">
|
||||
{summary.by_verdict.slice(0, 6).map((row) => (
|
||||
<div key={row.verdict} className="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-[#141413]">
|
||||
{verdictLabel(row.verdict, t)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-xs text-[#77736a]">
|
||||
{t("scoreRange", {
|
||||
min: row.min_score ?? 0,
|
||||
max: row.max_score ?? 0,
|
||||
avg: (row.avg_score ?? 0).toFixed(1),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-xs text-[#141413]">
|
||||
{row.total}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#e0ddd4] bg-white">
|
||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3 text-xs font-semibold text-[#141413]">
|
||||
{t("gateFailureTitle")}
|
||||
</div>
|
||||
<div className="divide-y divide-[#eee9dd]">
|
||||
{summary.gate_failures.slice(0, 6).map((row) => (
|
||||
<div key={row.gate} className="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-[#141413]">
|
||||
{gateLabel(row.gate, t)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-xs text-[#77736a]">
|
||||
{Object.entries(row.statuses ?? {})
|
||||
.map(([key, value]) => `${gateStatusLabel(key, t)}:${value}`)
|
||||
.join(" / ") || "--"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-xs text-[#141413]">
|
||||
{row.total}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AwoooPPage() {
|
||||
const t = useTranslations("awooop.home");
|
||||
const locale = useLocale();
|
||||
const [snapshot, setSnapshot] = useState<Snapshot>(emptySnapshot);
|
||||
const [qualitySummary, setQualitySummary] = useState<AutomationQualitySummary | null>(null);
|
||||
const [qualityError, setQualityError] = useState(false);
|
||||
const [status, setStatus] = useState<SnapshotStatus>("loading");
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(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() {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AutomationQualityPanel summary={qualitySummary} error={qualityError} />
|
||||
|
||||
<section className="border border-[#e0ddd4] bg-[#e0ddd4]">
|
||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -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%。
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user