From e7cf3f2aade5f57449596429e19c7f5c8ca9170b Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 12:46:51 +0800 Subject: [PATCH] feat(iwooos): show controlled cd lane live metric proof --- apps/api/src/api/v1/agents.py | 12 +- .../awoooi_priority_work_order_readback.py | 233 ++++++++++++++++++ ...awoooi_priority_work_order_readback_api.py | 60 +++++ apps/web/messages/en.json | 14 ++ apps/web/messages/zh-TW.json | 14 ++ .../app/[locale]/awooop/work-items/page.tsx | 176 +++++++++++++ 6 files changed, 508 insertions(+), 1 deletion(-) diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 41de77a3f..d12ce610f 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -340,9 +340,11 @@ from src.services.awoooi_gitea_onboarding_warning_step_template_copy_receipt imp ) from src.services.awoooi_priority_work_order_readback import ( apply_ai_loop_current_blocker_execution_queue, + apply_controlled_cd_lane_live_metric_readback, apply_harbor_registry_controlled_recovery_preflight, apply_stockplatform_public_api_controlled_recovery_preflight, apply_stockplatform_public_api_runtime_readback, + load_controlled_cd_lane_live_metric_readback, load_latest_awoooi_priority_work_order_readback, ) from src.services.awoooi_status_cleanup_dashboard import ( @@ -1089,7 +1091,8 @@ async def get_delivery_closure_workbench() -> dict[str, Any]: "讀取已提交的 AWOOOI P0/P1 主線工作順序快照;此端點只回傳" "目前 active P0、已關閉 P0、下一步順序、禁止事項與 evidence refs。" "它會讀取 StockPlatform public HTTPS health/freshness/ingestion live " - "readback;不讀 raw sessions / SQLite、不呼叫 GitHub / Gitea live API、" + "readback,並讀取 110 node_exporter 的 controlled CD lane textfile metrics;" + "不讀 raw sessions / SQLite、不呼叫 GitHub / Gitea live API、不 SSH、" "不讀 secret、不註冊 runner、不觸發 workflow、不操作 host / Docker / K8s / DB / firewall。" ), ) @@ -1122,6 +1125,13 @@ async def get_awoooi_priority_work_order_readback() -> dict[str, Any]: load_latest_ai_agent_log_controlled_writeback_executor_readback ) apply_ai_loop_current_blocker_execution_queue(payload, log_executor) + controlled_cd_lane_metrics = await asyncio.to_thread( + load_controlled_cd_lane_live_metric_readback + ) + apply_controlled_cd_lane_live_metric_readback( + payload, + controlled_cd_lane_metrics, + ) return redact_public_lan_topology(payload) except FileNotFoundError as exc: raise HTTPException( 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 9d7e00649..9a2e27ddb 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -9,6 +9,8 @@ from __future__ import annotations import json import re +import urllib.error +import urllib.request from datetime import datetime from pathlib import Path from typing import Any @@ -19,7 +21,15 @@ from src.services.snapshot_paths import default_operations_dir _DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _SNAPSHOT_FILE = "awoooi-priority-work-order-readback.snapshot.json" _SCHEMA_VERSION = "awoooi_priority_work_order_readback_v1" +_CONTROLLED_CD_LANE_LIVE_METRIC_SCHEMA_VERSION = ( + "controlled_cd_lane_live_metric_readback_v1" +) +_CONTROLLED_CD_LANE_NODE_EXPORTER_URL = "http://192.168.0.110:9100/metrics" _SHA_RE = re.compile(r"^[0-9a-f]{40}$") +_METRIC_LINE_RE = re.compile( + r"^(?P[a-zA-Z_:][a-zA-Z0-9_:]*)(?:\{[^}]*\})?\s+" + r"(?P[-+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][-+]?[0-9]+)?)$" +) def load_latest_awoooi_priority_work_order_readback( @@ -41,6 +51,141 @@ def load_latest_awoooi_priority_work_order_readback( return payload +def load_controlled_cd_lane_live_metric_readback( + metrics_url: str = _CONTROLLED_CD_LANE_NODE_EXPORTER_URL, + timeout_seconds: float = 3.0, +) -> dict[str, Any]: + """Read 110 node_exporter text metrics for the controlled CD lane. + + This is a read-only HTTP metrics probe. It does not SSH, read runner + registration contents, touch Docker/systemd, or perform runtime writes. + """ + try: + request = urllib.request.Request(metrics_url, method="GET") + with urllib.request.urlopen(request, timeout=timeout_seconds) as response: + raw = response.read(2_000_000).decode("utf-8", errors="replace") + except (OSError, TimeoutError, urllib.error.URLError) as exc: + return { + "schema_version": _CONTROLLED_CD_LANE_LIVE_METRIC_SCHEMA_VERSION, + "status": "controlled_cd_lane_live_metric_unavailable", + "source": "node_exporter_text_metrics", + "source_url": metrics_url, + "metadata_only": True, + "active_blockers": ["controlled_cd_lane_live_metric_unavailable"], + "error_class": exc.__class__.__name__, + "readback": { + "enforcer_metric_present": False, + "controlled_drain_active_allowed": False, + "controlled_drain_staging_allowed": False, + "enforcer_last_run_fresh": False, + }, + } + + metrics = _parse_prometheus_text_metrics(raw) + readback = _build_controlled_cd_lane_metric_readback(metrics) + active_blockers: list[str] = [] + if not readback["enforcer_metric_present"]: + active_blockers.append("controlled_cd_lane_enforcer_metric_missing") + if not readback["enforcer_last_run_fresh"]: + active_blockers.append("controlled_cd_lane_enforcer_metric_stale") + if not readback["controlled_drain_active_allowed"]: + active_blockers.append("controlled_cd_lane_active_not_allowed_yet") + if not readback["controlled_drain_staging_allowed"]: + active_blockers.append("controlled_cd_lane_staging_not_allowed_yet") + + return { + "schema_version": _CONTROLLED_CD_LANE_LIVE_METRIC_SCHEMA_VERSION, + "status": ( + "controlled_cd_lane_live_metric_readback_ready" + if readback["enforcer_metric_present"] + else "controlled_cd_lane_live_metric_missing" + ), + "source": "node_exporter_text_metrics", + "source_url": metrics_url, + "metadata_only": True, + "active_blockers": active_blockers, + "readback": readback, + } + + +def apply_controlled_cd_lane_live_metric_readback( + payload: dict[str, Any], + live_metric_readback: dict[str, Any], +) -> None: + """Overlay live controlled CD lane metric truth onto P0-006 readback.""" + readback = _dict(live_metric_readback.get("readback")) + state = _dict(payload.setdefault("mainline_execution_state", {})) + active_blockers = _strings(live_metric_readback.get("active_blockers")) + + metric_fields = { + "controlled_cd_lane_live_metric_status": str( + live_metric_readback.get("status") or "unknown" + ), + "controlled_cd_lane_live_metric_source": str( + live_metric_readback.get("source") or "node_exporter_text_metrics" + ), + "controlled_cd_lane_live_metric_metadata_only": bool( + live_metric_readback.get("metadata_only") is True + ), + "controlled_cd_lane_live_enforcer_metric_present": bool( + readback.get("enforcer_metric_present") is True + ), + "controlled_cd_lane_live_enforcer_last_run_timestamp": _float( + readback.get("enforcer_last_run_timestamp") + ), + "controlled_cd_lane_live_enforcer_last_run_fresh": bool( + readback.get("enforcer_last_run_fresh") is True + ), + "controlled_cd_lane_live_enforcer_apply_performed": _int( + readback.get("enforcer_apply_performed") + ), + "controlled_cd_lane_live_active_job_containers": _int( + readback.get("active_job_containers") + ), + "controlled_cd_lane_live_lane_process_count": _int( + readback.get("lane_process_count") + ), + "controlled_cd_lane_live_root_restore_sources_left": _int( + readback.get("root_restore_sources_left") + ), + "controlled_cd_lane_live_controlled_drain_active_allowed": bool( + readback.get("controlled_drain_active_allowed") is True + ), + "controlled_cd_lane_live_controlled_drain_staging_allowed": bool( + readback.get("controlled_drain_staging_allowed") is True + ), + "controlled_cd_lane_live_host_load5_per_core": _float( + readback.get("host_load5_per_core") + ), + "controlled_cd_lane_live_gitea_actions_active_container_count": _int( + readback.get("gitea_actions_active_container_count") + ), + "controlled_cd_lane_live_gitea_actions_active_process_count": _int( + readback.get("gitea_actions_active_process_count") + ), + "controlled_cd_lane_live_metric_blockers": active_blockers, + } + state.update(metric_fields) + + rollups = _dict(payload.setdefault("rollups", {})) + rollups.update(metric_fields) + rollups["controlled_cd_lane_live_metric_blocker_count"] = len(active_blockers) + + summary = _dict(payload.setdefault("summary", {})) + summary.update(metric_fields) + summary["controlled_cd_lane_live_metric_blocker_count"] = len(active_blockers) + + for item in _list(payload.get("in_progress_or_blocked_in_priority_order")): + workplan = _dict(item) + if workplan.get("workplan_id") != "P0-006": + continue + evidence = _dict(workplan.setdefault("evidence", {})) + evidence.update(metric_fields) + evidence["controlled_cd_lane_live_metric_blocker_count"] = len( + active_blockers + ) + + def apply_stockplatform_public_api_runtime_readback( payload: dict[str, Any], runtime_readback: dict[str, Any], @@ -2116,6 +2261,87 @@ def _require_mainline_consistency(payload: dict[str, Any], label: str) -> None: raise ValueError(f"{label}: next_execution_order must start with P0-006") +def _parse_prometheus_text_metrics(raw: str) -> dict[str, float]: + metrics: dict[str, float] = {} + allowed_names = { + "awoooi_host_gitea_actions_active_container_count", + "awoooi_host_gitea_actions_active_process_count", + "awoooi_host_load5_per_core", + "awoooi_runner_failclosed_enforcer_active_job_containers", + "awoooi_runner_failclosed_enforcer_apply_performed", + "awoooi_runner_failclosed_enforcer_controlled_drain_active_allowed", + "awoooi_runner_failclosed_enforcer_controlled_drain_staging_allowed", + "awoooi_runner_failclosed_enforcer_lane_process_count", + "awoooi_runner_failclosed_enforcer_last_run_timestamp", + "awoooi_runner_failclosed_enforcer_root_restore_sources_left", + } + for line in raw.splitlines(): + if not line or line.startswith("#"): + continue + match = _METRIC_LINE_RE.match(line.strip()) + if not match: + continue + name = match.group("name") + if name not in allowed_names: + continue + try: + metrics[name] = float(match.group("value")) + except ValueError: + continue + return metrics + + +def _build_controlled_cd_lane_metric_readback( + metrics: dict[str, float], +) -> dict[str, Any]: + last_run = metrics.get( + "awoooi_runner_failclosed_enforcer_last_run_timestamp", 0.0 + ) + now_seconds = datetime.now(ZoneInfo("Asia/Taipei")).timestamp() + enforcer_metric_present = ( + "awoooi_runner_failclosed_enforcer_last_run_timestamp" in metrics + ) + return { + "enforcer_metric_present": enforcer_metric_present, + "enforcer_last_run_timestamp": last_run, + "enforcer_last_run_age_seconds": max(0, int(now_seconds - last_run)) + if last_run + else 0, + "enforcer_last_run_fresh": bool(last_run and now_seconds - last_run <= 900), + "enforcer_apply_performed": _int( + metrics.get("awoooi_runner_failclosed_enforcer_apply_performed") + ), + "active_job_containers": _int( + metrics.get("awoooi_runner_failclosed_enforcer_active_job_containers") + ), + "lane_process_count": _int( + metrics.get("awoooi_runner_failclosed_enforcer_lane_process_count") + ), + "root_restore_sources_left": _int( + metrics.get("awoooi_runner_failclosed_enforcer_root_restore_sources_left") + ), + "controlled_drain_active_allowed": _int( + metrics.get( + "awoooi_runner_failclosed_enforcer_controlled_drain_active_allowed" + ) + ) + == 1, + "controlled_drain_staging_allowed": _int( + metrics.get( + "awoooi_runner_failclosed_enforcer_controlled_drain_staging_allowed" + ) + ) + == 1, + "host_load5_per_core": _float(metrics.get("awoooi_host_load5_per_core")), + "gitea_actions_active_container_count": _int( + metrics.get("awoooi_host_gitea_actions_active_container_count") + ), + "gitea_actions_active_process_count": _int( + metrics.get("awoooi_host_gitea_actions_active_process_count") + ), + } + + def _dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} @@ -2131,6 +2357,13 @@ def _int(value: Any) -> int: return 0 +def _float(value: Any) -> float: + try: + return float(str(value)) + except (TypeError, ValueError): + return 0.0 + + def _strings(value: Any) -> list[str]: return [str(item) for item in _list(value)] 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 254f813f8..60c814dc8 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 @@ -14,6 +14,7 @@ from src.services.ai_agent_log_controlled_writeback_executor_readback import ( ) from src.services.awoooi_priority_work_order_readback import ( apply_ai_loop_current_blocker_execution_queue, + apply_controlled_cd_lane_live_metric_readback, apply_harbor_registry_controlled_recovery_preflight, apply_stockplatform_public_api_controlled_recovery_preflight, apply_stockplatform_public_api_runtime_readback, @@ -104,6 +105,11 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( "load_latest_harbor_registry_controlled_recovery_preflight", _harbor_registry_ready, ) + monkeypatch.setattr( + agents, + "load_controlled_cd_lane_live_metric_readback", + _controlled_cd_lane_live_metrics, + ) app = FastAPI() app.include_router(router, prefix="/api/v1") client = TestClient(app) @@ -124,6 +130,28 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot( data["mainline_execution_state"]["ai_loop_current_blocker_registry_v2_ready"] is True ) + assert ( + data["mainline_execution_state"][ + "controlled_cd_lane_live_enforcer_metric_present" + ] + is True + ) + assert ( + data["mainline_execution_state"][ + "controlled_cd_lane_live_enforcer_last_run_fresh" + ] + is True + ) + assert ( + data["mainline_execution_state"][ + "controlled_cd_lane_live_controlled_drain_active_allowed" + ] + is False + ) + assert data["summary"]["controlled_cd_lane_live_metric_status"] == ( + "controlled_cd_lane_live_metric_readback_ready" + ) + assert data["summary"]["controlled_cd_lane_live_metric_metadata_only"] is True assert data["mainline_execution_state"][ "ai_loop_current_blocker_deploy_marker_readback_required" ] is False @@ -158,6 +186,11 @@ def test_awoooi_priority_work_order_readback_endpoint_redacts_ai_loop_queue( "load_latest_harbor_registry_controlled_recovery_preflight", _harbor_registry_upstream_502, ) + monkeypatch.setattr( + agents, + "load_controlled_cd_lane_live_metric_readback", + _controlled_cd_lane_live_metrics, + ) app = FastAPI() app.include_router(router, prefix="/api/v1") client = TestClient(app) @@ -929,6 +962,33 @@ def _stockplatform_runtime_ready() -> dict: } +def _controlled_cd_lane_live_metrics() -> dict: + return { + "schema_version": "controlled_cd_lane_live_metric_readback_v1", + "status": "controlled_cd_lane_live_metric_readback_ready", + "source": "node_exporter_text_metrics", + "metadata_only": True, + "active_blockers": [ + "controlled_cd_lane_active_not_allowed_yet", + "controlled_cd_lane_staging_not_allowed_yet", + ], + "readback": { + "enforcer_metric_present": True, + "enforcer_last_run_timestamp": 1782961969.0, + "enforcer_last_run_fresh": True, + "enforcer_apply_performed": 1, + "active_job_containers": 0, + "lane_process_count": 0, + "root_restore_sources_left": 0, + "controlled_drain_active_allowed": False, + "controlled_drain_staging_allowed": False, + "host_load5_per_core": 0.64, + "gitea_actions_active_container_count": 0, + "gitea_actions_active_process_count": 0, + }, + } + + def _stockplatform_runtime_blocked() -> dict: return { "schema_version": "stockplatform_public_api_runtime_readback_v1", diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 8cfdbcd9a..02b5ce7ae 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9111,6 +9111,20 @@ "value": "guardrails blocked" } }, + "liveMetrics": { + "enforcer": "110 enforcer", + "ready": "metric fresh", + "missing": "metric missing", + "enforcerDetail": "apply={apply} · metadata-only={metadata}", + "drainGuard": "Drain guard", + "drainGuardValue": "active={active} / staging={staging}", + "blockers": "blockers={count}", + "jobs": "Jobs / lane", + "jobsValue": "jobs={jobs} / lane={lane}", + "restoreSources": "restore sources={count}", + "load": "110 load", + "loadValue": "load5/core={value}" + }, "metrics": { "tags": "Tags", "groups": "Groups", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index d3098f05b..b4384522c 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9111,6 +9111,20 @@ "value": "guardrails blocked" } }, + "liveMetrics": { + "enforcer": "110 enforcer", + "ready": "metric 已更新", + "missing": "metric 未讀回", + "enforcerDetail": "apply={apply} · metadata-only={metadata}", + "drainGuard": "Drain guard", + "drainGuardValue": "active={active} / staging={staging}", + "blockers": "阻塞={count}", + "jobs": "Jobs / lane", + "jobsValue": "jobs={jobs} / lane={lane}", + "restoreSources": "restore sources={count}", + "load": "110 load", + "loadValue": "load5/core={value}" + }, "metrics": { "tags": "Tags", "groups": "分群鍵", 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 c99346ea0..6dad3a1ce 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1046,6 +1046,21 @@ type PriorityWorkOrderResponse = { ai_loop_current_blocker_safe_next_action_command?: string | null; ai_loop_current_blocker_safe_next_action_post_verifier?: string | null; ai_loop_current_blocker_safe_next_action_requires_local_console?: boolean | null; + controlled_cd_lane_live_metric_status?: string | null; + controlled_cd_lane_live_metric_metadata_only?: boolean | null; + controlled_cd_lane_live_enforcer_metric_present?: boolean | null; + controlled_cd_lane_live_enforcer_last_run_fresh?: boolean | null; + controlled_cd_lane_live_enforcer_apply_performed?: number | null; + controlled_cd_lane_live_active_job_containers?: number | null; + controlled_cd_lane_live_lane_process_count?: number | null; + controlled_cd_lane_live_root_restore_sources_left?: number | null; + controlled_cd_lane_live_controlled_drain_active_allowed?: boolean | null; + controlled_cd_lane_live_controlled_drain_staging_allowed?: boolean | null; + controlled_cd_lane_live_host_load5_per_core?: number | null; + controlled_cd_lane_live_gitea_actions_active_container_count?: number | null; + controlled_cd_lane_live_gitea_actions_active_process_count?: number | null; + controlled_cd_lane_live_metric_blocker_count?: number | null; + controlled_cd_lane_live_metric_blockers?: string[] | null; ai_loop_log_source_grouping_key_count?: number | null; ai_loop_log_source_grouping_keys?: string[] | null; ai_loop_log_source_tagging_contract_count?: number | null; @@ -1066,6 +1081,21 @@ type PriorityWorkOrderResponse = { ai_loop_current_blocker_safe_next_action_command?: string | null; ai_loop_current_blocker_safe_next_action_post_verifier?: string | null; ai_loop_current_blocker_safe_next_action_requires_local_console?: boolean | null; + controlled_cd_lane_live_metric_status?: string | null; + controlled_cd_lane_live_metric_metadata_only?: boolean | null; + controlled_cd_lane_live_enforcer_metric_present?: boolean | null; + controlled_cd_lane_live_enforcer_last_run_fresh?: boolean | null; + controlled_cd_lane_live_enforcer_apply_performed?: number | null; + controlled_cd_lane_live_active_job_containers?: number | null; + controlled_cd_lane_live_lane_process_count?: number | null; + controlled_cd_lane_live_root_restore_sources_left?: number | null; + controlled_cd_lane_live_controlled_drain_active_allowed?: boolean | null; + controlled_cd_lane_live_controlled_drain_staging_allowed?: boolean | null; + controlled_cd_lane_live_host_load5_per_core?: number | null; + controlled_cd_lane_live_gitea_actions_active_container_count?: number | null; + controlled_cd_lane_live_gitea_actions_active_process_count?: number | null; + controlled_cd_lane_live_metric_blocker_count?: number | null; + controlled_cd_lane_live_metric_blockers?: string[] | null; } | null; }>; }; @@ -7851,6 +7881,54 @@ function AiLoopLogSourceTagsPanel({ summary?.ai_loop_current_blocker_deployment_closure_state ?? evidence?.ai_loop_current_blocker_deployment_closure_state ?? ""; + const liveMetricStatus = + summary?.controlled_cd_lane_live_metric_status ?? + evidence?.controlled_cd_lane_live_metric_status ?? + ""; + const liveMetricMetadataOnly = + summary?.controlled_cd_lane_live_metric_metadata_only ?? + evidence?.controlled_cd_lane_live_metric_metadata_only ?? + false; + const liveEnforcerMetricPresent = + summary?.controlled_cd_lane_live_enforcer_metric_present ?? + evidence?.controlled_cd_lane_live_enforcer_metric_present ?? + false; + const liveEnforcerFresh = + summary?.controlled_cd_lane_live_enforcer_last_run_fresh ?? + evidence?.controlled_cd_lane_live_enforcer_last_run_fresh ?? + false; + const liveEnforcerApplyPerformed = + summary?.controlled_cd_lane_live_enforcer_apply_performed ?? + evidence?.controlled_cd_lane_live_enforcer_apply_performed ?? + 0; + const liveActiveAllowed = + summary?.controlled_cd_lane_live_controlled_drain_active_allowed ?? + evidence?.controlled_cd_lane_live_controlled_drain_active_allowed ?? + false; + const liveStagingAllowed = + summary?.controlled_cd_lane_live_controlled_drain_staging_allowed ?? + evidence?.controlled_cd_lane_live_controlled_drain_staging_allowed ?? + false; + const liveActiveJobContainers = + summary?.controlled_cd_lane_live_active_job_containers ?? + evidence?.controlled_cd_lane_live_active_job_containers ?? + 0; + const liveLaneProcessCount = + summary?.controlled_cd_lane_live_lane_process_count ?? + evidence?.controlled_cd_lane_live_lane_process_count ?? + 0; + const liveRootRestoreSourcesLeft = + summary?.controlled_cd_lane_live_root_restore_sources_left ?? + evidence?.controlled_cd_lane_live_root_restore_sources_left ?? + 0; + const liveHostLoad5PerCore = + summary?.controlled_cd_lane_live_host_load5_per_core ?? + evidence?.controlled_cd_lane_live_host_load5_per_core ?? + 0; + const liveMetricBlockerCount = + summary?.controlled_cd_lane_live_metric_blocker_count ?? + evidence?.controlled_cd_lane_live_metric_blocker_count ?? + 0; const currentBlockerId = summary?.ai_loop_current_blocker_id ?? ""; const controlledCdLaneGuardrailsBlocked = currentBlockerId === "controlled_cd_lane_guardrails_blocked"; @@ -7912,6 +7990,75 @@ function AiLoopLogSourceTagsPanel({ ids: receiptOutputIds, }, ]; + const liveMetricCards = + controlledCdLaneGuardrailsBlocked && liveMetricStatus + ? [ + { + key: "enforcer", + icon: ShieldCheck, + label: t("liveMetrics.enforcer"), + value: + liveEnforcerMetricPresent && liveEnforcerFresh + ? t("liveMetrics.ready") + : t("liveMetrics.missing"), + detail: t("liveMetrics.enforcerDetail", { + apply: String(liveEnforcerApplyPerformed), + metadata: String(Boolean(liveMetricMetadataOnly)), + }), + tone: + liveEnforcerMetricPresent && liveEnforcerFresh + ? "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]" + : "border-[#f0c6a8] bg-[#fff8f1] text-[#9a4d16]", + }, + { + key: "drain-guard", + icon: Activity, + label: t("liveMetrics.drainGuard"), + value: t("liveMetrics.drainGuardValue", { + active: String(Boolean(liveActiveAllowed)), + staging: String(Boolean(liveStagingAllowed)), + }), + detail: t("liveMetrics.blockers", { + count: liveMetricBlockerCount, + }), + tone: + liveActiveAllowed || liveStagingAllowed + ? "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]" + : "border-[#f0c6a8] bg-[#fff8f1] text-[#9a4d16]", + }, + { + key: "jobs", + icon: ListChecks, + label: t("liveMetrics.jobs"), + value: t("liveMetrics.jobsValue", { + jobs: liveActiveJobContainers, + lane: liveLaneProcessCount, + }), + detail: t("liveMetrics.restoreSources", { + count: liveRootRestoreSourcesLeft, + }), + tone: + liveActiveJobContainers === 0 && + liveLaneProcessCount === 0 && + liveRootRestoreSourcesLeft === 0 + ? "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]" + : "border-[#f0c6a8] bg-[#fff8f1] text-[#9a4d16]", + }, + { + key: "load", + icon: Gauge, + label: t("liveMetrics.load"), + value: t("liveMetrics.loadValue", { + value: liveHostLoad5PerCore.toFixed(2), + }), + detail: liveMetricStatus, + tone: + liveHostLoad5PerCore <= 1 + ? "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]" + : "border-[#f0c6a8] bg-[#fff8f1] text-[#9a4d16]", + }, + ] + : []; const rootCause = queueNormalizerFieldIds.some((id) => id.includes("server_accepts_key_then_session_timeout") ) @@ -8076,6 +8223,35 @@ function AiLoopLogSourceTagsPanel({ ))} ) : null} + {liveMetricCards.length > 0 ? ( +
+ {liveMetricCards.map((metric) => { + const Icon = metric.icon; + return ( +
+
+ + {metric.label} + +
+
+ {loading ? "--" : metric.value} +
+
+ {loading ? "--" : metric.detail} +
+
+ ); + })} +
+ ) : null}
{metrics.map((metric) => { const Icon = metric.icon;