feat(awooop): surface automation flow gates
This commit is contained in:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user