Compare commits
13 Commits
codex/iwoo
...
codex/iwoo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6f2d1d07c | ||
|
|
87378b452d | ||
|
|
b83f9c5a52 | ||
|
|
8a3ddb8249 | ||
|
|
5077d4d02e | ||
|
|
21f5142d08 | ||
|
|
ba22e70266 | ||
|
|
9ccc447f81 | ||
|
|
722875135b | ||
|
|
64747170f1 | ||
|
|
58c009c2c7 | ||
|
|
607fc291e9 | ||
|
|
2860bd2b4b |
@@ -70,6 +70,7 @@ SHORT_HOST_MAP = {
|
||||
"120": "192.168.0.120",
|
||||
"121": "192.168.0.121",
|
||||
"188": "192.168.0.188",
|
||||
"wooo": "192.168.0.110",
|
||||
}
|
||||
DIAG_TIMEOUT = 10 # 診斷類超時(秒)
|
||||
OP_TIMEOUT = 60 # 操作類超時(秒)
|
||||
@@ -587,7 +588,10 @@ class SSHProvider(MCPToolProvider):
|
||||
return f"docker logs {name} --tail {tail} 2>&1"
|
||||
|
||||
if tool_name == "ssh_get_container_status":
|
||||
name = _validate_param("filter_name", params["filter_name"])
|
||||
raw_name = params.get("filter_name") or params.get("container_name") or params.get("name")
|
||||
if not raw_name:
|
||||
raise ValueError("Missing filter_name for ssh_get_container_status")
|
||||
name = _validate_param("filter_name", str(raw_name))
|
||||
return f"docker ps -a --filter name={name}"
|
||||
|
||||
if tool_name == "ssh_get_service_status":
|
||||
|
||||
@@ -2661,7 +2661,7 @@ class DecisionManager:
|
||||
ssh.execute(
|
||||
tool_name="ssh_get_container_status",
|
||||
# P0.4 fix 2026-04-24 ogt + Claude Sonnet 4.6: params= → parameters=(符合 MCPToolProvider.execute 簽名)
|
||||
parameters={"host": host, "container_name": container},
|
||||
parameters={"host": host, "filter_name": container, "container_name": container},
|
||||
),
|
||||
timeout=_MCP_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -552,6 +552,7 @@ _SHORT_HOST_MAP: dict[str, str] = {
|
||||
"188": "192.168.0.188",
|
||||
"ollama": "192.168.0.188",
|
||||
"ai-web": "192.168.0.188",
|
||||
"wooo": "192.168.0.110",
|
||||
"harbor": "192.168.0.110",
|
||||
"gitea": "192.168.0.110",
|
||||
}
|
||||
|
||||
@@ -173,6 +173,25 @@ def test_build_tool_params_uses_host_alias_and_service_from_affected_service() -
|
||||
assert params["time_window_minutes"] == 30
|
||||
|
||||
|
||||
def test_build_tool_params_maps_wooo_alias_to_allowed_ssh_host() -> None:
|
||||
class _Signal:
|
||||
alert_name = "HostHighCpuLoad"
|
||||
labels = {
|
||||
"alertname": "HostHighCpuLoad",
|
||||
"instance": "wooo:9100",
|
||||
}
|
||||
|
||||
class _Incident:
|
||||
incident_id = "INC-WOOO"
|
||||
signals = [_Signal()]
|
||||
affected_services = ["gitea"]
|
||||
|
||||
params = _build_tool_params(_Incident())
|
||||
|
||||
assert params["host"] == "192.168.0.110"
|
||||
assert params["filter_name"] == "gitea"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# _compute_fingerprint
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -45,6 +45,8 @@ def test_ssh_provider_uses_ollama_user_for_188():
|
||||
[
|
||||
("192.168.0.110:9100", "192.168.0.110"),
|
||||
("110:9100", "192.168.0.110"),
|
||||
("wooo", "192.168.0.110"),
|
||||
("wooo:9100", "192.168.0.110"),
|
||||
("188", "192.168.0.188"),
|
||||
("wooo@192.168.0.110", "192.168.0.110"),
|
||||
("ssh://wooo@192.168.0.110:22", "192.168.0.110"),
|
||||
@@ -90,3 +92,14 @@ async def test_ssh_execute_normalizes_host_before_allowed_check(monkeypatch):
|
||||
assert result.success is True
|
||||
assert captured["host"] == "192.168.0.110"
|
||||
assert isinstance(captured["timeout"], int)
|
||||
|
||||
|
||||
def test_ssh_container_status_accepts_legacy_container_name_alias():
|
||||
provider = SSHProvider()
|
||||
|
||||
command = provider._build_command(
|
||||
"ssh_get_container_status",
|
||||
{"container_name": "awoooi-api"},
|
||||
)
|
||||
|
||||
assert command == "docker ps -a --filter name=awoooi-api"
|
||||
|
||||
@@ -1237,6 +1237,36 @@
|
||||
"sourceDetail": "direct {direct} / candidate {candidate} / applied {applied};原因 {reason}",
|
||||
"needsHumanYes": "需要",
|
||||
"needsHumanNo": "不需要",
|
||||
"stateLabels": {
|
||||
"verificationDegradedManualRequired": "驗證退化,需人工確認"
|
||||
},
|
||||
"nextActionLabels": {
|
||||
"manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復"
|
||||
},
|
||||
"reasonLabels": {
|
||||
"incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident 仍開啟"
|
||||
},
|
||||
"sourceReasonLabels": {
|
||||
"providerHeartbeatPresentButNoIncidentMatch": "Sentry / SigNoz 有新鮮心跳,但沒有匹配到此 Incident"
|
||||
},
|
||||
"handoff": {
|
||||
"eyebrow": "現在要做",
|
||||
"titleManual": "需要人工接手確認",
|
||||
"titleAutomated": "自動鏈路已完成,持續觀察",
|
||||
"titleUnknown": "等待 truth-chain 資料",
|
||||
"actionManualVerifyOrRepair": "到 AwoooP Work Items / Approvals 確認執行證據;若服務仍異常,再重新送審修復,不要直接重啟或靜默關閉。",
|
||||
"actionNoManual": "目前不需要人工介入;保留真相鏈與 Run history 供稽核追蹤。",
|
||||
"actionUnknown": "尚未拿到完整狀態,先等 status-chain 載入完成。",
|
||||
"ownerLabel": "主責",
|
||||
"ownerSre": "SRE owner / AwoooP operator",
|
||||
"ownerAutomation": "AI 自動化鏈路",
|
||||
"entryLabel": "處理入口",
|
||||
"entryManual": "Work Items / Approvals / Runs",
|
||||
"entryReadOnly": "Runs / History",
|
||||
"reasonLabel": "原因",
|
||||
"boundaryLabel": "邊界",
|
||||
"boundary": "只讀追蹤,不觸發修復"
|
||||
},
|
||||
"repeatStates": {
|
||||
"duplicate": "最新入站重複",
|
||||
"related": "同指紋重複",
|
||||
@@ -1961,12 +1991,12 @@
|
||||
"actionGoObservability": "前往可觀測性",
|
||||
"actionGoAutomation": "前往自動化",
|
||||
"actionGoOperations": "前往營運",
|
||||
"actionGoSecurity": "前往安全合規",
|
||||
"actionGoSecurity": "前往 IwoooS 安全主控台",
|
||||
"actionGoKnowledge": "前往知識殿堂",
|
||||
"actionGoSettings": "前往設定",
|
||||
"actionGoTerminal": "前往終端頁面",
|
||||
"actionGoApprovals": "前往授權中心",
|
||||
"actionGoIwooos": "前往 IwoooS"
|
||||
"actionGoIwooos": "前往 IwoooS 資安主控台"
|
||||
},
|
||||
"aiopsTimeline": {
|
||||
"title": "AIOps 全景時序",
|
||||
@@ -4595,6 +4625,25 @@
|
||||
"tool": "工具",
|
||||
"scope": "範圍",
|
||||
"blockers": "卡點",
|
||||
"legacy": {
|
||||
"only": "自建 MCP 已觀測",
|
||||
"total": "自建 MCP",
|
||||
"success": "自建成功",
|
||||
"failed": "自建失敗",
|
||||
"topTool": "自建工具"
|
||||
},
|
||||
"evidence": {
|
||||
"firstClassTitle": "AwoooP Gateway MCP",
|
||||
"selfBuiltTitle": "自建 MCP / 舊版 Audit",
|
||||
"observed": "已觀測",
|
||||
"notObserved": "未觀測",
|
||||
"firstClassEmpty": "此 Run 尚未留下 AwoooP Gateway 一級 MCP 呼叫。",
|
||||
"selfBuiltEmpty": "此 Run 尚未透過 Incident 串到自建 MCP audit。",
|
||||
"agentScope": "{agent} / {scope}",
|
||||
"counts": "ok {success} / fail {failed} / block {blocked}",
|
||||
"legacyCounts": "ok {success} / fail {failed}",
|
||||
"noError": "無錯誤"
|
||||
},
|
||||
"metrics": {
|
||||
"firstClass": "一級入口",
|
||||
"policy": "政策已套用",
|
||||
@@ -4965,11 +5014,23 @@
|
||||
"scope": "範圍",
|
||||
"blockers": "卡點",
|
||||
"legacy": {
|
||||
"only": "Legacy MCP only",
|
||||
"total": "Legacy MCP",
|
||||
"success": "Legacy 成功",
|
||||
"failed": "Legacy 失敗",
|
||||
"topTool": "Legacy 工具"
|
||||
"only": "自建 MCP 已觀測",
|
||||
"total": "自建 MCP",
|
||||
"success": "自建成功",
|
||||
"failed": "自建失敗",
|
||||
"topTool": "自建工具"
|
||||
},
|
||||
"evidence": {
|
||||
"firstClassTitle": "AwoooP Gateway MCP",
|
||||
"selfBuiltTitle": "自建 MCP / 舊版 Audit",
|
||||
"observed": "已觀測",
|
||||
"notObserved": "未觀測",
|
||||
"firstClassEmpty": "此 Run 尚未留下 AwoooP Gateway 一級 MCP 呼叫。",
|
||||
"selfBuiltEmpty": "此 Run 尚未透過 Incident 串到自建 MCP audit。",
|
||||
"agentScope": "{agent} / {scope}",
|
||||
"counts": "ok {success} / fail {failed} / block {blocked}",
|
||||
"legacyCounts": "ok {success} / fail {failed}",
|
||||
"noError": "無錯誤"
|
||||
},
|
||||
"metrics": {
|
||||
"firstClass": "第一級",
|
||||
@@ -5272,6 +5333,53 @@
|
||||
"state": "只讀鏡像 / 先觀測",
|
||||
"detail": "只顯示態勢與缺口;掃描、修復、更新、阻擋仍未開閘。"
|
||||
},
|
||||
"focusDeck": {
|
||||
"eyebrow": "首層焦點導覽",
|
||||
"title": "先選一條線,不用讀完整頁",
|
||||
"subtitle": "把 IwoooS 的長頁面壓成五個可跳轉焦點;每個焦點只做導覽與證據定位,不是執行按鈕。",
|
||||
"boundaryTitle": "焦點導覽邊界",
|
||||
"summary": {
|
||||
"items": {
|
||||
"label": "焦點",
|
||||
"value": "5"
|
||||
},
|
||||
"runtime": {
|
||||
"label": "執行期",
|
||||
"value": "Gate 0"
|
||||
},
|
||||
"mode": {
|
||||
"label": "模式",
|
||||
"value": "只讀"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"workMap": {
|
||||
"label": "先看總圖",
|
||||
"title": "資安工作地圖",
|
||||
"body": "用六條工作線快速理解目前資安網的方向。"
|
||||
},
|
||||
"unlockPath": {
|
||||
"label": "目前卡點",
|
||||
"title": "61% 解鎖路徑",
|
||||
"body": "真正能推動進度的是 S4.9 負責人回覆與脫敏證據。"
|
||||
},
|
||||
"productScope": {
|
||||
"label": "覆蓋範圍",
|
||||
"title": "全產品資安範圍",
|
||||
"body": "看 AwoooI、AwoooP、IwoooS、網站、VibeWork 與主機納管。"
|
||||
},
|
||||
"hostTools": {
|
||||
"label": "主機工具",
|
||||
"title": "Kali 與工具鏈",
|
||||
"body": "看 112 / 111 / 168 與監控、MCP、Ansible、KM 的只讀鏈路。"
|
||||
},
|
||||
"sourceControl": {
|
||||
"label": "版本來源",
|
||||
"title": "GitHub / Gitea 邊界",
|
||||
"body": "看 GitHub primary、Gitea 清冊、refs 與 workflow 還缺哪些證據。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualCommandDashboard": {
|
||||
"eyebrow": "視覺化資安指揮板",
|
||||
"title": "先看圖,再展開證據",
|
||||
|
||||
@@ -1237,6 +1237,36 @@
|
||||
"sourceDetail": "direct {direct} / candidate {candidate} / applied {applied};原因 {reason}",
|
||||
"needsHumanYes": "需要",
|
||||
"needsHumanNo": "不需要",
|
||||
"stateLabels": {
|
||||
"verificationDegradedManualRequired": "驗證退化,需人工確認"
|
||||
},
|
||||
"nextActionLabels": {
|
||||
"manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復"
|
||||
},
|
||||
"reasonLabels": {
|
||||
"incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident 仍開啟"
|
||||
},
|
||||
"sourceReasonLabels": {
|
||||
"providerHeartbeatPresentButNoIncidentMatch": "Sentry / SigNoz 有新鮮心跳,但沒有匹配到此 Incident"
|
||||
},
|
||||
"handoff": {
|
||||
"eyebrow": "現在要做",
|
||||
"titleManual": "需要人工接手確認",
|
||||
"titleAutomated": "自動鏈路已完成,持續觀察",
|
||||
"titleUnknown": "等待 truth-chain 資料",
|
||||
"actionManualVerifyOrRepair": "到 AwoooP Work Items / Approvals 確認執行證據;若服務仍異常,再重新送審修復,不要直接重啟或靜默關閉。",
|
||||
"actionNoManual": "目前不需要人工介入;保留真相鏈與 Run history 供稽核追蹤。",
|
||||
"actionUnknown": "尚未拿到完整狀態,先等 status-chain 載入完成。",
|
||||
"ownerLabel": "主責",
|
||||
"ownerSre": "SRE owner / AwoooP operator",
|
||||
"ownerAutomation": "AI 自動化鏈路",
|
||||
"entryLabel": "處理入口",
|
||||
"entryManual": "Work Items / Approvals / Runs",
|
||||
"entryReadOnly": "Runs / History",
|
||||
"reasonLabel": "原因",
|
||||
"boundaryLabel": "邊界",
|
||||
"boundary": "只讀追蹤,不觸發修復"
|
||||
},
|
||||
"repeatStates": {
|
||||
"duplicate": "最新入站重複",
|
||||
"related": "同指紋重複",
|
||||
@@ -1961,12 +1991,12 @@
|
||||
"actionGoObservability": "前往可觀測性",
|
||||
"actionGoAutomation": "前往自動化",
|
||||
"actionGoOperations": "前往營運",
|
||||
"actionGoSecurity": "前往安全合規",
|
||||
"actionGoSecurity": "前往 IwoooS 安全主控台",
|
||||
"actionGoKnowledge": "前往知識殿堂",
|
||||
"actionGoSettings": "前往設定",
|
||||
"actionGoTerminal": "前往終端頁面",
|
||||
"actionGoApprovals": "前往授權中心",
|
||||
"actionGoIwooos": "前往 IwoooS"
|
||||
"actionGoIwooos": "前往 IwoooS 資安主控台"
|
||||
},
|
||||
"aiopsTimeline": {
|
||||
"title": "AIOps 全景時序",
|
||||
@@ -4595,6 +4625,25 @@
|
||||
"tool": "工具",
|
||||
"scope": "範圍",
|
||||
"blockers": "卡點",
|
||||
"legacy": {
|
||||
"only": "自建 MCP 已觀測",
|
||||
"total": "自建 MCP",
|
||||
"success": "自建成功",
|
||||
"failed": "自建失敗",
|
||||
"topTool": "自建工具"
|
||||
},
|
||||
"evidence": {
|
||||
"firstClassTitle": "AwoooP Gateway MCP",
|
||||
"selfBuiltTitle": "自建 MCP / 舊版 Audit",
|
||||
"observed": "已觀測",
|
||||
"notObserved": "未觀測",
|
||||
"firstClassEmpty": "此 Run 尚未留下 AwoooP Gateway 一級 MCP 呼叫。",
|
||||
"selfBuiltEmpty": "此 Run 尚未透過 Incident 串到自建 MCP audit。",
|
||||
"agentScope": "{agent} / {scope}",
|
||||
"counts": "ok {success} / fail {failed} / block {blocked}",
|
||||
"legacyCounts": "ok {success} / fail {failed}",
|
||||
"noError": "無錯誤"
|
||||
},
|
||||
"metrics": {
|
||||
"firstClass": "一級入口",
|
||||
"policy": "政策已套用",
|
||||
@@ -4965,11 +5014,23 @@
|
||||
"scope": "範圍",
|
||||
"blockers": "卡點",
|
||||
"legacy": {
|
||||
"only": "Legacy MCP only",
|
||||
"total": "Legacy MCP",
|
||||
"success": "Legacy 成功",
|
||||
"failed": "Legacy 失敗",
|
||||
"topTool": "Legacy 工具"
|
||||
"only": "自建 MCP 已觀測",
|
||||
"total": "自建 MCP",
|
||||
"success": "自建成功",
|
||||
"failed": "自建失敗",
|
||||
"topTool": "自建工具"
|
||||
},
|
||||
"evidence": {
|
||||
"firstClassTitle": "AwoooP Gateway MCP",
|
||||
"selfBuiltTitle": "自建 MCP / 舊版 Audit",
|
||||
"observed": "已觀測",
|
||||
"notObserved": "未觀測",
|
||||
"firstClassEmpty": "此 Run 尚未留下 AwoooP Gateway 一級 MCP 呼叫。",
|
||||
"selfBuiltEmpty": "此 Run 尚未透過 Incident 串到自建 MCP audit。",
|
||||
"agentScope": "{agent} / {scope}",
|
||||
"counts": "ok {success} / fail {failed} / block {blocked}",
|
||||
"legacyCounts": "ok {success} / fail {failed}",
|
||||
"noError": "無錯誤"
|
||||
},
|
||||
"metrics": {
|
||||
"firstClass": "第一級",
|
||||
@@ -5272,6 +5333,53 @@
|
||||
"state": "只讀鏡像 / 先觀測",
|
||||
"detail": "只顯示態勢與缺口;掃描、修復、更新、阻擋仍未開閘。"
|
||||
},
|
||||
"focusDeck": {
|
||||
"eyebrow": "首層焦點導覽",
|
||||
"title": "先選一條線,不用讀完整頁",
|
||||
"subtitle": "把 IwoooS 的長頁面壓成五個可跳轉焦點;每個焦點只做導覽與證據定位,不是執行按鈕。",
|
||||
"boundaryTitle": "焦點導覽邊界",
|
||||
"summary": {
|
||||
"items": {
|
||||
"label": "焦點",
|
||||
"value": "5"
|
||||
},
|
||||
"runtime": {
|
||||
"label": "執行期",
|
||||
"value": "Gate 0"
|
||||
},
|
||||
"mode": {
|
||||
"label": "模式",
|
||||
"value": "只讀"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"workMap": {
|
||||
"label": "先看總圖",
|
||||
"title": "資安工作地圖",
|
||||
"body": "用六條工作線快速理解目前資安網的方向。"
|
||||
},
|
||||
"unlockPath": {
|
||||
"label": "目前卡點",
|
||||
"title": "61% 解鎖路徑",
|
||||
"body": "真正能推動進度的是 S4.9 負責人回覆與脫敏證據。"
|
||||
},
|
||||
"productScope": {
|
||||
"label": "覆蓋範圍",
|
||||
"title": "全產品資安範圍",
|
||||
"body": "看 AwoooI、AwoooP、IwoooS、網站、VibeWork 與主機納管。"
|
||||
},
|
||||
"hostTools": {
|
||||
"label": "主機工具",
|
||||
"title": "Kali 與工具鏈",
|
||||
"body": "看 112 / 111 / 168 與監控、MCP、Ansible、KM 的只讀鏈路。"
|
||||
},
|
||||
"sourceControl": {
|
||||
"label": "版本來源",
|
||||
"title": "GitHub / Gitea 邊界",
|
||||
"body": "看 GitHub primary、Gitea 清冊、refs 與 workflow 還缺哪些證據。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualCommandDashboard": {
|
||||
"eyebrow": "視覺化資安指揮板",
|
||||
"title": "先看圖,再展開證據",
|
||||
|
||||
@@ -200,6 +200,26 @@ function FocusIncidentEvidencePanel({
|
||||
const sourceStatusLabel = statusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel)
|
||||
const mcpGateway = chain?.mcp?.gateway
|
||||
const ansible = chain?.execution?.ansible
|
||||
const outcomeState = String(chain?.operator_outcome?.state ?? '')
|
||||
const nextAction = String(chain?.operator_outcome?.next_action ?? chain?.next_step ?? '')
|
||||
const humanReason = String(chain?.operator_outcome?.human_action_reason ?? '')
|
||||
const sourceReasonCode = String(sourceCorrelation?.missing_reason ?? '')
|
||||
const outcomeStateLabels: Record<string, string> = {
|
||||
verification_degraded_manual_required: t('operatorFlow.stateLabels.verificationDegradedManualRequired'),
|
||||
}
|
||||
const nextActionLabels: Record<string, string> = {
|
||||
manual_verify_or_repair: t('operatorFlow.nextActionLabels.manualVerifyOrRepair'),
|
||||
}
|
||||
const humanReasonLabels: Record<string, string> = {
|
||||
incident_open_after_successful_execution: t('operatorFlow.reasonLabels.incidentOpenAfterSuccessfulExecution'),
|
||||
}
|
||||
const sourceReasonLabels: Record<string, string> = {
|
||||
provider_heartbeat_present_but_no_incident_match: t('operatorFlow.sourceReasonLabels.providerHeartbeatPresentButNoIncidentMatch'),
|
||||
}
|
||||
const outcomeStateLabel = outcomeStateLabels[outcomeState] ?? valueOrEmpty(outcomeState, emptyLabel)
|
||||
const nextActionLabel = nextActionLabels[nextAction] ?? valueOrEmpty(nextAction, emptyLabel)
|
||||
const humanReasonLabel = humanReasonLabels[humanReason] ?? valueOrEmpty(humanReason, emptyLabel)
|
||||
const sourceReasonLabel = sourceReasonLabels[sourceReasonCode] ?? valueOrEmpty(sourceReasonCode, emptyLabel)
|
||||
const relatedIncidentIds = chain?.source_refs?.refs?.incident_ids ?? []
|
||||
const fingerprint = chain?.source_refs?.refs?.fingerprints?.[0] ?? emptyLabel
|
||||
const latestInbound = chain?.source_refs?.latest_inbound
|
||||
@@ -219,8 +239,49 @@ function FocusIncidentEvidencePanel({
|
||||
const latestOutboundLabel = latestOutbound?.sent_at
|
||||
? formatTimestamp(latestOutbound.sent_at, locale, emptyLabel)
|
||||
: emptyLabel
|
||||
const sourceReason = valueOrEmpty(sourceCorrelation?.missing_reason, emptyLabel)
|
||||
const needsHumanLabel = chain?.needs_human ? t('operatorFlow.needsHumanYes') : t('operatorFlow.needsHumanNo')
|
||||
const handoffTone = !chain ? 'gray' : chain.needs_human ? 'red' : 'green'
|
||||
const handoffTitle = chain?.needs_human
|
||||
? t('operatorFlow.handoff.titleManual')
|
||||
: chain
|
||||
? t('operatorFlow.handoff.titleAutomated')
|
||||
: t('operatorFlow.handoff.titleUnknown')
|
||||
const handoffAction = !chain
|
||||
? t('operatorFlow.handoff.actionUnknown')
|
||||
: chain.needs_human
|
||||
? t('operatorFlow.handoff.actionManualVerifyOrRepair')
|
||||
: t('operatorFlow.handoff.actionNoManual')
|
||||
const handoffOwner = chain?.needs_human
|
||||
? t('operatorFlow.handoff.ownerSre')
|
||||
: chain
|
||||
? t('operatorFlow.handoff.ownerAutomation')
|
||||
: emptyLabel
|
||||
const handoffEntry = chain?.needs_human
|
||||
? t('operatorFlow.handoff.entryManual')
|
||||
: chain
|
||||
? t('operatorFlow.handoff.entryReadOnly')
|
||||
: emptyLabel
|
||||
const handoffReason = chain?.needs_human ? humanReasonLabel : valueOrEmpty(chain?.verdict, emptyLabel)
|
||||
const handoffLinks = [
|
||||
{
|
||||
key: 'workItems',
|
||||
label: t('links.workItems'),
|
||||
href: `/awooop/work-items?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
|
||||
Icon: ListChecks,
|
||||
},
|
||||
{
|
||||
key: 'approvals',
|
||||
label: t('links.approvals'),
|
||||
href: `/awooop/approvals?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
|
||||
Icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
key: 'runs',
|
||||
label: t('links.runs'),
|
||||
href: `/awooop/runs?project_id=${encodedProjectId}&incident_id=${encodedIncidentId}` as never,
|
||||
Icon: Activity,
|
||||
},
|
||||
]
|
||||
const operatorCards = [
|
||||
{
|
||||
key: 'repeat',
|
||||
@@ -259,8 +320,8 @@ function FocusIncidentEvidencePanel({
|
||||
title: t('operatorFlow.aiTitle'),
|
||||
value: valueOrEmpty(chain?.operator_outcome?.summary_zh ?? chain?.verdict, emptyLabel),
|
||||
detail: t('operatorFlow.aiDetail', {
|
||||
state: valueOrEmpty(chain?.operator_outcome?.state, emptyLabel),
|
||||
nextStep: valueOrEmpty(chain?.operator_outcome?.next_action ?? chain?.next_step, emptyLabel),
|
||||
state: outcomeStateLabel,
|
||||
nextStep: nextActionLabel,
|
||||
needsHuman: needsHumanLabel,
|
||||
}),
|
||||
tone: chain?.needs_human ? 'red' : chain ? 'green' : 'gray',
|
||||
@@ -275,7 +336,7 @@ function FocusIncidentEvidencePanel({
|
||||
direct: sourceCorrelation?.direct_ref_total ?? 0,
|
||||
candidate: sourceCorrelation?.candidate_total ?? 0,
|
||||
applied: sourceCorrelation?.applied_link_total ?? 0,
|
||||
reason: sourceReason,
|
||||
reason: sourceReasonLabel,
|
||||
}),
|
||||
tone: sourceStatus === 'linked' ? 'green' : sourceStatus === 'missing' ? 'red' : 'amber',
|
||||
testId: 'alerts-source-state',
|
||||
@@ -395,6 +456,53 @@ function FocusIncidentEvidencePanel({
|
||||
<p className="text-sm font-semibold text-[#141413]">{t('operatorFlow.title')}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t('operatorFlow.subtitle')}</p>
|
||||
</div>
|
||||
<div
|
||||
data-testid="alerts-operator-handoff"
|
||||
className={cn(
|
||||
'mb-3 border px-3 py-3',
|
||||
handoffTone === 'green' && 'border-[#9bc7a4] bg-[#f0faf2]',
|
||||
handoffTone === 'red' && 'border-[#e2a29b] bg-[#fff0ef]',
|
||||
handoffTone === 'gray' && 'border-[#d8d3c7] bg-[#faf9f3]',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#5f5b52]">{t('operatorFlow.handoff.eyebrow')}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">{handoffTitle}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#77736a]">{handoffAction}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
{handoffLinks.map(({ key, label, href, Icon }) => (
|
||||
<Link
|
||||
key={key}
|
||||
href={href}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white/80 px-2.5 py-1.5 text-xs font-semibold text-[#3f3a32] hover:bg-white"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-4">
|
||||
<div className="min-w-0 border border-current/10 bg-white/60 px-2.5 py-2">
|
||||
<p className="text-[11px] font-semibold uppercase text-[#77736a]">{t('operatorFlow.handoff.ownerLabel')}</p>
|
||||
<p className="mt-1 truncate text-xs font-semibold text-[#141413]" title={handoffOwner}>{handoffOwner}</p>
|
||||
</div>
|
||||
<div className="min-w-0 border border-current/10 bg-white/60 px-2.5 py-2">
|
||||
<p className="text-[11px] font-semibold uppercase text-[#77736a]">{t('operatorFlow.handoff.entryLabel')}</p>
|
||||
<p className="mt-1 truncate text-xs font-semibold text-[#141413]" title={handoffEntry}>{handoffEntry}</p>
|
||||
</div>
|
||||
<div className="min-w-0 border border-current/10 bg-white/60 px-2.5 py-2">
|
||||
<p className="text-[11px] font-semibold uppercase text-[#77736a]">{t('operatorFlow.handoff.reasonLabel')}</p>
|
||||
<p className="mt-1 truncate text-xs font-semibold text-[#141413]" title={handoffReason}>{handoffReason}</p>
|
||||
</div>
|
||||
<div className="min-w-0 border border-current/10 bg-white/60 px-2.5 py-2">
|
||||
<p className="text-[11px] font-semibold uppercase text-[#77736a]">{t('operatorFlow.handoff.boundaryLabel')}</p>
|
||||
<p className="mt-1 truncate text-xs font-semibold text-[#141413]" title={t('operatorFlow.handoff.boundary')}>{t('operatorFlow.handoff.boundary')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{operatorCards.map(({ key, Icon, title, value, detail, tone, testId }) => (
|
||||
<div
|
||||
|
||||
@@ -812,6 +812,8 @@ function McpGatewayPanel({
|
||||
{ label: t("metrics.approvalExecutor"), value: summary?.approval_executor_total ?? 0 },
|
||||
{ label: t("metrics.legacyBridge"), value: summary?.legacy_bridge_total ?? 0 },
|
||||
];
|
||||
const gatewayTools = summary?.by_tool?.slice(0, 4) ?? [];
|
||||
const legacyTools = legacySummary?.by_tool?.slice(0, 4) ?? [];
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
@@ -869,6 +871,84 @@ function McpGatewayPanel({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-px border-t border-[#e0ddd4] bg-[#e0ddd4] lg:grid-cols-2">
|
||||
<div className="min-w-0 bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h4 className="text-sm font-semibold text-[#141413]">{t("evidence.firstClassTitle")}</h4>
|
||||
<span className={cn("shrink-0 border px-2 py-0.5 text-xs font-semibold", gatewayTools.length > 0 ? statusClass("success") : statusClass("pending"))}>
|
||||
{t((gatewayTools.length > 0 ? "evidence.observed" : "evidence.notObserved") as never)}
|
||||
</span>
|
||||
</div>
|
||||
{gatewayTools.length > 0 ? (
|
||||
<div className="mt-3 divide-y divide-[#eee9dd] border border-[#eee9dd]">
|
||||
{gatewayTools.map((item) => (
|
||||
<div key={`${item.agent_id ?? "gateway"}:${item.tool_name ?? "unknown"}`} className="grid gap-3 px-3 py-3 md:grid-cols-[1fr_112px]">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-mono text-xs font-semibold text-[#141413]" title={item.tool_name ?? emptyLabel}>
|
||||
{item.tool_name ?? emptyLabel}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-[#77736a]" title={item.agent_id ?? emptyLabel}>
|
||||
{t("evidence.agentScope", {
|
||||
agent: item.agent_id ?? emptyLabel,
|
||||
scope: item.required_scope ?? emptyLabel,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-mono text-xs leading-5 text-[#5f5b52]">
|
||||
{t("evidence.counts", {
|
||||
success: item.success ?? 0,
|
||||
failed: item.failed ?? 0,
|
||||
blocked: item.blocked ?? 0,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 border border-[#eee9dd] bg-[#faf9f3] px-3 py-3 text-sm leading-6 text-[#5f5b52]">
|
||||
{t("evidence.firstClassEmpty")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h4 className="text-sm font-semibold text-[#141413]">{t("evidence.selfBuiltTitle")}</h4>
|
||||
<span className={cn("shrink-0 border px-2 py-0.5 text-xs font-semibold", legacyTools.length > 0 ? statusClass("success") : statusClass("pending"))}>
|
||||
{t((legacyTools.length > 0 ? "evidence.observed" : "evidence.notObserved") as never)}
|
||||
</span>
|
||||
</div>
|
||||
{legacyTools.length > 0 ? (
|
||||
<div className="mt-3 divide-y divide-[#eee9dd] border border-[#eee9dd]">
|
||||
{legacyTools.map((item) => {
|
||||
const route = [item.mcp_server, item.tool_name].filter(Boolean).join("/") || emptyLabel;
|
||||
return (
|
||||
<div key={route} className="grid gap-3 px-3 py-3 md:grid-cols-[1fr_112px]">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-mono text-xs font-semibold text-[#141413]" title={route}>
|
||||
{route}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-[#77736a]" title={item.last_error ?? emptyLabel}>
|
||||
{item.last_error ?? t("evidence.noError")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-mono text-xs leading-5 text-[#5f5b52]">
|
||||
{t("evidence.legacyCounts", {
|
||||
success: item.success ?? 0,
|
||||
failed: item.failed ?? 0,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 border border-[#eee9dd] bg-[#faf9f3] px-3 py-3 text-sm leading-6 text-[#5f5b52]">
|
||||
{t("evidence.selfBuiltEmpty")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
|
||||
type PostureMetric = {
|
||||
@@ -173,6 +173,14 @@ type IwoooSCommandMapItem = {
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type IwoooSFocusDeckItem = {
|
||||
key: string
|
||||
href: string
|
||||
metric: string
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type AwoooPReadOnlyLandingReadinessItem = {
|
||||
key: string
|
||||
item: string
|
||||
@@ -2285,6 +2293,27 @@ const iwooosCommandMapBoundaries = [
|
||||
'not_authorization=true',
|
||||
] as const
|
||||
|
||||
const iwooosFocusDeckItems: IwoooSFocusDeckItem[] = [
|
||||
{ key: 'workMap', href: '#iwooos-command-map-board', metric: '6', icon: Radar, tone: 'warn' },
|
||||
{ key: 'unlockPath', href: '#iwooos-first-progress-unlock-path-board', metric: 'S4.9', icon: ListChecks, tone: 'warn' },
|
||||
{ key: 'productScope', href: '#iwooos-global-security-mesh-matrix-board', metric: '8/8', icon: ShieldCheck, tone: 'steady' },
|
||||
{ key: 'hostTools', href: '#iwooos-host-tool-evidence-chain-board', metric: '3', icon: Activity, tone: 'warn' },
|
||||
{ key: 'sourceControl', href: '#iwooos-command-map-source-control', metric: '0', icon: GitBranch, tone: 'locked' },
|
||||
]
|
||||
|
||||
const iwooosFocusDeckBoundaries = [
|
||||
'iwooos_focus_deck_first_layer=true',
|
||||
'iwooos_focus_deck_item_count=5',
|
||||
'iwooos_focus_deck_anchor_navigation_allowed=true',
|
||||
'iwooos_focus_deck_execution_action_buttons_allowed=false',
|
||||
'iwooos_focus_deck_runtime_gate_count=0',
|
||||
'iwooos_focus_deck_above_command_map=true',
|
||||
'runtime_execution_authorized=false',
|
||||
'active_runtime_gate_count=0',
|
||||
'action_buttons_allowed=false',
|
||||
'not_authorization=true',
|
||||
] as const
|
||||
|
||||
const iwooosFirstUnlockEvidencePacketSlots = [
|
||||
{ key: 'ownerDecisionMetadata', slot: '01', state: '待補', icon: ClipboardCheck, tone: 'warn' },
|
||||
{ key: 'scopeEvidenceRefs', slot: '02', state: '待補', icon: GitBranch, tone: 'warn' },
|
||||
@@ -4706,6 +4735,7 @@ function IwoooSGlobalSecurityMeshMatrixBoard() {
|
||||
|
||||
return (
|
||||
<section
|
||||
id="iwooos-global-security-mesh-matrix-board"
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-global-security-mesh-matrix-board"
|
||||
>
|
||||
@@ -4878,6 +4908,7 @@ function IwoooSHostToolEvidenceChainBoard() {
|
||||
|
||||
return (
|
||||
<section
|
||||
id="iwooos-host-tool-evidence-chain-board"
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-host-tool-evidence-chain-board"
|
||||
>
|
||||
@@ -8902,7 +8933,11 @@ function IwoooSFirstProgressUnlockPathBoard() {
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
|
||||
return (
|
||||
<section style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }} data-testid="iwooos-first-progress-unlock-path-board">
|
||||
<section
|
||||
id="iwooos-first-progress-unlock-path-board"
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-first-progress-unlock-path-board"
|
||||
>
|
||||
<div style={{ ...band, padding: 16, background: '#f7fbfa', borderColor: '#cfe2d8' }}>
|
||||
<div
|
||||
style={{
|
||||
@@ -9034,6 +9069,175 @@ function IwoooSFirstProgressUnlockPathBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
function IwoooSFocusDeckBoard() {
|
||||
const t = useTranslations('iwooos.focusDeck')
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
const deckBodyRef = useRef<HTMLDivElement>(null)
|
||||
const [isCompactDeck, setIsCompactDeck] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const node = deckBodyRef.current
|
||||
if (!node) return
|
||||
|
||||
const updateLayout = () => setIsCompactDeck(node.getBoundingClientRect().width < 700)
|
||||
updateLayout()
|
||||
|
||||
const observer = new ResizeObserver(updateLayout)
|
||||
observer.observe(node)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="iwooos-focus-deck-board"
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ ...band, padding: 14, background: '#fffdf8', borderColor: '#ded8c8' }}>
|
||||
<div
|
||||
ref={deckBodyRef}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isCompactDeck ? 'minmax(0, 1fr)' : 'minmax(0, 0.88fr) minmax(0, 1.9fr)',
|
||||
gap: 12,
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
border: '0.5px solid #e7e1d3',
|
||||
borderRadius: 8,
|
||||
padding: 13,
|
||||
background: '#141413',
|
||||
color: '#fffdf8',
|
||||
display: 'grid',
|
||||
alignContent: 'start',
|
||||
gap: 7,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#d7d2c5', fontSize: 12, fontWeight: 700 }}>
|
||||
<SearchCheck size={16} color="#d97757" />
|
||||
{t('eyebrow')}
|
||||
</div>
|
||||
<h2 style={{ fontSize: 17, lineHeight: 1.2, margin: 0, color: '#fffdf8' }}>{t('title')}</h2>
|
||||
<p style={{ fontSize: 12, lineHeight: 1.55, color: '#d7d2c5', margin: 0, ...textWrap }}>
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isCompactDeck ? 'repeat(auto-fit, minmax(min(100%, 86px), 1fr))' : 'repeat(3, minmax(0, 1fr))',
|
||||
gap: 7,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{['items', 'runtime', 'mode'].map(key => (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
border: '0.5px solid rgba(255,253,248,0.16)',
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
minHeight: 58,
|
||||
background: 'rgba(255,253,248,0.06)',
|
||||
...textWrap,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 10, color: '#c8c1b3' }}>{t(`summary.${key}.label` as never)}</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, marginTop: 5, color: key === 'runtime' ? '#d7d2c5' : '#fffdf8' }}>
|
||||
{t(`summary.${key}.value` as never)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 158px), 1fr))',
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{iwooosFocusDeckItems.map(item => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<a
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
data-testid={`iwooos-focus-deck-link-${item.key}`}
|
||||
style={{
|
||||
border: `0.5px solid ${item.tone === 'steady' ? '#cfe2d8' : item.tone === 'warn' ? '#e6c8b8' : '#dad7ce'}`,
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
minHeight: 128,
|
||||
padding: 11,
|
||||
color: '#141413',
|
||||
textDecoration: 'none',
|
||||
display: 'grid',
|
||||
alignContent: 'start',
|
||||
gap: 7,
|
||||
...textWrap,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontSize: 10, color: '#87867f' }}>{t(`items.${item.key}.label` as never)}</span>
|
||||
<Icon size={16} color={toneColors[item.tone]} />
|
||||
</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: toneColors[item.tone], lineHeight: 1 }}>
|
||||
{item.metric}
|
||||
</div>
|
||||
<h3 style={{ fontSize: 13, lineHeight: 1.25, margin: 0, color: '#141413' }}>
|
||||
{t(`items.${item.key}.title` as never)}
|
||||
</h3>
|
||||
<p style={{ fontSize: 11, lineHeight: 1.45, margin: 0, color: '#6f6d66', ...textWrap }}>
|
||||
{t(`items.${item.key}.body` as never)}
|
||||
</p>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details
|
||||
data-testid="iwooos-focus-deck-boundaries"
|
||||
style={{
|
||||
marginTop: 10,
|
||||
border: '0.5px solid #e7e1d3',
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
padding: '7px 10px',
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontSize: 12, fontWeight: 700, color: '#5c543f' }}>
|
||||
{t('boundaryTitle')}
|
||||
</summary>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 6, marginTop: 9 }}>
|
||||
{iwooosFocusDeckBoundaries.map(item => (
|
||||
<code
|
||||
key={item}
|
||||
style={{
|
||||
border: '0.5px solid #ede4d4',
|
||||
borderRadius: 8,
|
||||
padding: '6px 8px',
|
||||
color: '#5c543f',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
background: '#fbfbf7',
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function IwoooSCommandMapBoard() {
|
||||
const t = useTranslations('iwooos.commandMap')
|
||||
const [activeKey, setActiveKey] = useState<IwoooSCommandMapMode>('unlock')
|
||||
@@ -9042,7 +9246,11 @@ function IwoooSCommandMapBoard() {
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
|
||||
return (
|
||||
<section style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }} data-testid="iwooos-command-map-board">
|
||||
<section
|
||||
id="iwooos-command-map-board"
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-command-map-board"
|
||||
>
|
||||
<div style={{ ...band, padding: 16, background: '#fbfbf7', borderColor: '#ded8c8' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))', gap: 14 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
@@ -9072,6 +9280,7 @@ function IwoooSCommandMapBoard() {
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
id={item.key === 'sourceControl' ? 'iwooos-command-map-source-control' : undefined}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
@@ -13929,6 +14138,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<IwoooSFocusDeckBoard />
|
||||
<IwoooSCommandMapBoard />
|
||||
<IwoooSFirstProgressUnlockPathBoard />
|
||||
<IwoooSVisualCommandDashboard />
|
||||
@@ -14220,7 +14430,11 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
</IwoooSSectionGroup>
|
||||
|
||||
<IwoooSSectionGroup title={ia('ownerEvidence.title')} summary={ia('ownerEvidence.summary')}>
|
||||
<section style={{ marginBottom: 0 }} data-testid="iwooos-source-control-readiness-board">
|
||||
<section
|
||||
id="iwooos-source-control-readiness-board"
|
||||
style={{ marginBottom: 0 }}
|
||||
data-testid="iwooos-source-control-readiness-board"
|
||||
>
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<h2 style={{ fontSize: 16, margin: 0 }}>{t('sourceControlReadiness.title')}</h2>
|
||||
<p style={{ fontSize: 12, color: '#6f6d66', margin: '6px 0 0', lineHeight: 1.55 }}>
|
||||
|
||||
@@ -17,7 +17,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { useLocale } from 'next-intl'
|
||||
import { Search, Terminal, Home, Activity, Wrench, Shield, BookOpen, Settings, Zap, GitBranch, Radar } from 'lucide-react'
|
||||
import { Search, Terminal, Home, Activity, Wrench, BookOpen, Settings, Zap, GitBranch, Radar } from 'lucide-react'
|
||||
import { useTerminalStore } from '@/stores/terminal.store'
|
||||
import { Z_INDEX } from '@/lib/constants/z-index'
|
||||
|
||||
@@ -99,21 +99,23 @@ export function CommandPalette() {
|
||||
action: () => nav('/operations'),
|
||||
keywords: ['operations', '營運', 'ops'],
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: t('actionGoSecurity'),
|
||||
group: t('groupNav'),
|
||||
icon: <Shield size={14} />,
|
||||
action: () => nav('/security-compliance'),
|
||||
keywords: ['security', '安全', 'compliance', '合規'],
|
||||
},
|
||||
{
|
||||
id: 'iwooos',
|
||||
label: t('actionGoIwooos'),
|
||||
group: t('groupNav'),
|
||||
icon: <Radar size={14} />,
|
||||
action: () => nav('/iwooos'),
|
||||
keywords: ['iwooos', 'information security', '資安網', '資安態勢'],
|
||||
keywords: [
|
||||
'iwooos',
|
||||
'information security',
|
||||
'security',
|
||||
'安全',
|
||||
'安全合規',
|
||||
'compliance',
|
||||
'合規',
|
||||
'資安網',
|
||||
'資安態勢',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
|
||||
262
docs/LOGBOOK.md
262
docs/LOGBOOK.md
@@ -1,3 +1,155 @@
|
||||
## 2026-06-01|IwoooS 首層焦點導覽
|
||||
|
||||
**背景**:
|
||||
|
||||
- 使用者持續指出 IwoooS 頁面內容太多、太像長文字清單,不符合專業資安產品常見的圖表、分流、互動式理解方式。
|
||||
- 本輪不新增資安執行能力,只降低第一屏理解成本:把長頁面壓成五個頁內焦點,讓使用者先選線索,再跳到既有看板。
|
||||
|
||||
**本次調整**:
|
||||
|
||||
- `apps/web/src/app/[locale]/iwooos/page.tsx`:
|
||||
- 新增 `IwoooSFocusDeckBoard`,放在 hero 後、資安工作地圖前。
|
||||
- 五個焦點卡:資安工作地圖、61% 解鎖路徑、全產品資安範圍、Kali 與工具鏈、GitHub / Gitea 邊界。
|
||||
- 焦點導覽依可用內容寬度自動切換單欄 / 雙欄,避免手機或窄側欄畫面把文字擠成直排。
|
||||
- 焦點卡只做頁內 anchor navigation,不是 action button,不觸發掃描、修復、部署、SSH、Kali `/execute` 或版本來源變更。
|
||||
- 替工作地圖、解鎖路徑、全產品範圍、主機工具鏈與 source-control readiness 補上穩定 anchor id。
|
||||
- `apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:
|
||||
- 新增 `iwooos.focusDeck` 繁中文案;`en.json` 維持繁中鏡像。
|
||||
- `docs/security/iwooos-posture-projection.snapshot.json` / `security-mirror-status-rollup.snapshot.json`:
|
||||
- 補上 `focus_deck_first_layer=true`、`focus_deck_item_count=5`、`focus_deck_anchor_navigation_allowed=true`、`focus_deck_execution_action_buttons_allowed=false`、`focus_deck_runtime_gate_count=0`。
|
||||
- 新增 `S2.145` 進度 ledger;headline 不增加,因為這是 UX / framework 可理解度,不是 runtime 授權。
|
||||
- `scripts/security/security-mirror-progress-guard.py`:
|
||||
- 新增 guard,鎖住焦點導覽的位置、五個 anchor、snapshot 欄位與 runtime false 邊界。
|
||||
|
||||
**進度邊界**:
|
||||
|
||||
- 整體維持 `61%`。
|
||||
- 本輪屬於第一屏 UX / 導覽 / evidence 可理解度推進;runtime gate、Kali / SSH、掃描、修復、部署按鈕、GitHub primary 切換、Gitea 停用仍維持 `false / 0`。
|
||||
|
||||
## 2026-06-01|IwoooS 命令面板資安入口收斂
|
||||
|
||||
**背景**:
|
||||
|
||||
- 使用者指出「安全合規」與 `IwoooS` 兩個入口容易被理解成兩套資安系統。
|
||||
- 主線 sidebar 已收斂成單一 `IwoooS` 資安入口,`/security-compliance` 只保留相容與既有使用者熟悉頁面;本輪補齊命令面板,避免搜尋「安全合規 / compliance」時又回到舊獨立入口。
|
||||
|
||||
**本次調整**:
|
||||
|
||||
- `apps/web/src/components/command-palette/CommandPalette.tsx`:
|
||||
- 移除命令面板中的獨立 `security` 項目。
|
||||
- 將 `security`、`安全`、`安全合規`、`compliance`、`合規` 等搜尋詞全部收斂到 `IwoooS`,導向 `/iwooos`。
|
||||
- `apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:
|
||||
- 將命令面板顯示文字調整為「前往 IwoooS 資安主控台」;`en.json` 維持繁中鏡像。
|
||||
- `docs/security/iwooos-posture-projection.snapshot.json` / `security-mirror-status-rollup.snapshot.json`:
|
||||
- 補上 `command_palette_security_action_unified_to_iwooos=true`、`command_palette_security_compliance_direct_action_allowed=false`、`command_palette_security_keywords_route_to_iwooos=true`。
|
||||
- 新增 `S2.144` 進度 ledger;headline 仍不增加,因為這是入口收斂與理解成本降低,不是 runtime 授權。
|
||||
- `scripts/security/security-mirror-progress-guard.py`:
|
||||
- 新增 guard,禁止命令面板重新出現 `nav('/security-compliance')` 或獨立 `security` 項目。
|
||||
|
||||
**進度邊界**:
|
||||
|
||||
- 整體維持 `61%`。
|
||||
- 本輪屬於 framework / UX / evidence 可理解度推進;runtime gate、Kali / SSH、掃描、修復、部署按鈕、GitHub primary 切換、Gitea 停用仍維持 `false / 0`。
|
||||
|
||||
## 2026-05-31|Alerts 焦點告警補上處理狀態卡
|
||||
|
||||
**背景**:
|
||||
|
||||
- 使用者指出 Telegram 告警雖然已能 deep link 到 Alerts 真相鏈,但前端仍要 operator 自己讀多段狀態,無法第一眼判斷「是否重複、Telegram 是否回寫、AI 是否已處置、是否需要人工、Sentry / SigNoz 是否匹配」。
|
||||
- 本輪不新增決策規則、不觸發修復、不寫入 DB;只把既有 `status-chain` DB truth-chain 以 operator 可讀的四張狀態卡呈現在 Alerts 焦點告警區。
|
||||
|
||||
**本次調整**:
|
||||
|
||||
- `apps/web/src/app/[locale]/alerts/page.tsx`:
|
||||
- 在 `FocusIncidentEvidencePanel` 新增 `告警處理狀態` 區塊。
|
||||
- 四張卡直接讀 `source_refs` / `operator_outcome` / `source correlation`:
|
||||
- `重複 / 同指紋`:顯示最新入站是否重複、同 fingerprint 關聯 Incident 數。
|
||||
- `Telegram 回寫`:顯示 channel、send status、message type、出站數與最新送出時間。
|
||||
- `AI 處置判定`:顯示 AI summary、state、next step 與是否需要人工。
|
||||
- `Sentry / SigNoz 匹配`:顯示 direct / candidate / applied 與未匹配原因。
|
||||
- `apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:
|
||||
- 補齊繁中文案;`en.json` 維持繁中鏡像,避免前端 i18n 破裂。
|
||||
|
||||
**驗證**:
|
||||
|
||||
```text
|
||||
python3 -m json.tool apps/web/messages/zh-TW.json / en.json -> pass
|
||||
cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json -> pass
|
||||
git diff --check -> pass
|
||||
pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-alerts-operator-flow-20260531-r4.tsbuildinfo -> pass
|
||||
NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 pnpm --dir apps/web run build -> pass
|
||||
```
|
||||
|
||||
**本機 / Production 瀏覽器檢查**:
|
||||
|
||||
```text
|
||||
local:
|
||||
http://127.0.0.1:3108/zh-TW/alerts?project_id=awoooi&incident_id=INC-20260530-0DD83C&_v=operator-flow-local
|
||||
hasFlow=true
|
||||
cardCount=4
|
||||
canScroll=true
|
||||
horizontalOverflow=false
|
||||
|
||||
production:
|
||||
https://awoooi.wooo.work/zh-TW/alerts?project_id=awoooi&incident_id=INC-20260530-0DD83C&_v=d40c4a9f
|
||||
hasFlow=true
|
||||
cardCount=4
|
||||
canScroll=true
|
||||
horizontalOverflow=false
|
||||
repeat_state=同指紋重複,關聯 5 筆
|
||||
telegram_state=telegram / sent,出站 3
|
||||
ai_state=已執行但驗證退化,需人工確認
|
||||
source_state=provider_fresh_no_match / provider_heartbeat_present_but_no_incident_match
|
||||
screenshot=/tmp/awoooi-alerts-operator-flow-cards-d40c4a9f.png
|
||||
```
|
||||
|
||||
**Gitea / Production deploy**:
|
||||
|
||||
```text
|
||||
a73ccffb fix(web): surface alert operator flow state
|
||||
code-review run 2343 -> success
|
||||
cd run 2342 -> cancelled by newer main push d40c4a9f (workflow cancel-in-progress)
|
||||
|
||||
d40c4a9f feat(web): add IwoooS command map
|
||||
includes a73ccffb
|
||||
code-review run 2345 -> success
|
||||
cd run 2344 -> success
|
||||
|
||||
c80aae34 chore(cd): deploy d40c4a9 [skip ci]
|
||||
|
||||
k8s latest:
|
||||
awoooi-api image = 192.168.0.110:5000/awoooi/api:d40c4a9fdb680121181812394d0b0211d5d4818f
|
||||
awoooi-web image = 192.168.0.110:5000/awoooi/web:d40c4a9fdb680121181812394d0b0211d5d4818f
|
||||
awoooi-worker image = 192.168.0.110:5000/awoooi/api:d40c4a9fdb680121181812394d0b0211d5d4818f
|
||||
awoooi-api/web/worker rollout = successfully rolled out
|
||||
|
||||
production status-chain sample:
|
||||
incident_id=INC-20260530-0DD83C
|
||||
current_stage=execution_succeeded
|
||||
stage_status=success
|
||||
verdict=auto_repaired_verification_degraded
|
||||
repair_state=executed
|
||||
verification=degraded
|
||||
needs_human=true
|
||||
next_step=manual_verify_or_repair
|
||||
source_refs inbound_total=54 outbound_total=3
|
||||
latest_outbound=telegram sent error
|
||||
related_incidents=5
|
||||
fingerprint=e4f823b8be3d604c92fc776009f09cde
|
||||
```
|
||||
|
||||
**進度**:
|
||||
|
||||
```text
|
||||
Telegram/AwoooP/frontend truth-chain visibility: 95%
|
||||
Frontend AI automation management UI: 97%
|
||||
Sentry/SigNoz per-incident visibility: 92%
|
||||
MCP / self-hosted MCP visibility: 97%
|
||||
Ansible / PlayBook visibility: 90%
|
||||
Overall AI automation flywheel: 81%
|
||||
24h full AI Agent auto-repair production claim: 0% (尚未做 24h 無人工介入驗證,不宣稱達成)
|
||||
```
|
||||
|
||||
## 2026-05-31|IwoooS 資安工作地圖首層化
|
||||
|
||||
**背景**:
|
||||
@@ -25455,3 +25607,113 @@ production browser smoke:
|
||||
- 120 仍不可達,K3s node `mon` 是 `NotReady,SchedulingDisabled`;`mon1` 可承載 AWOOI workloads,但 full cold-start done criteria 尚未達成。
|
||||
- 110 backup aggregate `failed_count=1` 是 120 config capture 無法完成;必須 120 回來後重跑 `/backup/scripts/backup-configs.sh` 或 `/backup/scripts/backup-all.sh`,再補跑 Google Drive/rclone full sync。
|
||||
- `SLO_KMGrowthRate_Low` 仍為 warning(24h KM 約 19/20),不是網站 outage,但需後續追 KM 產出。
|
||||
|
||||
## 2026-06-01 | Alerts operator handoff 與 e2e-health SignOz 鏈路復核
|
||||
|
||||
**背景**:統帥指出 Telegram 告警與前端 `Alerts` 頁面仍看不出「AI 跑到哪個階段、是否真的自動修復、何時需要人工接手」。同時 Gitea `e2e-health` 曾回報 SignOz alert-chain metric stale,容易被誤判成 Sentry / SigNoz source ingestion 失效。
|
||||
|
||||
**完成變更**:
|
||||
- `apps/web/src/app/[locale]/alerts/page.tsx` 新增 operator handoff 區塊,將 raw code 轉成可判讀狀態:
|
||||
- `verification_degraded_manual_required` → 驗證退化,需人工確認。
|
||||
- `manual_verify_or_repair` → 人工確認修復狀態;需要時重新送審修復。
|
||||
- `incident_open_after_successful_execution` → 自動執行已完成,但 Incident 仍開啟。
|
||||
- `provider_heartbeat_present_but_no_incident_match` → Sentry / SigNoz 有新鮮心跳,但沒有匹配到此 Incident。
|
||||
- `Alerts` focus incident 區塊現在明確顯示:
|
||||
- 現在要做:需要人工接手確認。
|
||||
- 入口:Work Items / Approvals / Runs。
|
||||
- 負責:SRE owner / AwoooP operator。
|
||||
- 邊界:Alerts 頁只讀追蹤,不觸發修復。
|
||||
- `apps/web/messages/zh-TW.json`、`apps/web/messages/en.json` 已同步 i18n key,未新增硬編碼內網 API。
|
||||
|
||||
**部署與驗證**:
|
||||
- Commit:`607fc291 fix(web): clarify alert operator handoff`,已推 `gitea main`。
|
||||
- Gitea:`#2350 code-review` success,`#2349 cd` success;production image 已部署到 `64747170f142cd266dc8fc17b9130608bd213346`。
|
||||
- K8s:`awoooi-web` / `awoooi-api` / `awoooi-worker` rollout 全部成功。
|
||||
- Production browser:
|
||||
- `https://awoooi.wooo.work/zh-TW/alerts?project_id=awoooi&incident_id=INC-20260530-0DD83C&_v=64747170`
|
||||
- handoff 區塊可見,Work Items / Approvals / Runs links 可見。
|
||||
- `canScroll=true`,`horizontalOverflow=false`。
|
||||
- Incident card 已顯示「驗證退化,需人工確認」「人工確認修復狀態;需要時重新送審修復」。
|
||||
- Production status chain 仍正確維持:
|
||||
- `current_stage=execution_succeeded`
|
||||
- `needs_human=True`
|
||||
- `operator_state=verification_degraded_manual_required`
|
||||
- `human_reason=incident_open_after_successful_execution`
|
||||
- `source_reason=provider_heartbeat_present_but_no_incident_match`
|
||||
|
||||
**e2e-health 復核**:
|
||||
- Live DB 直接查證:`awooop_conversation_event` 24h 內有 Alertmanager / Sentry / SignOz source evidence,且 `source_envelope->>'provider'`、`provider_event_id`、`platform_subject_id` 均可導出 provider。
|
||||
- Prometheus 已抓到 `awoooi_alert_chain_last_success_timestamp`:
|
||||
- `alertmanager=1780245603.167113`
|
||||
- `sentry=1780245232.052464`
|
||||
- `signoz=1780245231.996483`
|
||||
- 重新執行 production smoke:
|
||||
- `scripts/alert_chain_smoke_test.py --api-url https://awoooi.wooo.work --metrics-api-url http://192.168.0.125:32334 --source-provider-heartbeat --source-provider-upstream-canary --source-link-canary-target-incident-id INC-20260505-25E744 --json`
|
||||
- 結果:`PASSED — 15/15 checks passed in 0.7s`。
|
||||
- 通過項目包含 API Health、Alertmanager/Sentry/SignOz webhook、source provider heartbeat、source provider upstream canary、source-link canary、SigNoz、OTEL Collector、Event Exporter。
|
||||
|
||||
**目前整體進度(本階段完成後)**:
|
||||
- Alerts 告警詳情可判讀性:約 99.0%;已能說明目前階段、人工接手原因、入口與處理邊界。
|
||||
- Telegram / DB / AwoooP / 前端 truth-chain 可視化:約 99.9%;仍需持續把每筆告警的 matching logs 與外部 provider 原始證據做更細 drill-down。
|
||||
- Sentry / SigNoz source correlation:約 99.0%;本輪 e2e-health 15/15 通過,不能再把 SignOz ingestion 當成未證實紅燈。
|
||||
- AwoooP Runs / Approvals / Work Items 前端同步:約 99.9%;告警已能 deep-link 到三個操作入口。
|
||||
- AI Provider lane visibility:約 94.0%;既有 health 已確認順序是 GCP-A -> GCP-B -> 111 local,Gemini 仍只應作最後 fallback。
|
||||
- MCP / 自建 MCP 可視化:約 96.9%;後續仍要把實際 MCP 呼叫、工具來源與決策依據逐筆呈現在 run timeline。
|
||||
- KM governance:約 82.5%;stale KM 降到門檻前仍是治理警報,不可視為服務故障。
|
||||
- Ansible / PlayBook 自動執行:約 0% runtime-ready;仍需證據確認 `ansible_playbook_binary_missing` gate 已解除。
|
||||
- 24h 完整自動修復 production claim:0%;目前有單點流程與 smoke pass,仍不能宣稱完整全自動修復閉環達成。
|
||||
- 完整 AI 自動化管理產品化:約 99.4%;產品可視化已接近完整,但真正「AI 自動監控 -> 自動匹配 -> 自動 PlayBook -> 自動修復 -> 自動學習/KM」仍需 24h production evidence 與前端逐段證據補齊。
|
||||
|
||||
## 2026-06-01 | Run detail MCP / 自建 MCP 證據矩陣上線
|
||||
|
||||
**背景**:統帥追問「到底有沒有真正用到所有 MCP / 自建 MCP」,原本 Run detail API 已有 `mcp_gateway` / `mcp_legacy`,但前端只顯示薄摘要,無法直接看出一級 AwoooP Gateway MCP 與自建 MCP audit 的實際工具、成功 / 失敗與錯誤原因。
|
||||
|
||||
**完成變更**:
|
||||
- `apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx` 的 `McpGatewayPanel` 新增兩欄證據矩陣:
|
||||
- `AwoooP Gateway MCP`:顯示一級 MCP gateway tool、agent/scope、success/failed/blocked。
|
||||
- `自建 MCP / 舊版 Audit`:顯示自建 MCP server/tool、success/failed、最後錯誤。
|
||||
- `apps/web/messages/zh-TW.json`、`apps/web/messages/en.json` 補齊 top-level `runDetail.gateway.evidence.*` i18n key,並把 `Legacy MCP` 文案改為「自建 MCP」,避免 operator 誤以為只是舊資料、不是實際自建工具證據。
|
||||
|
||||
**驗證與部署**:
|
||||
- Local validation:
|
||||
- `python3 -m json.tool apps/web/messages/zh-TW.json`
|
||||
- `python3 -m json.tool apps/web/messages/en.json`
|
||||
- `git diff --check`
|
||||
- `pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-run-mcp-evidence-20260601.tsbuildinfo`
|
||||
- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 pnpm --dir apps/web run build`
|
||||
- Commit:`ba22e702 fix(web): expose mcp evidence on run detail`,已推 `gitea main`。
|
||||
- `ba22e702` 的 `code-review #2352` success;其 `cd #2351` 被後續 main commit 取代而取消,不是 build failure。
|
||||
- 後續 main commit `21f5142d feat(web): add IwoooS focus deck` 保含本次 MCP evidence 改動,`code-review #2354` success,`cd #2353` success。
|
||||
- Production image / rollout:
|
||||
- `awoooi-api=192.168.0.110:5000/awoooi/api:21f5142d0816a6b2bbf119c10b9c29130c1e6810`
|
||||
- `awoooi-worker=192.168.0.110:5000/awoooi/api:21f5142d0816a6b2bbf119c10b9c29130c1e6810`
|
||||
- `awoooi-web=192.168.0.110:5000/awoooi/web:21f5142d0816a6b2bbf119c10b9c29130c1e6810`
|
||||
- `kubectl rollout status deployment/awoooi-api deployment/awoooi-worker deployment/awoooi-web` 全部成功。
|
||||
- Production browser:
|
||||
- `https://awoooi.wooo.work/zh-TW/awooop/runs/d17ff68c-6459-5ad4-b0d1-408fc5d6711d?project_id=awoooi&_v=21f5142d`
|
||||
- Run detail 無 `Failed to fetch`,無 `runDetail.gateway.evidence.*` key 漏出。
|
||||
- `canScroll=true`,`horizontalOverflow=false`。
|
||||
- MCP section 顯示:
|
||||
- 自建 MCP 已觀測。
|
||||
- 自建 MCP total `100`,成功 `88`,失敗 `12`。
|
||||
- Top tool `ssh_host/ssh_diagnose`。
|
||||
- 工具列包含 `ssh_host/ssh_diagnose`、`ssh_host/ssh_get_container_logs`、`ssh_host/ssh_get_container_status`、`ssh_host/ssh_get_top_processes`。
|
||||
- `AwoooP Gateway MCP` 目前未觀測,表示此 Run 是透過 Incident 串回 legacy/self-built MCP audit,不是一級 gateway call。
|
||||
|
||||
**新揭露技術債**:
|
||||
- 自建 MCP 不是「沒用」;本例已確認 100 筆 audit,但仍有 12 筆失敗。
|
||||
- 失敗原因包含:
|
||||
- `Host 'wooo' not in SSH_MCP_ALLOWED_HOSTS`
|
||||
- `'filter_name'`
|
||||
- 下一步應先盤點 `SSH_MCP_ALLOWED_HOSTS` 與 `ssh_get_container_status` 參數契約,再決定是否調整 allowlist / adapter,不可直接放寬 SSH scope。
|
||||
|
||||
**目前整體進度(本階段完成後)**:
|
||||
- MCP / 自建 MCP 可視化:約 99.2%;Run detail 已能直接看出一級 gateway vs 自建 audit 的來源、工具、成功 / 失敗與錯誤。
|
||||
- MCP 真實使用判讀:約 97.5%;本例已證明自建 MCP 有被使用,但還有 allowlist / adapter 失敗要修。
|
||||
- AwoooP Runs / Approvals / Work Items 前端同步:約 99.95%;Run detail 已能把 Incident、MCP、補救試跑、來源卷宗與 status chain 放在同一頁。
|
||||
- Telegram / DB / AwoooP / 前端 truth-chain 可視化:約 99.95%;仍需逐步把每筆 Telegram callback 與外部 provider 原始 log 做更深的 drill-down。
|
||||
- Sentry / SigNoz source correlation:約 99.0%;e2e-health 已通過,但每筆 Incident 的 source-link 人工審核仍需持續收斂。
|
||||
- KM governance:約 82.5%;stale KM 仍需 Hermes 主責產草稿、owner 審核後寫入。
|
||||
- Ansible / PlayBook 自動執行:約 0% runtime-ready;`ansible_playbook_binary_missing` gate 未解除前不可宣稱可自動執行。
|
||||
- 24h 完整自動修復 production claim:0%;目前可視化與單點 smoke 已補強,但完整自動修復閉環仍需 24h production evidence。
|
||||
- 完整 AI 自動化管理產品化:約 99.45%;可視化接近完整,下一段應處理 MCP allowlist / adapter 失敗與 Ansible runtime gate。
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
"apps/web/src/app/[locale]/governance/page.tsx",
|
||||
"apps/web/src/app/[locale]/alert-operation-logs/page.tsx",
|
||||
"apps/web/src/app/[locale]/awooop/approvals/page.tsx",
|
||||
"apps/web/src/app/[locale]/code-review/page.tsx"
|
||||
"apps/web/src/app/[locale]/code-review/page.tsx",
|
||||
"apps/web/src/components/command-palette/CommandPalette.tsx"
|
||||
],
|
||||
"summary": {
|
||||
"route_path": "/iwooos",
|
||||
"nav_entry_added": true,
|
||||
"command_palette_entry_added": true,
|
||||
"command_palette_security_action_unified_to_iwooos": true,
|
||||
"command_palette_security_compliance_direct_action_allowed": false,
|
||||
"command_palette_security_keywords_route_to_iwooos": true,
|
||||
"contract_count": 36,
|
||||
"active_runtime_gate_count": 0,
|
||||
"approval_queue_total": 8,
|
||||
@@ -56,6 +60,12 @@
|
||||
"command_map_navigation_controls_allowed": true,
|
||||
"command_map_execution_action_buttons_allowed": false,
|
||||
"command_map_runtime_gate_count": 0,
|
||||
"focus_deck_first_layer": true,
|
||||
"focus_deck_item_count": 5,
|
||||
"focus_deck_anchor_navigation_allowed": true,
|
||||
"focus_deck_execution_action_buttons_allowed": false,
|
||||
"focus_deck_runtime_gate_count": 0,
|
||||
"focus_deck_above_command_map": true,
|
||||
"long_form_sections_default_collapsed": true,
|
||||
"owner_response_validation_received_count": 0,
|
||||
"owner_response_validation_accepted_count": 0,
|
||||
@@ -295,6 +305,65 @@
|
||||
"not_authorization": true
|
||||
}
|
||||
],
|
||||
"focus_deck_items": [
|
||||
{
|
||||
"item_id": "workMap",
|
||||
"display_order": 1,
|
||||
"target_anchor": "#iwooos-command-map-board",
|
||||
"display_mode": "first_layer_focus_deck",
|
||||
"anchor_navigation_allowed": true,
|
||||
"execution_action_button_allowed": false,
|
||||
"runtime_gate_opened": false,
|
||||
"runtime_execution_authorized": false,
|
||||
"not_authorization": true
|
||||
},
|
||||
{
|
||||
"item_id": "unlockPath",
|
||||
"display_order": 2,
|
||||
"target_anchor": "#iwooos-first-progress-unlock-path-board",
|
||||
"display_mode": "first_layer_focus_deck",
|
||||
"anchor_navigation_allowed": true,
|
||||
"execution_action_button_allowed": false,
|
||||
"runtime_gate_opened": false,
|
||||
"runtime_execution_authorized": false,
|
||||
"not_authorization": true
|
||||
},
|
||||
{
|
||||
"item_id": "productScope",
|
||||
"display_order": 3,
|
||||
"target_anchor": "#iwooos-global-security-mesh-matrix-board",
|
||||
"display_mode": "first_layer_focus_deck",
|
||||
"anchor_navigation_allowed": true,
|
||||
"execution_action_button_allowed": false,
|
||||
"runtime_gate_opened": false,
|
||||
"runtime_execution_authorized": false,
|
||||
"not_authorization": true
|
||||
},
|
||||
{
|
||||
"item_id": "hostTools",
|
||||
"display_order": 4,
|
||||
"target_anchor": "#iwooos-host-tool-evidence-chain-board",
|
||||
"display_mode": "first_layer_focus_deck",
|
||||
"anchor_navigation_allowed": true,
|
||||
"execution_action_button_allowed": false,
|
||||
"runtime_gate_opened": false,
|
||||
"runtime_execution_authorized": false,
|
||||
"not_authorization": true
|
||||
},
|
||||
{
|
||||
"item_id": "sourceControl",
|
||||
"display_order": 5,
|
||||
"target_anchor": "#iwooos-command-map-source-control",
|
||||
"display_mode": "first_layer_focus_deck",
|
||||
"anchor_navigation_allowed": true,
|
||||
"execution_action_button_allowed": false,
|
||||
"runtime_gate_opened": false,
|
||||
"runtime_execution_authorized": false,
|
||||
"source_control_mutation_authorized": false,
|
||||
"github_primary_switch_authorized": false,
|
||||
"not_authorization": true
|
||||
}
|
||||
],
|
||||
"posture_pillars": [
|
||||
{
|
||||
"pillar_id": "exposure_posture",
|
||||
|
||||
@@ -2197,6 +2197,30 @@
|
||||
"runtime_delta": false,
|
||||
"execution_authorized": false,
|
||||
"not_authorization": true
|
||||
},
|
||||
{
|
||||
"delta_id": "s2_144_iwooos_command_palette_security_entry_unified",
|
||||
"display_order": 173,
|
||||
"completed_stage": "S2.144 IwoooS 命令面板資安入口收斂",
|
||||
"progress_axis": "framework_detail",
|
||||
"headline_percent_delta": 0,
|
||||
"framework_delta_visible": true,
|
||||
"why_headline_unchanged": "IwoooS 只把命令面板中的 security / 安全 / 安全合規 / compliance / 合規 等搜尋詞統一導向 /iwooos,移除命令面板對 /security-compliance 的獨立直達動作;command_palette_security_action_unified_to_iwooos=true、command_palette_security_compliance_direct_action_allowed=false、command_palette_security_keywords_route_to_iwooos=true、runtime_execution_authorized=false、active_runtime_gate_count=0,不把入口收斂當 runtime 授權、審批、掃描、修復、部署、主機更新、GitHub primary 切換或 Gitea 停用。",
|
||||
"runtime_delta": false,
|
||||
"execution_authorized": false,
|
||||
"not_authorization": true
|
||||
},
|
||||
{
|
||||
"delta_id": "s2_145_iwooos_first_layer_focus_deck",
|
||||
"display_order": 174,
|
||||
"completed_stage": "S2.145 IwoooS 首層焦點導覽",
|
||||
"progress_axis": "framework_detail",
|
||||
"headline_percent_delta": 0,
|
||||
"framework_delta_visible": true,
|
||||
"why_headline_unchanged": "IwoooS 只新增首層焦點導覽,把工作地圖、61% 解鎖路徑、全產品範圍、Kali / 主機工具鏈與 GitHub / Gitea 版本來源五個焦點壓成頁內 anchor navigation;iwooos_focus_deck_item_count=5、iwooos_focus_deck_anchor_navigation_allowed=true、iwooos_focus_deck_execution_action_buttons_allowed=false、iwooos_focus_deck_runtime_gate_count=0、iwooos_focus_deck_above_command_map=true、runtime_execution_authorized=false、active_runtime_gate_count=0,不把導覽卡視為掃描、修復、部署、主機更新、source-control mutation、GitHub primary 切換、Gitea 停用或 runtime gate。",
|
||||
"runtime_delta": false,
|
||||
"execution_authorized": false,
|
||||
"not_authorization": true
|
||||
}
|
||||
],
|
||||
"next_safe_actions": [
|
||||
|
||||
@@ -41,7 +41,7 @@ resources:
|
||||
images:
|
||||
- name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER
|
||||
newName: 192.168.0.110:5000/awoooi/api
|
||||
newTag: d40c4a9fdb680121181812394d0b0211d5d4818f
|
||||
newTag: 87378b452d8635b12ec23e33c95bfbedccc3de00
|
||||
- name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER
|
||||
newName: 192.168.0.110:5000/awoooi/web
|
||||
newTag: d40c4a9fdb680121181812394d0b0211d5d4818f
|
||||
newTag: 87378b452d8635b12ec23e33c95bfbedccc3de00
|
||||
|
||||
@@ -163,6 +163,9 @@ def validate(root: Path) -> None:
|
||||
sidebar = (root / "apps" / "web" / "src" / "components" / "layout" / "sidebar.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
command_palette = (
|
||||
root / "apps" / "web" / "src" / "components" / "command-palette" / "CommandPalette.tsx"
|
||||
).read_text(encoding="utf-8")
|
||||
web_messages_zh = load_json(root / "apps" / "web" / "messages" / "zh-TW.json")
|
||||
web_messages_en = load_json(root / "apps" / "web" / "messages" / "en.json")
|
||||
|
||||
@@ -219,6 +222,11 @@ def validate(root: Path) -> None:
|
||||
assert_text_not_contains("sidebar.iwooos_security_duplicate_label", sidebar, "labelKey: 'iwooosSecurityCompliance'")
|
||||
assert_text_contains("sidebar.security_compliance_alias", sidebar, "aliases: ['/security-compliance']")
|
||||
assert_text_not_contains("sidebar.duplicate_security_compliance_entry", sidebar, "id: 'security-compliance'")
|
||||
assert_text_contains("command_palette.iwooos_entry", command_palette, "id: 'iwooos'")
|
||||
assert_text_contains("command_palette.iwooos_route", command_palette, "nav('/iwooos')")
|
||||
assert_text_contains("command_palette.security_keyword", command_palette, "'安全合規'")
|
||||
assert_text_not_contains("command_palette.legacy_security_entry", command_palette, "id: 'security'")
|
||||
assert_text_not_contains("command_palette.legacy_security_compliance_route", command_palette, "nav('/security-compliance')")
|
||||
assert_equal(
|
||||
"web_messages.zh-TW.nav.iwooos",
|
||||
web_messages_zh["nav"]["iwooos"],
|
||||
@@ -600,6 +608,8 @@ def validate(root: Path) -> None:
|
||||
"s2_141_iwooos_all_product_coverage_snapshot",
|
||||
"s2_142_iwooos_first_unlock_path_first_layer",
|
||||
"s2_143_iwooos_command_map_first_layer",
|
||||
"s2_144_iwooos_command_palette_security_entry_unified",
|
||||
"s2_145_iwooos_first_layer_focus_deck",
|
||||
]
|
||||
assert_equal(
|
||||
"progress_delta_ledger.delta_ids",
|
||||
@@ -1127,13 +1137,14 @@ def validate(root: Path) -> None:
|
||||
]:
|
||||
assert_text_not_contains("web_messages.zh-TW.awooop.runs_wording", zh_awooop_runs_text, forbidden)
|
||||
|
||||
zh_awooop_run_detail_text = json.dumps(
|
||||
{
|
||||
"incidentEvidence": web_messages_zh["awooop"]["incidentEvidence"],
|
||||
"runDetail": web_messages_zh["awooop"]["runDetail"],
|
||||
"approvalDecision": web_messages_zh["awooop"]["approvalDecision"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
zh_awooop_run_detail_text = "\n".join(
|
||||
collect_string_values(
|
||||
{
|
||||
"incidentEvidence": web_messages_zh["awooop"]["incidentEvidence"],
|
||||
"runDetail": web_messages_zh["awooop"]["runDetail"],
|
||||
"approvalDecision": web_messages_zh["awooop"]["approvalDecision"],
|
||||
}
|
||||
)
|
||||
)
|
||||
for forbidden in [
|
||||
"Trace ID",
|
||||
@@ -1369,6 +1380,18 @@ def validate(root: Path) -> None:
|
||||
"iwooos_projection.summary.command_palette_entry_added",
|
||||
iwooos_projection["summary"]["command_palette_entry_added"],
|
||||
)
|
||||
assert_true(
|
||||
"iwooos_projection.summary.command_palette_security_action_unified_to_iwooos",
|
||||
iwooos_projection["summary"]["command_palette_security_action_unified_to_iwooos"],
|
||||
)
|
||||
assert_false(
|
||||
"iwooos_projection.summary.command_palette_security_compliance_direct_action_allowed",
|
||||
iwooos_projection["summary"]["command_palette_security_compliance_direct_action_allowed"],
|
||||
)
|
||||
assert_true(
|
||||
"iwooos_projection.summary.command_palette_security_keywords_route_to_iwooos",
|
||||
iwooos_projection["summary"]["command_palette_security_keywords_route_to_iwooos"],
|
||||
)
|
||||
assert_equal("iwooos_projection.summary.contract_count", iwooos_projection["summary"]["contract_count"], manifest_count)
|
||||
assert_equal(
|
||||
"iwooos_projection.summary.active_runtime_gate_count",
|
||||
@@ -1491,6 +1514,32 @@ def validate(root: Path) -> None:
|
||||
iwooos_projection["summary"]["command_map_runtime_gate_count"],
|
||||
0,
|
||||
)
|
||||
assert_true(
|
||||
"iwooos_projection.summary.focus_deck_first_layer",
|
||||
iwooos_projection["summary"]["focus_deck_first_layer"],
|
||||
)
|
||||
assert_equal(
|
||||
"iwooos_projection.summary.focus_deck_item_count",
|
||||
iwooos_projection["summary"]["focus_deck_item_count"],
|
||||
5,
|
||||
)
|
||||
assert_true(
|
||||
"iwooos_projection.summary.focus_deck_anchor_navigation_allowed",
|
||||
iwooos_projection["summary"]["focus_deck_anchor_navigation_allowed"],
|
||||
)
|
||||
assert_false(
|
||||
"iwooos_projection.summary.focus_deck_execution_action_buttons_allowed",
|
||||
iwooos_projection["summary"]["focus_deck_execution_action_buttons_allowed"],
|
||||
)
|
||||
assert_equal(
|
||||
"iwooos_projection.summary.focus_deck_runtime_gate_count",
|
||||
iwooos_projection["summary"]["focus_deck_runtime_gate_count"],
|
||||
0,
|
||||
)
|
||||
assert_true(
|
||||
"iwooos_projection.summary.focus_deck_above_command_map",
|
||||
iwooos_projection["summary"]["focus_deck_above_command_map"],
|
||||
)
|
||||
assert_true(
|
||||
"iwooos_projection.summary.all_product_coverage_snapshot_detail_ledger_collapsed",
|
||||
iwooos_projection["summary"]["all_product_coverage_snapshot_detail_ledger_collapsed"],
|
||||
@@ -11764,6 +11813,50 @@ def validate(root: Path) -> None:
|
||||
f"iwooos_projection.first_progress_unlock_path_steps.{item['step_id']}.not_authorization",
|
||||
item["not_authorization"],
|
||||
)
|
||||
assert_text_contains(
|
||||
"iwooos_page.focus_deck_testid",
|
||||
iwooos_projection_page,
|
||||
'data-testid="iwooos-focus-deck-board"',
|
||||
)
|
||||
assert_text_contains(
|
||||
"iwooos_page.focus_deck_component",
|
||||
iwooos_projection_page,
|
||||
"IwoooSFocusDeckBoard",
|
||||
)
|
||||
assert_text_before(
|
||||
"iwooos_page.focus_deck_before_command_map",
|
||||
iwooos_projection_page,
|
||||
"<IwoooSFocusDeckBoard />",
|
||||
"<IwoooSCommandMapBoard />",
|
||||
)
|
||||
assert_text_contains(
|
||||
"iwooos_page.focus_deck_boundaries",
|
||||
iwooos_projection_page,
|
||||
'data-testid="iwooos-focus-deck-boundaries"',
|
||||
)
|
||||
for text in [
|
||||
"href: '#iwooos-command-map-board'",
|
||||
"href: '#iwooos-first-progress-unlock-path-board'",
|
||||
"href: '#iwooos-global-security-mesh-matrix-board'",
|
||||
"href: '#iwooos-host-tool-evidence-chain-board'",
|
||||
"href: '#iwooos-command-map-source-control'",
|
||||
"iwooos-command-map-source-control",
|
||||
"iwooos_focus_deck_first_layer=true",
|
||||
"iwooos_focus_deck_item_count=5",
|
||||
"iwooos_focus_deck_anchor_navigation_allowed=true",
|
||||
"iwooos_focus_deck_execution_action_buttons_allowed=false",
|
||||
"iwooos_focus_deck_runtime_gate_count=0",
|
||||
"iwooos_focus_deck_above_command_map=true",
|
||||
"runtime_execution_authorized=false",
|
||||
"active_runtime_gate_count=0",
|
||||
"action_buttons_allowed=false",
|
||||
"not_authorization=true",
|
||||
]:
|
||||
assert_text_contains(
|
||||
"iwooos_page.focus_deck_boundary",
|
||||
iwooos_projection_page,
|
||||
text,
|
||||
)
|
||||
assert_text_contains(
|
||||
"iwooos_page.command_map_testid",
|
||||
iwooos_projection_page,
|
||||
@@ -11875,6 +11968,59 @@ def validate(root: Path) -> None:
|
||||
"iwooos_projection.command_map_items.boundary.host_mutation_authorized",
|
||||
boundary_item["host_mutation_authorized"],
|
||||
)
|
||||
expected_focus_deck_item_ids = [
|
||||
"workMap",
|
||||
"unlockPath",
|
||||
"productScope",
|
||||
"hostTools",
|
||||
"sourceControl",
|
||||
]
|
||||
focus_deck_items = iwooos_projection["focus_deck_items"]
|
||||
assert_equal(
|
||||
"iwooos_projection.focus_deck_items.ids",
|
||||
[item["item_id"] for item in focus_deck_items],
|
||||
expected_focus_deck_item_ids,
|
||||
)
|
||||
assert_equal(
|
||||
"iwooos_projection.focus_deck_items.display_order",
|
||||
[item["display_order"] for item in focus_deck_items],
|
||||
list(range(1, len(expected_focus_deck_item_ids) + 1)),
|
||||
)
|
||||
for item in focus_deck_items:
|
||||
assert_equal(
|
||||
f"iwooos_projection.focus_deck_items.{item['item_id']}.display_mode",
|
||||
item["display_mode"],
|
||||
"first_layer_focus_deck",
|
||||
)
|
||||
assert_true(
|
||||
f"iwooos_projection.focus_deck_items.{item['item_id']}.anchor_navigation_allowed",
|
||||
item["anchor_navigation_allowed"],
|
||||
)
|
||||
assert_false(
|
||||
f"iwooos_projection.focus_deck_items.{item['item_id']}.execution_action_button_allowed",
|
||||
item["execution_action_button_allowed"],
|
||||
)
|
||||
assert_false(
|
||||
f"iwooos_projection.focus_deck_items.{item['item_id']}.runtime_gate_opened",
|
||||
item["runtime_gate_opened"],
|
||||
)
|
||||
assert_false(
|
||||
f"iwooos_projection.focus_deck_items.{item['item_id']}.runtime_execution_authorized",
|
||||
item["runtime_execution_authorized"],
|
||||
)
|
||||
assert_true(
|
||||
f"iwooos_projection.focus_deck_items.{item['item_id']}.not_authorization",
|
||||
item["not_authorization"],
|
||||
)
|
||||
source_control_focus = next(item for item in focus_deck_items if item["item_id"] == "sourceControl")
|
||||
assert_false(
|
||||
"iwooos_projection.focus_deck_items.sourceControl.source_control_mutation_authorized",
|
||||
source_control_focus["source_control_mutation_authorized"],
|
||||
)
|
||||
assert_false(
|
||||
"iwooos_projection.focus_deck_items.sourceControl.github_primary_switch_authorized",
|
||||
source_control_focus["github_primary_switch_authorized"],
|
||||
)
|
||||
for text in [
|
||||
"iwooos_first_unlock_path_step_count=5",
|
||||
"iwooos_first_unlock_path_current_focus=s4_9_owner_response",
|
||||
@@ -12003,6 +12149,60 @@ def validate(root: Path) -> None:
|
||||
list(web_messages_en["iwooos"]["commandMap"]["items"][key].keys()),
|
||||
field,
|
||||
)
|
||||
assert_contains(
|
||||
"web_messages.zh-TW.iwooos.focusDeck",
|
||||
list(web_messages_zh["iwooos"].keys()),
|
||||
"focusDeck",
|
||||
)
|
||||
assert_contains(
|
||||
"web_messages.en.iwooos.focusDeck",
|
||||
list(web_messages_en["iwooos"].keys()),
|
||||
"focusDeck",
|
||||
)
|
||||
for key in ["eyebrow", "title", "subtitle", "summary", "items", "boundaryTitle"]:
|
||||
assert_contains(
|
||||
"web_messages.zh-TW.iwooos.focusDeck.keys",
|
||||
list(web_messages_zh["iwooos"]["focusDeck"].keys()),
|
||||
key,
|
||||
)
|
||||
assert_contains(
|
||||
"web_messages.en.iwooos.focusDeck.keys",
|
||||
list(web_messages_en["iwooos"]["focusDeck"].keys()),
|
||||
key,
|
||||
)
|
||||
for key in ["items", "runtime", "mode"]:
|
||||
assert_contains(
|
||||
"web_messages.zh-TW.iwooos.focusDeck.summary",
|
||||
list(web_messages_zh["iwooos"]["focusDeck"]["summary"].keys()),
|
||||
key,
|
||||
)
|
||||
assert_contains(
|
||||
"web_messages.en.iwooos.focusDeck.summary",
|
||||
list(web_messages_en["iwooos"]["focusDeck"]["summary"].keys()),
|
||||
key,
|
||||
)
|
||||
for key in expected_focus_deck_item_ids:
|
||||
assert_contains(
|
||||
"web_messages.zh-TW.iwooos.focusDeck.items",
|
||||
list(web_messages_zh["iwooos"]["focusDeck"]["items"].keys()),
|
||||
key,
|
||||
)
|
||||
assert_contains(
|
||||
"web_messages.en.iwooos.focusDeck.items",
|
||||
list(web_messages_en["iwooos"]["focusDeck"]["items"].keys()),
|
||||
key,
|
||||
)
|
||||
for field in ["label", "title", "body"]:
|
||||
assert_contains(
|
||||
f"web_messages.zh-TW.iwooos.focusDeck.items.{key}",
|
||||
list(web_messages_zh["iwooos"]["focusDeck"]["items"][key].keys()),
|
||||
field,
|
||||
)
|
||||
assert_contains(
|
||||
f"web_messages.en.iwooos.focusDeck.items.{key}",
|
||||
list(web_messages_en["iwooos"]["focusDeck"]["items"][key].keys()),
|
||||
field,
|
||||
)
|
||||
assert_text_contains(
|
||||
"iwooos_page.first_unlock_evidence_packet_testid",
|
||||
iwooos_projection_page,
|
||||
|
||||
Reference in New Issue
Block a user