feat(web): bridge security pages to IwoooS

This commit is contained in:
Your Name
2026-05-20 19:56:33 +08:00
parent 389b6f62ce
commit 9c8f172f9b
12 changed files with 388 additions and 5 deletions

View File

@@ -990,7 +990,35 @@
"issue": "Issue",
"count": "Count",
"error": "Load failed",
"noData": "No security events"
"noData": "No security events",
"iwooosBridge": {
"title": "Included in the IwoooS read-only security mirror",
"subtitle": "This existing security / compliance page now shows its IwoooS scope in reverse; it only displays progress, framework maturity, and runtime boundaries without adding scan, repair, approval, or deploy buttons.",
"compactTitle": "IwoooS",
"compactDetail": "58% / gate 0",
"openIwooos": "Open IwoooS",
"sourceLabel": "Integration source",
"sourceDetail": "SecurityPanel, CompliancePanel, standalone /security, and /compliance keep their original data sources; IwoooS only indexes the security mesh and aggregates mirror-only posture.",
"boundaryLabel": "Execution boundary",
"metrics": {
"overall": {
"label": "Overall mesh",
"detail": "headline progress"
},
"framework": {
"label": "Framework maturity",
"detail": "docs / schema / read-only evidence"
},
"runtimeGates": {
"label": "Runtime gates",
"detail": "not active"
},
"actions": {
"label": "Action buttons",
"detail": "not provided"
}
}
}
},
"iwooos": {
"eyebrow": "Information Security Mesh",

View File

@@ -991,7 +991,35 @@
"issue": "問題",
"count": "次數",
"error": "載入失敗",
"noData": "無安全事件"
"noData": "無安全事件",
"iwooosBridge": {
"title": "已納入 IwoooS 只讀資安鏡像",
"subtitle": "這個既有安全 / 合規頁面現在會反向標示 IwoooS 納管狀態;只顯示整體進度、框架成熟度與 runtime 邊界,不新增掃描、修復、批准或部署按鈕。",
"compactTitle": "IwoooS",
"compactDetail": "58% / gate 0",
"openIwooos": "開啟 IwoooS",
"sourceLabel": "整合來源",
"sourceDetail": "SecurityPanel、CompliancePanel、standalone /security 與 /compliance 仍保留原本資料來源IwoooS 只做資安網索引與 mirror-only 姿態彙整。",
"boundaryLabel": "執行邊界",
"metrics": {
"overall": {
"label": "整體資安網",
"detail": "headline progress"
},
"framework": {
"label": "框架成熟度",
"detail": "docs / schema / read-only evidence"
},
"runtimeGates": {
"label": "Runtime gates",
"detail": "未開啟"
},
"actions": {
"label": "執行按鈕",
"detail": "未提供"
}
}
}
},
"iwooos": {
"eyebrow": "Information Security Mesh",

View File

@@ -9,6 +9,7 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { AppLayout } from '@/components/layout'
import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -56,6 +57,8 @@ export default function CompliancePage({ params }: { params: { locale: string }
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
</div>
<IwoooSReadOnlyBridge />
{loading ? (
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
) : error ? (

View File

@@ -9,6 +9,7 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { AppLayout } from '@/components/layout'
import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -72,6 +73,8 @@ export default function SecurityPage({ params }: { params: { locale: string } })
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
</div>
<IwoooSReadOnlyBridge />
{loading ? (
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
) : error ? (

View File

@@ -8,6 +8,7 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -55,6 +56,8 @@ export function CompliancePanel() {
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0' }}>{t('subtitle')}</p>
</div>
<IwoooSReadOnlyBridge />
{loading ? (
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontSize: 13 }}>{t('loading')}</div>
) : error ? (

View File

@@ -8,6 +8,7 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -61,6 +62,8 @@ export function SecurityPanel() {
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0' }}>{t('subtitle')}</p>
</div>
<IwoooSReadOnlyBridge />
{loading ? (
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontSize: 13 }}>{t('loading')}</div>
) : error ? (

View File

@@ -0,0 +1,196 @@
'use client'
/**
* IwoooSReadOnlyBridge — 既有資安頁面到 IwoooS 的只讀橋接
* @created 2026-05-20 Codex Asia/Taipei
*/
import Link from 'next/link'
import { ArrowRight, ShieldCheck } from 'lucide-react'
import { useTranslations } from 'next-intl'
const metrics = [
{ key: 'overall', value: '58%' },
{ key: 'framework', value: '80-85%' },
{ key: 'runtimeGates', value: '0' },
{ key: 'actions', value: '0' },
] as const
const boundaries = [
'runtime_execution_authorized=false',
'active_runtime_gate_count=0',
'action_buttons_allowed=false',
'not_authorization=true',
] as const
export function IwoooSReadOnlyBridge() {
const t = useTranslations('security.iwooosBridge')
return (
<>
<style jsx>{`
@media (max-width: 640px) {
.iwooos-bridge-full {
display: none !important;
}
.iwooos-bridge-compact {
display: flex !important;
}
}
`}</style>
<section
className="iwooos-bridge-compact"
data-testid="iwooos-read-only-bridge"
style={{
display: 'none',
flexDirection: 'column',
alignItems: 'stretch',
gap: 6,
background: '#fff',
border: '0.5px solid #e0ddd4',
borderRadius: 8,
marginBottom: 12,
padding: '10px 8px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
<ShieldCheck size={15} strokeWidth={1.8} color="#d97757" style={{ flex: '0 0 auto' }} />
<div style={{ fontSize: 12, fontWeight: 700, color: '#141413', lineHeight: 1.2, minWidth: 0 }}>
{t('compactTitle')}
</div>
</div>
<div style={{ fontSize: 10, color: '#87867f', lineHeight: 1.35 }}>
{t('compactDetail')}
</div>
<Link
href="/iwooos"
title={t('openIwooos')}
aria-label={t('openIwooos')}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 0,
height: 26,
border: '0.5px solid #d97757',
background: '#fff',
color: '#141413',
textDecoration: 'none',
}}
>
<ArrowRight size={14} strokeWidth={1.8} />
</Link>
</section>
<section
className="iwooos-bridge-full"
data-testid="iwooos-read-only-bridge"
style={{
background: '#fff',
border: '0.5px solid #e0ddd4',
borderRadius: 12,
marginBottom: 16,
overflow: 'hidden',
}}
>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: 12,
padding: '14px 16px',
borderBottom: '0.5px solid #e0ddd4',
background: '#faf9f3',
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 0 }}>
<ShieldCheck size={18} strokeWidth={1.8} color="#d97757" style={{ marginTop: 2, flex: '0 0 auto' }} />
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#141413', lineHeight: 1.4 }}>
{t('title')}
</div>
<p style={{ fontSize: 12, color: '#5f5d56', margin: '4px 0 0', lineHeight: 1.65 }}>
{t('subtitle')}
</p>
</div>
</div>
<Link
href="/iwooos"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
minHeight: 32,
padding: '0 10px',
border: '0.5px solid #d97757',
background: '#fff',
color: '#141413',
textDecoration: 'none',
fontSize: 12,
fontWeight: 700,
whiteSpace: 'nowrap',
}}
>
{t('openIwooos')}
<ArrowRight size={14} strokeWidth={1.8} />
</Link>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
gap: 0,
borderBottom: '0.5px solid #e0ddd4',
}}
>
{metrics.map(metric => (
<div key={metric.key} style={{ padding: '12px 16px', borderRight: '0.5px solid #f0ede4' }}>
<div style={{ fontSize: 10, color: '#87867f', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 4 }}>
{t(`metrics.${metric.key}.label`)}
</div>
<div style={{ fontSize: 20, fontWeight: 700, color: '#141413', lineHeight: 1.2 }}>
{metric.value}
</div>
<div style={{ fontSize: 11, color: '#87867f', marginTop: 4, lineHeight: 1.5 }}>
{t(`metrics.${metric.key}.detail`)}
</div>
</div>
))}
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 0,
}}
>
<div style={{ padding: '12px 16px', borderRight: '0.5px solid #f0ede4' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#141413', marginBottom: 8 }}>
{t('sourceLabel')}
</div>
<p style={{ fontSize: 12, color: '#5f5d56', margin: 0, lineHeight: 1.65 }}>
{t('sourceDetail')}
</p>
</div>
<div style={{ padding: '12px 16px' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#141413', marginBottom: 8 }}>
{t('boundaryLabel')}
</div>
<div style={{ display: 'grid', gap: 5 }}>
{boundaries.map(boundary => (
<code key={boundary} style={{ fontSize: 11, color: '#141413', wordBreak: 'break-word' }}>
{boundary}
</code>
))}
</div>
</div>
</div>
</section>
</>
)
}