feat(awooop): surface status chain in operator console
All checks were successful
Code Review / ai-code-review (push) Successful in 9s
CD Pipeline / tests (push) Successful in 1m16s
CD Pipeline / build-and-deploy (push) Successful in 3m31s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s

This commit is contained in:
Your Name
2026-05-19 10:13:33 +08:00
parent 30b2f5bd6e
commit 784ebf49ef
8 changed files with 615 additions and 4 deletions

View File

@@ -85,6 +85,7 @@ class CallbackReplyItem(BaseModel):
agent_id: str | None = None
run_created_at: datetime | None = None
callback_reply: dict[str, Any]
awooop_status_chain: dict[str, Any] | None = None
run_detail_href: str | None = None

View File

@@ -10,8 +10,8 @@ from __future__ import annotations
import re
import uuid
from collections.abc import Mapping
from collections import defaultdict
from collections.abc import Mapping
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
@@ -36,6 +36,7 @@ from src.services.awooop_approval_token import issue_approval_token, record_appr
from src.services.awooop_truth_chain_service import (
_summarize_gateway_mcp,
_summarize_mcp,
fetch_truth_chain,
)
from src.services.run_state_machine import transition
@@ -357,8 +358,37 @@ async def list_callback_replies(
rows_result = await db.execute(list_sql, params)
rows = list(rows_result.mappings().all())
items = [_callback_reply_event_item(row) for row in rows]
status_chain_cache: dict[tuple[str, str], dict[str, Any]] = {}
for item in items:
incident = item.get("incident_id")
if not incident:
item["awooop_status_chain"] = _build_awooop_status_chain(
incident_ids=[],
source_id=None,
)
continue
incident_id = str(incident)
item_project_id = str(item.get("project_id") or project_id or "awoooi")
cache_key = (item_project_id, incident_id)
cached = status_chain_cache.get(cache_key)
if cached is not None:
item["awooop_status_chain"] = cached
continue
remediation_history = await _fetch_run_remediation_history(
[incident_id],
limit=5,
)
chain = await _fetch_awooop_status_chain(
incident_ids=[incident_id],
project_id=item_project_id,
remediation_history=remediation_history,
)
status_chain_cache[cache_key] = chain
item["awooop_status_chain"] = chain
return {
"items": [_callback_reply_event_item(row) for row in rows],
"items": items,
"total": total,
"page": page,
"per_page": per_page,
@@ -910,6 +940,214 @@ def _run_remediation_list_summary(
}
def _safe_int(value: Any) -> int:
try:
return int(value or 0)
except (TypeError, ValueError):
return 0
def _latest_remediation_history_item(
history: dict[str, Any] | None,
) -> dict[str, Any]:
if not isinstance(history, dict):
return {}
items = history.get("items") if isinstance(history.get("items"), list) else []
latest = items[0] if items and isinstance(items[0], dict) else {}
return latest
def _remediation_evidence_state(history: dict[str, Any] | None) -> str:
"""Classify ADR-100 evidence with the same operator semantics as Telegram."""
if not isinstance(history, dict):
return "missing"
total = _safe_int(history.get("total"))
if total <= 0:
if history.get("status") == "fetch_failed":
return "fetch_failed"
return "missing"
latest = _latest_remediation_history_item(history)
if latest.get("writes_incident_state") or latest.get("writes_auto_repair_result"):
return "write_observed"
if latest.get("allowed") is False or latest.get("success") is False:
return "blocked"
if (
str(latest.get("safety_level") or "").lower() == "read_only"
or str(latest.get("required_scope") or "").lower() == "read"
):
return "read_only"
return "observed"
def _select_status_chain_source_id(
incident_ids: list[str],
remediation_history: dict[str, Any] | None,
) -> str | None:
latest_incident_id = str(
_latest_remediation_history_item(remediation_history).get("incident_id") or ""
).strip()
if latest_incident_id and latest_incident_id in incident_ids:
return latest_incident_id
return incident_ids[0] if incident_ids else latest_incident_id or None
def _build_awooop_status_chain(
*,
incident_ids: list[str],
truth_chain: dict[str, Any] | None = None,
remediation_history: dict[str, Any] | None = None,
source_id: str | None = None,
fetch_error: str | None = None,
) -> dict[str, Any]:
"""Build the shared read-only status chain used by Telegram and Operator UI."""
truth_status = (
truth_chain.get("truth_status")
if isinstance(truth_chain, dict) and isinstance(truth_chain.get("truth_status"), dict)
else {}
)
quality = (
truth_chain.get("automation_quality")
if isinstance(truth_chain, dict) and isinstance(truth_chain.get("automation_quality"), dict)
else {}
)
facts = quality.get("facts") if isinstance(quality.get("facts"), dict) else {}
latest = _latest_remediation_history_item(remediation_history)
remediation_state = _remediation_evidence_state(remediation_history)
remediation_total = (
_safe_int(remediation_history.get("total"))
if isinstance(remediation_history, dict)
else 0
)
latest_route = _route_label_from_remediation(latest) if latest else "--"
current_stage = str(truth_status.get("current_stage") or "unknown")
stage_status = str(truth_status.get("stage_status") or "unknown")
verdict = str(quality.get("verdict") or "unknown")
verification = (
facts.get("verification_result")
or latest.get("verification_result_preview")
or "missing"
)
auto_repair_records = _safe_int(facts.get("auto_repair_execution_records"))
operation_records = _safe_int(facts.get("automation_operation_records"))
gateway_total = _safe_int(facts.get("mcp_gateway_total"))
km_entries = _safe_int(facts.get("knowledge_entries"))
needs_human = bool(truth_status.get("needs_human"))
if verdict == "auto_repaired_verified":
repair_state = "auto_repaired_verified"
next_step = "monitor_for_regression"
elif auto_repair_records > 0 or operation_records > 0:
repair_state = (
"executed_pending_verification"
if str(verification) == "missing"
else "executed"
)
next_step = "verify_execution_result"
elif remediation_state == "read_only":
repair_state = "read_only_dry_run"
next_step = "approve_or_escalate_from_awooop"
elif remediation_state == "write_observed":
repair_state = "write_observed_manual_review"
next_step = "review_write_evidence"
elif remediation_state == "blocked":
repair_state = "blocked_manual_required"
next_step = "manual_investigation"
elif needs_human:
repair_state = "manual_required"
next_step = "manual_investigation"
else:
repair_state = "no_execution_evidence"
next_step = "collect_evidence_or_wait"
if remediation_state in {"blocked", "fetch_failed"}:
needs_human = True
if (
remediation_state == "write_observed"
and repair_state != "auto_repaired_verified"
):
needs_human = True
blockers = [
str(item)
for item in [
*(truth_status.get("blockers") if isinstance(truth_status.get("blockers"), list) else []),
*(quality.get("blockers") if isinstance(quality.get("blockers"), list) else []),
]
if item
]
if fetch_error:
blockers.append("truth_chain_fetch_failed")
return {
"schema_version": "awooop_status_chain_v1",
"source": "truth_chain+adr100_history",
"source_id": source_id,
"incident_ids": incident_ids,
"current_stage": current_stage,
"stage_status": stage_status,
"verdict": verdict,
"repair_state": repair_state,
"verification": str(verification),
"needs_human": needs_human,
"next_step": next_step,
"blockers": blockers[:8],
"fetch_error": fetch_error,
"evidence": {
"auto_repair_records": auto_repair_records,
"operation_records": operation_records,
"mcp_gateway_total": gateway_total,
"knowledge_entries": km_entries,
"remediation_total": remediation_total,
"remediation_state": remediation_state,
"latest_route": latest_route,
"latest_mode": latest.get("mode"),
"latest_at": latest.get("created_at"),
"latest_preview": latest.get("verification_result_preview"),
},
"writes": {
"incident": latest.get("writes_incident_state"),
"auto_repair": latest.get("writes_auto_repair_result"),
},
}
async def _fetch_awooop_status_chain(
*,
incident_ids: list[str],
project_id: str,
remediation_history: dict[str, Any] | None,
) -> dict[str, Any]:
"""Fetch read-only truth-chain state and merge it with ADR-100 evidence."""
source_id = _select_status_chain_source_id(incident_ids, remediation_history)
truth_chain: dict[str, Any] | None = None
fetch_error: str | None = None
if source_id:
try:
truth_chain = await fetch_truth_chain(
source_id=source_id,
project_id=project_id or "awoooi",
)
except Exception as exc:
fetch_error = str(exc)
logger.warning(
"operator_awooop_status_chain_fetch_failed",
source_id=source_id,
project_id=project_id,
error=fetch_error,
)
return _build_awooop_status_chain(
incident_ids=incident_ids,
truth_chain=truth_chain,
remediation_history=remediation_history,
source_id=source_id,
fetch_error=fetch_error,
)
def _validate_remediation_status_filter(value: str | None) -> None:
if value is None:
return
@@ -1384,6 +1622,11 @@ async def get_run_detail(
)
legacy_mcp_history = await _fetch_run_legacy_mcp_history(incident_ids)
remediation_history = await _fetch_run_remediation_history(incident_ids)
awooop_status_chain = await _fetch_awooop_status_chain(
incident_ids=incident_ids,
project_id=run.project_id,
remediation_history=remediation_history,
)
timeline: list[dict[str, Any]] = [
_timeline_item(
@@ -1550,6 +1793,7 @@ async def get_run_detail(
"mcp_gateway": mcp_gateway_summary,
"mcp_legacy": legacy_mcp_history,
"remediation_history": remediation_history,
"awooop_status_chain": awooop_status_chain,
"timeline": timeline,
"counts": {
"steps": len(step_items),

View File

@@ -11,15 +11,16 @@ from src.api.v1.platform.operator_runs import (
ListRunsResponse,
)
from src.services.platform_operator_service import (
_build_awooop_status_chain,
_callback_reply_event_item,
_callback_reply_summary_matches_status,
_collect_run_incident_ids,
_callback_reply_event_item,
_legacy_mcp_timeline_status,
_legacy_mcp_timeline_summary,
_list_filter_context_limit,
_outbound_timeline_title,
_outbound_timeline_status,
_outbound_timeline_summary,
_outbound_timeline_title,
_remediation_summary_matches_incident_id,
_remediation_summary_matches_status,
_remediation_timeline_summary,
@@ -348,6 +349,11 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
"action": "detail",
"incident_id": "INC-20260513-79ED5E",
},
"awooop_status_chain": {
"schema_version": "awooop_status_chain_v1",
"repair_state": "read_only_dry_run",
"needs_human": True,
},
"run_detail_href": (
"/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38"
"?project_id=awoooi"
@@ -362,6 +368,9 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
dumped = response.model_dump(mode="json")
assert dumped["items"][0]["status"] == "fallback_sent"
assert dumped["items"][0]["callback_reply"]["action"] == "detail"
assert dumped["items"][0]["awooop_status_chain"]["repair_state"] == (
"read_only_dry_run"
)
assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi")
@@ -396,6 +405,97 @@ def test_remediation_timeline_summary_surfaces_route_and_write_flags() -> None:
assert "writes_auto_repair=False" in summary
def test_awooop_status_chain_marks_verified_repair() -> None:
chain = _build_awooop_status_chain(
incident_ids=["INC-20260513-79ED5E"],
source_id="INC-20260513-79ED5E",
truth_chain={
"truth_status": {
"current_stage": "execution_succeeded",
"stage_status": "success",
"needs_human": False,
"blockers": [],
},
"automation_quality": {
"verdict": "auto_repaired_verified",
"facts": {
"auto_repair_execution_records": 1,
"automation_operation_records": 1,
"verification_result": "healthy",
"mcp_gateway_total": 2,
"knowledge_entries": 1,
},
"blockers": [],
},
},
remediation_history={
"total": 1,
"items": [
{
"incident_id": "INC-20260513-79ED5E",
"agent_id": "auto_repair_executor",
"tool_name": "rollout_restart",
"required_scope": "write",
"verification_result_preview": "healthy",
"writes_incident_state": True,
"writes_auto_repair_result": True,
}
],
},
)
assert chain["repair_state"] == "auto_repaired_verified"
assert chain["verification"] == "healthy"
assert chain["needs_human"] is False
assert chain["next_step"] == "monitor_for_regression"
assert chain["evidence"]["latest_route"] == "auto_repair_executor/rollout_restart/write"
def test_awooop_status_chain_marks_read_only_manual_gate() -> None:
chain = _build_awooop_status_chain(
incident_ids=["INC-20260513-79ED5E"],
source_id="INC-20260513-79ED5E",
truth_chain={
"truth_status": {
"current_stage": "approval_required",
"stage_status": "waiting",
"needs_human": True,
"blockers": ["pending_human_approval"],
},
"automation_quality": {
"verdict": "approval_required",
"facts": {
"auto_repair_execution_records": 0,
"automation_operation_records": 0,
"verification_result": "missing",
"mcp_gateway_total": 1,
"knowledge_entries": 0,
},
"blockers": [],
},
},
remediation_history={
"total": 2,
"items": [
{
"incident_id": "INC-20260513-79ED5E",
"agent_id": "investigator",
"tool_name": "ssh_diagnose",
"required_scope": "read",
"verification_result_preview": "degraded",
"writes_incident_state": False,
"writes_auto_repair_result": False,
}
],
},
)
assert chain["repair_state"] == "read_only_dry_run"
assert chain["needs_human"] is True
assert chain["next_step"] == "approve_or_escalate_from_awooop"
assert chain["blockers"] == ["pending_human_approval"]
def test_legacy_mcp_timeline_summary_surfaces_tool_context() -> None:
record = {
"incident_id": "INC-20260514-F85F21",

View File

@@ -2224,6 +2224,33 @@
"writeFlags": "incident={incident} / autoRepair={autoRepair}",
"runLink": "Run Timeline"
},
"statusChain": {
"title": "AwoooP Status Chain",
"subtitle": "Source {source}; Source ID {sourceId}",
"empty": "This item is not linked to readable truth-chain / ADR-100 history yet.",
"emptyValue": "--",
"blockers": "Blockers",
"writeFlags": "incident={incident} / autoRepair={autoRepair}",
"human": {
"yes": "Needs human",
"no": "No human gate"
},
"fields": {
"stage": "Stage",
"repair": "AI Repair",
"verification": "Verification",
"nextStep": "Next Step",
"writes": "Write Flags",
"verdict": "Verdict"
},
"evidence": {
"autoRepair": "Auto-repair",
"ops": "Ops",
"mcp": "MCP",
"km": "KM",
"adr100": "ADR-100 Route"
}
},
"runDetail": {
"back": "Back to Run Monitor",
"title": "Run Disposition Timeline",

View File

@@ -2225,6 +2225,33 @@
"writeFlags": "incident={incident} / autoRepair={autoRepair}",
"runLink": "Run Timeline"
},
"statusChain": {
"title": "AwoooP 狀態鏈",
"subtitle": "來源 {source}Source ID {sourceId}",
"empty": "此項目尚未連到可判讀的 truth-chain / ADR-100 history。",
"emptyValue": "--",
"blockers": "卡點",
"writeFlags": "incident={incident} / autoRepair={autoRepair}",
"human": {
"yes": "需人工",
"no": "不需人工"
},
"fields": {
"stage": "階段",
"repair": "AI 修復",
"verification": "驗證",
"nextStep": "下一步",
"writes": "寫入旗標",
"verdict": "判定"
},
"evidence": {
"autoRepair": "Auto-repair",
"ops": "Ops",
"mcp": "MCP",
"km": "KM",
"adr100": "ADR-100 Route"
}
},
"runDetail": {
"back": "返回 Run 監控",
"title": "Run 處置脈絡",

View File

@@ -30,6 +30,10 @@ import {
import { Link } from "@/i18n/routing";
import { IncidentEvidenceHeader } from "@/components/awooop/incident-evidence-header";
import {
AwoooPStatusChainPanel,
type AwoooPStatusChain,
} from "@/components/awooop/status-chain";
import { cn } from "@/lib/utils";
interface RunDetail {
@@ -195,6 +199,7 @@ interface RunDetailResponse {
mcp_gateway?: McpGatewaySummary;
mcp_legacy?: LegacyMcpEvidence;
remediation_history?: RunRemediationHistory;
awooop_status_chain?: AwoooPStatusChain | null;
counts: {
steps: number;
inbound_events: number;
@@ -968,6 +973,8 @@ export default function RunDetailPage({
writesAutoRepairResult={latestRemediation?.writes_auto_repair_result}
/>
<AwoooPStatusChainPanel chain={detail?.awooop_status_chain} />
<RunActionPanel run={run} counts={detail?.counts} emptyLabel={t("empty")} />
<McpGatewayPanel

View File

@@ -8,6 +8,10 @@
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { useTranslations } from "next-intl";
import { Link } from "@/i18n/routing";
import {
AwoooPStatusChainPanel,
type AwoooPStatusChain,
} from "@/components/awooop/status-chain";
import {
Activity,
BellOff,
@@ -291,6 +295,7 @@ interface CallbackReplyEvent {
run_state?: string | null;
agent_id?: string | null;
run_detail_href?: string | null;
awooop_status_chain?: AwoooPStatusChain | null;
}
interface CallbackRepliesResponse {
@@ -1358,6 +1363,11 @@ function CallbackReplyEvidencePanel({
<p className="mt-3 line-clamp-2 whitespace-pre-line text-xs leading-5 text-[#77736a]">
{event.content_preview || t("previewEmpty")}
</p>
<AwoooPStatusChainPanel
chain={event.awooop_status_chain}
compact
className="mt-3"
/>
<Link
href={runHref as never}
className="mt-3 inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"

View File

@@ -0,0 +1,195 @@
"use client";
import { Activity, CheckCircle2, Route, ShieldAlert, TriangleAlert } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
export interface AwoooPStatusChain {
schema_version?: string;
source?: string;
source_id?: string | null;
incident_ids?: string[];
current_stage?: string | null;
stage_status?: string | null;
verdict?: string | null;
repair_state?: string | null;
verification?: string | null;
needs_human?: boolean | null;
next_step?: string | null;
blockers?: string[];
fetch_error?: string | null;
evidence?: {
auto_repair_records?: number | null;
operation_records?: number | null;
mcp_gateway_total?: number | null;
knowledge_entries?: number | null;
remediation_total?: number | null;
remediation_state?: string | null;
latest_route?: string | null;
latest_mode?: string | null;
latest_at?: string | null;
latest_preview?: string | null;
};
writes?: {
incident?: boolean | null;
auto_repair?: boolean | null;
};
}
function toneClass(chain?: AwoooPStatusChain | null) {
if (!chain) return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
if (chain.needs_human) return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
if (chain.repair_state === "auto_repaired_verified") {
return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
}
if (chain.repair_state === "executed" || chain.stage_status === "success") {
return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
}
return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
}
function chainIcon(chain?: AwoooPStatusChain | null) {
if (!chain) return Route;
if (chain.needs_human) return TriangleAlert;
if (chain.repair_state === "auto_repaired_verified") return CheckCircle2;
if (chain.repair_state === "blocked_manual_required") return ShieldAlert;
return Activity;
}
function boolValue(value: boolean | null | undefined, emptyLabel: string) {
if (value === true) return "true";
if (value === false) return "false";
return emptyLabel;
}
function valueOrEmpty(value: unknown, emptyLabel: string) {
if (value === null || value === undefined || value === "") return emptyLabel;
return String(value);
}
export function AwoooPStatusChainPanel({
chain,
compact = false,
className,
}: {
chain?: AwoooPStatusChain | null;
compact?: boolean;
className?: string;
}) {
const t = useTranslations("awooop.statusChain");
const Icon = chainIcon(chain);
const tone = toneClass(chain);
const emptyLabel = t("emptyValue");
const evidence = chain?.evidence ?? {};
const blockers = chain?.blockers ?? [];
if (!chain) {
return (
<section className={cn("border border-[#e0ddd4] bg-white", compact ? "p-3" : "", className)}>
<div className={cn("flex items-start gap-3", !compact && "px-4 py-3")}>
<span className="flex h-8 w-8 shrink-0 items-center justify-center border border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]">
<Route className="h-4 w-4" aria-hidden="true" />
</span>
<div>
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t("empty")}</p>
</div>
</div>
</section>
);
}
const metrics = [
{ label: t("fields.stage"), value: `${valueOrEmpty(chain.current_stage, emptyLabel)} / ${valueOrEmpty(chain.stage_status, emptyLabel)}` },
{ label: t("fields.repair"), value: valueOrEmpty(chain.repair_state, emptyLabel) },
{ label: t("fields.verification"), value: valueOrEmpty(chain.verification, emptyLabel) },
{ label: t("fields.nextStep"), value: valueOrEmpty(chain.next_step, emptyLabel) },
];
const evidenceMetrics = [
{ label: t("evidence.autoRepair"), value: evidence.auto_repair_records ?? 0 },
{ label: t("evidence.ops"), value: evidence.operation_records ?? 0 },
{ label: t("evidence.mcp"), value: evidence.mcp_gateway_total ?? 0 },
{ label: t("evidence.km"), value: evidence.knowledge_entries ?? 0 },
];
return (
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
<div className={cn(
"flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3]",
compact ? "px-3 py-3" : "px-4 py-3"
)}>
<div className="flex min-w-0 items-start gap-3">
<span className={cn("flex h-9 w-9 shrink-0 items-center justify-center border", tone)}>
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
<div className="min-w-0">
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 text-xs leading-5 text-[#77736a]">
{t("subtitle", {
source: valueOrEmpty(chain.source, emptyLabel),
sourceId: valueOrEmpty(chain.source_id, emptyLabel),
})}
</p>
</div>
</div>
<span className={cn("shrink-0 border px-2 py-0.5 text-xs font-semibold", tone)}>
{chain.needs_human ? t("human.yes") : t("human.no")}
</span>
</div>
<div className={cn("grid gap-px bg-[#e0ddd4]", compact ? "md:grid-cols-2" : "md:grid-cols-4")}>
{metrics.map((item) => (
<div key={item.label} className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<p className="mt-2 truncate font-mono text-sm font-semibold text-[#141413]" title={item.value}>
{item.value}
</p>
</div>
))}
</div>
{!compact && (
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-5">
{evidenceMetrics.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-xl font-semibold text-[#141413]">{item.value}</p>
</div>
))}
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("evidence.adr100")}</p>
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(evidence.latest_route, emptyLabel)}>
{valueOrEmpty(evidence.latest_route, emptyLabel)}
</p>
</div>
</div>
)}
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2">
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("fields.writes")}</p>
<p className="mt-2 font-mono text-sm text-[#141413]">
{t("writeFlags", {
incident: boolValue(chain.writes?.incident, emptyLabel),
autoRepair: boolValue(chain.writes?.auto_repair, emptyLabel),
})}
</p>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("fields.verdict")}</p>
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(chain.verdict, emptyLabel)}>
{valueOrEmpty(chain.verdict, emptyLabel)}
</p>
</div>
</div>
{blockers.length > 0 && (
<div className="border-t border-[#eee9dd] bg-[#fff7e8] px-4 py-3 text-xs leading-5 text-[#8a5a08]">
<span className="font-semibold">{t("blockers")}</span>{" "}
<span className="font-mono">{blockers.slice(0, compact ? 2 : 4).join(", ")}</span>
</div>
)}
</section>
);
}