feat(mcp): add governed cross-product control plane
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
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
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
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
This commit is contained in:
516
apps/web/src/app/[locale]/automation/mcp/page.tsx
Normal file
516
apps/web/src/app/[locale]/automation/mcp/page.tsx
Normal file
@@ -0,0 +1,516 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Boxes,
|
||||
CheckCircle2,
|
||||
Database,
|
||||
GitBranch,
|
||||
Network,
|
||||
RefreshCw,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Workflow,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { getRuntimeApiV1BaseUrl } from '@/lib/runtime-api-base'
|
||||
|
||||
type Filter = 'all' | 'existing' | 'candidate' | 'gap'
|
||||
|
||||
interface CapabilitySummary {
|
||||
capability_id: string
|
||||
title: string
|
||||
decision: string
|
||||
risk_tier: string
|
||||
deployment_allowed: boolean
|
||||
}
|
||||
|
||||
interface Capability extends CapabilitySummary {
|
||||
source_type: string
|
||||
catalog_source: string
|
||||
catalog_url?: string | null
|
||||
scope: string
|
||||
data_boundary: string
|
||||
version_state: string
|
||||
digest_state: string
|
||||
sbom_state: string
|
||||
signature_state: string
|
||||
rag_policy: string
|
||||
blockers: string[]
|
||||
}
|
||||
|
||||
interface ProductRow {
|
||||
product_id: string
|
||||
canonical_repo: string
|
||||
source_control_status: string
|
||||
prod_sha_short?: string
|
||||
inventory_state: string
|
||||
existing_capability_ids: string[]
|
||||
recommended_capability_ids: string[]
|
||||
rejected_capability_ids: string[]
|
||||
existing_capabilities?: CapabilitySummary[]
|
||||
recommended_capabilities?: CapabilitySummary[]
|
||||
blockers: string[]
|
||||
}
|
||||
|
||||
interface RuntimeReadback {
|
||||
status: string
|
||||
provider_registry?: {
|
||||
status: string
|
||||
provider_count: number
|
||||
provider_names: string[]
|
||||
tool_count: number
|
||||
}
|
||||
gateway_audit_24h?: {
|
||||
status: string
|
||||
call_count: number
|
||||
success_count: number
|
||||
blocked_count: number
|
||||
failed_count: number
|
||||
body_storage?: string
|
||||
}
|
||||
rag?: {
|
||||
status: string
|
||||
total_chunks: number
|
||||
sources: number
|
||||
}
|
||||
federated_product_runtime_receipt_count?: number
|
||||
federated_product_runtime_receipt_expected?: number
|
||||
}
|
||||
|
||||
interface Overview {
|
||||
generated_at: string
|
||||
status: string
|
||||
summary: {
|
||||
product_count: number
|
||||
mcp_source_detected_product_count: number
|
||||
mcp_source_gap_product_count: number
|
||||
quarantined_or_remediation_product_count: number
|
||||
capability_count: number
|
||||
external_candidate_count: number
|
||||
external_promotion_ready_count: number
|
||||
active_blocker_count: number
|
||||
runtime_provider_count: number
|
||||
runtime_tool_count: number
|
||||
}
|
||||
architecture: {
|
||||
decision: string
|
||||
gateway: string
|
||||
provider_registry: string
|
||||
rag: string
|
||||
separate_gateway_allowed: boolean
|
||||
separate_rag_database_allowed: boolean
|
||||
}
|
||||
security_policy: {
|
||||
external_content_default: string
|
||||
external_rag_auto_write_allowed: boolean
|
||||
gateway_request_body_storage: string
|
||||
distributed_rate_limit_required: boolean
|
||||
}
|
||||
supply_chain_policy: {
|
||||
floating_versions_allowed: boolean
|
||||
npx_yes_allowed: boolean
|
||||
latest_tag_allowed: boolean
|
||||
required_before_canary: string[]
|
||||
}
|
||||
version_lifecycle: {
|
||||
desired_loop: string[]
|
||||
discovery_cadence: string
|
||||
runtime_state: string
|
||||
active_blockers: string[]
|
||||
}
|
||||
runtime: RuntimeReadback
|
||||
capabilities: Capability[]
|
||||
products: ProductRow[]
|
||||
active_blockers: string[]
|
||||
next_actions: string[]
|
||||
}
|
||||
|
||||
const toneForDecision = (decision: string) => {
|
||||
if (decision === 'integrated' || decision.startsWith('federate')) {
|
||||
return 'border-[#8fc29a] bg-[#f0faf2] text-[#17602a]'
|
||||
}
|
||||
if (decision === 'rejected' || decision === 'quarantine') {
|
||||
return 'border-[#efb4a7] bg-[#fff2ef] text-[#a53f2b]'
|
||||
}
|
||||
return 'border-[#e8c77a] bg-[#fff9e9] text-[#7b5815]'
|
||||
}
|
||||
|
||||
const isGap = (product: ProductRow) =>
|
||||
product.inventory_state.startsWith('no_') || product.blockers.length > 0
|
||||
|
||||
function StatusBadge({ value }: { value: string }) {
|
||||
return (
|
||||
<span className={`inline-flex rounded-full border px-2.5 py-1 text-[11px] font-semibold ${toneForDecision(value)}`}>
|
||||
{value.replaceAll('_', ' ')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
label,
|
||||
value,
|
||||
detail,
|
||||
tone = 'neutral',
|
||||
}: {
|
||||
label: string
|
||||
value: number | string
|
||||
detail: string
|
||||
tone?: 'neutral' | 'ok' | 'warn' | 'danger'
|
||||
}) {
|
||||
const toneClass = {
|
||||
neutral: 'text-[#44423d]',
|
||||
ok: 'text-[#17602a]',
|
||||
warn: 'text-[#7b5815]',
|
||||
danger: 'text-[#a53f2b]',
|
||||
}[tone]
|
||||
return (
|
||||
<article className="rounded-2xl border border-[#dedbd1] bg-white p-4 shadow-[0_1px_2px_rgba(20,20,19,0.04)]">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{label}</p>
|
||||
<p className={`mt-2 text-3xl font-bold tracking-tight ${toneClass}`}>{value}</p>
|
||||
<p className="mt-2 text-xs leading-5 text-[#77736a]">{detail}</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default function McpControlPlanePage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('mcpControlPlane')
|
||||
const [overview, setOverview] = useState<Overview | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [filter, setFilter] = useState<Filter>('all')
|
||||
|
||||
const loadOverview = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const base = getRuntimeApiV1BaseUrl()
|
||||
if (!base) {
|
||||
setError(t('states.apiMissing'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${base}/mcp-control-plane/overview`, {
|
||||
cache: 'no-store',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
setOverview(await response.json() as Overview)
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : t('states.unknownError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
void loadOverview()
|
||||
}, [loadOverview])
|
||||
|
||||
const filteredProducts = useMemo(() => {
|
||||
if (!overview) return []
|
||||
if (filter === 'existing') return overview.products.filter(row => row.existing_capability_ids.length > 0)
|
||||
if (filter === 'candidate') return overview.products.filter(row => row.recommended_capability_ids.length > 0)
|
||||
if (filter === 'gap') return overview.products.filter(isGap)
|
||||
return overview.products
|
||||
}, [filter, overview])
|
||||
|
||||
const externalCapabilities = useMemo(
|
||||
() => overview?.capabilities.filter(row => row.source_type === 'external') ?? [],
|
||||
[overview],
|
||||
)
|
||||
|
||||
const runtime = overview?.runtime
|
||||
const gateway = runtime?.gateway_audit_24h
|
||||
const rag = runtime?.rag
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<main className="min-h-[calc(100vh-68px)] overflow-x-hidden bg-[#f5f4ed] text-[#141413]">
|
||||
<div className="mx-auto max-w-[1500px] p-4 sm:p-6 lg:p-8">
|
||||
<header className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="max-w-3xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-[#d8d4c9] bg-white">
|
||||
<Network className="h-5 w-5 text-[#d97757]" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.16em] text-[#77736a]">{t('eyebrow')}</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('title')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-sm leading-6 text-[#5f5c55]">{t('subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadOverview()}
|
||||
disabled={loading}
|
||||
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-[#cbc7bc] bg-white px-4 text-sm font-semibold text-[#44423d] transition hover:border-[#a9a49a] disabled:cursor-wait disabled:opacity-60"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} aria-hidden="true" />
|
||||
{t('refresh')}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="mt-5" aria-live="polite">
|
||||
{loading && !overview ? (
|
||||
<div className="rounded-2xl border border-[#dedbd1] bg-white p-8 text-center text-sm text-[#77736a]">
|
||||
{t('states.loading')}
|
||||
</div>
|
||||
) : error && !overview ? (
|
||||
<div role="alert" className="rounded-2xl border border-[#efb4a7] bg-[#fff2ef] p-5 text-sm text-[#8e3827]">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
{t('states.error')}
|
||||
</div>
|
||||
<p className="mt-2">{error}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{overview ? (
|
||||
<div className="mt-6 space-y-6">
|
||||
<section aria-labelledby="mcp-summary-title">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 id="mcp-summary-title" className="text-lg font-bold">{t('summary.title')}</h2>
|
||||
<p className="mt-1 text-xs text-[#77736a]">
|
||||
{t('summary.generatedAt', { value: overview.generated_at })}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge value={overview.status} />
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-6">
|
||||
<MetricCard label={t('metrics.products')} value={overview.summary.product_count} detail={t('metrics.productsDetail')} />
|
||||
<MetricCard label={t('metrics.detected')} value={overview.summary.mcp_source_detected_product_count} detail={t('metrics.detectedDetail')} tone="ok" />
|
||||
<MetricCard label={t('metrics.gaps')} value={overview.summary.mcp_source_gap_product_count} detail={t('metrics.gapsDetail')} tone="warn" />
|
||||
<MetricCard label={t('metrics.providers')} value={overview.summary.runtime_provider_count} detail={t('metrics.providersDetail', { tools: overview.summary.runtime_tool_count })} tone={runtime?.status === 'ready' ? 'ok' : 'warn'} />
|
||||
<MetricCard label={t('metrics.external')} value={overview.summary.external_candidate_count} detail={t('metrics.externalDetail', { ready: overview.summary.external_promotion_ready_count })} tone="warn" />
|
||||
<MetricCard label={t('metrics.blockers')} value={overview.summary.active_blocker_count} detail={t('metrics.blockersDetail')} tone="danger" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-[1.4fr_1fr]" aria-labelledby="architecture-title">
|
||||
<article className="rounded-2xl border border-[#b9cfea] bg-[#f4f8ff] p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<Workflow className="mt-0.5 h-5 w-5 shrink-0 text-[#356aa0]" aria-hidden="true" />
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-[#356aa0]">{t('architecture.badge')}</p>
|
||||
<h2 id="architecture-title" className="mt-1 text-lg font-bold">{t('architecture.title')}</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-[#4e5965]">{t('architecture.detail')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
{[
|
||||
[t('architecture.gateway'), overview.architecture.gateway],
|
||||
[t('architecture.registry'), overview.architecture.provider_registry],
|
||||
[t('architecture.rag'), overview.architecture.rag],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="rounded-xl border border-[#cfdded] bg-white/80 p-3">
|
||||
<dt className="text-[11px] font-semibold text-[#6a7682]">{label}</dt>
|
||||
<dd className="mt-1 break-words text-xs font-semibold text-[#25384b]">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-[#dedbd1] bg-white p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-[#6d665b]" aria-hidden="true" />
|
||||
<h2 className="text-lg font-bold">{t('runtime.title')}</h2>
|
||||
</div>
|
||||
<dl className="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="rounded-xl bg-[#f6f5ef] p-3">
|
||||
<dt className="text-xs text-[#77736a]">{t('runtime.calls24h')}</dt>
|
||||
<dd className="mt-1 text-xl font-bold">{gateway?.call_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl bg-[#f6f5ef] p-3">
|
||||
<dt className="text-xs text-[#77736a]">{t('runtime.blocked24h')}</dt>
|
||||
<dd className="mt-1 text-xl font-bold text-[#a53f2b]">{gateway?.blocked_count ?? 0}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl bg-[#f6f5ef] p-3">
|
||||
<dt className="text-xs text-[#77736a]">{t('runtime.ragChunks')}</dt>
|
||||
<dd className="mt-1 text-xl font-bold">{rag?.total_chunks ?? 0}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl bg-[#f6f5ef] p-3">
|
||||
<dt className="text-xs text-[#77736a]">{t('runtime.federated')}</dt>
|
||||
<dd className="mt-1 text-xl font-bold">
|
||||
{runtime?.federated_product_runtime_receipt_count ?? 0}/{runtime?.federated_product_runtime_receipt_expected ?? 12}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="mt-3 text-xs leading-5 text-[#77736a]">{t('runtime.hashOnly')}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-[#dedbd1] bg-white p-4 sm:p-5" aria-labelledby="product-matrix-title">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h2 id="product-matrix-title" className="text-lg font-bold">{t('products.title')}</h2>
|
||||
<p className="mt-1 text-sm text-[#77736a]">{t('products.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2" role="group" aria-label={t('products.filterLabel')}>
|
||||
{(['all', 'existing', 'candidate', 'gap'] as Filter[]).map(value => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
aria-pressed={filter === value}
|
||||
onClick={() => setFilter(value)}
|
||||
className={`rounded-lg border px-3 py-2 text-xs font-semibold transition ${filter === value ? 'border-[#d97757] bg-[#fff4ee] text-[#a84e33]' : 'border-[#d8d4c9] bg-white text-[#666159] hover:border-[#aaa59a]'}`}
|
||||
>
|
||||
{t(`products.filters.${value}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredProducts.map(product => (
|
||||
<article key={product.product_id} className="rounded-xl border border-[#e2dfd6] bg-[#fbfaf6] p-4" data-testid={`mcp-product-${product.product_id}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate font-bold">{product.product_id}</h3>
|
||||
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]">{product.canonical_repo}</p>
|
||||
</div>
|
||||
{product.existing_capability_ids.length > 0 ? (
|
||||
<CheckCircle2 className="h-5 w-5 shrink-0 text-[#2f7d54]" aria-label={t('products.hasExisting')} />
|
||||
) : (
|
||||
<AlertTriangle className="h-5 w-5 shrink-0 text-[#9a6a22]" aria-label={t('products.noExisting')} />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<StatusBadge value={product.inventory_state} />
|
||||
<span className="rounded-full border border-[#d8d4c9] bg-white px-2.5 py-1 text-[11px] text-[#666159]">
|
||||
{t('products.source', { value: product.source_control_status })}
|
||||
</span>
|
||||
</div>
|
||||
<dl className="mt-4 grid grid-cols-3 gap-2 text-center">
|
||||
<div className="rounded-lg bg-white p-2"><dt className="text-[10px] text-[#77736a]">{t('products.existing')}</dt><dd className="mt-1 font-bold">{product.existing_capability_ids.length}</dd></div>
|
||||
<div className="rounded-lg bg-white p-2"><dt className="text-[10px] text-[#77736a]">{t('products.recommended')}</dt><dd className="mt-1 font-bold">{product.recommended_capability_ids.length}</dd></div>
|
||||
<div className="rounded-lg bg-white p-2"><dt className="text-[10px] text-[#77736a]">{t('products.blockers')}</dt><dd className="mt-1 font-bold text-[#a53f2b]">{product.blockers.length}</dd></div>
|
||||
</dl>
|
||||
<div className="mt-4 space-y-2">
|
||||
{(product.existing_capabilities ?? []).slice(0, 2).map(item => (
|
||||
<div key={item.capability_id} className="flex items-center justify-between gap-2 text-xs">
|
||||
<span className="min-w-0 truncate text-[#4f4c46]">{item.title}</span>
|
||||
<StatusBadge value={item.decision} />
|
||||
</div>
|
||||
))}
|
||||
{(product.recommended_capabilities ?? []).slice(0, 2).map(item => (
|
||||
<div key={item.capability_id} className="flex items-center justify-between gap-2 text-xs">
|
||||
<span className="min-w-0 truncate text-[#4f4c46]">{item.title}</span>
|
||||
<StatusBadge value={item.decision} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="min-w-0 rounded-2xl border border-[#dedbd1] bg-white p-4 sm:p-5" aria-labelledby="external-title">
|
||||
<div className="flex items-start gap-3">
|
||||
<Search className="mt-0.5 h-5 w-5 text-[#6d665b]" aria-hidden="true" />
|
||||
<div>
|
||||
<h2 id="external-title" className="text-lg font-bold">{t('external.title')}</h2>
|
||||
<p className="mt-1 text-sm text-[#77736a]">{t('external.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 max-w-full overflow-x-auto overscroll-x-contain">
|
||||
<table className="min-w-[980px] w-full border-collapse text-left text-xs">
|
||||
<caption className="sr-only">{t('external.caption')}</caption>
|
||||
<thead>
|
||||
<tr className="border-b border-[#dedbd1] text-[#6c685f]">
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.capability')}</th>
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.source')}</th>
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.decision')}</th>
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.scope')}</th>
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.version')}</th>
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.supply')}</th>
|
||||
<th className="px-3 py-3 font-semibold">{t('external.columns.promote')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{externalCapabilities.map(item => (
|
||||
<tr key={item.capability_id} className="border-b border-[#ece9e1] align-top last:border-0">
|
||||
<td className="px-3 py-3"><div className="font-semibold text-[#302f2b]">{item.title}</div><div className="mt-1 font-mono text-[10px] text-[#8a867e]">{item.capability_id}</div></td>
|
||||
<td className="px-3 py-3 text-[#5f5b54]">{item.catalog_source}</td>
|
||||
<td className="px-3 py-3"><StatusBadge value={item.decision} /></td>
|
||||
<td className="px-3 py-3 text-[#5f5b54]">{item.scope}</td>
|
||||
<td className="px-3 py-3 text-[#5f5b54]">{item.version_state}</td>
|
||||
<td className="px-3 py-3 text-[#5f5b54]">{item.digest_state} / {item.sbom_state} / {item.signature_state}</td>
|
||||
<td className="px-3 py-3">
|
||||
{item.deployment_allowed ? <CheckCircle2 className="h-4 w-4 text-[#2f7d54]" aria-label={t('external.allowed')} /> : <XCircle className="h-4 w-4 text-[#b2432d]" aria-label={t('external.blocked')} />}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[1.35fr_1fr]">
|
||||
<article className="rounded-2xl border border-[#dedbd1] bg-white p-5" aria-labelledby="lifecycle-title">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-5 w-5 text-[#6d665b]" aria-hidden="true" />
|
||||
<h2 id="lifecycle-title" className="text-lg font-bold">{t('lifecycle.title')}</h2>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-[#77736a]">{t('lifecycle.subtitle', { cadence: overview.version_lifecycle.discovery_cadence })}</p>
|
||||
<ol className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{overview.version_lifecycle.desired_loop.map((step, index) => (
|
||||
<li key={step} className="flex items-center gap-3 rounded-xl border border-[#e2dfd6] bg-[#fbfaf6] p-3 text-xs">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#ece8df] font-bold text-[#5f5b54]">{index + 1}</span>
|
||||
<span className="font-semibold text-[#4d4a44]">{step.replaceAll('_', ' ')}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div role="status" className="mt-4 rounded-xl border border-[#e8c77a] bg-[#fff9e9] p-3 text-xs leading-5 text-[#705219]">
|
||||
{t('lifecycle.runtimeState', { value: overview.version_lifecycle.runtime_state })}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-[#dedbd1] bg-white p-5" aria-labelledby="security-title">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-[#2f7d54]" aria-hidden="true" />
|
||||
<h2 id="security-title" className="text-lg font-bold">{t('security.title')}</h2>
|
||||
</div>
|
||||
<ul className="mt-4 space-y-3 text-sm text-[#555149]">
|
||||
<li className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-[#2f7d54]" aria-hidden="true" />{t('security.hashOnly')}</li>
|
||||
<li className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-[#2f7d54]" aria-hidden="true" />{t('security.distributedRateLimit')}</li>
|
||||
<li className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-[#2f7d54]" aria-hidden="true" />{t('security.quarantine')}</li>
|
||||
<li className="flex gap-2"><XCircle className="mt-0.5 h-4 w-4 shrink-0 text-[#b2432d]" aria-hidden="true" />{t('security.github')}</li>
|
||||
<li className="flex gap-2"><Boxes className="mt-0.5 h-4 w-4 shrink-0 text-[#7b5815]" aria-hidden="true" />{t('security.supplyChain')}</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-[#efb4a7] bg-[#fff8f6] p-5" aria-labelledby="blockers-title">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-[#a53f2b]" aria-hidden="true" />
|
||||
<h2 id="blockers-title" className="text-lg font-bold">{t('blockers.title')}</h2>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wide text-[#8e3827]">{t('blockers.active')}</h3>
|
||||
<ul className="mt-2 space-y-2 text-xs leading-5 text-[#6a4a42]">
|
||||
{overview.active_blockers.map(item => <li key={item}>• {item.replaceAll('_', ' ')}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wide text-[#356aa0]">{t('blockers.next')}</h3>
|
||||
<ol className="mt-2 space-y-2 text-xs leading-5 text-[#465463]">
|
||||
{overview.next_actions.map((item, index) => <li key={item}>{index + 1}. {item.replaceAll('_', ' ')}</li>)}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
GitBranch,
|
||||
Network,
|
||||
Wrench,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
@@ -36,6 +37,17 @@ export default function AutomationPage({ params }: { params: { locale: string }
|
||||
const ta = useTranslations('automation')
|
||||
|
||||
const modules: AutomationModule[] = [
|
||||
{
|
||||
id: 'mcp-control-plane',
|
||||
label: t('mcpControlPlane'),
|
||||
description: ta('mcpControlPlaneDesc'),
|
||||
href: `/${params.locale}/automation/mcp`,
|
||||
icon: Network,
|
||||
accentColor: 'text-[#356aa0]',
|
||||
bgColor: 'bg-[#f4f8ff]',
|
||||
borderColor: 'border-[#b9cfea] hover:border-[#356aa0]',
|
||||
badgeColor: 'bg-[#f4f8ff] border-[#b9cfea] text-[#356aa0]',
|
||||
},
|
||||
{
|
||||
id: 'auto-repair',
|
||||
label: t('autoRepair'),
|
||||
@@ -84,7 +96,7 @@ export default function AutomationPage({ params }: { params: { locale: string }
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-[#77736a]">
|
||||
AI Automation
|
||||
{ta('eyebrow')}
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold text-[#141413]">
|
||||
{t('knowledgeAutomation')}
|
||||
@@ -97,7 +109,7 @@ export default function AutomationPage({ params }: { params: { locale: string }
|
||||
</div>
|
||||
|
||||
{/* Module cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{modules.map(({ id, label, description, href, icon: Icon, accentColor, bgColor, borderColor, badgeColor }) => (
|
||||
<Link
|
||||
key={id}
|
||||
|
||||
@@ -105,10 +105,11 @@ export const PRODUCT_NAV_SECTIONS: ProductNavSection[] = [
|
||||
href: '/knowledge-base',
|
||||
labelKey: 'knowledgeAutomation',
|
||||
Icon: BookOpen,
|
||||
aliases: ['/knowledge', '/automation', '/auto-repair', '/drift', '/neural-command'],
|
||||
aliases: ['/knowledge', '/automation', '/automation/mcp', '/auto-repair', '/drift', '/neural-command'],
|
||||
children: [
|
||||
{ id: 'knowledge', href: '/knowledge', labelKey: 'knowledge' },
|
||||
{ id: 'automation', href: '/automation', labelKey: 'automation' },
|
||||
{ id: 'mcp-control-plane', href: '/automation/mcp', labelKey: 'mcpControlPlane' },
|
||||
{ id: 'auto-repair', href: '/auto-repair', labelKey: 'autoRepair' },
|
||||
{ id: 'drift', href: '/drift', labelKey: 'drift' },
|
||||
{ id: 'neural-command', href: '/neural-command', labelKey: 'neuralCommand' },
|
||||
|
||||
Reference in New Issue
Block a user