feat(web): route owner decision record outcomes

This commit is contained in:
Your Name
2026-05-21 18:42:33 +08:00
parent c95f2045ca
commit f9a40e6c93
8 changed files with 470 additions and 2 deletions

View File

@@ -3391,6 +3391,82 @@
}
}
},
"ownerResponseFormalRecordCandidateOutcomeBoard": {
"title": "人工決策正式紀錄候選結果分流",
"subtitle": "正式紀錄候選通過預檢後,仍只能進入只讀結果分流;分流會指出等待、退回草稿、補證、可交人工紀錄負責人、隔離或拒收,但不會自動升格正式紀錄,也不會批准或執行。現在分流=8、可交接=0、已升格=0、執行期閘門=0。",
"laneLabel": "結果分流",
"resultLabel": "分流結果",
"guardLabel": "仍不會做",
"boundaryTitle": "正式紀錄候選分流維持的保護線",
"summary": {
"lanes": {
"label": "分流",
"detail": "八條候選結果分流先可見,避免候選直接升格。"
},
"ready": {
"label": "可交接",
"detail": "目前為 0還沒有候選可交人工紀錄負責人。"
},
"promoted": {
"label": "已升格",
"detail": "目前為 0沒有候選被升格正式紀錄。"
},
"runtime": {
"label": "執行期閘門",
"detail": "目前為 0候選分流不會啟動執行期。"
}
},
"items": {
"remainCandidateWaiting": {
"title": "維持候選等待",
"body": "資料尚未足以交人工紀錄負責人時,候選維持等待,不升格也不退回。",
"result": "只更新只讀等待狀態。",
"guard": "不建立正式紀錄、不建立審批紀錄。"
},
"returnToDraft": {
"title": "退回草稿補齊",
"body": "若候選缺少追溯、範圍、角色或版本欄位,先退回草稿層補齊。",
"result": "只標記退回原因與待補欄位。",
"guard": "不刪草稿、不改證據、不提高進度。"
},
"needsEvidenceRefresh": {
"title": "要求證據更新",
"body": "若候選引用的證據版本過期、缺少脫敏證明或與範圍不一致,先要求補證。",
"result": "只列出需要更新的證據參照。",
"guard": "不讀取外部系統、不保存原始載荷。"
},
"needsReviewerClarification": {
"title": "要求審查說明",
"body": "若審查角色、責任範圍或簽核語義不清,候選必須回到人工說明。",
"result": "只標記需要哪一類人工說明。",
"guard": "不自動通知、不代填姓名、不替任何人批准。"
},
"readyForRecordOwner": {
"title": "可交紀錄負責人",
"body": "候選若欄位齊全,可進入人工紀錄負責人交接,但仍不是正式紀錄。",
"result": "只標記可交接,等待人工確認。",
"guard": "不自動升格、不建立正式紀錄、不建立審批紀錄。"
},
"quarantineSensitivePayload": {
"title": "隔離敏感載荷",
"body": "若候選含機密明文、token、cookie、private key 或 exploit payload必須先隔離。",
"result": "只標記隔離原因與來源欄位。",
"guard": "不展示、不保存、不轉送任何機密明文值。"
},
"rejectMutationRequest": {
"title": "拒收變更要求",
"body": "若候選夾帶專案庫、refs、workflow、secrets、主機或部署變更要求先拒收。",
"result": "只標記拒收原因,等待重新提交只讀候選。",
"guard": "不建立專案庫、不改 refs、不改 workflow / secrets。"
},
"runtimeOrCutoverGateRequired": {
"title": "另開執行或切換閘門",
"body": "若候選需要掃描、修復、主機更新、主要來源切換或 Gitea 停用,必須另開人工閘門。",
"result": "只標記需要哪一種後續閘門。",
"guard": "不呼叫 Kali、不開 SSH、不切 GitHub 主要來源、不停用 Gitea。"
}
}
},
"awooopReadOnlyLandingReadiness": {
"title": "AwoooP Read-Only Landing Readiness",
"subtitle": "S2.51 turns the AwoooP main-line read-only consumption path for IwoooS / security mirror state into an intake readiness board. This is landing readiness, not production_landing_enabled, and it does not connect an execution router.",

View File

@@ -3392,6 +3392,82 @@
}
}
},
"ownerResponseFormalRecordCandidateOutcomeBoard": {
"title": "人工決策正式紀錄候選結果分流",
"subtitle": "正式紀錄候選通過預檢後,仍只能進入只讀結果分流;分流會指出等待、退回草稿、補證、可交人工紀錄負責人、隔離或拒收,但不會自動升格正式紀錄,也不會批准或執行。現在分流=8、可交接=0、已升格=0、執行期閘門=0。",
"laneLabel": "結果分流",
"resultLabel": "分流結果",
"guardLabel": "仍不會做",
"boundaryTitle": "正式紀錄候選分流維持的保護線",
"summary": {
"lanes": {
"label": "分流",
"detail": "八條候選結果分流先可見,避免候選直接升格。"
},
"ready": {
"label": "可交接",
"detail": "目前為 0還沒有候選可交人工紀錄負責人。"
},
"promoted": {
"label": "已升格",
"detail": "目前為 0沒有候選被升格正式紀錄。"
},
"runtime": {
"label": "執行期閘門",
"detail": "目前為 0候選分流不會啟動執行期。"
}
},
"items": {
"remainCandidateWaiting": {
"title": "維持候選等待",
"body": "資料尚未足以交人工紀錄負責人時,候選維持等待,不升格也不退回。",
"result": "只更新只讀等待狀態。",
"guard": "不建立正式紀錄、不建立審批紀錄。"
},
"returnToDraft": {
"title": "退回草稿補齊",
"body": "若候選缺少追溯、範圍、角色或版本欄位,先退回草稿層補齊。",
"result": "只標記退回原因與待補欄位。",
"guard": "不刪草稿、不改證據、不提高進度。"
},
"needsEvidenceRefresh": {
"title": "要求證據更新",
"body": "若候選引用的證據版本過期、缺少脫敏證明或與範圍不一致,先要求補證。",
"result": "只列出需要更新的證據參照。",
"guard": "不讀取外部系統、不保存原始載荷。"
},
"needsReviewerClarification": {
"title": "要求審查說明",
"body": "若審查角色、責任範圍或簽核語義不清,候選必須回到人工說明。",
"result": "只標記需要哪一類人工說明。",
"guard": "不自動通知、不代填姓名、不替任何人批准。"
},
"readyForRecordOwner": {
"title": "可交紀錄負責人",
"body": "候選若欄位齊全,可進入人工紀錄負責人交接,但仍不是正式紀錄。",
"result": "只標記可交接,等待人工確認。",
"guard": "不自動升格、不建立正式紀錄、不建立審批紀錄。"
},
"quarantineSensitivePayload": {
"title": "隔離敏感載荷",
"body": "若候選含機密明文、token、cookie、private key 或 exploit payload必須先隔離。",
"result": "只標記隔離原因與來源欄位。",
"guard": "不展示、不保存、不轉送任何機密明文值。"
},
"rejectMutationRequest": {
"title": "拒收變更要求",
"body": "若候選夾帶專案庫、refs、workflow、secrets、主機或部署變更要求先拒收。",
"result": "只標記拒收原因,等待重新提交只讀候選。",
"guard": "不建立專案庫、不改 refs、不改 workflow / secrets。"
},
"runtimeOrCutoverGateRequired": {
"title": "另開執行或切換閘門",
"body": "若候選需要掃描、修復、主機更新、主要來源切換或 Gitea 停用,必須另開人工閘門。",
"result": "只標記需要哪一種後續閘門。",
"guard": "不呼叫 Kali、不開 SSH、不切 GitHub 主要來源、不停用 Gitea。"
}
}
},
"awooopReadOnlyLandingReadiness": {
"title": "AwoooP 只讀接入就緒度",
"subtitle": "S2.51 把 AwoooP 主線要如何只讀消費 IwoooS / 資安鏡像狀態整理成接入準備面板。這是接入就緒度,不是 production landing enabled也不接 execution router。",

View File

@@ -179,6 +179,14 @@ type OwnerResponseFormalRecordCandidatePreflightItem = {
tone: 'steady' | 'warn' | 'locked'
}
type OwnerResponseFormalRecordCandidateOutcomeLane = {
key: string
lane: string
value: string
icon: typeof ShieldCheck
tone: 'steady' | 'warn' | 'locked'
}
type CoverageGroup = {
key: string
count: string
@@ -770,6 +778,41 @@ const ownerResponseFormalRecordCandidatePreflightBoundaries = [
'gitea_disablement_authorized=false',
]
const ownerResponseFormalRecordCandidateOutcomeLanes: OwnerResponseFormalRecordCandidateOutcomeLane[] = [
{ key: 'remainCandidateWaiting', lane: 'V1', value: '0', icon: Clock3, tone: 'warn' },
{ key: 'returnToDraft', lane: 'V2', value: '0', icon: FileText, tone: 'warn' },
{ key: 'needsEvidenceRefresh', lane: 'V3', value: '0', icon: SearchCheck, tone: 'warn' },
{ key: 'needsReviewerClarification', lane: 'V4', value: '0', icon: ClipboardCheck, tone: 'warn' },
{ key: 'readyForRecordOwner', lane: 'V5', value: '0', icon: CheckCircle2, tone: 'steady' },
{ key: 'quarantineSensitivePayload', lane: 'V6', value: '0', icon: Lock, tone: 'locked' },
{ key: 'rejectMutationRequest', lane: 'V7', value: '0', icon: AlertTriangle, tone: 'locked' },
{ key: 'runtimeOrCutoverGateRequired', lane: 'V8', value: '0', icon: ShieldCheck, tone: 'locked' },
]
const ownerResponseFormalRecordCandidateOutcomeBoundaries = [
'owner_response_formal_record_candidate_outcome_lane_count=8',
'owner_response_formal_record_candidate_ready_count=0',
'owner_response_formal_record_candidate_returned_count=0',
'owner_response_formal_record_candidate_quarantine_count=0',
'owner_response_formal_record_candidate_rejected_count=0',
'owner_response_formal_record_candidate_promoted_count=0',
'owner_response_formal_record_candidate_review_only=true',
'owner_response_formal_record_auto_promotion_allowed=false',
'owner_response_formal_record_write_authorized=false',
'owner_response_formal_record_approval_authorized=false',
'owner_response_formal_record_execution_authorized=false',
'runtime_execution_authorized=false',
'active_runtime_gate_count=0',
'action_buttons_allowed=false',
'not_authorization=true',
'secret_value_collection_allowed=false',
'repo_creation_authorized=false',
'refs_sync_authorized=false',
'workflow_modification_authorized=false',
'github_primary_switch_authorized=false',
'gitea_disablement_authorized=false',
]
const coverageGroups: CoverageGroup[] = [
{
key: 'signals',
@@ -2599,6 +2642,130 @@ function OwnerResponseFormalRecordCandidatePreflightBoard() {
)
}
function OwnerResponseFormalRecordCandidateOutcomeCard({
item,
}: {
item: OwnerResponseFormalRecordCandidateOutcomeLane
}) {
const t = useTranslations('iwooos.ownerResponseFormalRecordCandidateOutcomeBoard')
const Icon = item.icon
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
return (
<div style={{ ...band, minHeight: 184, padding: 16, ...textWrap }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, minWidth: 0 }}>
<Icon size={18} color={toneColors[item.tone]} style={{ flex: '0 0 auto' }} />
<span style={{ fontSize: 11, color: '#87867f' }}>{t('laneLabel')}</span>
</div>
<span style={{ fontSize: 11, color: toneColors[item.tone] }}>{item.lane}</span>
</div>
<div style={{ fontSize: 28, fontWeight: 700, color: toneColors[item.tone], marginTop: 12, lineHeight: 1 }}>
{item.value}
</div>
<h2 style={{ fontSize: 14, margin: '10px 0 6px', color: '#141413' }}>
{t(`items.${item.key}.title` as never)}
</h2>
<p style={{ fontSize: 12, lineHeight: 1.55, color: '#6f6d66', margin: 0, ...textWrap }}>
{t(`items.${item.key}.body` as never)}
</p>
<div style={{ marginTop: 10, display: 'grid', gap: 5 }}>
<span style={{ fontSize: 11, color: '#87867f' }}>{t('resultLabel')}</span>
<span style={{ fontSize: 11, color: toneColors[item.tone], lineHeight: 1.45, ...textWrap }}>
{t(`items.${item.key}.result` as never)}
</span>
<span style={{ fontSize: 11, color: '#87867f', marginTop: 4 }}>{t('guardLabel')}</span>
<span style={{ fontSize: 11, color: '#6f6d66', lineHeight: 1.45, ...textWrap }}>
{t(`items.${item.key}.guard` as never)}
</span>
</div>
</div>
)
}
function OwnerResponseFormalRecordCandidateOutcomeBoard() {
const t = useTranslations('iwooos.ownerResponseFormalRecordCandidateOutcomeBoard')
const summaryItems = [
{ key: 'lanes', value: '8', tone: 'warn' as const },
{ key: 'ready', value: '0', tone: 'steady' as const },
{ key: 'promoted', value: '0', tone: 'locked' as const },
{ key: 'runtime', value: '0', tone: 'locked' as const },
]
return (
<section style={{ marginBottom: 14 }} data-testid="iwooos-owner-response-formal-record-candidate-outcome-board">
<div style={{ marginBottom: 14 }}>
<h2 style={{ fontSize: 16, margin: 0 }}>{t('title')}</h2>
<p style={{ fontSize: 12, color: '#6f6d66', margin: '6px 0 0', lineHeight: 1.55 }}>
{t('subtitle')}
</p>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: 10,
marginBottom: 12,
}}
>
{summaryItems.map(item => (
<div key={item.key} style={{ ...band, padding: 14, minHeight: 96 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span style={{ fontSize: 11, color: '#87867f' }}>{t(`summary.${item.key}.label` as never)}</span>
<ToneDot tone={item.tone} />
</div>
<div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1, marginTop: 9, color: '#141413' }}>
{item.value}
</div>
<p style={{ fontSize: 11, color: '#6f6d66', lineHeight: 1.45, margin: '8px 0 0' }}>
{t(`summary.${item.key}.detail` as never)}
</p>
</div>
))}
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))',
gap: 12,
}}
>
{ownerResponseFormalRecordCandidateOutcomeLanes.map(item => (
<OwnerResponseFormalRecordCandidateOutcomeCard key={item.key} item={item} />
))}
</div>
<div style={{ ...band, marginTop: 12, padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<ShieldCheck size={16} color={toneColors.locked} />
<h3 style={{ fontSize: 14, margin: 0 }}>{t('boundaryTitle')}</h3>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))',
gap: 7,
}}
>
{ownerResponseFormalRecordCandidateOutcomeBoundaries.map(item => (
<span
key={item}
style={{
border: '0.5px solid #eee9dd',
borderRadius: 8,
padding: '7px 9px',
color: '#4f4c45',
fontSize: 11,
lineHeight: 1.4,
overflowWrap: 'anywhere',
}}
>
{item}
</span>
))}
</div>
</div>
</section>
)
}
function CoverageCard({ item }: { item: CoverageGroup }) {
const t = useTranslations('iwooos.coverage')
const Icon = item.icon
@@ -3744,6 +3911,8 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
<OwnerResponseFormalRecordCandidatePreflightBoard />
<OwnerResponseFormalRecordCandidateOutcomeBoard />
<section style={{ marginBottom: 14 }}>
<div style={{ marginBottom: 14 }}>
<h2 style={{ fontSize: 16, margin: 0 }}>{t('awooopReadOnlyLandingReadiness.title')}</h2>