feat(awooop): surface alert automation blocker map
This commit is contained in:
@@ -8623,6 +8623,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"automationBlockerMap": {
|
||||
"eyebrow": "告警自動化卡點總盤",
|
||||
"title": "為什麼仍需人工處理",
|
||||
"subtitle": "把焦點事故從收件、證據、候選、PlayBook、安全路由、放行、Verifier 到學習回寫拆成可量化 lane;先看卡在哪裡,再看下方詳細證據。",
|
||||
"completion": "自動化閉環就緒度",
|
||||
"boundary": "目前仍有 {blocked} 個阻擋點;這是只讀判讀,不代表 runtime gate 已開或可直接套用修復。",
|
||||
"blockedLabel": "卡點 {count}",
|
||||
"nextAction": "下一步:{value}",
|
||||
"metrics": {
|
||||
"manual": "人工閘門",
|
||||
"gap": "自動化缺口",
|
||||
"verified": "已驗證修復",
|
||||
"runtime": "Runtime gate"
|
||||
},
|
||||
"tones": {
|
||||
"live": "就緒",
|
||||
"in_progress": "補齊中",
|
||||
"blocked": "卡住",
|
||||
"watching": "觀察"
|
||||
},
|
||||
"lanes": {
|
||||
"evidence": {
|
||||
"title": "證據收斂",
|
||||
"detail": "確認 MCP、timeline、來源關聯是否足以支撐 AI 判斷。"
|
||||
},
|
||||
"candidate": {
|
||||
"title": "修復候選",
|
||||
"detail": "確認是否已產生可審查的候選、草案或 dry-run candidate。"
|
||||
},
|
||||
"playbook": {
|
||||
"title": "PlayBook / Ansible",
|
||||
"detail": "確認服務專屬 PlayBook、catalog 與信任路徑,不把診斷命令當修復。"
|
||||
},
|
||||
"safeRoute": {
|
||||
"title": "安全路由",
|
||||
"detail": "確認 allowlisted route、check-mode、apply path 與禁止事項。"
|
||||
},
|
||||
"release": {
|
||||
"title": "執行放行合約",
|
||||
"detail": "確認 owner 放行、維護窗口、rollback、blast radius 與 write 邊界。"
|
||||
},
|
||||
"verifier": {
|
||||
"title": "套用後 Verifier",
|
||||
"detail": "確認修復後驗證計畫與結果回寫,不以看起來正常當成功。"
|
||||
},
|
||||
"learning": {
|
||||
"title": "KM / Trust 回寫",
|
||||
"detail": "確認 KM、PlayBook trust 與學習責任已沉澱,避免下次重複人工判斷。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assetLedger": {
|
||||
"eyebrow": "自動化資產總帳",
|
||||
"title": "KM / PlayBook / 腳本 / 排程 / Verifier 沉澱矩陣",
|
||||
|
||||
@@ -8623,6 +8623,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"automationBlockerMap": {
|
||||
"eyebrow": "告警自動化卡點總盤",
|
||||
"title": "為什麼仍需人工處理",
|
||||
"subtitle": "把焦點事故從收件、證據、候選、PlayBook、安全路由、放行、Verifier 到學習回寫拆成可量化 lane;先看卡在哪裡,再看下方詳細證據。",
|
||||
"completion": "自動化閉環就緒度",
|
||||
"boundary": "目前仍有 {blocked} 個阻擋點;這是只讀判讀,不代表 runtime gate 已開或可直接套用修復。",
|
||||
"blockedLabel": "卡點 {count}",
|
||||
"nextAction": "下一步:{value}",
|
||||
"metrics": {
|
||||
"manual": "人工閘門",
|
||||
"gap": "自動化缺口",
|
||||
"verified": "已驗證修復",
|
||||
"runtime": "Runtime gate"
|
||||
},
|
||||
"tones": {
|
||||
"live": "就緒",
|
||||
"in_progress": "補齊中",
|
||||
"blocked": "卡住",
|
||||
"watching": "觀察"
|
||||
},
|
||||
"lanes": {
|
||||
"evidence": {
|
||||
"title": "證據收斂",
|
||||
"detail": "確認 MCP、timeline、來源關聯是否足以支撐 AI 判斷。"
|
||||
},
|
||||
"candidate": {
|
||||
"title": "修復候選",
|
||||
"detail": "確認是否已產生可審查的候選、草案或 dry-run candidate。"
|
||||
},
|
||||
"playbook": {
|
||||
"title": "PlayBook / Ansible",
|
||||
"detail": "確認服務專屬 PlayBook、catalog 與信任路徑,不把診斷命令當修復。"
|
||||
},
|
||||
"safeRoute": {
|
||||
"title": "安全路由",
|
||||
"detail": "確認 allowlisted route、check-mode、apply path 與禁止事項。"
|
||||
},
|
||||
"release": {
|
||||
"title": "執行放行合約",
|
||||
"detail": "確認 owner 放行、維護窗口、rollback、blast radius 與 write 邊界。"
|
||||
},
|
||||
"verifier": {
|
||||
"title": "套用後 Verifier",
|
||||
"detail": "確認修復後驗證計畫與結果回寫,不以看起來正常當成功。"
|
||||
},
|
||||
"learning": {
|
||||
"title": "KM / Trust 回寫",
|
||||
"detail": "確認 KM、PlayBook trust 與學習責任已沉澱,避免下次重複人工判斷。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assetLedger": {
|
||||
"eyebrow": "自動化資產總帳",
|
||||
"title": "KM / PlayBook / 腳本 / 排程 / Verifier 沉澱矩陣",
|
||||
|
||||
@@ -1047,6 +1047,26 @@ type AutomationAssetLedgerItem = {
|
||||
primaryRef: string;
|
||||
};
|
||||
|
||||
type AutomationBlockerLaneKey =
|
||||
| "evidence"
|
||||
| "candidate"
|
||||
| "playbook"
|
||||
| "safeRoute"
|
||||
| "release"
|
||||
| "verifier"
|
||||
| "learning";
|
||||
|
||||
type AutomationBlockerLane = {
|
||||
key: AutomationBlockerLaneKey;
|
||||
icon: typeof Activity;
|
||||
tone: WorkStatus;
|
||||
ready: number;
|
||||
total: number;
|
||||
blocked: number;
|
||||
primaryRef: string;
|
||||
nextAction: string;
|
||||
};
|
||||
|
||||
const toCount = (value: number | null | undefined) =>
|
||||
Number.isFinite(value) && value ? Math.max(0, Math.trunc(value)) : 0;
|
||||
|
||||
@@ -1066,6 +1086,152 @@ function ledgerTone(item: Pick<AutomationAssetLedgerItem, "ready" | "pending" |
|
||||
return "watching";
|
||||
}
|
||||
|
||||
function statusLooksReady(status: string | null | undefined) {
|
||||
const value = String(status ?? "").toLowerCase();
|
||||
return Boolean(value) && !value.startsWith("blocked") && !value.includes("missing");
|
||||
}
|
||||
|
||||
function blockedFromReady(total: number, ready: number) {
|
||||
return Math.max(0, total - Math.max(0, Math.min(total, ready)));
|
||||
}
|
||||
|
||||
function laneToneFromCounts(ready: number, total: number, blocked: number): WorkStatus {
|
||||
if (blocked > 0) return "blocked";
|
||||
if (ready > 0 && ready < total) return "in_progress";
|
||||
if (total > 0 && ready >= total) return "live";
|
||||
return "watching";
|
||||
}
|
||||
|
||||
function buildAutomationBlockerLanes(
|
||||
telemetry: Telemetry,
|
||||
focusedDraft: RepairCandidateDraftFocus | null
|
||||
): AutomationBlockerLane[] {
|
||||
const chain = telemetry.statusChain;
|
||||
const closure = chain?.automation_handoff?.closure_readiness;
|
||||
const preflight = closure?.controlled_execution_preflight;
|
||||
const release = closure?.execution_release_contract;
|
||||
const ansible = chain?.execution?.ansible;
|
||||
const playbookCount = (chain?.execution?.playbook_ids?.length ?? 0)
|
||||
+ (chain?.execution?.playbook_paths?.length ?? 0);
|
||||
const mcpTotal = toCount(chain?.mcp?.gateway?.total ?? chain?.evidence?.mcp_gateway_total);
|
||||
const timelineTotal = toCount(telemetry.incidentTimeline?.events?.length);
|
||||
const sourceTotal = toCount(chain?.source_refs?.correlation?.provider_event_total)
|
||||
+ toCount(chain?.source_refs?.correlation?.direct_ref_total)
|
||||
+ toCount(chain?.source_refs?.correlation?.candidate_total)
|
||||
+ toCount(chain?.source_refs?.correlation?.applied_link_total);
|
||||
const checkModeTotal = toCount(ansible?.check_mode_total) + toCount(ansible?.pending_check_mode_total);
|
||||
const ansibleCandidateCount = toCount(ansible?.candidate_count);
|
||||
const releaseFieldReady = (key: string) => {
|
||||
const field = release?.fields?.find((item) => item.key === key);
|
||||
return statusLooksReady(field?.status) ? 1 : 0;
|
||||
};
|
||||
const releaseNextAction = release?.next_steps?.map((item) => item.work_item_id).filter(Boolean)[0] ?? chain?.next_step ?? "--";
|
||||
const sourceCorrelationStatus = chain?.source_refs?.correlation?.status ?? chain?.source_refs?.correlation?.verification_status ?? "--";
|
||||
const evidenceReady = (mcpTotal > 0 ? 1 : 0)
|
||||
+ (timelineTotal > 0 ? 1 : 0)
|
||||
+ (sourceTotal > 0 ? 1 : 0);
|
||||
const candidateReady = (focusedDraft ? 1 : 0)
|
||||
+ (chain?.automation_handoff?.candidate || release?.route_id ? 1 : 0)
|
||||
+ (ansibleCandidateCount > 0 ? 1 : 0)
|
||||
+ (checkModeTotal > 0 ? 1 : 0);
|
||||
const playbookReady = (playbookCount > 0 ? 1 : 0)
|
||||
+ (ansibleCandidateCount > 0 ? 1 : 0)
|
||||
+ (chain?.automation_handoff?.candidate?.catalog_id ? 1 : 0);
|
||||
const verifierReady = releaseFieldReady("post_apply_verifier")
|
||||
+ (String(chain?.verification ?? "").toLowerCase().includes("verified") ? 1 : 0)
|
||||
+ (toCount(closure?.release_verifier_package?.post_release_verifier_ready_count) > 0 ? 1 : 0);
|
||||
const learningReady = (toCount(chain?.evidence?.knowledge_entries) > 0 ? 1 : 0)
|
||||
+ releaseFieldReady("km_writeback_owner")
|
||||
+ releaseFieldReady("playbook_trust_owner");
|
||||
const lanes: AutomationBlockerLane[] = [
|
||||
{
|
||||
key: "evidence",
|
||||
icon: Fingerprint,
|
||||
ready: evidenceReady,
|
||||
total: 3,
|
||||
blocked: blockedFromReady(3, evidenceReady),
|
||||
primaryRef: `MCP ${mcpTotal} / timeline ${timelineTotal} / source ${sourceTotal}`,
|
||||
nextAction: String(sourceCorrelationStatus),
|
||||
tone: laneToneFromCounts(evidenceReady, 3, blockedFromReady(3, evidenceReady)),
|
||||
},
|
||||
{
|
||||
key: "candidate",
|
||||
icon: SearchCheck,
|
||||
ready: candidateReady,
|
||||
total: 4,
|
||||
blocked: blockedFromReady(4, candidateReady),
|
||||
primaryRef: focusedDraft?.workItemId ?? chain?.automation_handoff?.candidate?.catalog_id ?? release?.route_id ?? "--",
|
||||
nextAction: String(chain?.next_step ?? "--"),
|
||||
tone: laneToneFromCounts(candidateReady, 4, blockedFromReady(4, candidateReady)),
|
||||
},
|
||||
{
|
||||
key: "playbook",
|
||||
icon: FileText,
|
||||
ready: playbookReady,
|
||||
total: 3,
|
||||
blocked: blockedFromReady(3, playbookReady),
|
||||
primaryRef: chain?.execution?.playbook_paths?.[0] ?? chain?.automation_handoff?.candidate?.apply_playbook_path ?? "--",
|
||||
nextAction: String(ansible?.not_used_reason ?? chain?.operator_outcome?.next_action ?? "--"),
|
||||
tone: laneToneFromCounts(playbookReady, 3, blockedFromReady(3, playbookReady)),
|
||||
},
|
||||
{
|
||||
key: "safeRoute",
|
||||
icon: Network,
|
||||
ready: toCount(preflight?.ready_count),
|
||||
total: toCount(preflight?.total_count) || 7,
|
||||
blocked: toCount(preflight?.blocked_count) || blockedFromReady(7, toCount(preflight?.ready_count)),
|
||||
primaryRef: preflight?.routes?.[0]?.route_id ?? release?.route_id ?? "--",
|
||||
nextAction: String(preflight?.blocked_reason ?? preflight?.next_action ?? "--"),
|
||||
tone: laneToneFromCounts(
|
||||
toCount(preflight?.ready_count),
|
||||
toCount(preflight?.total_count) || 7,
|
||||
toCount(preflight?.blocked_count) || blockedFromReady(7, toCount(preflight?.ready_count))
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "release",
|
||||
icon: ShieldCheck,
|
||||
ready: toCount(release?.ready_count),
|
||||
total: toCount(release?.total_count) || 11,
|
||||
blocked: toCount(release?.blocked_count) || blockedFromReady(11, toCount(release?.ready_count)),
|
||||
primaryRef: release?.work_item_id ?? "--",
|
||||
nextAction: String(release?.blocked_reason ?? releaseNextAction),
|
||||
tone: laneToneFromCounts(
|
||||
toCount(release?.ready_count),
|
||||
toCount(release?.total_count) || 11,
|
||||
toCount(release?.blocked_count) || blockedFromReady(11, toCount(release?.ready_count))
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "verifier",
|
||||
icon: CheckCircle2,
|
||||
ready: verifierReady,
|
||||
total: 3,
|
||||
blocked: blockedFromReady(3, verifierReady),
|
||||
primaryRef: release?.fields?.find((item) => item.key === "post_apply_verifier")?.value
|
||||
?? chain?.automation_handoff?.asset_ids?.verifier
|
||||
?? "--",
|
||||
nextAction: String(chain?.verification ?? closure?.release_verifier_package?.status ?? "--"),
|
||||
tone: laneToneFromCounts(verifierReady, 3, blockedFromReady(3, verifierReady)),
|
||||
},
|
||||
{
|
||||
key: "learning",
|
||||
icon: Database,
|
||||
ready: learningReady,
|
||||
total: 3,
|
||||
blocked: blockedFromReady(3, learningReady),
|
||||
primaryRef: `KM ${toCount(chain?.evidence?.knowledge_entries)} / PlayBook ${playbookCount}`,
|
||||
nextAction: String(release?.fields?.find((item) => item.key === "km_writeback_owner")?.next_step ?? chain?.operator_outcome?.next_action ?? "--"),
|
||||
tone: laneToneFromCounts(learningReady, 3, blockedFromReady(3, learningReady)),
|
||||
},
|
||||
];
|
||||
return lanes.map((lane) => ({
|
||||
...lane,
|
||||
ready: Math.max(0, Math.min(lane.total, lane.ready)),
|
||||
blocked: Math.max(0, Math.min(lane.total, lane.blocked)),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildAutomationAssetLedger(
|
||||
telemetry: Telemetry,
|
||||
focusedDraft: RepairCandidateDraftFocus | null
|
||||
@@ -3408,6 +3574,188 @@ function OperatorSopRail({
|
||||
);
|
||||
}
|
||||
|
||||
function AutomationBlockerMapPanel({
|
||||
telemetry,
|
||||
focusedDraft,
|
||||
loading,
|
||||
}: {
|
||||
telemetry: Telemetry;
|
||||
focusedDraft: RepairCandidateDraftFocus | null;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const t = useTranslations("awooop.workItems.automationBlockerMap");
|
||||
const locale = useLocale();
|
||||
const lanes = useMemo(
|
||||
() => buildAutomationBlockerLanes(telemetry, focusedDraft),
|
||||
[focusedDraft, telemetry]
|
||||
);
|
||||
const ready = lanes.reduce((sum, lane) => sum + lane.ready, 0);
|
||||
const total = lanes.reduce((sum, lane) => sum + lane.total, 0);
|
||||
const blocked = lanes.reduce((sum, lane) => sum + lane.blocked, 0);
|
||||
const completion = total > 0 ? Math.round((ready / total) * 100) : 0;
|
||||
const manualGate = toCount(telemetry.eventRecurrence?.summary?.manual_gate_group_total);
|
||||
const automationGap = toCount(telemetry.eventRecurrence?.summary?.automation_gap_group_total);
|
||||
const verified = toCount(telemetry.quality?.verified_auto_repair_total);
|
||||
const runtimeGate = toCount(telemetry.reportSourceHealth?.rollups?.runtime_gate_count);
|
||||
const topBlocked = [...lanes]
|
||||
.filter((lane) => lane.blocked > 0)
|
||||
.sort((a, b) => b.blocked - a.blocked)
|
||||
.slice(0, 3);
|
||||
const formatCount = useCallback(
|
||||
(value: number) => value.toLocaleString(locale === "zh-TW" ? "zh-TW" : "en-US"),
|
||||
[locale]
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="border border-[#e0ddd4] bg-white"
|
||||
data-testid="awooop-automation-blocker-map"
|
||||
>
|
||||
<div className="grid gap-px bg-[#e0ddd4] xl:grid-cols-[0.74fr_1.26fr]">
|
||||
<div className="min-w-0 bg-white p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center border border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]">
|
||||
<TriangleAlert className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.08em] text-[#9f2f25]">
|
||||
{t("eyebrow")}
|
||||
</p>
|
||||
<h3 className="mt-1 text-lg font-semibold tracking-normal text-[#141413]">
|
||||
{t("title")}
|
||||
</h3>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-[#5f5b52]">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-px bg-[#e0ddd4] sm:grid-cols-4">
|
||||
{[
|
||||
{ key: "manual", value: manualGate },
|
||||
{ key: "gap", value: automationGap },
|
||||
{ key: "verified", value: verified },
|
||||
{ key: "runtime", value: runtimeGate },
|
||||
].map((metric) => (
|
||||
<div key={metric.key} className="min-w-0 bg-[#faf9f3] px-3 py-2">
|
||||
<p className="text-[11px] font-semibold text-[#77736a]">
|
||||
{t(`metrics.${metric.key}` as never)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-xl font-semibold text-[#141413]">
|
||||
{loading ? "--" : formatCount(metric.value)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border border-[#e0ddd4] bg-[#faf9f3] p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold text-[#141413]">{t("completion")}</p>
|
||||
<span className="font-mono text-xs font-semibold text-[#9f2f25]">
|
||||
{loading ? "--" : `${completion}% · ${ready}/${total}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 bg-white">
|
||||
<div
|
||||
className="h-full bg-[#d97757]"
|
||||
style={{ width: `${Math.min(100, completion)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("boundary", { blocked })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-2 sm:grid-cols-3">
|
||||
{(topBlocked.length > 0 ? topBlocked : lanes.slice(0, 3)).map((lane) => (
|
||||
<div key={lane.key} className="min-w-0 border border-[#e0ddd4] bg-white px-3 py-2">
|
||||
<p className="text-[11px] font-semibold text-[#77736a]">
|
||||
{t(`lanes.${lane.key}.title` as never)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-lg font-semibold text-[#141413]">
|
||||
{loading ? "--" : `${lane.ready}/${lane.total}`}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-[11px] text-[#5f5b52]">
|
||||
{t("blockedLabel", { count: lane.blocked })}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 bg-white p-4">
|
||||
<div className="grid gap-2">
|
||||
{lanes.map((lane, index) => {
|
||||
const Icon = lane.icon;
|
||||
const percent = lane.total > 0 ? Math.round((lane.ready / lane.total) * 100) : 0;
|
||||
return (
|
||||
<article
|
||||
key={lane.key}
|
||||
className="grid gap-3 border border-[#e0ddd4] bg-[#faf9f3] p-3 md:grid-cols-[minmax(0,0.72fr)_minmax(180px,0.28fr)]"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-[11px] font-semibold text-[#77736a]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<Icon className="h-4 w-4 text-[#d97757]" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-[#141413]">
|
||||
{t(`lanes.${lane.key}.title` as never)}
|
||||
</h4>
|
||||
<span
|
||||
className={cn(
|
||||
"border px-1.5 py-0.5 text-[10px] font-semibold",
|
||||
lane.tone === "blocked"
|
||||
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
|
||||
: lane.tone === "live"
|
||||
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
|
||||
: lane.tone === "in_progress"
|
||||
? "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"
|
||||
: "border-[#d8d3c7] bg-white text-[#5f5b52]"
|
||||
)}
|
||||
>
|
||||
{t(`tones.${lane.tone}` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||
{t(`lanes.${lane.key}.detail` as never)}
|
||||
</p>
|
||||
<div className="mt-2 min-w-0 break-all font-mono text-[11px] leading-4 text-[#77736a]">
|
||||
{lane.primaryRef}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-mono text-lg font-semibold text-[#141413]">
|
||||
{loading ? "--" : `${lane.ready}/${lane.total}`}
|
||||
</span>
|
||||
<span className="text-[11px] font-semibold text-[#9f2f25]">
|
||||
{t("blockedLabel", { count: lane.blocked })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 bg-white">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full",
|
||||
lane.tone === "live" ? "bg-[#78a87f]" : "bg-[#d97757]"
|
||||
)}
|
||||
style={{ width: `${Math.min(100, percent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 break-all text-[11px] leading-4 text-[#5f5b52]">
|
||||
{t("nextAction", { value: lane.nextAction })}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function AutomationAssetLedgerPanel({
|
||||
telemetry,
|
||||
focusedDraft,
|
||||
@@ -7247,6 +7595,12 @@ export default function AwoooPWorkItemsPage() {
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<AutomationBlockerMapPanel
|
||||
telemetry={telemetry}
|
||||
focusedDraft={focusedRepairCandidateDraft}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<div className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-4">
|
||||
{summary.map((item) => (
|
||||
<SummaryCell key={item.label} label={item.label} value={item.value} icon={item.icon} />
|
||||
|
||||
Reference in New Issue
Block a user