diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index 953befb1..564c490d 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -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 下受控自動處理。" diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index 027ee0c2..bf8ebf2c 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -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(): diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index f0003826..fabdb7f8 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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": "仍不會做", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 8d7ba06f..7a9ad80b 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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": "仍不會做", diff --git a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx index 909bc756..897fb16a 100644 --- a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx +++ b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx @@ -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(null); + const [workFilter, setWorkFilter] = useState("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 (
@@ -586,24 +650,125 @@ export function AutonomousRuntimeReceiptPanel({ {mode === "full" ? ( -
-
-

{t("policy.label")}

-

{riskText || "--"}

+ <> +
+
+

{t("policy.label")}

+

{riskText || "--"}

+
+
+

{t("policy.critical")}

+

+ {policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"} +

+
+
+

{t("nextAction")}

+

+ {ledger?.next_executor_action ?? "--"} +

+
-
-

{t("policy.critical")}

-

- {policy?.critical_break_glass_required ? t("policy.breakGlass") : "--"} -

+
+
+
+
+
+
+
+

+ {t("workBoard.subtitle", { + ordered: numberValue(orderedWorkTotal), + sources: numberValue(sourceFamilyTotal), + })} +

+
+
+
+ {t("workBoard.completedOfTotal", { + completed: numberValue(workCompleted), + total: numberValue(workTotal), + })} + {workCompletionPercent}% +
+
+
+
+
+
+
+ {workFilters.map((filter) => { + const Icon = filter.icon; + const selected = workFilter === filter.key; + return ( + + ); + })} +
+
+
+

{t("workBoard.sourceCoverage")}

+

+ {numberValue(sourceFamilyTotal)} +

+

+ {t("workBoard.sourceCoverageDetail")} +

+
+
+
+ {visibleWorkItems.length > 0 ? visibleWorkItems.map((item) => ( +
+
+
+
+
{item.title ?? item.work_item_id ?? "--"}
+
+ {item.exit_criteria ?? item.next_controlled_action ?? item.work_item_id ?? "--"} +
+
+
+ + {t(`workBoard.statuses.${item.status ?? "unknown"}` as never)} + + + {shortRef(item.work_item_id)} + +
+
+ )) : ( +
+ {t("workBoard.empty")} +
+ )} +
-
-

{t("nextAction")}

-

- {ledger?.next_executor_action ?? "--"} -

-
-
+ ) : null}
);