feat(web): enrich Runs apply gate handoff
Some checks failed
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m37s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m43s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m37s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m43s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -10081,6 +10081,18 @@
|
||||
"workItem": "Work Item",
|
||||
"applyCandidate": "Apply 候選",
|
||||
"verifier": "Verifier 資產",
|
||||
"ownerGate": "Owner 審查 Gate",
|
||||
"nextAction": "下一步",
|
||||
"catalog": "候選 PlayBook",
|
||||
"risk": "風險",
|
||||
"matchScore": "匹配分數",
|
||||
"checkPlaybook": "乾跑 PlayBook",
|
||||
"applyPlaybook": "套用 PlayBook",
|
||||
"dryRunAsset": "乾跑資產",
|
||||
"applyAsset": "套用候選資產",
|
||||
"verifierAsset": "Verifier 資產",
|
||||
"checklistTitle": "Owner 審查清單",
|
||||
"forbiddenTitle": "禁止動作",
|
||||
"gates": {
|
||||
"dryRun": "乾跑",
|
||||
"applyGate": "套用審查",
|
||||
|
||||
@@ -10081,6 +10081,18 @@
|
||||
"workItem": "Work Item",
|
||||
"applyCandidate": "Apply 候選",
|
||||
"verifier": "Verifier 資產",
|
||||
"ownerGate": "Owner 審查 Gate",
|
||||
"nextAction": "下一步",
|
||||
"catalog": "候選 PlayBook",
|
||||
"risk": "風險",
|
||||
"matchScore": "匹配分數",
|
||||
"checkPlaybook": "乾跑 PlayBook",
|
||||
"applyPlaybook": "套用 PlayBook",
|
||||
"dryRunAsset": "乾跑資產",
|
||||
"applyAsset": "套用候選資產",
|
||||
"verifierAsset": "Verifier 資產",
|
||||
"checklistTitle": "Owner 審查清單",
|
||||
"forbiddenTitle": "禁止動作",
|
||||
"gates": {
|
||||
"dryRun": "乾跑",
|
||||
"applyGate": "套用審查",
|
||||
|
||||
@@ -419,6 +419,10 @@ export function AwoooPStatusChainPanel({
|
||||
const mcpGatewayTotal = mcpGateway.total ?? 0;
|
||||
const mcpGatewayProblemTotal = (mcpGateway.failed ?? 0) + (mcpGateway.blocked ?? 0);
|
||||
const executionTotal = execution.operation_total ?? 0;
|
||||
const handoffCandidate = automationHandoff?.candidate;
|
||||
const handoffAssets = automationHandoff?.asset_ids;
|
||||
const ownerReviewChecklist = automationHandoff?.owner_review_checklist ?? [];
|
||||
const forbiddenActions = automationHandoff?.forbidden_actions ?? [];
|
||||
const sourceToolchainTone: SourceFlowTone = sourceCorrelation
|
||||
? (sourceLinkVerified ? "success" : (sourceVerificationBlocked ? "blocked" : "warning"))
|
||||
: "neutral";
|
||||
@@ -516,6 +520,18 @@ export function AwoooPStatusChainPanel({
|
||||
},
|
||||
];
|
||||
const handoffGates = automationHandoff?.gates ?? [];
|
||||
const handoffCandidateCards = [
|
||||
{ key: "catalog", label: t("applyGate.catalog"), value: handoffCandidate?.catalog_id },
|
||||
{ key: "risk", label: t("applyGate.risk"), value: handoffCandidate?.risk_level },
|
||||
{ key: "matchScore", label: t("applyGate.matchScore"), value: handoffCandidate?.match_score },
|
||||
{ key: "checkPlaybook", label: t("applyGate.checkPlaybook"), value: handoffCandidate?.check_mode_playbook_path },
|
||||
{ key: "applyPlaybook", label: t("applyGate.applyPlaybook"), value: handoffCandidate?.apply_playbook_path },
|
||||
];
|
||||
const handoffAssetCards = [
|
||||
{ key: "dryRunAsset", label: t("applyGate.dryRunAsset"), value: handoffAssets?.dry_run },
|
||||
{ key: "applyAsset", label: t("applyGate.applyAsset"), value: handoffAssets?.apply_candidate },
|
||||
{ key: "verifierAsset", label: t("applyGate.verifierAsset"), value: handoffAssets?.verifier },
|
||||
];
|
||||
const handoffGateLabel = (key: string | null | undefined) => {
|
||||
const labels: Record<string, string> = {
|
||||
dry_run: t("applyGate.gates.dryRun"),
|
||||
@@ -775,23 +791,77 @@ export function AwoooPStatusChainPanel({
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.workItem")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.work_item_id, emptyLabel)}>
|
||||
<p className="mt-2 break-all font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.work_item_id, emptyLabel)}>
|
||||
{valueOrEmpty(automationHandoff.work_item_id, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.applyCandidate")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.candidate?.apply_playbook_path, emptyLabel)}>
|
||||
{valueOrEmpty(automationHandoff.candidate?.apply_playbook_path, emptyLabel)}
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.ownerGate")}</p>
|
||||
<p className="mt-2 break-words font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.owner_review_gate, emptyLabel)}>
|
||||
{valueOrEmpty(automationHandoff.owner_review_gate, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.verifier")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.asset_ids?.verifier, emptyLabel)}>
|
||||
{valueOrEmpty(automationHandoff.asset_ids?.verifier, emptyLabel)}
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.nextAction")}</p>
|
||||
<p className="mt-2 break-words font-mono text-sm text-[#141413]" title={valueOrEmpty(automationHandoff.next_action, emptyLabel)}>
|
||||
{valueOrEmpty(automationHandoff.next_action, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-5">
|
||||
{handoffCandidateCards.map((item) => (
|
||||
<div key={item.key} 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 break-all font-mono text-sm text-[#141413]" title={valueOrEmpty(item.value, emptyLabel)}>
|
||||
{valueOrEmpty(item.value, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
|
||||
{handoffAssetCards.map((item) => (
|
||||
<div key={item.key} 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 break-all font-mono text-sm text-[#141413]" title={valueOrEmpty(item.value, emptyLabel)}>
|
||||
{valueOrEmpty(item.value, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-2">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.checklistTitle")}</p>
|
||||
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
|
||||
{ownerReviewChecklist.length}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{(ownerReviewChecklist.length ? ownerReviewChecklist : [emptyLabel]).map((item, index) => (
|
||||
<li key={`${item}-${index}`} className="flex min-w-0 items-start gap-2 text-xs leading-5 text-[#141413]">
|
||||
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#17602a]" aria-hidden="true" />
|
||||
<span className="min-w-0 break-words font-mono">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.forbiddenTitle")}</p>
|
||||
<span className="border border-[#e2a29b] bg-[#fff0ef] px-2 py-0.5 font-mono text-[11px] text-[#9f2f25]">
|
||||
{forbiddenActions.length}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{(forbiddenActions.length ? forbiddenActions : [emptyLabel]).map((item, index) => (
|
||||
<li key={`${item}-${index}`} className="flex min-w-0 items-start gap-2 text-xs leading-5 text-[#141413]">
|
||||
<TriangleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#9f2f25]" aria-hidden="true" />
|
||||
<span className="min-w-0 break-words font-mono">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
## 2026-06-25|Runs apply gate handoff 補成可審查處置包
|
||||
|
||||
**背景**:`INC-20260625-977E5F` 已能在 Runs 事故焦點狀態鏈顯示 `ansible_check_mode_only`、`dry_run=passed`、`apply_gate=blocked`、`verifier=blocked`,但 UI 仍只顯示少數欄位。使用者仍無法快速判斷 owner 要審什麼、候選 PlayBook 是哪一個、乾跑 / 套用 / verifier 資產 ID 是什麼,以及哪些動作明確禁止。
|
||||
|
||||
**完成**:
|
||||
- `AwoooPStatusChainPanel` 將 `automation_handoff` 延伸為可審查處置包。
|
||||
- 新增候選 PlayBook 卡:catalog、risk、match score、check-mode playbook、apply playbook。
|
||||
- 新增資產卡:dry-run asset、apply candidate asset、verifier asset。
|
||||
- 新增 owner review checklist 與 forbidden actions 區塊;長 ID / playbook path 使用換行處理,避免 mobile 水平溢出。
|
||||
- `zh-TW` / `en` messages 同步新增 apply gate handoff 文案。
|
||||
|
||||
**本地驗證**:
|
||||
- i18n leaf diff:`zh=13689`、`en=13689`、`missingEn=0`、`missingZh=0`、`typeDiff=0`。
|
||||
- `pnpm --filter @awoooi/web typecheck` 通過。
|
||||
- `python3 scripts/security/source-control-owner-response-guard.py --root .`:`SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`。
|
||||
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。
|
||||
- `git diff --check` 通過。
|
||||
|
||||
**完成度同步**:
|
||||
- Runs apply gate handoff 可審查性:`70% -> 78%`。
|
||||
- AwoooP Runs 可判讀性:`71% -> 74%`。
|
||||
- 真正 AI 自動化 verified repair 成功率仍不提高;此段只是把現有 dry-run / owner gate / verifier 缺口變成可審查、可沉澱的操作面板。
|
||||
|
||||
**邊界**:本段不執行 Ansible apply、不重啟 `node-exporter-188`、不 SSH、不發 Telegram、不新增操作按鈕、不寫 runtime state、不開 runtime gate。正式站驗證待下一個 deploy marker 後重跑 desktop / mobile smoke。
|
||||
|
||||
## 2026-06-25|Runs incident filter 預篩選修正避免 502
|
||||
|
||||
**背景**:`4e329bce` 部署後,正式 API `/api/v1/platform/status-chain?incident_id=INC-20260625-977E5F` 已回 `automation_handoff.kind=ansible_check_mode_apply_gate`,但 `/zh-TW/awooop/runs?incident_id=INC-20260625-977E5F` 看不到 `乾跑後套用閘門`。追查發現 `/api/v1/platform/runs/list` 不帶 incident filter 回 `200`,一帶 `INC-20260625-977E5F` 回 `502 Bad Gateway`;原因是舊 filter 先載入 project 下大量 runs,再逐筆聚合 message context,production 歷史量一大就 timeout。
|
||||
|
||||
@@ -310,6 +310,23 @@ Tenants 目前已讀到:
|
||||
|
||||
完成度同步:Runs incident drilldown 可判讀性 `90% -> 100%`;AwoooP Runs apply-gate 正式頁驗證 `100%`;真正修復自動執行成功率仍不提高。
|
||||
|
||||
### 2.5.13 Runs apply gate handoff 審查包
|
||||
|
||||
`INC-20260625-977E5F` 已有 `automation_handoff`,但畫面只呈現三段 Gate 與部分 ID,operator 仍不容易知道「要審什麼、為什麼不能按、下一步資產在哪」。本段把 API 已有欄位完整產品化成審查包。
|
||||
|
||||
| 區塊 | 調整 |
|
||||
|---|---|
|
||||
| Owner Gate | 顯示 `work_item_id`、`owner_review_gate`、`next_action` |
|
||||
| 候選 PlayBook | 顯示 catalog、risk、match score、check-mode playbook、apply playbook |
|
||||
| 資產 ID | 顯示 dry-run asset、apply candidate asset、verifier asset |
|
||||
| 審查清單 | 顯示 `owner_review_checklist`,讓人工接手不再只看到「需人工」 |
|
||||
| 禁止動作 | 顯示 `forbidden_actions`,避免誤把 dry-run 或 approval UI 當執行授權 |
|
||||
| Mobile | 長 ID / playbook path 改用換行處理,避免水平溢出 |
|
||||
|
||||
本地驗證:i18n leaf diff `0`、`pnpm --filter @awoooi/web typecheck`、`source-control-owner-response-guard.py`、`security-mirror-progress-guard.py`、`git diff --check` 皆通過。
|
||||
|
||||
完成度同步:Runs apply gate handoff 可審查性 `70% -> 78%`;AwoooP Runs 可判讀性 `71% -> 74%`。真正自動修復成功率不提高;正式站驗證需 deploy marker 後重跑。
|
||||
|
||||
## 3. 頁面 UI/UX 現況盤點
|
||||
|
||||
2026-06-25 對正式站桌機 / mobile 抽查:
|
||||
|
||||
Reference in New Issue
Block a user