feat(awooop): surface automation flow gates
All checks were successful
CD Pipeline / tests (push) Successful in 1m26s
Code Review / ai-code-review (push) Successful in 26s
CD Pipeline / build-and-deploy (push) Successful in 4m12s
CD Pipeline / post-deploy-checks (push) Successful in 2m37s

This commit is contained in:
Your Name
2026-06-01 11:09:55 +08:00
parent 61675911f7
commit fbcef599f9
5 changed files with 749 additions and 0 deletions

View File

@@ -81,6 +81,7 @@ type RecurrenceRepairStatus =
| "run_completed_no_repair"
| "source_correlation_review"
| "no_repair_record";
type AutomationFlowStatus = "passed" | "warning" | "blocked" | "no_data" | string;
interface RemediationSummary {
schema_version?: string;
@@ -213,6 +214,52 @@ interface OperatorSummaryCacheInfo {
expires_at?: string;
}
interface AutomationFlowGateExample {
incident_id?: string | null;
alertname?: string | null;
verdict?: string | null;
truth_stage?: string | null;
source_statuses?: Record<string, string>;
blockers?: string[];
}
interface AutomationFlowGate {
gate: string;
status: AutomationFlowStatus;
passed_total: number;
warning_total: number;
missing_total: number;
failed_total: number;
evaluated_total: number;
passed_percent: number;
quality_gates?: string[];
next_action?: string;
examples?: AutomationFlowGateExample[];
}
interface AutomationFlowGateSummary {
schema_version?: "automation_flow_gate_summary_v1" | string;
evaluated_total: number;
overall_status: AutomationFlowStatus;
blocked_gates: string[];
warning_gates: string[];
gates: AutomationFlowGate[];
}
interface AutomationQualitySummary {
schema_version?: "automation_quality_summary_v1" | string;
incident_total: number;
evaluated_total: number;
verified_auto_repair_total: number;
average_score: number;
production_claim?: {
can_claim_full_auto_repair?: boolean;
reason?: string | null;
} | null;
automation_flow_gates?: AutomationFlowGateSummary | null;
cache?: OperatorSummaryCacheInfo | null;
}
interface Run {
run_id: string;
project_id: string;
@@ -2776,6 +2823,280 @@ function CallbackReplyAuditSummaryPanel({
);
}
const AUTOMATION_FLOW_GATE_LABEL_KEYS: Record<string, string> = {
alert_intake: "alert_intake",
mcp_investigation: "mcp_investigation",
approval_policy: "approval_policy",
execution_recorded: "execution_recorded",
repair_recorded: "repair_recorded",
verification_recorded: "verification_recorded",
knowledge_recorded: "knowledge_recorded",
operator_visible: "operator_visible",
};
const AUTOMATION_FLOW_ACTION_KEYS: Record<string, string> = {
repair_alert_intake_or_outbound_mirror: "repair_alert_intake_or_outbound_mirror",
route_incident_to_mcp_gateway_and_evidence_collectors:
"route_incident_to_mcp_gateway_and_evidence_collectors",
resolve_pending_or_expired_human_gate: "resolve_pending_or_expired_human_gate",
record_effective_execution_or_mark_manual_no_action:
"record_effective_execution_or_mark_manual_no_action",
write_auto_repair_execution_or_blocker_reason:
"write_auto_repair_execution_or_blocker_reason",
run_post_execution_verification: "run_post_execution_verification",
write_km_or_learning_evidence: "write_km_or_learning_evidence",
repair_timeline_or_operator_notification_visibility:
"repair_timeline_or_operator_notification_visibility",
};
const AUTOMATION_FLOW_CLAIM_REASON_KEYS: Record<string, string> = {
all_evaluated_incidents_auto_repaired_verified:
"all_evaluated_incidents_auto_repaired_verified",
some_incidents_are_not_auto_repaired_verified:
"some_incidents_are_not_auto_repaired_verified",
};
function automationFlowStatusClass(status?: AutomationFlowStatus | null) {
if (status === "passed") {
return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
}
if (status === "warning") {
return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
}
if (status === "blocked") {
return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
}
return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
}
function automationFlowStatusLabelKey(status?: AutomationFlowStatus | null) {
if (
status === "passed" ||
status === "warning" ||
status === "blocked" ||
status === "no_data"
) {
return `statuses.${status}`;
}
return "statuses.unknown";
}
function automationFlowStatusIcon(status?: AutomationFlowStatus | null) {
if (status === "passed") return ShieldCheck;
if (status === "warning") return TriangleAlert;
if (status === "blocked") return AlertCircle;
return SearchCheck;
}
function AutomationFlowGatePanel({
summary,
error,
}: {
summary: AutomationQualitySummary | null;
error: string | null;
}) {
const t = useTranslations("awooop.runs.automationFlow");
const flow = summary?.automation_flow_gates ?? null;
const overallStatus = flow?.overall_status ?? "no_data";
const OverallIcon = automationFlowStatusIcon(overallStatus);
const cacheAge = Math.max(0, Math.round(summary?.cache?.age_seconds ?? 0));
const cacheLabel = summary?.cache
? summary.cache.status === "hit"
? t("cacheHit", { age: cacheAge, ttl: summary.cache.ttl_seconds ?? 0 })
: t("cacheMiss", { age: cacheAge, ttl: summary.cache.ttl_seconds ?? 0 })
: null;
const claimReady = Boolean(summary?.production_claim?.can_claim_full_auto_repair);
const claimReason = summary?.production_claim?.reason ?? "unknown";
const claimReasonKey = AUTOMATION_FLOW_CLAIM_REASON_KEYS[claimReason];
const claimReasonLabel = claimReasonKey
? t(`claimReasons.${claimReasonKey}` as never)
: claimReason;
return (
<section id="automation-flow-gates" className="border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex items-center gap-2">
<Waypoints className="h-4 w-4 text-[#1f5b9b]" aria-hidden="true" />
<div>
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="text-xs text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
{cacheLabel ? (
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 text-xs font-semibold text-[#5f5b52]">
{cacheLabel}
</span>
) : null}
<span
className={cn(
"inline-flex items-center gap-1.5 border px-2 py-0.5 text-xs font-semibold",
automationFlowStatusClass(overallStatus)
)}
>
<OverallIcon className="h-3.5 w-3.5" aria-hidden="true" />
{t(automationFlowStatusLabelKey(overallStatus) as never)}
</span>
</div>
</div>
{error ? (
<div className="px-4 py-4 text-sm text-[#9f2f25]">
{t("error", { error })}
</div>
) : !summary || !flow ? (
<div className="px-4 py-4 text-sm text-[#5f5b52]">
{t("empty")}
</div>
) : (
<>
<div
className={cn(
"border-b px-4 py-3 text-xs leading-5",
claimReady
? "border-[#c8dfcb] bg-[#f4fbf5] text-[#17602a]"
: "border-[#eed2ce] bg-[#fff6f5] text-[#8f2c22]"
)}
>
<div className="flex flex-wrap items-center gap-2 font-semibold">
{claimReady ? (
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
) : (
<AlertCircle className="h-4 w-4" aria-hidden="true" />
)}
<span>{claimReady ? t("claimReady") : t("claimBlocked")}</span>
</div>
<p className="mt-1">{t("claimReason", { reason: claimReasonLabel })}</p>
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
{[
{
label: t("metrics.evaluated"),
value: flow.evaluated_total,
detail: t("metrics.evaluatedDetail", {
incidents: summary.incident_total ?? 0,
}),
},
{
label: t("metrics.verifiedRepair"),
value: summary.verified_auto_repair_total ?? 0,
detail: t("metrics.verifiedRepairDetail"),
},
{
label: t("metrics.blockedGates"),
value: flow.blocked_gates?.length ?? 0,
detail: t("metrics.blockedGatesDetail"),
},
{
label: t("metrics.warningGates"),
value: flow.warning_gates?.length ?? 0,
detail: t("metrics.warningGatesDetail"),
},
].map((item) => (
<div key={item.label} className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<p className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
{item.value}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">{item.detail}</p>
</div>
))}
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
{flow.gates.map((gate) => {
const StatusIcon = automationFlowStatusIcon(gate.status);
const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate];
const gateLabel = gateLabelKey
? t(`gates.${gateLabelKey}` as never)
: gate.gate;
const actionKey = gate.next_action
? AUTOMATION_FLOW_ACTION_KEYS[gate.next_action]
: null;
const actionLabel = actionKey
? t(`actions.${actionKey}` as never)
: gate.next_action ?? "--";
const passedPercent = Math.max(
0,
Math.min(100, Number.isFinite(gate.passed_percent) ? gate.passed_percent : 0)
);
const example = gate.examples?.[0];
const sourceStatuses = Object.entries(example?.source_statuses ?? {})
.map(([key, value]) => `${key}:${value}`)
.join(", ");
return (
<article key={gate.gate} className="bg-white px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold text-[#141413]">{gateLabel}</p>
<p className="mt-1 font-mono text-[11px] text-[#77736a]">
{gate.quality_gates?.join(" / ") || gate.gate}
</p>
</div>
<span
className={cn(
"inline-flex shrink-0 items-center gap-1 border px-2 py-0.5 text-xs font-semibold",
automationFlowStatusClass(gate.status)
)}
>
<StatusIcon className="h-3.5 w-3.5" aria-hidden="true" />
{t(automationFlowStatusLabelKey(gate.status) as never)}
</span>
</div>
<div className="mt-3 h-1.5 bg-[#ece7dc]">
<div
className={cn(
"h-full",
gate.status === "passed"
? "bg-[#4f9d5f]"
: gate.status === "warning"
? "bg-[#c58a24]"
: "bg-[#c65145]"
)}
style={{ width: `${passedPercent}%` }}
/>
</div>
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">
{t("coverage", { percent: passedPercent.toFixed(1) })}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("counts", {
passed: gate.passed_total ?? 0,
warning: gate.warning_total ?? 0,
missing: gate.missing_total ?? 0,
failed: gate.failed_total ?? 0,
})}
</p>
<p className="mt-2 text-xs font-semibold leading-5 text-[#141413]">
{t("nextAction", { action: actionLabel })}
</p>
{example ? (
<div className="mt-2 space-y-1 text-xs leading-5 text-[#77736a]">
<p>
{t("example", {
incidentId: example.incident_id ?? "--",
verdict: example.verdict ?? "--",
})}
</p>
{sourceStatuses ? (
<p className="break-words font-mono">
{t("sourceStatuses", { statuses: sourceStatuses })}
</p>
) : null}
</div>
) : null}
</article>
);
})}
</div>
</>
)}
</section>
);
}
function CallbackReplyEvidencePanel({
events,
total,
@@ -3339,6 +3660,9 @@ export default function RunsPage() {
const [callbackEventsError, setCallbackEventsError] = useState<string | null>(null);
const [aiRouteStatus, setAiRouteStatus] = useState<AiRouteStatusResponse | null>(null);
const [aiRouteStatusError, setAiRouteStatusError] = useState<string | null>(null);
const [automationQualitySummary, setAutomationQualitySummary] =
useState<AutomationQualitySummary | null>(null);
const [automationQualityError, setAutomationQualityError] = useState<string | null>(null);
const [tenants, setTenants] = useState<Tenant[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
@@ -3491,6 +3815,32 @@ export default function RunsPage() {
setCallbackEventsLoading(false);
}
try {
const qualityParams = new URLSearchParams();
qualityParams.set("project_id", projectFilter || "awoooi");
qualityParams.set("hours", "24");
qualityParams.set("limit", "30");
if (options?.refresh) {
qualityParams.set("refresh", "true");
}
const qualityRes = await fetch(
`${API_BASE}/api/v1/platform/truth-chain/quality/summary?${qualityParams.toString()}`
);
if (qualityRes.ok) {
const qualityData: AutomationQualitySummary = await qualityRes.json();
setAutomationQualitySummary(qualityData);
setAutomationQualityError(null);
} else {
setAutomationQualitySummary(null);
setAutomationQualityError(`HTTP ${qualityRes.status}`);
}
} catch (qualityError) {
setAutomationQualitySummary(null);
setAutomationQualityError(
qualityError instanceof Error ? qualityError.message : "automation quality failed"
);
}
try {
const routeStatusRes = await fetch(
`${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
@@ -3714,6 +4064,11 @@ export default function RunsPage() {
})}
</section>
<AutomationFlowGatePanel
summary={automationQualitySummary}
error={automationQualityError}
/>
<SecurityRunStateCandidatePanel />
<GitHubRunReadinessBoundaryPanel />