diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 51aac1e49..ad327bdc8 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -5006,6 +5006,149 @@ def _stockplatform_ai_recommendations_wait( ) +def _active_p0_blocker_group_counts(active_blockers: list[str]) -> dict[str, int]: + return { + "host_boot_detection": sum( + 1 for blocker in active_blockers if blocker in _REBOOT_HOST_BOOT_DETECTION_BLOCKERS + ), + "windows99_vmware": sum( + 1 for blocker in active_blockers if blocker.startswith("windows99_") + ), + "service_data_backup": sum( + 1 + for blocker in active_blockers + if blocker + in { + "backup_core_green_not_1", + "host_188_service_green_not_1", + "post_start_blocked_not_zero", + "product_data_green_not_1", + "service_green_not_1", + "wazuh_dashboard_degraded", + } + ), + "other": sum( + 1 + for blocker in active_blockers + if blocker not in _REBOOT_HOST_BOOT_DETECTION_BLOCKERS + and not blocker.startswith("windows99_") + and blocker + not in { + "backup_core_green_not_1", + "host_188_service_green_not_1", + "post_start_blocked_not_zero", + "product_data_green_not_1", + "service_green_not_1", + "wazuh_dashboard_degraded", + } + ), + } + + +def _build_current_p0_execution_focus(state: dict[str, Any]) -> dict[str, Any]: + active_blockers = _strings(state.get("active_p0_live_active_blockers")) + group_counts = _active_p0_blocker_group_counts(active_blockers) + service_blocker_count = _int( + state.get("controlled_service_data_backup_blocker_count") + ) + service_backup_closed = ( + state.get("controlled_service_data_backup_can_clear_blockers") is True + or service_blocker_count == 0 + ) + locator_dry_run_ready = bool( + state.get("windows99_vmx_source_locator_controlled_relink_dry_run_ready") + is True + or state.get("windows99_vmx_source_locator_candidate_relink_dry_run_ready") + is True + or str(state.get("windows99_vmx_source_locator_status") or "") + == "collector_readback_scoped_relink_dry_run_ready" + ) + autostart_check_ready = bool( + state.get("windows99_vmware_autostart_check_mode_package_ready") is True + and state.get("windows99_vmware_autostart_check_mode_allowed") is True + ) + + ready_packages: list[dict[str, Any]] = [] + if locator_dry_run_ready: + ready_packages.append( + { + "id": "windows99_vmx_source_scoped_relink_dry_run", + "status": str( + state.get("windows99_vmx_source_locator_status") or "ready" + ), + "target_aliases": _strings( + state.get("windows99_vmx_source_locator_target_aliases") + ), + "next_executable_step": str( + state.get("windows99_vmx_source_locator_next_executable_step") + or "rerun_no_secret_collector_and_scorecard" + ), + } + ) + if autostart_check_ready: + ready_packages.append( + { + "id": "windows99_vmware_autostart_check_mode", + "status": str( + state.get("windows99_vmware_autostart_check_mode_package_status") + or "ready" + ), + "target_aliases": _strings( + state.get("windows99_vmware_autostart_check_mode_target_vm_aliases") + ), + "next_executable_step": str( + state.get("windows99_vmware_autostart_check_mode_next_executable_step") + or "" + ), + } + ) + + if service_blocker_count and not service_backup_closed: + stage = "service_data_backup_readback_not_green" + lane = "service_data_backup_readback" + next_executable_step = "collect_service_data_backup_readback_no_restart_no_db_write" + elif ready_packages: + stage = "windows99_no_secret_verifier_rerun_ready" + lane = "windows99_vmware_verify_collection" + next_executable_step = "rerun_no_secret_collector_and_scorecard_no_vm_power_change" + elif group_counts["host_boot_detection"]: + stage = "host_probe_verifier_only_required" + lane = "reboot_event_detector_and_host_probe" + next_executable_step = "rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot" + else: + stage = "p0_readback_review_required" + lane = "p0_mainline_readback" + next_executable_step = str(state.get("active_p0_next_safe_action") or "") + + focus = { + "schema_version": "awoooi_current_p0_execution_focus_v1", + "workplan_id": str(state.get("active_p0_workplan_id") or ""), + "state": str(state.get("active_p0_state") or ""), + "stage": stage, + "lane": lane, + "primary_blocker": str(state.get("active_p0_primary_blocker") or ""), + "next_executable_step": next_executable_step, + "readiness_percent": _int(state.get("active_p0_readiness_percent")), + "active_blocker_count": len(active_blockers), + "blocker_group_counts": group_counts, + "ready_package_count": len(ready_packages), + "ready_packages": ready_packages, + "no_secret_verifier_ready": bool(ready_packages), + "scoped_relink_dry_run_ready": locator_dry_run_ready, + "autostart_check_mode_ready": autostart_check_ready, + "service_data_backup_closed": service_backup_closed, + "operation_boundaries": { + "secret_value_read": False, + "host_reboot": False, + "vm_power_change": False, + "remote_write": False, + "database_write": False, + "github_api": False, + }, + } + return focus + + def _apply_parallel_mainline_execution_overlay(payload: dict[str, Any]) -> None: state = _dict(payload.setdefault("mainline_execution_state", {})) items = _list(payload.get("commander_inserted_requirement_work_items")) @@ -5116,6 +5259,8 @@ def _set_rollups_and_summary( ) active_blockers = _strings(state.get("active_p0_live_active_blockers")) active_gap_count = _int(state.get("active_p0_immediate_apply_gap_count")) + current_p0_focus = _build_current_p0_execution_focus(state) + payload["current_p0_execution_focus"] = current_p0_focus payload["rollups"] = { "completed_p0_count": len(completed), @@ -5533,9 +5678,29 @@ def _set_rollups_and_summary( "stale_current_head_metadata_normalized": runtime_source_truth_available, "stale_cd_runs_reopen_closed_work": False, "break_glass_reboot_drill_required": True, + "current_p0_execution_focus_stage": current_p0_focus["stage"], + "current_p0_execution_focus_lane": current_p0_focus["lane"], + "current_p0_execution_focus_ready_package_count": current_p0_focus[ + "ready_package_count" + ], + "current_p0_execution_focus_no_secret_verifier_ready": current_p0_focus[ + "no_secret_verifier_ready" + ], } payload["summary"] = { "status": str(payload.get("status") or ""), + "current_p0_execution_focus": current_p0_focus, + "current_p0_execution_focus_stage": current_p0_focus["stage"], + "current_p0_execution_focus_lane": current_p0_focus["lane"], + "current_p0_execution_focus_next_executable_step": current_p0_focus[ + "next_executable_step" + ], + "current_p0_execution_focus_ready_package_count": current_p0_focus[ + "ready_package_count" + ], + "current_p0_execution_focus_no_secret_verifier_ready": current_p0_focus[ + "no_secret_verifier_ready" + ], "active_p0_workplan_id": str(state.get("active_p0_workplan_id") or ""), "active_p0_state": str(state.get("active_p0_state") or ""), "active_p0_readiness_percent": _int( diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index 285a490b0..1ddea8512 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -759,6 +759,29 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert state["active_p0_next_safe_action"] == ( "rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot" ) + focus = payload["current_p0_execution_focus"] + assert focus["schema_version"] == "awoooi_current_p0_execution_focus_v1" + assert focus["workplan_id"] == "P0-006" + assert focus["stage"] == "windows99_no_secret_verifier_rerun_ready" + assert focus["lane"] == "windows99_vmware_verify_collection" + assert focus["next_executable_step"] == ( + "rerun_no_secret_collector_and_scorecard_no_vm_power_change" + ) + assert focus["blocker_group_counts"]["host_boot_detection"] > 0 + assert focus["blocker_group_counts"]["windows99_vmware"] == 2 + assert focus["ready_package_count"] == 2 + assert focus["no_secret_verifier_ready"] is True + assert focus["scoped_relink_dry_run_ready"] is True + assert focus["autostart_check_mode_ready"] is True + assert focus["service_data_backup_closed"] is True + assert focus["operation_boundaries"] == { + "secret_value_read": False, + "host_reboot": False, + "vm_power_change": False, + "remote_write": False, + "database_write": False, + "github_api": False, + } assert ( state["active_p0_projection_source"] == "reboot_auto_recovery_slo_scorecard" @@ -1008,6 +1031,21 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert payload["summary"]["active_p0_next_safe_action"] == ( "rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot" ) + assert payload["summary"]["current_p0_execution_focus_stage"] == ( + "windows99_no_secret_verifier_rerun_ready" + ) + assert payload["summary"]["current_p0_execution_focus_lane"] == ( + "windows99_vmware_verify_collection" + ) + assert payload["summary"]["current_p0_execution_focus_ready_package_count"] == 2 + assert ( + payload["summary"]["current_p0_execution_focus_no_secret_verifier_ready"] + is True + ) + assert payload["rollups"]["current_p0_execution_focus_stage"] == ( + "windows99_no_secret_verifier_rerun_ready" + ) + assert payload["rollups"]["current_p0_execution_focus_ready_package_count"] == 2 assert ( payload["summary"]["active_p0_projection_source"] == "reboot_auto_recovery_slo_scorecard" diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 389ed5cb9..43e382e6e 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9539,9 +9539,15 @@ "loading": "Reading production work-order", "completion": "Overall completion", "currentP0": "Current P0", + "focusStage": "Current stage", "primaryBlocker": "Primary blocker", "blockerCount": "{count} blockers", + "readyPackages": "Executable packages", "nextAction": "Next ordered action", + "verifierReady": "no-secret verifier ready", + "verifierWaiting": "verifier waiting", + "boundary": "no secret / no host change / no DB write", + "rawSignal": "Raw signal", "statuses": { "done": "Completed", "in_progress": "In progress", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index f26602def..8ed5bdf57 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9539,9 +9539,15 @@ "loading": "讀取正式 work-order", "completion": "整體完成度", "currentP0": "目前 P0", + "focusStage": "目前階段", "primaryBlocker": "主要阻擋", "blockerCount": "{count} 個 blocker", + "readyPackages": "可執行套件", "nextAction": "下一個 ordered action", + "verifierReady": "no-secret verifier ready", + "verifierWaiting": "verifier waiting", + "boundary": "不讀 secret / 不動主機 / 不寫 DB", + "rawSignal": "原始訊號", "statuses": { "done": "已完成", "in_progress": "進行中", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 59fc75596..86e48f991 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1117,6 +1117,38 @@ type AiControlledRepairLoopRunbookMatrix = { } | null; }; +type CurrentP0ExecutionFocus = { + schema_version?: string | null; + workplan_id?: string | null; + state?: string | null; + stage?: string | null; + lane?: string | null; + primary_blocker?: string | null; + next_executable_step?: string | null; + readiness_percent?: number | null; + active_blocker_count?: number | null; + ready_package_count?: number | null; + no_secret_verifier_ready?: boolean | null; + scoped_relink_dry_run_ready?: boolean | null; + autostart_check_mode_ready?: boolean | null; + service_data_backup_closed?: boolean | null; + operation_boundaries?: { + secret_value_read?: boolean | null; + host_reboot?: boolean | null; + vm_power_change?: boolean | null; + remote_write?: boolean | null; + database_write?: boolean | null; + github_api?: boolean | null; + } | null; + ready_packages?: Array<{ + id?: string | null; + status?: string | null; + target_aliases?: string[] | null; + next_executable_step?: string | null; + }> | null; + blocker_group_counts?: Record | null; +}; + type PriorityWorkOrderResponse = { current_head?: { gitea_main_sha?: string | null; @@ -1137,7 +1169,14 @@ type PriorityWorkOrderResponse = { next_action?: string | null; } | null; } | null; + current_p0_execution_focus?: CurrentP0ExecutionFocus | null; summary?: { + current_p0_execution_focus?: CurrentP0ExecutionFocus | null; + current_p0_execution_focus_stage?: string | null; + current_p0_execution_focus_lane?: string | null; + current_p0_execution_focus_next_executable_step?: string | null; + current_p0_execution_focus_ready_package_count?: number | null; + current_p0_execution_focus_no_secret_verifier_ready?: boolean | null; active_p0_state?: string | null; active_p0_workplan_id?: string | null; active_p0_readiness_percent?: number | null; @@ -9616,7 +9655,12 @@ function securityOperatorLabel(value: string | null | undefined, isZh: boolean) "候選身分待確認", host_unreachable_after_reboot: "重開機後主機未連線", reboot_event_required_host_unreachable: "重開機目標不可達", + host_probe_verifier_only_required: "Host probe 只驗證", + p0_readback_review_required: "P0 讀回待判讀", + service_data_backup_readback_not_green: "Service/Data/Backup 未綠", waiting_readback: "等待正式讀回", + windows99_no_secret_verifier_rerun_ready: "Windows99 verifier 可重跑", + windows99_vmware_verify_collection: "Windows99 VMware 驗證收件", windows99_remote_execution_channel_unavailable: "Windows99 通道未連線", windows99_vmware_autostart_readback_missing: "VMware 自啟讀回缺口", windows99_vmware_vmx_missing: "VMX 來源缺口", @@ -9627,7 +9671,12 @@ function securityOperatorLabel(value: string | null | undefined, isZh: boolean) "Candidate identity pending", host_unreachable_after_reboot: "Host unreachable after reboot", reboot_event_required_host_unreachable: "Reboot target unreachable", + host_probe_verifier_only_required: "Host probe verify-only", + p0_readback_review_required: "P0 readback review", + service_data_backup_readback_not_green: "Service/Data/Backup not green", waiting_readback: "Waiting readback", + windows99_no_secret_verifier_rerun_ready: "Windows99 verifier rerun ready", + windows99_vmware_verify_collection: "Windows99 VMware verification", windows99_remote_execution_channel_unavailable: "Windows99 channel offline", windows99_vmware_autostart_readback_missing: "VMware autostart readback gap", windows99_vmware_vmx_missing: "VMX source gap", @@ -11411,12 +11460,19 @@ function pickMainlineVisibleWorkItems( function MainlineWorkProgressStrip({ priority, loading, + locale, }: { priority: PriorityWorkOrderResponse | null; loading: boolean; + locale: string; }) { const t = useTranslations("awooop.workItems.mainlineProgress"); + const isZh = locale === "zh-TW"; const summary = priority?.summary; + const focus = + priority?.current_p0_execution_focus ?? + summary?.current_p0_execution_focus ?? + null; const items = (priority?.commander_inserted_requirement_work_items ?? []) .slice() .sort((left, right) => (left.order ?? 0) - (right.order ?? 0)); @@ -11446,8 +11502,40 @@ function MainlineWorkProgressStrip({ const currentState = summary?.active_p0_state ?? "--"; const activeBlockers = summary?.active_p0_live_active_blockers ?? []; const primaryBlocker = - summary?.active_p0_primary_blocker ?? activeBlockers[0] ?? "--"; - const visibleItems = pickMainlineVisibleWorkItems(items, nextId); + focus?.primary_blocker ?? + summary?.active_p0_primary_blocker ?? + activeBlockers[0] ?? + "--"; + const focusStage = + focus?.stage ?? + summary?.current_p0_execution_focus_stage ?? + currentState; + const focusLane = + focus?.lane ?? + summary?.current_p0_execution_focus_lane ?? + "--"; + const focusNext = + focus?.next_executable_step ?? + summary?.current_p0_execution_focus_next_executable_step ?? + nextAction; + const focusReadyPackages = toCount( + focus?.ready_package_count ?? + summary?.current_p0_execution_focus_ready_package_count + ); + const focusNoSecretReady = + focus?.no_secret_verifier_ready ?? + summary?.current_p0_execution_focus_no_secret_verifier_ready ?? + false; + const boundary = focus?.operation_boundaries; + const boundaryClosed = + boundary && + boundary.secret_value_read === false && + boundary.host_reboot === false && + boundary.vm_power_change === false && + boundary.remote_write === false && + boundary.database_write === false && + boundary.github_api === false; + const visibleItems = pickMainlineVisibleWorkItems(items, nextId, 6); const statusMetrics = [ { key: "done", @@ -11563,8 +11651,11 @@ function MainlineWorkProgressStrip({
{item.id}
-
- {item.normalized_work_item ?? item.request} +
+ {item.lane ?? + item.mapped_workplan_id ?? + item.normalized_work_item ?? + item.request}
); @@ -11587,19 +11678,43 @@ function MainlineWorkProgressStrip({ {loading ? "--" : `${readiness}%`}
- {currentState} + {loading ? "--" : securityOperatorLabel(currentState, isZh)} +
+ + +
+
+
+ {t("focusStage")} +
+
+ {loading ? "--" : securityOperatorLabel(focusStage, isZh)} +
+
+
+
+ {t("readyPackages")} +
+
+ {loading ? "--" : focusReadyPackages} +
-
- {t("primaryBlocker")} +
+
+ {t("primaryBlocker")} +
+ + {t("blockerCount", { count: activeBlockers.length })} +
-
- {loading ? "--" : primaryBlocker} +
+ {loading ? "--" : securityOperatorLabel(primaryBlocker, isZh)}
-
- {t("blockerCount", { count: activeBlockers.length })} +
+ {loading ? "--" : focusLane}
@@ -11607,9 +11722,39 @@ function MainlineWorkProgressStrip({
{t("nextAction")}
-
- {loading ? "--" : nextAction} +
+ {loading ? "--" : securityNextActionLabel(focusNext, isZh)}
+
+ + {focusNoSecretReady ? t("verifierReady") : t("verifierWaiting")} + + + {t("boundary")} + +
+
+ + {t("rawSignal")} + +
+ {focusNext} +
+
@@ -12513,6 +12658,7 @@ export default function AwoooPWorkItemsPage() {