From 771176c79727208fcb3692701a4379920ba5af44 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 09:46:32 +0800 Subject: [PATCH 1/2] fix(reboot): surface privileged edge apply blocker --- .../reboot_auto_recovery_slo_scorecard.py | 27 +++++++++++++++++-- ..._reboot_auto_recovery_slo_scorecard_api.py | 20 +++++++++++--- .../public-maintenance-edge-fallback-apply.sh | 10 +++---- .../reboot-auto-recovery-slo-exporter.sh | 7 +++++ .../reboot-auto-recovery-slo-scorecard.py | 12 +++++++++ ...test_reboot_auto_recovery_slo_scorecard.py | 6 +++++ 6 files changed, 72 insertions(+), 10 deletions(-) diff --git a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py index 08ced1e53..a7943d7e3 100644 --- a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py +++ b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py @@ -91,6 +91,7 @@ _PUBLIC_MAINTENANCE_ROUTE_BLOCKERS = { } _PUBLIC_MAINTENANCE_EDGE_BLOCKERS = { "edge_public_maintenance_live_config_drift", + "privileged_edge_apply_channel_unavailable", } _PUBLIC_MAINTENANCE_BLOCKERS = ( _PUBLIC_MAINTENANCE_ROUTE_BLOCKERS | _PUBLIC_MAINTENANCE_EDGE_BLOCKERS @@ -2428,6 +2429,14 @@ def _build_public_maintenance_fallback_readback(value: Any) -> dict[str, Any]: ): blockers.append("public_route_unreachable_without_external_l1_fallback") edge_blockers = _strings(public_maintenance.get("edge_fallback_blockers")) + edge_fallback = _dict(public_maintenance.get("edge_fallback")) + if ( + public_maintenance.get("edge_fallback_runtime_readback_present") is True + and public_maintenance.get("edge_fallback_ready") is not True + and edge_fallback.get("sudo_n_ready") is False + and "privileged_edge_apply_channel_unavailable" not in edge_blockers + ): + edge_blockers.append("privileged_edge_apply_channel_unavailable") for blocker in edge_blockers: if blocker not in blockers: blockers.append(blocker) @@ -2441,7 +2450,7 @@ def _build_public_maintenance_fallback_readback(value: Any) -> dict[str, Any]: public_maintenance.get("edge_fallback_runtime_readback_present") is True ), "edge_fallback_ready": public_maintenance.get("edge_fallback_ready"), - "edge_fallback": _dict(public_maintenance.get("edge_fallback")), + "edge_fallback": edge_fallback, "edge_fallback_blockers": edge_blockers, "target_url_count": _int(public_maintenance.get("target_url_count")), "route_count": _int(public_maintenance.get("route_count")), @@ -3499,6 +3508,7 @@ def _reboot_sop_primary_blocker(active_blockers: list[str]) -> str: "windows99_console_verify_output_truncated", "public_route_raw_5xx_without_maintenance_fallback", "public_route_unreachable_without_external_l1_fallback", + "privileged_edge_apply_channel_unavailable", "edge_public_maintenance_live_config_drift", "public_maintenance_fallback_runtime_readback_missing", "post_start_blocked_not_zero", @@ -3713,11 +3723,24 @@ def _active_blocker_action_row( next_safe_action = ( "run_public_maintenance_fallback_probe_and_rerun_scorecard_no_reload" ) + controlled_apply_mode = "check_mode_then_separate_edge_or_nginx_apply" + if blocker == "privileged_edge_apply_channel_unavailable": + next_safe_action = ( + "restore_privileged_edge_apply_channel_then_rerun_edge_fallback_" + "check_and_nginx_t_apply_package_no_app_restart" + ) + controlled_apply_mode = ( + "blocked_until_passwordless_privileged_edge_channel_readback" + ) + elif blocker == "edge_public_maintenance_live_config_drift": + next_safe_action = ( + "run_public_maintenance_edge_fallback_check_then_prepare_" + "privileged_nginx_t_apply_package_no_app_restart" + ) post_verifier = ( "python3 scripts/reboot-recovery/public-maintenance-fallback-probe.py" ) evidence_inputs = ["public_maintenance_fallback"] - controlled_apply_mode = "check_mode_then_separate_edge_or_nginx_apply" elif "stockplatform" in blocker or "product_data" in blocker: category = "product_data_freshness" owner_lane = "stockplatform_freshness_and_ingestion" diff --git a/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py b/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py index 516e73ffd..4fe10814c 100644 --- a/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py +++ b/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py @@ -85,7 +85,10 @@ PUBLIC_MAINTENANCE_RUNTIME_EDGE_DRIFT = { "ready": False, "edge_fallback_runtime_readback_present": True, "edge_fallback_ready": False, - "edge_fallback_blockers": ["edge_public_maintenance_live_config_drift"], + "edge_fallback_blockers": [ + "edge_public_maintenance_live_config_drift", + "privileged_edge_apply_channel_unavailable", + ], "edge_fallback": { "runtime_readback_present": True, "ready": False, @@ -103,9 +106,15 @@ PUBLIC_MAINTENANCE_RUNTIME_EDGE_DRIFT = { "maintenance_file_present": False, "maintenance_file_has_marker": False, "sudo_n_ready": False, - "blockers": ["edge_public_maintenance_live_config_drift"], + "blockers": [ + "edge_public_maintenance_live_config_drift", + "privileged_edge_apply_channel_unavailable", + ], }, - "blockers": ["edge_public_maintenance_live_config_drift"], + "blockers": [ + "edge_public_maintenance_live_config_drift", + "privileged_edge_apply_channel_unavailable", + ], } _REPO_ROOT = Path(__file__).resolve().parents[3] @@ -280,6 +289,7 @@ def test_reboot_auto_recovery_slo_scorecard_overlays_edge_fallback_live_drift( ) assert "edge_public_maintenance_live_config_drift" in payload["active_blockers"] + assert "privileged_edge_apply_channel_unavailable" in payload["active_blockers"] assert payload["public_maintenance_fallback"]["ready"] is False assert ( payload["public_maintenance_fallback"][ @@ -292,6 +302,10 @@ def test_reboot_auto_recovery_slo_scorecard_overlays_edge_fallback_live_drift( payload["public_maintenance_fallback"]["edge_fallback"]["sudo_n_ready"] is False ) + assert ( + "privileged_edge_apply_channel_unavailable" + in payload["public_maintenance_fallback"]["edge_fallback_blockers"] + ) assert payload["readback"]["public_maintenance_edge_fallback_ready"] is False assert ( payload["readback"][ diff --git a/scripts/reboot-recovery/public-maintenance-edge-fallback-apply.sh b/scripts/reboot-recovery/public-maintenance-edge-fallback-apply.sh index 0064ab8f2..c2324425e 100755 --- a/scripts/reboot-recovery/public-maintenance-edge-fallback-apply.sh +++ b/scripts/reboot-recovery/public-maintenance-edge-fallback-apply.sh @@ -104,11 +104,8 @@ else echo "MAINTENANCE_FILE_PRESENT=0" echo "MAINTENANCE_FILE_HAS_MARKER=0" fi -if sudo -n true >/dev/null 2>&1; then - echo "SUDO_N_READY=1" -else - echo "SUDO_N_READY=0" -fi +SUDO_N_READY=$(if sudo -n true >/dev/null 2>&1; then echo 1; else echo 0; fi) +echo "SUDO_N_READY=${SUDO_N_READY}" REMOTE_CONF_READABLE=$(if [ -r "$REMOTE_CONF" ]; then echo 1; else echo 0; fi) LIVE_CONF_HAS_INTERCEPT=$(if [ -r "$REMOTE_CONF" ] && grep -q 'proxy_intercept_errors on;' "$REMOTE_CONF"; then echo 1; else echo 0; fi) LIVE_CONF_HAS_ERROR_PAGE_503=$(if [ -r "$REMOTE_CONF" ] && grep -q 'error_page 502 503 504 =503 /__awoooi-maintenance.html;' "$REMOTE_CONF"; then echo 1; else echo 0; fi) @@ -131,6 +128,9 @@ if [ "$REMOTE_CONF_READABLE" = 1 ] \ else echo "EDGE_FALLBACK_READY=0" echo "BLOCKER edge_public_maintenance_live_config_drift" + if [ "$SUDO_N_READY" = 0 ]; then + echo "BLOCKER privileged_edge_apply_channel_unavailable" + fi fi REMOTE } diff --git a/scripts/reboot-recovery/reboot-auto-recovery-slo-exporter.sh b/scripts/reboot-recovery/reboot-auto-recovery-slo-exporter.sh index 2e37d60c3..8caafc5ec 100755 --- a/scripts/reboot-recovery/reboot-auto-recovery-slo-exporter.sh +++ b/scripts/reboot-recovery/reboot-auto-recovery-slo-exporter.sh @@ -315,6 +315,13 @@ edge_ready = kv.get("EDGE_FALLBACK_READY") == "1" edge_present = bool(kv.get("AWOOOI_PUBLIC_MAINTENANCE_EDGE_FALLBACK")) if edge_present and not edge_ready and not edge_blockers: edge_blockers.append("edge_public_maintenance_live_config_drift") +if ( + edge_present + and not edge_ready + and kv.get("SUDO_N_READY") == "0" + and "privileged_edge_apply_channel_unavailable" not in edge_blockers +): + edge_blockers.append("privileged_edge_apply_channel_unavailable") blockers = list(payload.get("blockers") or []) for blocker in edge_blockers: diff --git a/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py b/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py index 6f8f1b049..45321308f 100755 --- a/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py +++ b/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py @@ -539,6 +539,12 @@ def parse_public_maintenance_edge_readback(text: str) -> dict[str, Any]: ready = kv.get("EDGE_FALLBACK_READY") == "1" if not ready and not blockers: blockers.append("edge_public_maintenance_live_config_drift") + if ( + not ready + and kv.get("SUDO_N_READY") == "0" + and "privileged_edge_apply_channel_unavailable" not in blockers + ): + blockers.append("privileged_edge_apply_channel_unavailable") return { "runtime_readback_present": True, "ready": ready, @@ -1434,6 +1440,11 @@ def choose_safe_next_step( "prepare_public_maintenance_fallback_l1_external_static_origin_or_cdn_" "healthcheck_decision_record_then_controlled_cutover_with_rollback" ) + if "privileged_edge_apply_channel_unavailable" in public_blockers: + return ( + "restore_privileged_edge_apply_channel_then_rerun_edge_fallback_check_" + "and_nginx_t_apply_package_no_app_restart" + ) if "edge_public_maintenance_live_config_drift" in public_blockers: return ( "run_public_maintenance_edge_fallback_check_then_prepare_privileged_" @@ -1613,6 +1624,7 @@ def reboot_sop_primary_blocker(active_blockers: list[str]) -> str: "windows99_console_verify_output_truncated", "public_route_raw_5xx_without_maintenance_fallback", "public_route_unreachable_without_external_l1_fallback", + "privileged_edge_apply_channel_unavailable", "edge_public_maintenance_live_config_drift", "public_maintenance_fallback_runtime_readback_missing", "post_start_blocked_not_zero", diff --git a/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py b/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py index 3b0180a56..2f064d741 100644 --- a/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py +++ b/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py @@ -148,6 +148,7 @@ MAINTENANCE_FILE_HAS_MARKER=0 SUDO_N_READY=0 EDGE_FALLBACK_READY=0 BLOCKER edge_public_maintenance_live_config_drift +BLOCKER privileged_edge_apply_channel_unavailable """ @@ -625,6 +626,7 @@ def test_edge_public_maintenance_live_config_drift_blocks_even_when_routes_are_2 assert payload["status"] == "blocked_reboot_auto_recovery_slo_not_ready" assert "edge_public_maintenance_live_config_drift" in payload["active_blockers"] + assert "privileged_edge_apply_channel_unavailable" in payload["active_blockers"] assert payload["public_maintenance_fallback"]["ready"] is False assert ( payload["public_maintenance_fallback"][ @@ -637,6 +639,10 @@ def test_edge_public_maintenance_live_config_drift_blocks_even_when_routes_are_2 payload["public_maintenance_fallback"]["edge_fallback"]["sudo_n_ready"] is False ) + assert ( + "privileged_edge_apply_channel_unavailable" + in payload["public_maintenance_fallback"]["edge_fallback_blockers"] + ) assert ( payload["required_checks"]["public_maintenance_fallback_runtime_ready"] is False From b8e987bde417b9c57a2087403d02ddc4c78b0337 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 09:49:22 +0800 Subject: [PATCH 2/2] fix(work-items): surface mainline progress order --- apps/web/messages/en.json | 54 +++-- apps/web/messages/zh-TW.json | 18 ++ .../app/[locale]/awooop/work-items/page.tsx | 216 ++++++++++++++++++ 3 files changed, 270 insertions(+), 18 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 8437b6612..a193ee3ee 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9531,31 +9531,49 @@ "noSecret": "no secret read" } }, + "mainlineProgress": { + "eyebrow": "Mainline Work Progress", + "title": "All Work Items And Priority Order", + "subtitle": "Reads the production priority work-order readback and surfaces totals, completion, current P0, next action, and the first 8 ordered items before the deeper automation / ledger views.", + "loading": "Reading production work-order", + "completion": "Overall completion", + "currentP0": "Current P0", + "primaryBlocker": "Primary blocker", + "blockerCount": "{count} blockers", + "nextAction": "Next ordered action", + "statuses": { + "done": "Completed", + "in_progress": "In progress", + "pending": "Not started", + "blocked": "Blocked", + "deferred": "Deferred" + } + }, "commanderInsertedRequirements": { - "eyebrow": "主線優先序", - "title": "統帥插入需求工作項", - "subtitle": "把本輪中途插入的要求收成正式工作項,依 P0/P1/P2/P3 排序,並顯示狀態、驗收條件與下一步。", - "total": "總工作項", - "next": "目前優先項", - "nextAction": "下一步", - "acceptance": "驗收條件", - "rowNextAction": "下一步", - "spotlight": "主線焦點", - "spotlightSubtitle": "直接露出目前 P0、OpenClaw Live Ops 與非文字牆 UX,避免工作項藏在長清單裡。", - "fullLedger": "完整總帳 {count} 項", - "loading": "讀取插入需求工作項", - "empty": "尚未讀回插入需求工作項。", + "eyebrow": "Mainline priority", + "title": "Commander Inserted Requirement Work Items", + "subtitle": "Turns midstream requirements into ordered P0/P1/P2/P3 work items with status, acceptance, and next action.", + "total": "Total work items", + "next": "Current priority", + "nextAction": "Next action", + "acceptance": "Acceptance", + "rowNextAction": "Next action", + "spotlight": "Mainline spotlight", + "spotlightSubtitle": "Pins current P0, OpenClaw Live Ops, and UI work so they do not hide in a long ledger.", + "fullLedger": "Full ledger {count} items", + "loading": "Reading inserted requirement work items", + "empty": "No inserted requirement work items read back yet.", "order": "order={order}", "source": "source={source}", "metrics": { "active": "active" }, "statuses": { - "done": "已完成", - "inProgress": "進行中", - "pending": "未開始", - "blocked": "阻塞", - "deferred": "延後" + "done": "Completed", + "inProgress": "In progress", + "pending": "Not started", + "blocked": "Blocked", + "deferred": "Deferred" } }, "operatorSop": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index c547fa54f..37c766e21 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9531,6 +9531,24 @@ "noSecret": "no secret read" } }, + "mainlineProgress": { + "eyebrow": "主線工作進度", + "title": "全部工作項目與優先順序", + "subtitle": "直接讀正式 priority work-order readback,先看總數、完成度、目前 P0、下一步與前 8 個 ordered items;細節再下鑽 automation / ledger。", + "loading": "讀取正式 work-order", + "completion": "整體完成度", + "currentP0": "目前 P0", + "primaryBlocker": "主要阻擋", + "blockerCount": "{count} 個 blocker", + "nextAction": "下一個 ordered action", + "statuses": { + "done": "已完成", + "in_progress": "進行中", + "pending": "未開始", + "blocked": "阻塞", + "deferred": "延後" + } + }, "commanderInsertedRequirements": { "eyebrow": "主線優先序", "title": "統帥插入需求工作項", 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 72120e046..59b2273fb 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1091,6 +1091,7 @@ type PriorityWorkOrderResponse = { active_p0_state?: string | null; active_p0_workplan_id?: string | null; active_p0_readiness_percent?: number | null; + active_p0_primary_blocker?: string | null; active_p0_live_active_blockers?: string[] | null; next_executable_mainline_workplan_id?: string | null; next_executable_mainline_state?: string | null; @@ -11065,6 +11066,216 @@ function commanderStatusTone(status: string) { } } +function MainlineWorkProgressStrip({ + priority, + loading, +}: { + priority: PriorityWorkOrderResponse | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.mainlineProgress"); + const summary = priority?.summary; + const items = (priority?.commander_inserted_requirement_work_items ?? []) + .slice() + .sort((left, right) => (left.order ?? 0) - (right.order ?? 0)); + const total = toCount( + summary?.commander_inserted_requirement_work_item_count ?? items.length + ); + const done = toCount(summary?.commander_inserted_requirement_done_count); + const inProgress = toCount( + summary?.commander_inserted_requirement_in_progress_count + ); + const pending = toCount(summary?.commander_inserted_requirement_pending_count); + const blocked = toCount(summary?.commander_inserted_requirement_blocked_count); + const deferred = toCount(summary?.commander_inserted_requirement_deferred_count); + const completion = total > 0 ? Math.round((done / total) * 100) : 0; + const readiness = toCount(summary?.active_p0_readiness_percent); + const nextId = + summary?.commander_inserted_requirement_next_id ?? items[0]?.id ?? "--"; + const nextPriority = + summary?.commander_inserted_requirement_next_priority ?? + items[0]?.priority ?? + "--"; + const nextAction = + summary?.commander_inserted_requirement_next_action ?? + items[0]?.next_action ?? + "--"; + const currentP0 = summary?.active_p0_workplan_id ?? "--"; + 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 = items.slice(0, 8); + const statusMetrics = [ + { + key: "done", + label: t("statuses.done"), + value: done, + tone: commanderStatusTone("done"), + }, + { + key: "inProgress", + label: t("statuses.in_progress"), + value: inProgress, + tone: commanderStatusTone("in_progress"), + }, + { + key: "pending", + label: t("statuses.pending"), + value: pending, + tone: commanderStatusTone("pending"), + }, + { + key: "blocked", + label: t("statuses.blocked"), + value: blocked, + tone: commanderStatusTone("blocked"), + }, + { + key: "deferred", + label: t("statuses.deferred"), + value: deferred, + tone: commanderStatusTone("deferred"), + }, + ]; + + return ( +
+
+
+
+
+
+ + + + {loading ? t("loading") : `${nextPriority}:${nextId}`} + +
+

+ {t("title")} +

+

+ {t("subtitle")} +

+
+
+
+ {t("completion")} + + {loading ? "--" : `${completion}% · ${done}/${total}`} + +
+
+
+
+
+
+ +
+ {statusMetrics.map((metric) => ( +
+
{metric.label}
+
+ {loading ? "--" : metric.value} +
+
+ ))} +
+ +
+ {visibleItems.map((item) => { + const itemPriority = String(item.priority ?? "--"); + const itemStatus = String(item.status ?? "pending"); + return ( +
+
+ + {itemPriority} + + + {t(`statuses.${itemStatus}` as never)} + +
+
+ {item.id} +
+
+ {item.normalized_work_item ?? item.request} +
+
+ ); + })} +
+
+ +
+
+
+
+
+ {t("currentP0")} +
+ + {loading ? "--" : currentP0} + +
+
+ {loading ? "--" : `${readiness}%`} +
+
+ {currentState} +
+
+ +
+
+ {t("primaryBlocker")} +
+
+ {loading ? "--" : primaryBlocker} +
+
+ {t("blockerCount", { count: activeBlockers.length })} +
+
+ +
+
+ {t("nextAction")} +
+
+ {loading ? "--" : nextAction} +
+
+
+
+
+
+ ); +} + function CommanderInsertedRequirementsPanel({ priority, loading, @@ -11775,6 +11986,11 @@ export default function AwoooPWorkItemsPage() { + +