feat(web): add IwoooS topology atlas
This commit is contained in:
@@ -5447,6 +5447,113 @@
|
||||
"boundaryTitle": "視覺模型邊界",
|
||||
"boundaryIntro": "這張圖是首屏理解模型,不是操作面板;所有高風險動作仍需人工批准與後續 runtime gate。"
|
||||
},
|
||||
"topologyAtlas": {
|
||||
"eyebrow": "專業架構與拓樸圖譜",
|
||||
"title": "用圖譜看攻擊面、資產關係與證據流",
|
||||
"subtitle": "把主流資安產品常見的 graph、attack path、blast radius 與 evidence lane 濃縮成四個可切換視角;少文字、多圖表,仍維持 Gate 0。",
|
||||
"tabsLabel": "架構拓樸圖譜視角",
|
||||
"mapLabel": "圖譜視角",
|
||||
"panelLabels": {
|
||||
"evidence": "目前可見",
|
||||
"next": "下一步",
|
||||
"locked": "仍鎖住"
|
||||
},
|
||||
"lenses": {
|
||||
"architecture": {
|
||||
"title": "架構分層",
|
||||
"mapTitle": "Code → Asset → Host → Evidence → Gate",
|
||||
"detail": "用五層結構看 IwoooS:產品與網站、版本來源、Kali / 開發主機、監控與 AwoooP、最後才是執行閘。",
|
||||
"evidence": "8 類產品 / 網站、3 台主機、6 條工具鏈已進入同一張只讀圖譜。",
|
||||
"next": "把 S4.9 owner response 與脫敏證據接成可驗證節點。",
|
||||
"locked": "架構圖不是 runtime 授權,不代表可以掃描或修復。"
|
||||
},
|
||||
"topology": {
|
||||
"title": "主機拓樸",
|
||||
"mapTitle": "112 / 111 / 168 Observe-only Fabric",
|
||||
"detail": "把 Kali 112、開發主機 111 / 168、監控工具與 AwoooP 真相鏈放在同一張拓樸圖,而不是分散在長文件裡。",
|
||||
"evidence": "目前只呈現觀測窗口、證據位置與人工 gate,沒有執行 SSH、掃描或主機設定變更。",
|
||||
"next": "等 runtime gate 與掃描範圍批准後,才把 read-only evidence 轉入受控探測。",
|
||||
"locked": "host_change_authorized=false,scan_authorized=false。"
|
||||
},
|
||||
"attackSurface": {
|
||||
"title": "攻擊面路徑",
|
||||
"mapTitle": "External Surface → Source Control → Runtime Boundary",
|
||||
"detail": "把公開入口、產品、版本來源與 Gate 0 串成攻擊面圖;目標是優先看清可被利用的關聯,不先提高限制。",
|
||||
"evidence": "目前可看見 8 類資產與 S4.9 版本來源卡點,但 blast radius 仍維持 0,避免誤導成已完成攻防驗證。",
|
||||
"next": "先完成 GitHub primary / Gitea owner evidence,再讓風險路徑有可信來源。",
|
||||
"locked": "source_control_mutation_authorized=false,GitHub primary switch 未授權。"
|
||||
},
|
||||
"evidenceFlow": {
|
||||
"title": "證據流",
|
||||
"mapTitle": "Monitoring → AwoooP Truth Chain → Human Gate",
|
||||
"detail": "用 evidence lane 表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。",
|
||||
"evidence": "AwoooP 跨 Session 狀態已接線,IwoooS 只讀鏡像與 progress guard 已有證據。",
|
||||
"next": "補 reviewer 接受紀錄與 owner decision record,才可能進入下一個 runtime gate。",
|
||||
"locked": "active_runtime_gate_count=0,沒有任何自動執行按鈕。"
|
||||
}
|
||||
},
|
||||
"nodes": {
|
||||
"productSurface": {
|
||||
"title": "產品 / 網站"
|
||||
},
|
||||
"sourceControl": {
|
||||
"title": "版本來源"
|
||||
},
|
||||
"kali": {
|
||||
"title": "Kali 112"
|
||||
},
|
||||
"devHosts": {
|
||||
"title": "開發主機"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "監控 / 工具"
|
||||
},
|
||||
"awooopTruth": {
|
||||
"title": "AwoooP 真相鏈"
|
||||
},
|
||||
"runtimeGate": {
|
||||
"title": "Gate 0"
|
||||
}
|
||||
},
|
||||
"layers": {
|
||||
"externalSurface": {
|
||||
"title": "外部資產面",
|
||||
"body": "公開網站、產品入口與 VibeWork 先進入可理解範圍。"
|
||||
},
|
||||
"codeSupply": {
|
||||
"title": "版本來源面",
|
||||
"body": "GitHub primary / Gitea 遷移仍等 S4.9 證據。"
|
||||
},
|
||||
"hostFabric": {
|
||||
"title": "主機拓樸面",
|
||||
"body": "112、111、168 維持 observe-only 顯示。"
|
||||
},
|
||||
"evidenceOps": {
|
||||
"title": "證據營運面",
|
||||
"body": "監控、KM、MCP、Ansible 與 AwoooP 對齊。"
|
||||
},
|
||||
"gateControl": {
|
||||
"title": "執行閘面",
|
||||
"body": "runtime gate、掃描與修復仍全部鎖住。"
|
||||
}
|
||||
},
|
||||
"charts": {
|
||||
"contextDepth": {
|
||||
"label": "關聯深度",
|
||||
"body": "已把 code-to-runtime 的理解路徑壓成四段。"
|
||||
},
|
||||
"blastRadius": {
|
||||
"label": "爆炸半徑",
|
||||
"body": "未授權探測前維持 0,不誤導成已驗證攻擊半徑。"
|
||||
},
|
||||
"evidenceFreshness": {
|
||||
"label": "證據新鮮度",
|
||||
"body": "目前真正卡點仍是 S4.9 owner evidence。"
|
||||
}
|
||||
},
|
||||
"boundaryTitle": "圖譜邊界",
|
||||
"boundaryIntro": "這張圖是專業可視化與理解層,不是掃描拓樸、不是自動修復、不是 GitHub / Gitea 切換授權。"
|
||||
},
|
||||
"gateRadar": {
|
||||
"eyebrow": "執行閘雷達",
|
||||
"title": "一眼看懂哪裡可前進、哪裡不能碰",
|
||||
|
||||
@@ -5447,6 +5447,113 @@
|
||||
"boundaryTitle": "視覺模型邊界",
|
||||
"boundaryIntro": "這張圖是首屏理解模型,不是操作面板;所有高風險動作仍需人工批准與後續 runtime gate。"
|
||||
},
|
||||
"topologyAtlas": {
|
||||
"eyebrow": "專業架構與拓樸圖譜",
|
||||
"title": "用圖譜看攻擊面、資產關係與證據流",
|
||||
"subtitle": "把主流資安產品常見的 graph、attack path、blast radius 與 evidence lane 濃縮成四個可切換視角;少文字、多圖表,仍維持 Gate 0。",
|
||||
"tabsLabel": "架構拓樸圖譜視角",
|
||||
"mapLabel": "圖譜視角",
|
||||
"panelLabels": {
|
||||
"evidence": "目前可見",
|
||||
"next": "下一步",
|
||||
"locked": "仍鎖住"
|
||||
},
|
||||
"lenses": {
|
||||
"architecture": {
|
||||
"title": "架構分層",
|
||||
"mapTitle": "Code → Asset → Host → Evidence → Gate",
|
||||
"detail": "用五層結構看 IwoooS:產品與網站、版本來源、Kali / 開發主機、監控與 AwoooP、最後才是執行閘。",
|
||||
"evidence": "8 類產品 / 網站、3 台主機、6 條工具鏈已進入同一張只讀圖譜。",
|
||||
"next": "把 S4.9 owner response 與脫敏證據接成可驗證節點。",
|
||||
"locked": "架構圖不是 runtime 授權,不代表可以掃描或修復。"
|
||||
},
|
||||
"topology": {
|
||||
"title": "主機拓樸",
|
||||
"mapTitle": "112 / 111 / 168 Observe-only Fabric",
|
||||
"detail": "把 Kali 112、開發主機 111 / 168、監控工具與 AwoooP 真相鏈放在同一張拓樸圖,而不是分散在長文件裡。",
|
||||
"evidence": "目前只呈現觀測窗口、證據位置與人工 gate,沒有執行 SSH、掃描或主機設定變更。",
|
||||
"next": "等 runtime gate 與掃描範圍批准後,才把 read-only evidence 轉入受控探測。",
|
||||
"locked": "host_change_authorized=false,scan_authorized=false。"
|
||||
},
|
||||
"attackSurface": {
|
||||
"title": "攻擊面路徑",
|
||||
"mapTitle": "External Surface → Source Control → Runtime Boundary",
|
||||
"detail": "把公開入口、產品、版本來源與 Gate 0 串成攻擊面圖;目標是優先看清可被利用的關聯,不先提高限制。",
|
||||
"evidence": "目前可看見 8 類資產與 S4.9 版本來源卡點,但 blast radius 仍維持 0,避免誤導成已完成攻防驗證。",
|
||||
"next": "先完成 GitHub primary / Gitea owner evidence,再讓風險路徑有可信來源。",
|
||||
"locked": "source_control_mutation_authorized=false,GitHub primary switch 未授權。"
|
||||
},
|
||||
"evidenceFlow": {
|
||||
"title": "證據流",
|
||||
"mapTitle": "Monitoring → AwoooP Truth Chain → Human Gate",
|
||||
"detail": "用 evidence lane 表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。",
|
||||
"evidence": "AwoooP 跨 Session 狀態已接線,IwoooS 只讀鏡像與 progress guard 已有證據。",
|
||||
"next": "補 reviewer 接受紀錄與 owner decision record,才可能進入下一個 runtime gate。",
|
||||
"locked": "active_runtime_gate_count=0,沒有任何自動執行按鈕。"
|
||||
}
|
||||
},
|
||||
"nodes": {
|
||||
"productSurface": {
|
||||
"title": "產品 / 網站"
|
||||
},
|
||||
"sourceControl": {
|
||||
"title": "版本來源"
|
||||
},
|
||||
"kali": {
|
||||
"title": "Kali 112"
|
||||
},
|
||||
"devHosts": {
|
||||
"title": "開發主機"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "監控 / 工具"
|
||||
},
|
||||
"awooopTruth": {
|
||||
"title": "AwoooP 真相鏈"
|
||||
},
|
||||
"runtimeGate": {
|
||||
"title": "Gate 0"
|
||||
}
|
||||
},
|
||||
"layers": {
|
||||
"externalSurface": {
|
||||
"title": "外部資產面",
|
||||
"body": "公開網站、產品入口與 VibeWork 先進入可理解範圍。"
|
||||
},
|
||||
"codeSupply": {
|
||||
"title": "版本來源面",
|
||||
"body": "GitHub primary / Gitea 遷移仍等 S4.9 證據。"
|
||||
},
|
||||
"hostFabric": {
|
||||
"title": "主機拓樸面",
|
||||
"body": "112、111、168 維持 observe-only 顯示。"
|
||||
},
|
||||
"evidenceOps": {
|
||||
"title": "證據營運面",
|
||||
"body": "監控、KM、MCP、Ansible 與 AwoooP 對齊。"
|
||||
},
|
||||
"gateControl": {
|
||||
"title": "執行閘面",
|
||||
"body": "runtime gate、掃描與修復仍全部鎖住。"
|
||||
}
|
||||
},
|
||||
"charts": {
|
||||
"contextDepth": {
|
||||
"label": "關聯深度",
|
||||
"body": "已把 code-to-runtime 的理解路徑壓成四段。"
|
||||
},
|
||||
"blastRadius": {
|
||||
"label": "爆炸半徑",
|
||||
"body": "未授權探測前維持 0,不誤導成已驗證攻擊半徑。"
|
||||
},
|
||||
"evidenceFreshness": {
|
||||
"label": "證據新鮮度",
|
||||
"body": "目前真正卡點仍是 S4.9 owner evidence。"
|
||||
}
|
||||
},
|
||||
"boundaryTitle": "圖譜邊界",
|
||||
"boundaryIntro": "這張圖是專業可視化與理解層,不是掃描拓樸、不是自動修復、不是 GitHub / Gitea 切換授權。"
|
||||
},
|
||||
"gateRadar": {
|
||||
"eyebrow": "執行閘雷達",
|
||||
"title": "一眼看懂哪裡可前進、哪裡不能碰",
|
||||
|
||||
@@ -9,17 +9,25 @@ import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Bell,
|
||||
Boxes,
|
||||
CheckCircle2,
|
||||
ClipboardCheck,
|
||||
Cloud,
|
||||
Code2,
|
||||
Database,
|
||||
Clock3,
|
||||
FileText,
|
||||
FileWarning,
|
||||
GitBranch,
|
||||
ListChecks,
|
||||
Lock,
|
||||
Network,
|
||||
Radar,
|
||||
Route,
|
||||
SearchCheck,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Workflow,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
@@ -188,6 +196,39 @@ type IwoooSImmediateVisualMeshNode = {
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type IwoooSTopologyAtlasMode = 'architecture' | 'topology' | 'attackSurface' | 'evidenceFlow'
|
||||
|
||||
type IwoooSTopologyAtlasLens = {
|
||||
key: IwoooSTopologyAtlasMode
|
||||
metric: string
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type IwoooSTopologyAtlasNode = {
|
||||
key: string
|
||||
metric: string
|
||||
x: number
|
||||
y: number
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type IwoooSTopologyAtlasLayer = {
|
||||
key: string
|
||||
metric: string
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type IwoooSTopologyAtlasChart = {
|
||||
key: string
|
||||
value: string
|
||||
percent: number
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type IwoooSGateRadarMode = 'visible' | 'blocker' | 'review' | 'locked'
|
||||
|
||||
type IwoooSGateRadarLane = {
|
||||
@@ -2363,6 +2404,74 @@ const iwooosImmediateVisualMeshBoundaries = [
|
||||
'not_authorization=true',
|
||||
] as const
|
||||
|
||||
const iwooosTopologyAtlasLenses: IwoooSTopologyAtlasLens[] = [
|
||||
{ key: 'architecture', metric: '5層', icon: Boxes, tone: 'steady' },
|
||||
{ key: 'topology', metric: '3主機', icon: Network, tone: 'warn' },
|
||||
{ key: 'attackSurface', metric: '8面', icon: Route, tone: 'warn' },
|
||||
{ key: 'evidenceFlow', metric: 'Gate 0', icon: Workflow, tone: 'locked' },
|
||||
]
|
||||
|
||||
const iwooosTopologyAtlasNodes: IwoooSTopologyAtlasNode[] = [
|
||||
{ key: 'productSurface', metric: '8', x: 14, y: 30, icon: Boxes, tone: 'steady' },
|
||||
{ key: 'sourceControl', metric: 'S4.9', x: 35, y: 18, icon: Code2, tone: 'warn' },
|
||||
{ key: 'kali', metric: '112', x: 58, y: 30, icon: Server, tone: 'warn' },
|
||||
{ key: 'devHosts', metric: '111 / 168', x: 77, y: 50, icon: Network, tone: 'warn' },
|
||||
{ key: 'monitoring', metric: '6', x: 37, y: 66, icon: Radar, tone: 'steady' },
|
||||
{ key: 'awooopTruth', metric: '已接線', x: 62, y: 78, icon: Workflow, tone: 'steady' },
|
||||
{ key: 'runtimeGate', metric: '0', x: 88, y: 24, icon: Lock, tone: 'locked' },
|
||||
]
|
||||
|
||||
const iwooosTopologyAtlasEdges = [
|
||||
['productSurface', 'sourceControl'],
|
||||
['sourceControl', 'kali'],
|
||||
['sourceControl', 'monitoring'],
|
||||
['kali', 'devHosts'],
|
||||
['monitoring', 'awooopTruth'],
|
||||
['awooopTruth', 'runtimeGate'],
|
||||
] as const
|
||||
|
||||
const iwooosTopologyAtlasCompactPositions: Record<string, { x: number; y: number }> = {
|
||||
productSurface: { x: 28, y: 24 },
|
||||
sourceControl: { x: 72, y: 24 },
|
||||
kali: { x: 28, y: 43 },
|
||||
devHosts: { x: 72, y: 43 },
|
||||
monitoring: { x: 28, y: 63 },
|
||||
awooopTruth: { x: 72, y: 63 },
|
||||
runtimeGate: { x: 50, y: 82 },
|
||||
}
|
||||
|
||||
const iwooosTopologyAtlasLayers: IwoooSTopologyAtlasLayer[] = [
|
||||
{ key: 'externalSurface', metric: '8', icon: Cloud, tone: 'steady' },
|
||||
{ key: 'codeSupply', metric: 'S4.9', icon: GitBranch, tone: 'warn' },
|
||||
{ key: 'hostFabric', metric: '3', icon: Server, tone: 'warn' },
|
||||
{ key: 'evidenceOps', metric: '6', icon: Database, tone: 'steady' },
|
||||
{ key: 'gateControl', metric: '0', icon: Lock, tone: 'locked' },
|
||||
]
|
||||
|
||||
const iwooosTopologyAtlasCharts: IwoooSTopologyAtlasChart[] = [
|
||||
{ key: 'contextDepth', value: '4-hop', percent: 74, icon: Route, tone: 'steady' },
|
||||
{ key: 'blastRadius', value: '0', percent: 0, icon: Lock, tone: 'locked' },
|
||||
{ key: 'evidenceFreshness', value: 'S4.9', percent: 61, icon: ClipboardCheck, tone: 'warn' },
|
||||
]
|
||||
|
||||
const iwooosTopologyAtlasBoundaries = [
|
||||
'iwooos_topology_atlas_first_layer=true',
|
||||
'iwooos_topology_atlas_lens_count=4',
|
||||
'iwooos_topology_atlas_node_count=7',
|
||||
'iwooos_topology_atlas_layer_count=5',
|
||||
'iwooos_topology_atlas_technical_chart_count=3',
|
||||
'iwooos_topology_atlas_interactive_lens_allowed=true',
|
||||
'iwooos_topology_atlas_execution_action_buttons_allowed=false',
|
||||
'iwooos_topology_atlas_runtime_gate_count=0',
|
||||
'iwooos_topology_atlas_scan_authorized=false',
|
||||
'iwooos_topology_atlas_host_change_authorized=false',
|
||||
'iwooos_topology_atlas_source_control_mutation_authorized=false',
|
||||
'runtime_execution_authorized=false',
|
||||
'active_runtime_gate_count=0',
|
||||
'action_buttons_allowed=false',
|
||||
'not_authorization=true',
|
||||
] as const
|
||||
|
||||
const iwooosGateRadarLanes: IwoooSGateRadarLane[] = [
|
||||
{ key: 'visible', metric: '8/8', score: 86, icon: ShieldCheck, tone: 'steady' },
|
||||
{ key: 'blocker', metric: 'S4.9', score: 61, icon: FileWarning, tone: 'warn' },
|
||||
@@ -9531,6 +9640,374 @@ function IwoooSImmediateVisualMeshBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
function IwoooSTopologyAtlasBoard() {
|
||||
const t = useTranslations('iwooos.topologyAtlas')
|
||||
const [activeKey, setActiveKey] = useState<IwoooSTopologyAtlasMode>('architecture')
|
||||
const activeLens = iwooosTopologyAtlasLenses.find(item => item.key === activeKey) ?? iwooosTopologyAtlasLenses[0]
|
||||
const ActiveIcon = activeLens.icon
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
const atlasCanvasRef = useRef<HTMLDivElement>(null)
|
||||
const [isCompactAtlas, setIsCompactAtlas] = useState(false)
|
||||
const topologyAtlasNodes = iwooosTopologyAtlasNodes.map(item => {
|
||||
if (!isCompactAtlas) return item
|
||||
const compactPosition = iwooosTopologyAtlasCompactPositions[item.key]
|
||||
return compactPosition ? { ...item, ...compactPosition } : item
|
||||
})
|
||||
const nodeByKey = new Map(topologyAtlasNodes.map(item => [item.key, item]))
|
||||
|
||||
useEffect(() => {
|
||||
const node = atlasCanvasRef.current
|
||||
if (!node) return
|
||||
|
||||
const updateLayout = () => setIsCompactAtlas(node.getBoundingClientRect().width < 520)
|
||||
updateLayout()
|
||||
|
||||
const observer = new ResizeObserver(updateLayout)
|
||||
observer.observe(node)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="iwooos-topology-atlas-board"
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ ...band, padding: 14, background: '#f7f8fb', borderColor: '#cfd8e3' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))',
|
||||
gap: 10,
|
||||
alignItems: 'end',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#2f4f67', fontSize: 12, fontWeight: 700 }}>
|
||||
<Network size={17} color="#315f7d" />
|
||||
{t('eyebrow')}
|
||||
</div>
|
||||
<h2 style={{ fontSize: 17, lineHeight: 1.25, margin: '7px 0 0', color: '#141413' }}>{t('title')}</h2>
|
||||
<p style={{ fontSize: 12, lineHeight: 1.55, color: '#3f5b6f', margin: '6px 0 0', ...textWrap }}>
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={t('tabsLabel')}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 128px), 1fr))',
|
||||
gap: 7,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{iwooosTopologyAtlasLenses.map(item => {
|
||||
const Icon = item.icon
|
||||
const selected = activeKey === item.key
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={selected}
|
||||
data-testid={`iwooos-topology-atlas-mode-${item.key}`}
|
||||
onClick={() => setActiveKey(item.key)}
|
||||
style={{
|
||||
border: selected ? `1px solid ${toneColors[item.tone]}` : '0.5px solid #d9e0e7',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
background: selected ? '#fff' : '#fbfcfd',
|
||||
color: '#141413',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
minWidth: 0,
|
||||
boxShadow: selected ? '0 8px 22px rgba(30, 58, 79, 0.08)' : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<Icon size={15} color={toneColors[item.tone]} />
|
||||
<span style={{ color: toneColors[item.tone], fontSize: 12, fontWeight: 700 }}>{item.metric}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.25, marginTop: 8, fontWeight: 700, ...textWrap }}>
|
||||
{t(`lenses.${item.key}.title` as never)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="iwooos-topology-atlas-canvas"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))',
|
||||
gap: 10,
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={atlasCanvasRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
minHeight: isCompactAtlas ? 430 : 342,
|
||||
border: '0.5px solid #172331',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'linear-gradient(135deg, #0d141b 0%, #111b23 52%, #18232d 100%)',
|
||||
color: '#f8fbfd',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 100 86"
|
||||
preserveAspectRatio="none"
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.95 }}
|
||||
>
|
||||
<defs>
|
||||
<pattern id="iwooos-topology-grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="0.5" />
|
||||
</pattern>
|
||||
<linearGradient id="iwooos-topology-path" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor="#58a27d" />
|
||||
<stop offset="55%" stopColor="#d19455" />
|
||||
<stop offset="100%" stopColor="#a7a6a0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100" height="86" fill="url(#iwooos-topology-grid)" />
|
||||
<circle cx="58" cy="43" r="31" fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth="1" />
|
||||
<circle cx="58" cy="43" r="20" fill="none" stroke="rgba(255,255,255,0.12)" strokeWidth="0.8" />
|
||||
{iwooosTopologyAtlasEdges.map(([fromKey, toKey]) => {
|
||||
const from = nodeByKey.get(fromKey)
|
||||
const to = nodeByKey.get(toKey)
|
||||
if (!from || !to) return null
|
||||
return (
|
||||
<line
|
||||
key={`${fromKey}-${toKey}`}
|
||||
x1={from.x}
|
||||
y1={from.y}
|
||||
x2={to.x}
|
||||
y2={to.y}
|
||||
stroke="url(#iwooos-topology-path)"
|
||||
strokeWidth="1.35"
|
||||
strokeDasharray={to.tone === 'locked' ? '3 3' : undefined}
|
||||
strokeLinecap="round"
|
||||
opacity={to.tone === 'locked' ? 0.62 : 0.86}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
|
||||
<div style={{ position: 'absolute', left: 16, top: 14, right: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11, color: '#b8c7d3', fontWeight: 700 }}>{t('mapLabel')}</div>
|
||||
<div style={{ fontSize: 18, lineHeight: 1.15, color: '#fff', marginTop: 4, fontWeight: 700, ...textWrap }}>
|
||||
{t(`lenses.${activeLens.key}.mapTitle` as never)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 58,
|
||||
height: 58,
|
||||
borderRadius: 58,
|
||||
border: '1px solid rgba(255,255,255,0.22)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: `conic-gradient(${toneColors[activeLens.tone]} 0 245deg, rgba(255,255,255,0.16) 245deg 360deg)`,
|
||||
boxShadow: 'inset 0 0 0 13px rgba(13,20,27,0.72)',
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
>
|
||||
<ActiveIcon size={20} color="#fffdf8" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{topologyAtlasNodes.map(node => {
|
||||
const Icon = node.icon
|
||||
const active = activeKey === 'architecture' || (activeKey === 'topology' && ['kali', 'devHosts', 'monitoring'].includes(node.key)) || (activeKey === 'attackSurface' && ['productSurface', 'sourceControl', 'runtimeGate'].includes(node.key)) || (activeKey === 'evidenceFlow' && ['monitoring', 'awooopTruth', 'runtimeGate'].includes(node.key))
|
||||
return (
|
||||
<div
|
||||
key={node.key}
|
||||
data-testid={`iwooos-topology-atlas-node-${node.key}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${node.x}%`,
|
||||
top: `${node.y}%`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
minWidth: 86,
|
||||
maxWidth: 116,
|
||||
border: `1px solid ${active ? toneColors[node.tone] : 'rgba(255,255,255,0.2)'}`,
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
background: active ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.78)',
|
||||
color: '#141413',
|
||||
boxShadow: active ? '0 12px 24px rgba(0,0,0,0.18)' : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 7 }}>
|
||||
<Icon size={15} color={toneColors[node.tone]} />
|
||||
<span style={{ color: toneColors[node.tone], fontSize: 11, fontWeight: 700 }}>{node.metric}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10.5, lineHeight: 1.25, marginTop: 6, color: '#2f3a40', fontWeight: 700, ...textWrap }}>
|
||||
{t(`nodes.${node.key}.title` as never)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 8, minWidth: 0 }}>
|
||||
<div
|
||||
data-testid="iwooos-topology-atlas-active-panel"
|
||||
style={{
|
||||
border: '0.5px solid #d9e0e7',
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
padding: 12,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: toneColors[activeLens.tone], fontSize: 12, fontWeight: 700 }}>
|
||||
<ActiveIcon size={16} />
|
||||
{activeLens.metric}
|
||||
</div>
|
||||
<h3 style={{ margin: '7px 0 0', fontSize: 15, lineHeight: 1.25, color: '#141413' }}>
|
||||
{t(`lenses.${activeLens.key}.title` as never)}
|
||||
</h3>
|
||||
<p style={{ margin: '7px 0 0', fontSize: 12, lineHeight: 1.55, color: '#41576a', ...textWrap }}>
|
||||
{t(`lenses.${activeLens.key}.detail` as never)}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 130px), 1fr))',
|
||||
gap: 7,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
{(['evidence', 'next', 'locked'] as const).map(key => (
|
||||
<div key={key} style={{ border: '0.5px solid #e4e9ee', borderRadius: 8, background: '#fbfcfd', padding: 9, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, color: '#6b7a86', fontWeight: 700 }}>{t(`panelLabels.${key}`)}</div>
|
||||
<div style={{ marginTop: 5, fontSize: 11, lineHeight: 1.45, color: '#263640', ...textWrap }}>
|
||||
{t(`lenses.${activeLens.key}.${key}` as never)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 138px), 1fr))',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{iwooosTopologyAtlasLayers.map(item => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.key} style={{ border: '0.5px solid #d9e0e7', borderRadius: 8, background: '#fff', padding: 10, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<Icon size={15} color={toneColors[item.tone]} />
|
||||
<span style={{ color: toneColors[item.tone], fontSize: 12, fontWeight: 700 }}>{item.metric}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11.5, lineHeight: 1.3, marginTop: 8, color: '#1f2d35', fontWeight: 700, ...textWrap }}>
|
||||
{t(`layers.${item.key}.title` as never)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10.5, lineHeight: 1.35, color: '#64737d', marginTop: 5, ...textWrap }}>
|
||||
{t(`layers.${item.key}.body` as never)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="iwooos-topology-atlas-technical-charts"
|
||||
style={{
|
||||
marginTop: 10,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{iwooosTopologyAtlasCharts.map(item => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.key} style={{ border: '0.5px solid #d9e0e7', borderRadius: 8, background: '#fff', padding: 10, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
|
||||
<Icon size={15} color={toneColors[item.tone]} />
|
||||
<span style={{ fontSize: 11, color: '#536674', fontWeight: 700, ...textWrap }}>
|
||||
{t(`charts.${item.key}.label` as never)}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: toneColors[item.tone], fontSize: 12, fontWeight: 700 }}>{item.value}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 8, height: 8, borderRadius: 8, background: '#edf1f4', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
width: `${item.percent}%`,
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
background: toneColors[item.tone],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p style={{ margin: '7px 0 0', fontSize: 10.5, lineHeight: 1.4, color: '#63727c', ...textWrap }}>
|
||||
{t(`charts.${item.key}.body` as never)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<details
|
||||
data-testid="iwooos-topology-atlas-boundaries"
|
||||
style={{
|
||||
marginTop: 10,
|
||||
border: '0.5px solid #d9e0e7',
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
padding: '7px 10px',
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontSize: 12, fontWeight: 700, color: '#3f5b6f' }}>
|
||||
{t('boundaryTitle')}
|
||||
</summary>
|
||||
<p style={{ fontSize: 11, color: '#3f5b6f', lineHeight: 1.45, margin: '8px 0', ...textWrap }}>
|
||||
{t('boundaryIntro')}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 6 }}>
|
||||
{iwooosTopologyAtlasBoundaries.map(item => (
|
||||
<code
|
||||
key={item}
|
||||
style={{
|
||||
border: '0.5px solid #e4e9ee',
|
||||
borderRadius: 8,
|
||||
padding: '6px 8px',
|
||||
color: '#41576a',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
background: '#f7f8fb',
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function IwoooSGateRadarBoard() {
|
||||
const t = useTranslations('iwooos.gateRadar')
|
||||
const [activeKey, setActiveKey] = useState<IwoooSGateRadarMode>('visible')
|
||||
@@ -14655,6 +15132,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
|
||||
<IwoooSFocusDeckBoard />
|
||||
<IwoooSImmediateVisualMeshBoard />
|
||||
<IwoooSTopologyAtlasBoard />
|
||||
<IwoooSGateRadarBoard />
|
||||
<IwoooSCommandMapBoard />
|
||||
<IwoooSFirstProgressUnlockPathBoard />
|
||||
|
||||
Reference in New Issue
Block a user