feat(governance): 新增全產品 Code Review 防木馬 Gate
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m49s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m49s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge'
|
||||
import { publicBoundaryText } from '@/lib/public-security-redaction'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
@@ -13,15 +14,92 @@ import {
|
||||
ExternalLink,
|
||||
FileCheck2,
|
||||
GitBranch,
|
||||
Globe2,
|
||||
Gauge,
|
||||
ListChecks,
|
||||
LockKeyhole,
|
||||
PackageSearch,
|
||||
SearchCheck,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Workflow,
|
||||
} from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
type ProductCodeReviewGateSnapshot = {
|
||||
schema_version: string
|
||||
generated_at: string
|
||||
program_status: {
|
||||
overall_completion_percent: number
|
||||
current_task_id: string
|
||||
next_task_id: string
|
||||
read_only_mode: boolean
|
||||
}
|
||||
rollups: {
|
||||
product_scope_count: number
|
||||
public_route_count_minimum: number
|
||||
source_candidate_repo_count: number
|
||||
pre_deploy_gate_count: number
|
||||
post_deploy_gate_count: number
|
||||
ai_reviewer_count: number
|
||||
mainstream_tool_count: number
|
||||
owner_review_required_count: number
|
||||
critical_gap_count: number
|
||||
active_write_gate_count: number
|
||||
action_button_count: number
|
||||
}
|
||||
tenant_asset_inventory_summary: {
|
||||
public_route_count?: number
|
||||
source_candidate_repo_count?: number
|
||||
}
|
||||
product_review_matrix: Array<{
|
||||
product_id: string
|
||||
product_name: string
|
||||
category: string
|
||||
owner_lane: string
|
||||
coverage_status: string
|
||||
public_route_count: number
|
||||
source_repo_count: number
|
||||
gate_state: string
|
||||
runtime_gate_count: number
|
||||
action_button_count: number
|
||||
}>
|
||||
pre_deploy_gates: Array<{
|
||||
gate_id: string
|
||||
label: string
|
||||
status: string
|
||||
owner_agent: string
|
||||
current_gap: string
|
||||
next_action: string
|
||||
}>
|
||||
post_deploy_gates: Array<{
|
||||
gate_id: string
|
||||
label: string
|
||||
status: string
|
||||
owner_agent: string
|
||||
current_gap: string
|
||||
next_action: string
|
||||
}>
|
||||
ai_reviewer_lanes: Array<{
|
||||
agent_id: string
|
||||
label: string
|
||||
role: string
|
||||
allowed_output: string
|
||||
write_allowed: boolean
|
||||
}>
|
||||
mainstream_tool_lanes: Array<{
|
||||
tool_id: string
|
||||
label: string
|
||||
category: string
|
||||
integration_status: string
|
||||
recommended_role: string
|
||||
blocked_now: string[]
|
||||
}>
|
||||
gate_boundaries: Record<string, boolean>
|
||||
}
|
||||
|
||||
const agents = [
|
||||
{ name: 'Hermes', roleKey: 'hermes', state: 'wired' },
|
||||
{ name: 'OpenClaw', roleKey: 'openclaw', state: 'wired' },
|
||||
@@ -89,9 +167,50 @@ const summaryCards = [
|
||||
{ key: 'report', icon: Gauge, iconClassName: 'text-lime-300' },
|
||||
]
|
||||
|
||||
async function fetchProductCodeReviewGate(): Promise<ProductCodeReviewGateSnapshot | null> {
|
||||
if (!API_BASE) return null
|
||||
const response = await fetch(`${API_BASE}/api/v1/agents/product-code-review-gate`, {
|
||||
cache: 'no-store',
|
||||
})
|
||||
if (!response.ok) return null
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export default function CodeReviewPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('codeReview')
|
||||
const [gateSnapshot, setGateSnapshot] = useState<ProductCodeReviewGateSnapshot | null>(null)
|
||||
const [gateLoaded, setGateLoaded] = useState(false)
|
||||
const evidenceHref = `/${params.locale}/awooop/runs`
|
||||
const products = useMemo(
|
||||
() => gateSnapshot?.product_review_matrix ?? [],
|
||||
[gateSnapshot]
|
||||
)
|
||||
const preDeployGates = useMemo(
|
||||
() => gateSnapshot?.pre_deploy_gates.slice(0, 4) ?? [],
|
||||
[gateSnapshot]
|
||||
)
|
||||
const postDeployGates = useMemo(
|
||||
() => gateSnapshot?.post_deploy_gates.slice(0, 4) ?? [],
|
||||
[gateSnapshot]
|
||||
)
|
||||
const toolLanes = useMemo(
|
||||
() => gateSnapshot?.mainstream_tool_lanes ?? [],
|
||||
[gateSnapshot]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
fetchProductCodeReviewGate()
|
||||
.then((payload) => {
|
||||
if (!cancelled) setGateSnapshot(payload)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setGateLoaded(true)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
@@ -117,6 +236,209 @@ export default function CodeReviewPage({ params }: { params: { locale: string }
|
||||
|
||||
<IwoooSReadOnlyBridge variant="dark" />
|
||||
|
||||
<section
|
||||
id="product-code-review-gate"
|
||||
data-testid="product-code-review-gate"
|
||||
className="grid min-w-0 gap-4 rounded border border-gray-800 bg-gray-950 p-4"
|
||||
>
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-start">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-xs font-mono uppercase text-amber-300">
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
{t('gateBoard.eyebrow')}
|
||||
</div>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">{t('gateBoard.title')}</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-gray-400">{t('gateBoard.subtitle')}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs font-mono text-gray-400 sm:grid-cols-4 lg:min-w-[30rem]">
|
||||
<div className="rounded border border-gray-800 bg-black/25 px-3 py-2">
|
||||
<div className="text-gray-500">{t('gateBoard.metrics.current')}</div>
|
||||
<div className="mt-1 text-lg text-white">{gateSnapshot?.program_status.current_task_id ?? '--'}</div>
|
||||
</div>
|
||||
<div className="rounded border border-gray-800 bg-black/25 px-3 py-2">
|
||||
<div className="text-gray-500">{t('gateBoard.metrics.next')}</div>
|
||||
<div className="mt-1 text-lg text-white">{gateSnapshot?.program_status.next_task_id ?? '--'}</div>
|
||||
</div>
|
||||
<div className="rounded border border-gray-800 bg-black/25 px-3 py-2">
|
||||
<div className="text-gray-500">{t('gateBoard.metrics.writeGate')}</div>
|
||||
<div className="mt-1 text-lg text-emerald-200">{gateSnapshot?.rollups.active_write_gate_count ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded border border-gray-800 bg-black/25 px-3 py-2">
|
||||
<div className="text-gray-500">{t('gateBoard.metrics.actions')}</div>
|
||||
<div className="mt-1 text-lg text-emerald-200">{gateSnapshot?.rollups.action_button_count ?? 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-6">
|
||||
{[
|
||||
{
|
||||
key: 'products',
|
||||
value: gateSnapshot?.rollups.product_scope_count ?? products.length,
|
||||
icon: Globe2,
|
||||
},
|
||||
{
|
||||
key: 'routes',
|
||||
value: gateSnapshot?.tenant_asset_inventory_summary.public_route_count ?? gateSnapshot?.rollups.public_route_count_minimum ?? 0,
|
||||
icon: GitBranch,
|
||||
},
|
||||
{
|
||||
key: 'pre',
|
||||
value: gateSnapshot?.rollups.pre_deploy_gate_count ?? 0,
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
key: 'post',
|
||||
value: gateSnapshot?.rollups.post_deploy_gate_count ?? 0,
|
||||
icon: CheckCircle2,
|
||||
},
|
||||
{
|
||||
key: 'reviewers',
|
||||
value: gateSnapshot?.rollups.ai_reviewer_count ?? 0,
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
key: 'tools',
|
||||
value: gateSnapshot?.rollups.mainstream_tool_count ?? 0,
|
||||
icon: PackageSearch,
|
||||
},
|
||||
].map((metric) => {
|
||||
const Icon = metric.icon
|
||||
return (
|
||||
<div key={metric.key} className="rounded border border-gray-800 bg-black/25 p-3">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<Icon className="h-4 w-4 text-sky-300" />
|
||||
{t(`gateBoard.metricCards.${metric.key}.label` as never)}
|
||||
</div>
|
||||
<div className="mt-3 text-2xl font-semibold text-white">{metric.value}</div>
|
||||
<div className="mt-1 text-xs leading-5 text-gray-500">{t(`gateBoard.metricCards.${metric.key}.detail` as never)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-5">
|
||||
{['assets', 'preDeploy', 'aiReview', 'postDeploy', 'ownerGate'].map((step, index) => (
|
||||
<div key={step} className="min-w-0 rounded border border-gray-800 bg-black/20 px-3 py-3">
|
||||
<div className="font-mono text-xs text-gray-500">{String(index + 1).padStart(2, '0')}</div>
|
||||
<div className="mt-2 text-sm font-semibold text-white">{t(`gateBoard.flow.${step}.title` as never)}</div>
|
||||
<div className="mt-1 text-xs leading-5 text-gray-400">{t(`gateBoard.flow.${step}.detail` as never)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!gateLoaded ? (
|
||||
<div className="rounded border border-gray-800 bg-black/20 px-3 py-2 text-sm text-gray-500">
|
||||
{t('gateBoard.loading')}
|
||||
</div>
|
||||
) : gateSnapshot ? null : (
|
||||
<div className="rounded border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-100">
|
||||
{t('gateBoard.unavailable')}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section
|
||||
data-testid="product-code-review-matrix"
|
||||
className="grid min-w-0 gap-4 rounded border border-gray-800 bg-gray-950 p-4"
|
||||
>
|
||||
<div className="grid gap-2 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Globe2 className="h-4 w-4 text-sky-300" />
|
||||
{t('products.title')}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{t('products.subtitle')}</div>
|
||||
</div>
|
||||
<div className="rounded border border-emerald-500/30 px-3 py-1 text-xs font-mono text-emerald-200">
|
||||
{publicBoundaryText('runtime_execution_authorized=false')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-4">
|
||||
{products.map((product) => (
|
||||
<div key={product.product_id} className="min-w-0 rounded border border-gray-800 bg-black/20 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-xs text-gray-500">{product.product_id}</div>
|
||||
<div className="mt-1 break-words text-sm font-semibold text-white">{product.product_name}</div>
|
||||
</div>
|
||||
<span className="shrink-0 rounded bg-sky-500/10 px-2 py-1 text-xs font-mono text-sky-200">
|
||||
{product.gate_state === 'owner_review_required'
|
||||
? t('products.states.ownerReview')
|
||||
: product.gate_state === 'source_mapping_required'
|
||||
? t('products.states.sourceMapping')
|
||||
: t('products.states.visible')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="rounded border border-gray-800 px-2 py-2">
|
||||
<div className="text-gray-500">{t('products.labels.routes')}</div>
|
||||
<div className="mt-1 font-mono text-white">{product.public_route_count}</div>
|
||||
</div>
|
||||
<div className="rounded border border-gray-800 px-2 py-2">
|
||||
<div className="text-gray-500">{t('products.labels.sources')}</div>
|
||||
<div className="mt-1 font-mono text-white">{product.source_repo_count}</div>
|
||||
</div>
|
||||
<div className="rounded border border-gray-800 px-2 py-2">
|
||||
<div className="text-gray-500">{t('products.labels.gates')}</div>
|
||||
<div className="mt-1 font-mono text-emerald-200">{product.runtime_gate_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 break-words text-xs leading-5 text-gray-500">
|
||||
{product.owner_lane} · {product.coverage_status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<div data-testid="product-code-review-prepost-gates" className="rounded border border-gray-800 bg-gray-950 p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<ShieldCheck className="h-4 w-4 text-emerald-300" />
|
||||
{t('prepost.title')}
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-mono uppercase text-gray-500">{t('prepost.pre')}</div>
|
||||
{preDeployGates.map((gate) => (
|
||||
<div key={gate.gate_id} className="rounded border border-gray-800 bg-black/20 px-3 py-2">
|
||||
<div className="text-sm font-semibold text-white">{gate.label}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{gate.owner_agent} · {gate.status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-mono uppercase text-gray-500">{t('prepost.post')}</div>
|
||||
{postDeployGates.map((gate) => (
|
||||
<div key={gate.gate_id} className="rounded border border-gray-800 bg-black/20 px-3 py-2">
|
||||
<div className="text-sm font-semibold text-white">{gate.label}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{gate.owner_agent} · {gate.status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-testid="product-code-review-tool-lanes" className="rounded border border-gray-800 bg-gray-950 p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<PackageSearch className="h-4 w-4 text-amber-300" />
|
||||
{t('tools.title')}
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
{toolLanes.map((tool) => (
|
||||
<div key={tool.tool_id} className="rounded border border-gray-800 bg-black/20 px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-semibold text-white">{tool.label}</div>
|
||||
<div className="font-mono text-xs text-amber-200">{t('tools.candidate')}</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-gray-500">{tool.category} · {tool.integration_status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="code-review-codex-handoff-board"
|
||||
data-testid="code-review-codex-handoff-board"
|
||||
|
||||
Reference in New Issue
Block a user