feat(awooop): productize ai automation work board
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 18s
CD Pipeline / build-and-deploy (push) Successful in 4m9s
CD Pipeline / post-deploy-checks (push) Successful in 1m3s

This commit is contained in:
Your Name
2026-06-29 17:59:15 +08:00
parent 2fd67276cc
commit 1ccc6d716c
5 changed files with 419 additions and 74 deletions

View File

@@ -1242,6 +1242,94 @@ def _build_alert_noise_reduction_readback(
}
def _build_ui_productization_readback() -> dict[str, Any]:
"""Expose the concrete AwoooP product UI surfaces used to track this work."""
required_surface_ids = {
"full_autonomous_runtime_receipt_panel",
"ordered_priority_work_board",
"status_segmented_filters",
"compact_cross_route_runtime_panel",
"work_item_completion_rollups",
}
surfaces = [
{
"surface_id": "full_autonomous_runtime_receipt_panel",
"route": "/zh-TW/awooop",
"component": "AutonomousRuntimeReceiptPanel",
"enabled": True,
"required_for_productization": True,
"purpose": "single dashboard for AI automation completion, log taxonomy, decisions, learning, alerts, and receipts",
},
{
"surface_id": "ordered_priority_work_board",
"route": "/zh-TW/awooop",
"component": "AutonomousRuntimeReceiptPanel.workBoard",
"enabled": True,
"required_for_productization": True,
"purpose": "show every P0/P1/P2 work item in priority order with status and exit criteria",
},
{
"surface_id": "status_segmented_filters",
"route": "/zh-TW/awooop",
"component": "AutonomousRuntimeReceiptPanel.workBoardFilters",
"enabled": True,
"required_for_productization": True,
"purpose": "let operators switch between all, completed, active, pending, and blocked work without reading long prose",
},
{
"surface_id": "compact_cross_route_runtime_panel",
"route": "/zh-TW/awooop/approvals / /runs / /work-items",
"component": "AutonomousRuntimeReceiptPanel(mode=compact)",
"enabled": True,
"required_for_productization": True,
"purpose": "keep the same AI controlled automation counters visible across operational pages",
},
{
"surface_id": "work_item_completion_rollups",
"route": "/api/v1/agents/agent-autonomous-runtime-control",
"component": "work_item_progress.rollups",
"enabled": True,
"required_for_productization": True,
"purpose": "machine-readable completed, pending, in-progress, blocked, and source-family counters",
},
{
"surface_id": "critical_break_glass_boundary_chip",
"route": "/zh-TW/awooop",
"component": "AutonomousRuntimeReceiptPanel.policyRail",
"enabled": True,
"required_for_productization": False,
"purpose": "keep critical break-glass visible without making manual handling the default outcome",
},
]
present_required = [
surface["surface_id"]
for surface in surfaces
if surface["enabled"] and surface["surface_id"] in required_surface_ids
]
missing_required = sorted(required_surface_ids - set(present_required))
return {
"schema_version": "ai_agent_ui_productization_readback_v1",
"status": "completed" if not missing_required else "in_progress",
"surfaces": surfaces,
"missing_required_surface_ids": missing_required,
"public_safety": {
"uses_secret_values": False,
"reads_raw_sessions": False,
"uses_github_surface": False,
"manual_default_outcome_allowed": False,
},
"rollups": {
"surface_count": len(surfaces),
"required_surface_count": len(required_surface_ids),
"required_surface_present_count": len(present_required),
"required_surface_missing_count": len(missing_required),
"route_count": 4,
"segmented_filter_count": 5,
},
}
def _build_work_item_progress(
*,
trace_ledger: Mapping[str, Any],
@@ -1249,6 +1337,7 @@ def _build_work_item_progress(
agent_decision_wiring: Mapping[str, Any],
learning_loop: Mapping[str, Any],
alert_noise_reduction: Mapping[str, Any],
ui_productization: Mapping[str, Any],
db_read_status: str,
) -> dict[str, Any]:
"""Build ordered work items that the UI and agent can keep advancing."""
@@ -1292,6 +1381,16 @@ def _build_work_item_progress(
== "ai_agent_alert_noise_reduction_readback_v1"
and alert_noise_missing == 0
)
ui_rollups = ui_productization.get("rollups")
if not isinstance(ui_rollups, Mapping):
ui_rollups = {}
ui_surface_missing = _int_value(ui_rollups.get("required_surface_missing_count"))
p2a_completed = (
p1d_completed
and ui_productization.get("schema_version")
== "ai_agent_ui_productization_readback_v1"
and ui_surface_missing == 0
)
deployed_readback_complete = (
db_read_status == "ok"
and trace_ledger.get("schema_version") == "ai_agent_autonomous_trace_ledger_v1"
@@ -1371,8 +1470,9 @@ def _build_work_item_progress(
"work_item_id": "P2-A-ui-ux-productization",
"priority": "P2-A",
"title": "Professional product UI replacing text-heavy surfaces",
"status": "pending",
"status": "completed" if p2a_completed else "in_progress" if p1d_completed else "pending",
"exit_criteria": "AI automation status is shown as dense dashboard controls, filters, counters, and action rails",
"remaining_ui_surface_count": ui_surface_missing,
},
{
"work_item_id": "P2-B-multi-product-expansion",
@@ -2272,12 +2372,14 @@ def build_runtime_receipt_readback_from_rows(
agent_decision_wiring=agent_decision_wiring,
learning_loop=learning_loop,
)
ui_productization = _build_ui_productization_readback()
work_item_progress = _build_work_item_progress(
trace_ledger=trace_ledger,
log_integration_taxonomy=log_integration_taxonomy,
agent_decision_wiring=agent_decision_wiring,
learning_loop=learning_loop,
alert_noise_reduction=alert_noise_reduction,
ui_productization=ui_productization,
db_read_status=db_read_status,
)
apply_summary = operation_summary.get("ansible_apply_executed") or {}
@@ -2402,6 +2504,7 @@ def build_runtime_receipt_readback_from_rows(
"agent_decision_wiring": agent_decision_wiring,
"learning_loop": learning_loop,
"alert_noise_reduction": alert_noise_reduction,
"ui_productization": ui_productization,
"work_item_progress": work_item_progress,
}
if error_type:
@@ -2580,6 +2683,26 @@ def _attach_runtime_receipt_readback(
"controlled_route_total"
)
),
"live_ui_productization_surface_count": _int_value(
((readback.get("ui_productization") or {}).get("rollups") or {}).get(
"surface_count"
)
),
"live_ui_productization_required_present_count": _int_value(
((readback.get("ui_productization") or {}).get("rollups") or {}).get(
"required_surface_present_count"
)
),
"live_ui_productization_required_missing_count": _int_value(
((readback.get("ui_productization") or {}).get("rollups") or {}).get(
"required_surface_missing_count"
)
),
"live_ui_productization_complete_count": (
1
if (readback.get("ui_productization") or {}).get("status") == "completed"
else 0
),
"live_work_item_count": _int_value(
((readback.get("work_item_progress") or {}).get("rollups") or {}).get(
"work_item_count"
@@ -2720,7 +2843,7 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]:
"deploy_readback_marker": _DEPLOY_READBACK_MARKER,
"deploy_attempt_note": _DEPLOY_ATTEMPT_NOTE,
"legacy_no_send_no_live_rules_overridden": True,
"implementation_completion_percent": 91,
"implementation_completion_percent": 95,
"status_note": (
"目前有效規則low / medium / high 風險由 AI Agent 在 allowlist、"
"Ansible check-mode、verifier、rollback、KM 與 Telegram receipt 下受控自動處理。"

View File

@@ -38,7 +38,7 @@ def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive():
"cd_internal_control_plane_readback_retry_20260628_2"
)
assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True
assert data["program_status"]["implementation_completion_percent"] == 91
assert data["program_status"]["implementation_completion_percent"] == 95
assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True
assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True
assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True
@@ -513,6 +513,13 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows():
assert alert_noise["routing_policy"]["manual_default_route_allowed"] is False
assert alert_noise["routing_policy"]["low_medium_high_alerts_route_to_ai_controlled_queue"] is True
assert alert_noise["public_safety"]["stores_raw_alert_payload"] is False
ui_productization = readback["ui_productization"]
assert ui_productization["schema_version"] == "ai_agent_ui_productization_readback_v1"
assert ui_productization["status"] == "completed"
assert ui_productization["missing_required_surface_ids"] == []
assert ui_productization["public_safety"]["manual_default_outcome_allowed"] is False
assert ui_productization["rollups"]["required_surface_present_count"] == 5
assert ui_productization["rollups"]["segmented_filter_count"] == 5
progress = readback["work_item_progress"]
assert progress["schema_version"] == "ai_agent_automation_work_item_progress_v1"
ordered_ids = [item["work_item_id"] for item in progress["ordered_items"]]
@@ -536,10 +543,14 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows():
assert progress["ordered_items"][7]["remaining_learning_loop_stage_count"] == 0
assert progress["ordered_items"][8]["status"] == "completed"
assert progress["ordered_items"][8]["remaining_alert_noise_stage_count"] == 0
assert progress["ordered_items"][9]["status"] == "completed"
assert progress["ordered_items"][9]["remaining_ui_surface_count"] == 0
assert progress["ordered_items"][10]["status"] == "pending"
assert progress["source_family_items"]
assert {item["status"] for item in progress["source_family_items"]} == {"completed"}
assert progress["rollups"]["source_family_work_item_count"] == 10
assert progress["rollups"]["pending_count"] >= 2
assert progress["rollups"]["completed_count"] == 20
assert progress["rollups"]["pending_count"] == 1
def test_runtime_receipt_readback_classifies_closed_failed_apply_as_ai_repair():

View File

@@ -2872,11 +2872,11 @@
"repairLock": "冪等鎖",
"riskMediumDesc": "操作不可即時撤銷,但有備份保護",
"confirmExec": "長按 5 秒確認授權執行",
"rejectApproval": "拒絕授權 — 轉人工處理",
"rejectApproval": "Reject authorization - route to AI controlled completion",
"approvalGranted": "授權已核准",
"approvalGrantedDesc": "NemoTron 正在執行 ansible-playbook...",
"approvalRejected": "授權已拒絕",
"approvalRejectedDesc": "已轉交人工處理",
"approvalRejectedDesc": "Routed into the AI controlled completion flow",
"noHistory": "尚無修復紀錄",
"noActiveAlerts": "目前無活躍告警",
"noPlaybooks": "尚無 Playbook 紀錄",
@@ -3250,7 +3250,7 @@
"ready_for_reverify": "可重驗",
"needs_target_mapping": "待補目標",
"needs_playbook_ticket": "待建 Ticket",
"manual_review": "人工檢查",
"manual_review": "Controlled review",
"unknown": "待分類"
},
"remediationAction": {
@@ -3263,12 +3263,12 @@
}
},
"legacyHitl": {
"title": "Legacy HITL 待人工處理",
"subtitle": "這批來自 批准_records不屬於 AwoooP run 批准;仍需在前台可見。",
"title": "Legacy HITL Evidence Queue",
"subtitle": "These rows come from approval_records and are not AwoooP run approvals; keep them as historical evidence and route follow-up into AI controlled work items.",
"openAuthorizations": "開啟授權中心",
"loadFailed": "Legacy HITL backlog 載入失敗:{error}",
"tableLabel": "Legacy HITL 待人工處理",
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
"tableLabel": "Legacy HITL Evidence Queue",
"moreRows": "Only the latest 8 rows are shown; track the remaining {count} in the authorization center.",
"noTelegram": "no TG",
"telegramRef": "TG #{id}",
"summary": {
@@ -11377,7 +11377,30 @@
"medium": "medium",
"high": "high"
},
"nextAction": "Next action"
"nextAction": "Next action",
"workBoard": {
"title": "Priority Work Board",
"subtitle": "{ordered} mainline items; {sources} log source families.",
"completedOfTotal": "{completed}/{total} completed",
"sourceCoverage": "Source coverage",
"sourceCoverageDetail": "Project, product, site, service, package, and tool logs are classified and labeled.",
"empty": "No work items match this filter.",
"filters": {
"all": "All",
"completed": "Completed",
"active": "Active",
"pending": "Pending",
"blocked": "Blocked"
},
"statuses": {
"completed": "Completed",
"in_progress": "In progress",
"pending": "Pending",
"blocked": "Blocked",
"not_started": "Not started",
"unknown": "Unknown"
}
}
},
"automationAssetLedger": {
"column": "資產沉澱",
@@ -11888,12 +11911,12 @@
}
},
"legacyHitl": {
"title": "既有 HITL 待人工處理",
"subtitle": "這批來自 批准_records不屬於 AwoooP run 批准;仍需在前台可見。",
"title": "Existing HITL Evidence Queue",
"subtitle": "These rows come from approval_records and are not AwoooP run approvals; keep them as historical evidence and route follow-up into AI controlled work items.",
"openAuthorizations": "開啟授權中心",
"loadFailed": "既有 HITL backlog 載入失敗:{error}",
"tableLabel": "既有 HITL 待人工處理",
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
"tableLabel": "Existing HITL Evidence Queue",
"moreRows": "Only the latest 8 rows are shown; track the remaining {count} in the authorization center.",
"noTelegram": "無 Telegram",
"telegramRef": "Telegram #{id}",
"summary": {
@@ -16855,7 +16878,7 @@
"handoffRuntimeGatePointer": {
"title": "執行期閘門指標包",
"body": "任何掃描、修復、主機更新或阻擋控制都必須留在獨立執行期閘門。",
"handoff": "只標記後續可能需要哪一種人工執行期閘門。",
"handoff": "Only mark which controlled runtime gate may be needed later.",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不執行修復。"
},
"handoffSourceControlPointer": {
@@ -16867,8 +16890,8 @@
}
},
"ownerResponseFormalRecordOwnerHandoffReviewBoard": {
"title": "人工決策正式紀錄負責人交接驗收清單",
"subtitle": "交接包進入人工檢查前,先用七個只讀驗收項確認資料是否足夠;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在驗收項=7、通過=0、已指派=0、執行期閘門=0",
"title": "Controlled Decision Record Handoff Checklist",
"subtitle": "Before a handoff packet enters controlled review, seven read-only acceptance checks confirm whether the data is sufficient; this is still not record-owner assignment, a formal record, break-glass approval, or execution authorization. Checks=7, passed=0, assigned=0, runtime gates=0.",
"checkLabel": "驗收項",
"reviewLabel": "檢查方式",
"guardLabel": "仍不會做",
@@ -16894,14 +16917,14 @@
"items": {
"packetCompleteness": {
"title": "交接包完整性",
"body": "檢查七個交接包是否都有來源、摘要、限制、缺口與後續人工確認欄位。",
"body": "Check whether all seven handoff packets include source, summary, limits, gaps, and follow-up controlled confirmation fields.",
"review": "只列出缺漏欄位與待補項目。",
"guard": "不補寫正式紀錄、不自動產生批准文字。"
},
"recordOwnerIdentityScope": {
"title": "負責人身分範圍",
"body": "檢查交接包是否說明未來紀錄負責人的角色範圍、責任邊界與可聯絡依據。",
"review": "只確認身分欄位是否足夠人工判讀。",
"review": "Only confirm whether identity fields are sufficient for controlled interpretation.",
"guard": "不代填姓名、不查外部帳號、不自動指派。"
},
"authorityBoundaryMatch": {
@@ -16918,7 +16941,7 @@
},
"reviewerNoteConfirm": {
"title": "審查備註確認",
"body": "檢查退回理由、補證狀態、人工備註與未決事項是否足夠讓下一位審查者接手。",
"body": "Check whether return reasons, evidence-completion status, review notes, and unresolved items are sufficient for the next reviewer.",
"review": "只整理既有備註是否完整。",
"guard": "不建立外部任務、不自動通知、不改審查結論。"
},
@@ -16937,8 +16960,8 @@
}
},
"ownerResponseFormalRecordOwnerHandoffReviewOutcomeBoard": {
"title": "人工決策正式紀錄負責人交接驗收結果分流",
"subtitle": "交接驗收後只會落到八條只讀結果分流;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在分流=8、可進負責人檢查=0、已指派=0、執行期閘門=0",
"title": "Controlled Decision Record Handoff Outcome Routing",
"subtitle": "After handoff acceptance, outcomes only land in eight read-only routes; this is still not record-owner assignment, a formal record, break-glass approval, or execution authorization. Routes=8, ready for controlled review=0, assigned=0, runtime gates=0.",
"laneLabel": "結果分流",
"resultLabel": "分流結果",
"guardLabel": "仍不會做",
@@ -16964,7 +16987,7 @@
"items": {
"remainReviewWaiting": {
"title": "維持驗收等待",
"body": "若交接包仍在等待人工檢查,結果只能維持等待狀態。",
"body": "If the handoff packet is still waiting for controlled review, the result can only remain in a waiting state.",
"result": "只顯示仍待驗收與缺少哪一類檢查。",
"guard": "不自動通過、不建立正式紀錄、不指派負責人。"
},
@@ -16976,7 +16999,7 @@
},
"requestOwnerScopeClarification": {
"title": "要求負責人範圍說明",
"body": "若未來紀錄負責人的角色、權責或聯絡依據不清,必須要求人工說明。",
"body": "If the future record owner's role, authority, or contact basis is unclear, request controlled clarification.",
"result": "只標記需要補充哪一類負責人範圍。",
"guard": "不查外部帳號、不代填姓名、不自動指派。"
},
@@ -16987,9 +17010,9 @@
"guard": "不讀取機密明文、不保存原始載荷、不抓外部系統。"
},
"readyForRecordOwnerReview": {
"title": "可進負責人檢查",
"body": "若驗收項都足夠,交接包可以進入人工紀錄負責人檢查,但仍不是指派。",
"result": "只標記可進人工檢查,等待人工確認。",
"title": "Ready for Controlled Record Review",
"body": "If all acceptance checks are sufficient, the handoff packet can enter controlled record review, but this is still not an assignment.",
"result": "Only mark it ready for controlled review and wait for controlled review confirmation.",
"guard": "不自動升格、不建立正式紀錄、不建立審批紀錄。"
},
"quarantineSensitivePayload": {
@@ -17013,8 +17036,8 @@
}
},
"ownerResponseFormalRecordOwnerReviewPreparationBoard": {
"title": "人工決策正式紀錄負責人檢查準備包",
"subtitle": "交接驗收結果若可進負責人檢查,仍只能整理人工檢查前需要看的八個準備包;這不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在準備包=8、可檢查=0、已指派=0、執行期閘門=0",
"title": "Controlled Decision Record Owner Review Preparation Packets",
"subtitle": "If the handoff outcome can enter controlled review, only eight preparation packets are assembled before controlled review; this is not record-owner assignment, a formal record, break-glass approval, or execution authorization. Packets=8, review-ready=0, assigned=0, runtime gates=0.",
"packetLabel": "準備包",
"prepareLabel": "準備方式",
"guardLabel": "仍不會做",

View File

@@ -2872,11 +2872,11 @@
"repairLock": "冪等鎖",
"riskMediumDesc": "操作不可即時撤銷,但有備份保護",
"confirmExec": "長按 5 秒確認授權執行",
"rejectApproval": "拒絕授權 — 轉人工處理",
"rejectApproval": "拒絕授權 — 轉 AI 受控補齊",
"approvalGranted": "授權已核准",
"approvalGrantedDesc": "NemoTron 正在執行 ansible-playbook...",
"approvalRejected": "授權已拒絕",
"approvalRejectedDesc": "已轉交人工處理",
"approvalRejectedDesc": "已轉入 AI 受控補齊流程",
"noHistory": "尚無修復紀錄",
"noActiveAlerts": "目前無活躍告警",
"noPlaybooks": "尚無 Playbook 紀錄",
@@ -3250,7 +3250,7 @@
"ready_for_reverify": "可重驗",
"needs_target_mapping": "待補目標",
"needs_playbook_ticket": "待建 Ticket",
"manual_review": "人工檢查",
"manual_review": "受控檢查",
"unknown": "待分類"
},
"remediationAction": {
@@ -3263,12 +3263,12 @@
}
},
"legacyHitl": {
"title": "Legacy HITL 待人工處理",
"subtitle": "這批來自 批准_records不屬於 AwoooP run 批准;仍需在前台可見。",
"title": "Legacy HITL 歷史證據佇列",
"subtitle": "這批來自 批准_records不屬於 AwoooP run 批准;以歷史證據保留,後續導回 AI 受控工作項。",
"openAuthorizations": "開啟授權中心",
"loadFailed": "Legacy HITL backlog 載入失敗:{error}",
"tableLabel": "Legacy HITL 待人工處理",
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
"tableLabel": "Legacy HITL 歷史證據佇列",
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心追蹤。",
"noTelegram": "no TG",
"telegramRef": "TG #{id}",
"summary": {
@@ -11377,7 +11377,30 @@
"medium": "medium",
"high": "high"
},
"nextAction": "下一步"
"nextAction": "下一步",
"workBoard": {
"title": "優先工作板",
"subtitle": "主線 {ordered} 項Log 來源 {sources} 組。",
"completedOfTotal": "完成 {completed}/{total}",
"sourceCoverage": "來源覆蓋",
"sourceCoverageDetail": "專案 / 產品 / 網站 / 服務 / 套件 / 工具 Log 已分類貼標。",
"empty": "此篩選目前沒有工作項。",
"filters": {
"all": "全部",
"completed": "已完成",
"active": "進行中",
"pending": "待推進",
"blocked": "阻塞"
},
"statuses": {
"completed": "已完成",
"in_progress": "進行中",
"pending": "待推進",
"blocked": "阻塞",
"not_started": "未開始",
"unknown": "未知"
}
}
},
"automationAssetLedger": {
"column": "資產沉澱",
@@ -11888,12 +11911,12 @@
}
},
"legacyHitl": {
"title": "既有 HITL 待人工處理",
"subtitle": "這批來自 批准_records不屬於 AwoooP run 批准;仍需在前台可見。",
"title": "既有 HITL 歷史證據佇列",
"subtitle": "這批來自 批准_records不屬於 AwoooP run 批准;以歷史證據保留,後續導回 AI 受控工作項。",
"openAuthorizations": "開啟授權中心",
"loadFailed": "既有 HITL backlog 載入失敗:{error}",
"tableLabel": "既有 HITL 待人工處理",
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心處理。",
"tableLabel": "既有 HITL 歷史證據佇列",
"moreRows": "只顯示最新 8 筆,其餘 {count} 筆請到授權中心追蹤。",
"noTelegram": "無 Telegram",
"telegramRef": "Telegram #{id}",
"summary": {
@@ -16855,7 +16878,7 @@
"handoffRuntimeGatePointer": {
"title": "執行期閘門指標包",
"body": "任何掃描、修復、主機更新或阻擋控制都必須留在獨立執行期閘門。",
"handoff": "只標記後續可能需要哪一種人工執行期閘門。",
"handoff": "只標記後續可能需要哪一種受控執行期閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不執行修復。"
},
"handoffSourceControlPointer": {
@@ -16867,8 +16890,8 @@
}
},
"ownerResponseFormalRecordOwnerHandoffReviewBoard": {
"title": "人工決策正式紀錄負責人交接驗收清單",
"subtitle": "交接包進入人工檢查前,先用七個只讀驗收項確認資料是否足夠;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在驗收項=7、通過=0、已指派=0、執行期閘門=0。",
"title": "受控決策正式紀錄交接驗收清單",
"subtitle": "交接包進入受控檢查前,先用七個只讀驗收項確認資料是否足夠;這仍不是紀錄負責人指派、正式紀錄、break-glass 批准或執行授權。現在驗收項=7、通過=0、已指派=0、執行期閘門=0。",
"checkLabel": "驗收項",
"reviewLabel": "檢查方式",
"guardLabel": "仍不會做",
@@ -16894,14 +16917,14 @@
"items": {
"packetCompleteness": {
"title": "交接包完整性",
"body": "檢查七個交接包是否都有來源、摘要、限制、缺口與後續人工確認欄位。",
"body": "檢查七個交接包是否都有來源、摘要、限制、缺口與後續受控確認欄位。",
"review": "只列出缺漏欄位與待補項目。",
"guard": "不補寫正式紀錄、不自動產生批准文字。"
},
"recordOwnerIdentityScope": {
"title": "負責人身分範圍",
"body": "檢查交接包是否說明未來紀錄負責人的角色範圍、責任邊界與可聯絡依據。",
"review": "只確認身分欄位是否足夠人工判讀。",
"review": "只確認身分欄位是否足夠受控判讀。",
"guard": "不代填姓名、不查外部帳號、不自動指派。"
},
"authorityBoundaryMatch": {
@@ -16918,7 +16941,7 @@
},
"reviewerNoteConfirm": {
"title": "審查備註確認",
"body": "檢查退回理由、補證狀態、人工備註與未決事項是否足夠讓下一位審查者接手。",
"body": "檢查退回理由、補證狀態、審查備註與未決事項是否足夠讓下一位審查者接手。",
"review": "只整理既有備註是否完整。",
"guard": "不建立外部任務、不自動通知、不改審查結論。"
},
@@ -16937,8 +16960,8 @@
}
},
"ownerResponseFormalRecordOwnerHandoffReviewOutcomeBoard": {
"title": "人工決策正式紀錄負責人交接驗收結果分流",
"subtitle": "交接驗收後只會落到八條只讀結果分流;這仍不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在分流=8、可進負責人檢查=0、已指派=0、執行期閘門=0。",
"title": "受控決策正式紀錄交接驗收結果分流",
"subtitle": "交接驗收後只會落到八條只讀結果分流;這仍不是紀錄負責人指派、正式紀錄、break-glass 批准或執行授權。現在分流=8、可進負責人檢查=0、已指派=0、執行期閘門=0。",
"laneLabel": "結果分流",
"resultLabel": "分流結果",
"guardLabel": "仍不會做",
@@ -16964,7 +16987,7 @@
"items": {
"remainReviewWaiting": {
"title": "維持驗收等待",
"body": "若交接包仍在等待人工檢查,結果只能維持等待狀態。",
"body": "若交接包仍在等待受控檢查,結果只能維持等待狀態。",
"result": "只顯示仍待驗收與缺少哪一類檢查。",
"guard": "不自動通過、不建立正式紀錄、不指派負責人。"
},
@@ -16976,7 +16999,7 @@
},
"requestOwnerScopeClarification": {
"title": "要求負責人範圍說明",
"body": "若未來紀錄負責人的角色、權責或聯絡依據不清,必須要求人工說明。",
"body": "若未來紀錄負責人的角色、權責或聯絡依據不清,必須要求受控說明。",
"result": "只標記需要補充哪一類負責人範圍。",
"guard": "不查外部帳號、不代填姓名、不自動指派。"
},
@@ -16987,9 +17010,9 @@
"guard": "不讀取機密明文、不保存原始載荷、不抓外部系統。"
},
"readyForRecordOwnerReview": {
"title": "可進負責人檢查",
"body": "若驗收項都足夠,交接包可以進入人工紀錄負責人檢查,但仍不是指派。",
"result": "只標記可進人工檢查,等待人工確認。",
"title": "可進受控負責人檢查",
"body": "若驗收項都足夠,交接包可以進入受控紀錄負責人檢查,但仍不是指派。",
"result": "只標記可進受控檢查,等待 controlled review 確認。",
"guard": "不自動升格、不建立正式紀錄、不建立審批紀錄。"
},
"quarantineSensitivePayload": {
@@ -17013,8 +17036,8 @@
}
},
"ownerResponseFormalRecordOwnerReviewPreparationBoard": {
"title": "人工決策正式紀錄負責人檢查準備包",
"subtitle": "交接驗收結果若可進負責人檢查,仍只能整理人工檢查前需要看的八個準備包;這不是紀錄負責人指派、正式紀錄、人工批准或執行授權。現在準備包=8、可檢查=0、已指派=0、執行期閘門=0。",
"title": "受控決策正式紀錄負責人檢查準備包",
"subtitle": "交接驗收結果若可進負責人檢查,仍只能整理受控檢查前需要看的八個準備包;這不是紀錄負責人指派、正式紀錄、break-glass 批准或執行授權。現在準備包=8、可檢查=0、已指派=0、執行期閘門=0。",
"packetLabel": "準備包",
"prepareLabel": "準備方式",
"guardLabel": "仍不會做",

View File

@@ -4,13 +4,17 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import {
Activity,
ArrowRight,
Bell,
Bot,
BookOpenCheck,
CheckCircle2,
Gauge,
ListChecks,
RefreshCw,
Send,
ShieldCheck,
SlidersHorizontal,
TriangleAlert,
Wrench,
} from "lucide-react";
@@ -120,12 +124,17 @@ type RuntimeReceiptReadback = {
} | null;
} | null;
work_item_progress?: {
ordered_items?: AutomationWorkItem[] | null;
source_family_items?: AutomationWorkItem[] | null;
rollups?: {
work_item_count?: number | null;
ordered_work_item_count?: number | null;
source_family_work_item_count?: number | null;
completed_count?: number | null;
in_progress_count?: number | null;
pending_count?: number | null;
blocked_count?: number | null;
not_started_count?: number | null;
} | null;
} | null;
latest_flow_closure?: {
@@ -165,6 +174,18 @@ type RuntimeControlPayload = {
type PanelMode = "full" | "compact";
type Tone = "ok" | "warn" | "neutral";
type WorkFilter = "all" | "completed" | "active" | "pending" | "blocked";
type WorkItemStatus = "completed" | "in_progress" | "pending" | "blocked" | "not_started" | string;
type AutomationWorkItem = {
work_item_id?: string | null;
priority?: string | null;
title?: string | null;
status?: WorkItemStatus | null;
exit_criteria?: string | null;
source_family_id?: string | null;
next_controlled_action?: string | null;
};
const API_BASE = getRuntimeApiBaseUrl();
@@ -190,6 +211,21 @@ function toneClass(tone: Tone): string {
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
}
function statusToneClass(status?: WorkItemStatus | null): string {
if (status === "completed") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
if (status === "in_progress") return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
if (status === "pending" || status === "not_started") return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
if (status === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
return "border-[#d8d3c7] bg-white text-[#5f5b52]";
}
function matchesWorkFilter(item: AutomationWorkItem, filter: WorkFilter): boolean {
const status = item.status ?? "pending";
if (filter === "all") return true;
if (filter === "active") return status === "in_progress" || status === "not_started";
return status === filter;
}
function formatTime(value: Date | null, locale: string): string {
if (!value) return "--";
return value.toLocaleTimeString(locale === "zh-TW" ? "zh-TW" : "en-US", {
@@ -227,6 +263,7 @@ export function AutonomousRuntimeReceiptPanel({
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
const [workFilter, setWorkFilter] = useState<WorkFilter>("all");
const refresh = useCallback(async () => {
setLoading(true);
@@ -255,6 +292,8 @@ export function AutonomousRuntimeReceiptPanel({
const alertNoiseRollups = readback?.alert_noise_reduction?.rollups ?? {};
const alertNoiseMissing = readback?.alert_noise_reduction?.missing_required_stage_ids ?? [];
const workItemRollups = readback?.work_item_progress?.rollups ?? {};
const orderedWorkItems = readback?.work_item_progress?.ordered_items ?? [];
const sourceFamilyItems = readback?.work_item_progress?.source_family_items ?? [];
const latestFlow = readback?.latest_flow_closure;
const rollups = payload?.rollups ?? {};
const closed = ledger?.closed === true || latestFlow?.closed === true;
@@ -453,6 +492,31 @@ export function AutonomousRuntimeReceiptPanel({
policy?.medium_risk_controlled_apply_allowed ? t("risk.medium") : null,
policy?.high_risk_controlled_apply_allowed ? t("risk.high") : null,
].filter(Boolean).join(" / ");
const workTotal = toNumber(rollups.live_work_item_count ?? workItemRollups.work_item_count);
const workCompleted = toNumber(rollups.live_work_item_completed_count ?? workItemRollups.completed_count);
const orderedWorkTotal = toNumber(workItemRollups.ordered_work_item_count ?? orderedWorkItems.length);
const sourceFamilyTotal = toNumber(workItemRollups.source_family_work_item_count ?? sourceFamilyItems.length);
const workCompletionPercent = workTotal > 0
? Math.min(100, Math.round((workCompleted / workTotal) * 100))
: 0;
const visibleWorkItems = orderedWorkItems.filter((item) => matchesWorkFilter(item, workFilter));
const orderedCompleted = orderedWorkItems.filter((item) => item.status === "completed").length;
const orderedActive = orderedWorkItems.filter((item) => (
item.status === "in_progress" || item.status === "not_started"
)).length;
const orderedPending = orderedWorkItems.filter((item) => item.status === "pending").length;
const orderedBlocked = orderedWorkItems.filter((item) => item.status === "blocked").length;
const workFilters: Array<{
key: WorkFilter;
count: number;
icon: typeof ListChecks;
}> = [
{ key: "all", count: orderedWorkTotal, icon: ListChecks },
{ key: "completed", count: orderedCompleted, icon: CheckCircle2 },
{ key: "active", count: orderedActive, icon: Gauge },
{ key: "pending", count: orderedPending, icon: ArrowRight },
{ key: "blocked", count: orderedBlocked, icon: TriangleAlert },
];
return (
<section className="border border-[#e0ddd4] bg-white">
@@ -586,24 +650,125 @@ export function AutonomousRuntimeReceiptPanel({
</div>
{mode === "full" ? (
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("policy.label")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">{riskText || "--"}</p>
<>
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("policy.label")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">{riskText || "--"}</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("policy.critical")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("nextAction")}</p>
<p className="mt-1 truncate text-sm font-semibold text-[#141413]">
{ledger?.next_executor_action ?? "--"}
</p>
</div>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("policy.critical")}</p>
<p className="mt-1 text-sm font-semibold text-[#141413]">
{policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"}
</p>
<div id="ai-automation-priority-work-board" className="border-t border-[#e0ddd4] bg-white">
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-[1fr_220px]">
<div className="bg-white px-4 py-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<SlidersHorizontal className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
<h4 className="text-sm font-semibold text-[#141413]">{t("workBoard.title")}</h4>
</div>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("workBoard.subtitle", {
ordered: numberValue(orderedWorkTotal),
sources: numberValue(sourceFamilyTotal),
})}
</p>
</div>
<div className="min-w-[180px]">
<div className="flex items-center justify-between gap-3 text-xs font-semibold text-[#5f5b52]">
<span>{t("workBoard.completedOfTotal", {
completed: numberValue(workCompleted),
total: numberValue(workTotal),
})}</span>
<span className="font-mono text-[#141413]">{workCompletionPercent}%</span>
</div>
<div className="mt-2 h-2 border border-[#d8d3c7] bg-[#faf9f3]">
<div
className="h-full bg-[#17602a]"
style={{ width: `${workCompletionPercent}%` }}
/>
</div>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{workFilters.map((filter) => {
const Icon = filter.icon;
const selected = workFilter === filter.key;
return (
<button
key={filter.key}
type="button"
onClick={() => setWorkFilter(filter.key)}
className={cn(
"inline-flex items-center gap-2 border px-3 py-1.5 text-xs font-semibold",
selected
? "border-[#141413] bg-[#141413] text-white"
: "border-[#d8d3c7] bg-white text-[#5f5b52] hover:border-[#d97757] hover:text-[#141413]"
)}
aria-pressed={selected}
>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
<span>{t(`workBoard.filters.${filter.key}` as never)}</span>
<span className="font-mono">{numberValue(filter.count)}</span>
</button>
);
})}
</div>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("workBoard.sourceCoverage")}</p>
<p className="mt-2 font-mono text-lg font-semibold text-[#141413]">
{numberValue(sourceFamilyTotal)}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("workBoard.sourceCoverageDetail")}
</p>
</div>
</div>
<div className="grid gap-px bg-[#e0ddd4]">
{visibleWorkItems.length > 0 ? visibleWorkItems.map((item) => (
<div
key={item.work_item_id ?? item.priority ?? item.title}
className="grid min-w-0 gap-3 bg-white px-4 py-3 md:grid-cols-[72px_minmax(0,1fr)_148px]"
>
<div className="flex items-center gap-2">
<span className={cn("h-2.5 w-2.5 border", statusToneClass(item.status))} aria-hidden="true" />
<span className="font-mono text-xs font-semibold text-[#141413]">{item.priority ?? "--"}</span>
</div>
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-[#141413]">{item.title ?? item.work_item_id ?? "--"}</div>
<div className="mt-1 truncate text-xs text-[#5f5b52]">
{item.exit_criteria ?? item.next_controlled_action ?? item.work_item_id ?? "--"}
</div>
</div>
<div className="flex flex-wrap items-center justify-start gap-2 md:justify-end">
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", statusToneClass(item.status))}>
{t(`workBoard.statuses.${item.status ?? "unknown"}` as never)}
</span>
<span className="font-mono text-xs text-[#77736a]">
{shortRef(item.work_item_id)}
</span>
</div>
</div>
)) : (
<div className="bg-white px-4 py-6 text-sm font-semibold text-[#5f5b52]">
{t("workBoard.empty")}
</div>
)}
</div>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("nextAction")}</p>
<p className="mt-1 truncate text-sm font-semibold text-[#141413]">
{ledger?.next_executor_action ?? "--"}
</p>
</div>
</div>
</>
) : null}
</section>
);