feat(awooop): surface automation flow gates
This commit is contained in:
@@ -681,6 +681,184 @@ def _automation_quality_score_bucket(score: int) -> str:
|
||||
return "red"
|
||||
|
||||
|
||||
_AUTOMATION_FLOW_GATE_DEFINITIONS: tuple[dict[str, Any], ...] = (
|
||||
{
|
||||
"gate": "alert_intake",
|
||||
"quality_gates": ("source_persisted", "outbound_recorded"),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "repair_alert_intake_or_outbound_mirror",
|
||||
},
|
||||
{
|
||||
"gate": "mcp_investigation",
|
||||
"quality_gates": ("evidence_collected", "mcp_gateway_observed"),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "route_incident_to_mcp_gateway_and_evidence_collectors",
|
||||
},
|
||||
{
|
||||
"gate": "approval_policy",
|
||||
"quality_gates": ("approval_state",),
|
||||
"allow_not_applicable": True,
|
||||
"next_action": "resolve_pending_or_expired_human_gate",
|
||||
},
|
||||
{
|
||||
"gate": "execution_recorded",
|
||||
"quality_gates": ("execution_recorded",),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "record_effective_execution_or_mark_manual_no_action",
|
||||
},
|
||||
{
|
||||
"gate": "repair_recorded",
|
||||
"quality_gates": ("auto_repair_recorded",),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "write_auto_repair_execution_or_blocker_reason",
|
||||
},
|
||||
{
|
||||
"gate": "verification_recorded",
|
||||
"quality_gates": ("verification_recorded",),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "run_post_execution_verification",
|
||||
},
|
||||
{
|
||||
"gate": "knowledge_recorded",
|
||||
"quality_gates": ("learning_recorded",),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "write_km_or_learning_evidence",
|
||||
},
|
||||
{
|
||||
"gate": "operator_visible",
|
||||
"quality_gates": ("outbound_recorded", "timeline_recorded"),
|
||||
"allow_not_applicable": False,
|
||||
"next_action": "repair_timeline_or_operator_notification_visibility",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _quality_gate_status_map(quality: dict[str, Any]) -> dict[str, str]:
|
||||
statuses: dict[str, str] = {}
|
||||
for row in quality.get("gates") or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
name = str(row.get("name") or "")
|
||||
if not name:
|
||||
continue
|
||||
statuses[name] = str(row.get("status") or "missing")
|
||||
return statuses
|
||||
|
||||
|
||||
def _automation_flow_gate_record_status(
|
||||
definition: dict[str, Any],
|
||||
gate_statuses: dict[str, str],
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
aliases = definition.get("source_gate_aliases") or {}
|
||||
source_statuses: dict[str, str] = {}
|
||||
normalized: list[str] = []
|
||||
for source_gate in definition["quality_gates"]:
|
||||
mapped_gate = str(aliases.get(source_gate, source_gate))
|
||||
raw_status = gate_statuses.get(mapped_gate, "missing")
|
||||
status = str(raw_status or "missing")
|
||||
source_statuses[mapped_gate] = status
|
||||
if status == "not_applicable" and not definition.get("allow_not_applicable"):
|
||||
status = "missing"
|
||||
normalized.append(status)
|
||||
|
||||
if any(status == "failed" for status in normalized):
|
||||
return "failed", source_statuses
|
||||
if any(status == "missing" for status in normalized):
|
||||
return "missing", source_statuses
|
||||
if any(status == "warning" for status in normalized):
|
||||
return "warning", source_statuses
|
||||
return "passed", source_statuses
|
||||
|
||||
|
||||
def _automation_flow_gate_summary(records: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
applicable_records = [
|
||||
record
|
||||
for record in records
|
||||
if isinstance(record.get("automation_quality"), dict)
|
||||
and record["automation_quality"].get("applicable") is True
|
||||
]
|
||||
evaluated_total = len(applicable_records)
|
||||
gate_rows: list[dict[str, Any]] = []
|
||||
|
||||
for definition in _AUTOMATION_FLOW_GATE_DEFINITIONS:
|
||||
counts = {"passed": 0, "warning": 0, "missing": 0, "failed": 0}
|
||||
examples: list[dict[str, Any]] = []
|
||||
for record in applicable_records:
|
||||
quality = record["automation_quality"]
|
||||
incident = record.get("incident") if isinstance(record.get("incident"), dict) else {}
|
||||
truth_status = (
|
||||
record.get("truth_status")
|
||||
if isinstance(record.get("truth_status"), dict)
|
||||
else {}
|
||||
)
|
||||
status, source_statuses = _automation_flow_gate_record_status(
|
||||
definition,
|
||||
_quality_gate_status_map(quality),
|
||||
)
|
||||
counts[status] += 1
|
||||
if status != "passed" and len(examples) < 5:
|
||||
examples.append({
|
||||
"incident_id": incident.get("incident_id"),
|
||||
"alertname": incident.get("alertname"),
|
||||
"verdict": quality.get("verdict"),
|
||||
"truth_stage": truth_status.get("current_stage"),
|
||||
"source_statuses": source_statuses,
|
||||
"blockers": list(quality.get("blockers") or [])[:6],
|
||||
})
|
||||
|
||||
blocked_total = counts["failed"] + counts["missing"]
|
||||
if evaluated_total == 0:
|
||||
status = "no_data"
|
||||
elif blocked_total > 0:
|
||||
status = "blocked"
|
||||
elif counts["warning"] > 0:
|
||||
status = "warning"
|
||||
else:
|
||||
status = "passed"
|
||||
passed_percent = (
|
||||
round((counts["passed"] / evaluated_total) * 100, 1)
|
||||
if evaluated_total
|
||||
else 0.0
|
||||
)
|
||||
gate_rows.append({
|
||||
"gate": definition["gate"],
|
||||
"status": status,
|
||||
"passed_total": counts["passed"],
|
||||
"warning_total": counts["warning"],
|
||||
"missing_total": counts["missing"],
|
||||
"failed_total": counts["failed"],
|
||||
"evaluated_total": evaluated_total,
|
||||
"passed_percent": passed_percent,
|
||||
"quality_gates": list(definition["quality_gates"]),
|
||||
"next_action": definition["next_action"],
|
||||
"examples": examples,
|
||||
})
|
||||
|
||||
blocked_gates = [
|
||||
row["gate"]
|
||||
for row in gate_rows
|
||||
if row["status"] in {"blocked", "no_data"}
|
||||
]
|
||||
warning_gates = [row["gate"] for row in gate_rows if row["status"] == "warning"]
|
||||
if evaluated_total == 0:
|
||||
overall_status = "no_data"
|
||||
elif blocked_gates:
|
||||
overall_status = "blocked"
|
||||
elif warning_gates:
|
||||
overall_status = "warning"
|
||||
else:
|
||||
overall_status = "passed"
|
||||
|
||||
return {
|
||||
"schema_version": "automation_flow_gate_summary_v1",
|
||||
"evaluated_total": evaluated_total,
|
||||
"overall_status": overall_status,
|
||||
"blocked_gates": blocked_gates,
|
||||
"warning_gates": warning_gates,
|
||||
"gates": gate_rows,
|
||||
}
|
||||
|
||||
|
||||
def _int_value(value: Any, fallback: int = 0) -> int:
|
||||
try:
|
||||
return int(value)
|
||||
@@ -999,6 +1177,7 @@ def summarize_automation_quality_records(
|
||||
"score_buckets": score_buckets,
|
||||
"by_verdict": by_verdict,
|
||||
"gate_failures": failing_gates,
|
||||
"automation_flow_gates": _automation_flow_gate_summary(records),
|
||||
"execution_backend_summary": _execution_backend_summary(records),
|
||||
"ansible_runtime": ansible_runtime,
|
||||
"examples": examples[:25],
|
||||
|
||||
@@ -899,6 +899,107 @@ def test_automation_quality_summary_denies_full_claim_when_unverified() -> None:
|
||||
assert summary["examples"][1]["score_bucket"] == "yellow"
|
||||
|
||||
|
||||
def test_automation_quality_summary_builds_operator_flow_gates() -> None:
|
||||
def quality(
|
||||
*,
|
||||
incident_id: str,
|
||||
verdict: str,
|
||||
score: int,
|
||||
gates: list[dict[str, str]],
|
||||
blockers: list[str] | None = None,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"incident": {
|
||||
"incident_id": incident_id,
|
||||
"alertname": "container restart loop",
|
||||
"severity": "P3",
|
||||
"status": "INVESTIGATING",
|
||||
},
|
||||
"truth_status": {
|
||||
"current_stage": "execution_succeeded",
|
||||
"stage_status": "success",
|
||||
"needs_human": verdict != "auto_repaired_verified",
|
||||
},
|
||||
"automation_quality": {
|
||||
"applicable": True,
|
||||
"verdict": verdict,
|
||||
"score": score,
|
||||
"facts": {
|
||||
"automation_operation_records": 1,
|
||||
"effective_execution_records": 1 if verdict == "auto_repaired_verified" else 0,
|
||||
"auto_repair_execution_records": 1 if verdict == "auto_repaired_verified" else 0,
|
||||
},
|
||||
"gates": gates,
|
||||
"blockers": blockers or [],
|
||||
},
|
||||
"execution": {
|
||||
"automation_operation_log": [],
|
||||
"auto_repair_executions": [],
|
||||
"ansible": {"considered": False, "records": [], "candidate_catalog": {"candidates": []}},
|
||||
},
|
||||
}
|
||||
|
||||
passed_gates = [
|
||||
{"name": "source_persisted", "status": "passed"},
|
||||
{"name": "outbound_recorded", "status": "passed"},
|
||||
{"name": "evidence_collected", "status": "passed"},
|
||||
{"name": "mcp_gateway_observed", "status": "passed"},
|
||||
{"name": "approval_state", "status": "not_applicable"},
|
||||
{"name": "execution_recorded", "status": "passed"},
|
||||
{"name": "auto_repair_recorded", "status": "passed"},
|
||||
{"name": "verification_recorded", "status": "passed"},
|
||||
{"name": "learning_recorded", "status": "passed"},
|
||||
{"name": "timeline_recorded", "status": "passed"},
|
||||
]
|
||||
blocked_gates = [
|
||||
{"name": "source_persisted", "status": "passed"},
|
||||
{"name": "outbound_recorded", "status": "passed"},
|
||||
{"name": "evidence_collected", "status": "missing"},
|
||||
{"name": "mcp_gateway_observed", "status": "missing"},
|
||||
{"name": "approval_state", "status": "warning"},
|
||||
{"name": "execution_recorded", "status": "missing"},
|
||||
{"name": "auto_repair_recorded", "status": "missing"},
|
||||
{"name": "verification_recorded", "status": "not_applicable"},
|
||||
{"name": "learning_recorded", "status": "not_applicable"},
|
||||
{"name": "timeline_recorded", "status": "missing"},
|
||||
]
|
||||
|
||||
summary = summarize_automation_quality_records(
|
||||
project_id="awoooi",
|
||||
window_hours=24,
|
||||
limit=2,
|
||||
records=[
|
||||
quality(
|
||||
incident_id="INC-PASS",
|
||||
verdict="auto_repaired_verified",
|
||||
score=100,
|
||||
gates=passed_gates,
|
||||
),
|
||||
quality(
|
||||
incident_id="INC-BLOCKED",
|
||||
verdict="manual_required_no_action",
|
||||
score=40,
|
||||
gates=blocked_gates,
|
||||
blockers=["mcp_gateway_observed", "execution_recorded"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
flow = summary["automation_flow_gates"]
|
||||
gates = {row["gate"]: row for row in flow["gates"]}
|
||||
|
||||
assert flow["schema_version"] == "automation_flow_gate_summary_v1"
|
||||
assert flow["overall_status"] == "blocked"
|
||||
assert gates["alert_intake"]["status"] == "passed"
|
||||
assert gates["mcp_investigation"]["missing_total"] == 1
|
||||
assert gates["approval_policy"]["warning_total"] == 1
|
||||
assert gates["execution_recorded"]["missing_total"] == 1
|
||||
assert gates["verification_recorded"]["missing_total"] == 1
|
||||
assert gates["knowledge_recorded"]["missing_total"] == 1
|
||||
assert gates["operator_visible"]["missing_total"] == 1
|
||||
assert gates["mcp_investigation"]["examples"][0]["incident_id"] == "INC-BLOCKED"
|
||||
|
||||
|
||||
def test_ansible_runtime_readiness_reports_check_mode_blockers() -> None:
|
||||
readiness = _ansible_runtime_readiness()
|
||||
|
||||
|
||||
@@ -4171,6 +4171,63 @@
|
||||
}
|
||||
},
|
||||
"runs": {
|
||||
"automationFlow": {
|
||||
"title": "AI automation flow gates",
|
||||
"subtitle": "24h window: alert intake, MCP investigation, approval / policy, execution, repair, verification, KM, and operator visibility.",
|
||||
"empty": "No automation flow records are ready for evaluation yet.",
|
||||
"error": "AI automation flow gates failed to load: {error}",
|
||||
"cacheHit": "Cache hit {age}s / TTL {ttl}s",
|
||||
"cacheMiss": "Fresh aggregation / TTL {ttl}s",
|
||||
"claimReady": "Full auto-repair claim is allowed",
|
||||
"claimBlocked": "Full auto-repair claim is blocked",
|
||||
"claimReason": "Reason: {reason}",
|
||||
"coverage": "{percent}% passed",
|
||||
"counts": "pass {passed} / warn {warning} / miss {missing} / fail {failed}",
|
||||
"nextAction": "Next: {action}",
|
||||
"example": "Example: {incidentId} / {verdict}",
|
||||
"sourceStatuses": "Source gates: {statuses}",
|
||||
"statuses": {
|
||||
"passed": "Passed",
|
||||
"warning": "Warning",
|
||||
"blocked": "Blocked",
|
||||
"no_data": "No data",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"claimReasons": {
|
||||
"all_evaluated_incidents_auto_repaired_verified": "Every evaluated incident has auto-repair and verification evidence",
|
||||
"some_incidents_are_not_auto_repaired_verified": "Some incidents still lack auto-repair or verification evidence"
|
||||
},
|
||||
"metrics": {
|
||||
"evaluated": "Evaluated incidents",
|
||||
"evaluatedDetail": "{incidents} truth-chain samples in the 24h window.",
|
||||
"verifiedRepair": "Verified auto-repairs",
|
||||
"verifiedRepairDetail": "Only successful repairs with post-execution verification are counted.",
|
||||
"blockedGates": "Blocked gates",
|
||||
"blockedGatesDetail": "Any missing / failed gate blocks a full automation claim.",
|
||||
"warningGates": "Warning gates",
|
||||
"warningGatesDetail": "Observable, but still needs more context or human judgment."
|
||||
},
|
||||
"gates": {
|
||||
"alert_intake": "Alert intake / notification mirror",
|
||||
"mcp_investigation": "MCP investigation and evidence",
|
||||
"approval_policy": "Approval / safety policy",
|
||||
"execution_recorded": "Execution record",
|
||||
"repair_recorded": "Auto-repair record",
|
||||
"verification_recorded": "Post-execution verification",
|
||||
"knowledge_recorded": "KM / learning write-back",
|
||||
"operator_visible": "Operator visibility"
|
||||
},
|
||||
"actions": {
|
||||
"repair_alert_intake_or_outbound_mirror": "Repair alert intake or outbound mirror",
|
||||
"route_incident_to_mcp_gateway_and_evidence_collectors": "Route the incident through MCP Gateway and evidence collectors",
|
||||
"resolve_pending_or_expired_human_gate": "Resolve pending / expired human gates",
|
||||
"record_effective_execution_or_mark_manual_no_action": "Record effective execution, or explicitly 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 and store the result",
|
||||
"write_km_or_learning_evidence": "Write KM / learning evidence",
|
||||
"repair_timeline_or_operator_notification_visibility": "Repair timeline or operator notification visibility"
|
||||
}
|
||||
},
|
||||
"securityRunStateCandidate": {
|
||||
"title": "IwoooS 執行狀態只讀候選",
|
||||
"subtitle": "執行監控只顯示資安鏡像可以被 AwoooP 執行視角理解;這不是已建立執行紀錄,也不會接上執行路由器。",
|
||||
|
||||
@@ -4171,6 +4171,63 @@
|
||||
}
|
||||
},
|
||||
"runs": {
|
||||
"automationFlow": {
|
||||
"title": "AI 自動化流程 Gate",
|
||||
"subtitle": "24h 視窗:告警入庫、MCP 調查、審批 / 政策、執行、修復、驗證、KM 與 Operator 可見性。",
|
||||
"empty": "尚無可評估的自動化流程資料。",
|
||||
"error": "AI 自動化流程 Gate 載入失敗:{error}",
|
||||
"cacheHit": "快取命中 {age}s / TTL {ttl}s",
|
||||
"cacheMiss": "剛重新聚合 / TTL {ttl}s",
|
||||
"claimReady": "可以宣稱全自動修復",
|
||||
"claimBlocked": "不可宣稱全自動修復",
|
||||
"claimReason": "原因:{reason}",
|
||||
"coverage": "{percent}% 通過",
|
||||
"counts": "pass {passed} / warn {warning} / miss {missing} / fail {failed}",
|
||||
"nextAction": "下一步:{action}",
|
||||
"example": "例:{incidentId} / {verdict}",
|
||||
"sourceStatuses": "來源 Gate:{statuses}",
|
||||
"statuses": {
|
||||
"passed": "Passed",
|
||||
"warning": "Warning",
|
||||
"blocked": "Blocked",
|
||||
"no_data": "No data",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"claimReasons": {
|
||||
"all_evaluated_incidents_auto_repaired_verified": "所有已評估事件都有自動修復與驗證證據",
|
||||
"some_incidents_are_not_auto_repaired_verified": "仍有事件缺少自動修復或驗證證據"
|
||||
},
|
||||
"metrics": {
|
||||
"evaluated": "已評估事件",
|
||||
"evaluatedDetail": "24h 視窗共 {incidents} 件 truth-chain 樣本。",
|
||||
"verifiedRepair": "已驗證自動修復",
|
||||
"verifiedRepairDetail": "只有修復成功且完成事後驗證才計入。",
|
||||
"blockedGates": "Blocked Gate",
|
||||
"blockedGatesDetail": "有 missing / failed 就不能宣稱完整自動化。",
|
||||
"warningGates": "Warning Gate",
|
||||
"warningGatesDetail": "仍可觀測,但需要補脈絡或人工判斷。"
|
||||
},
|
||||
"gates": {
|
||||
"alert_intake": "告警入庫 / 通知鏡像",
|
||||
"mcp_investigation": "MCP 調查與 evidence",
|
||||
"approval_policy": "審批 / 安全政策",
|
||||
"execution_recorded": "執行紀錄",
|
||||
"repair_recorded": "自動修復紀錄",
|
||||
"verification_recorded": "事後驗證",
|
||||
"knowledge_recorded": "KM / 學習回寫",
|
||||
"operator_visible": "Operator 可見性"
|
||||
},
|
||||
"actions": {
|
||||
"repair_alert_intake_or_outbound_mirror": "修復告警入庫或 outbound mirror",
|
||||
"route_incident_to_mcp_gateway_and_evidence_collectors": "把事件導入 MCP Gateway 與 evidence collectors",
|
||||
"resolve_pending_or_expired_human_gate": "處理 pending / expired 人工 gate",
|
||||
"record_effective_execution_or_mark_manual_no_action": "記錄有效執行,或明確標成人工 no-action",
|
||||
"write_auto_repair_execution_or_blocker_reason": "寫入 auto-repair execution 或 blocker reason",
|
||||
"run_post_execution_verification": "執行事後驗證並保存結果",
|
||||
"write_km_or_learning_evidence": "回寫 KM / learning evidence",
|
||||
"repair_timeline_or_operator_notification_visibility": "修復 timeline 或 operator notification 可見性"
|
||||
}
|
||||
},
|
||||
"securityRunStateCandidate": {
|
||||
"title": "IwoooS 執行狀態只讀候選",
|
||||
"subtitle": "執行監控只顯示資安鏡像可以被 AwoooP 執行視角理解;這不是已建立執行紀錄,也不會接上執行路由器。",
|
||||
|
||||
@@ -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