Compare commits

..

13 Commits

Author SHA1 Message Date
AWOOOI CD
e6f2d1d07c chore(cd): deploy 87378b4 [skip ci] 2026-06-01 01:18:25 +08:00
Your Name
87378b452d fix(api): normalize ssh mcp evidence inputs
All checks were successful
CD Pipeline / tests (push) Successful in 1m29s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m31s
CD Pipeline / post-deploy-checks (push) Successful in 1m40s
2026-06-01 01:11:26 +08:00
Your Name
b83f9c5a52 fix(web): make IwoooS focus deck responsive
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Code Review / ai-code-review (push) Has been cancelled
2026-06-01 01:09:41 +08:00
Your Name
8a3ddb8249 docs: record mcp evidence matrix rollout 2026-06-01 01:06:41 +08:00
AWOOOI CD
5077d4d02e chore(cd): deploy 21f5142 [skip ci] 2026-06-01 01:02:59 +08:00
Your Name
21f5142d08 feat(web): add IwoooS focus deck
All checks were successful
CD Pipeline / tests (push) Successful in 1m32s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m51s
CD Pipeline / post-deploy-checks (push) Successful in 2m11s
2026-06-01 00:54:58 +08:00
Your Name
ba22e70266 fix(web): expose mcp evidence on run detail
Some checks failed
CD Pipeline / tests (push) Successful in 1m36s
Code Review / ai-code-review (push) Successful in 18s
CD Pipeline / build-and-deploy (push) Has started running
CD Pipeline / post-deploy-checks (push) Has been cancelled
2026-06-01 00:52:19 +08:00
Your Name
9ccc447f81 docs: record alerts handoff e2e verification 2026-06-01 00:42:36 +08:00
AWOOOI CD
722875135b chore(cd): deploy 6474717 [skip ci] 2026-06-01 00:28:44 +08:00
Your Name
64747170f1 fix(web): unify IwoooS security entry
All checks were successful
CD Pipeline / tests (push) Successful in 1m35s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 4m2s
CD Pipeline / post-deploy-checks (push) Successful in 2m29s
2026-06-01 00:21:11 +08:00
AWOOOI CD
58c009c2c7 chore(cd): deploy 607fc29 [skip ci] 2026-06-01 00:20:07 +08:00
Your Name
607fc291e9 fix(web): clarify alert operator handoff
Some checks failed
CD Pipeline / tests (push) Successful in 1m33s
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / build-and-deploy (push) Successful in 4m6s
CD Pipeline / post-deploy-checks (push) Has been cancelled
2026-06-01 00:14:43 +08:00
Your Name
2860bd2b4b docs(logbook): record alerts operator flow rollout [skip ci] 2026-06-01 00:02:06 +08:00
16 changed files with 1256 additions and 44 deletions

View File

@@ -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":

View File

@@ -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,
)

View File

@@ -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",
}

View File

@@ -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
# ─────────────────────────────────────────────────────────────────────────────

View File

@@ -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"

View File

@@ -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": "先看圖,再展開證據",

View File

@@ -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": "先看圖,再展開證據",

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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 }}>

View File

@@ -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',

View File

@@ -1,3 +1,155 @@
## 2026-06-01IwoooS 首層焦點導覽
**背景**
- 使用者持續指出 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` 進度 ledgerheadline 不增加,因為這是 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-01IwoooS 命令面板資安入口收斂
**背景**
- 使用者指出「安全合規」與 `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` 進度 ledgerheadline 仍不增加,因為這是入口收斂與理解成本降低,不是 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-31Alerts 焦點告警補上處理狀態卡
**背景**
- 使用者指出 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-31IwoooS 資安工作地圖首層化
**背景**
@@ -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` 仍為 warning24h 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` successproduction 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 localGemini 仍只應作最後 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 claim0%;目前有單點流程與 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 claim0%;目前可視化與單點 smoke 已補強,但完整自動修復閉環仍需 24h production evidence。
- 完整 AI 自動化管理產品化:約 99.45%;可視化接近完整,下一段應處理 MCP allowlist / adapter 失敗與 Ansible runtime gate。

View File

@@ -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",

View File

@@ -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 navigationiwooos_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": [

View File

@@ -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

View File

@@ -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,