feat(governance): add remediation dry run entrypoint
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { ShieldCheck, AlertTriangle } from 'lucide-react'
|
||||
import { ShieldCheck, AlertTriangle, PlayCircle, SearchCheck } from 'lucide-react'
|
||||
import { SloKpiCard, type SloMetric } from '@/components/governance/slo-kpi-card'
|
||||
import { SloViolationChart, type ViolationDataPoint } from '@/components/governance/slo-violation-chart'
|
||||
import { GlassCard } from '@/components/ui/glass-card'
|
||||
@@ -144,6 +144,28 @@ interface SummaryApiResponse {
|
||||
days?: number
|
||||
}
|
||||
|
||||
interface RemediationDryRunResponse {
|
||||
mode?: string
|
||||
allowed?: boolean
|
||||
executed?: boolean
|
||||
verification_result_preview?: string
|
||||
post_state_summary?: {
|
||||
tool_count?: number
|
||||
tools?: string[]
|
||||
has_state?: boolean
|
||||
}
|
||||
mcp_route?: {
|
||||
agent_id?: string
|
||||
tool_name?: string
|
||||
required_scope?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
interface RemediationActionState {
|
||||
status: 'loading' | 'done' | 'error'
|
||||
data?: RemediationDryRunResponse
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helpers
|
||||
// =============================================================================
|
||||
@@ -220,6 +242,16 @@ function compactLabel(value?: string | null, fallback = '--'): string {
|
||||
return value.length > 54 ? `${value.slice(0, 54)}...` : value
|
||||
}
|
||||
|
||||
async function requestRemediationDryRun(workItemId: string): Promise<RemediationDryRunResponse> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/ai/slo/remediation/dry-run`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ work_item_id: workItemId, mode: 'auto' }),
|
||||
})
|
||||
if (!response.ok) throw new Error(`dry_run_failed:${response.status}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
function buildMetrics(api: SloApiResponse): SloMetric[] {
|
||||
const adr100Metrics = api.adr100?.metrics
|
||||
if (adr100Metrics?.length) {
|
||||
@@ -276,6 +308,7 @@ function buildMetrics(api: SloApiResponse): SloMetric[] {
|
||||
|
||||
function VerificationCoveragePanel({ coverage }: { coverage?: Adr100VerificationCoverage }) {
|
||||
const t = useTranslations('governance.slo.coverage')
|
||||
const [actionState, setActionState] = useState<Record<string, RemediationActionState>>({})
|
||||
const color = coverageTone(coverage?.status)
|
||||
const rows = [
|
||||
{ label: t('totalAuto'), value: String(coverage?.total_auto ?? '--') },
|
||||
@@ -287,6 +320,25 @@ function VerificationCoveragePanel({ coverage }: { coverage?: Adr100Verification
|
||||
const recentFindings = coverage?.recent_non_success ?? []
|
||||
const remediationQueue = coverage?.remediation_queue
|
||||
|
||||
const handleDryRun = async (workItemId: string) => {
|
||||
setActionState(prev => ({
|
||||
...prev,
|
||||
[workItemId]: { status: 'loading' },
|
||||
}))
|
||||
try {
|
||||
const data = await requestRemediationDryRun(workItemId)
|
||||
setActionState(prev => ({
|
||||
...prev,
|
||||
[workItemId]: { status: 'done', data },
|
||||
}))
|
||||
} catch {
|
||||
setActionState(prev => ({
|
||||
...prev,
|
||||
[workItemId]: { status: 'error' },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
@@ -379,7 +431,7 @@ function VerificationCoveragePanel({ coverage }: { coverage?: Adr100Verification
|
||||
{(remediationQueue.items ?? []).slice(0, 4).map(item => (
|
||||
<div key={item.work_item_id} style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(130px, 0.8fr) minmax(180px, 1fr) minmax(160px, 1fr)',
|
||||
gridTemplateColumns: 'minmax(130px, 0.8fr) minmax(180px, 1fr) minmax(150px, 0.9fr) minmax(150px, 0.8fr)',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
@@ -410,6 +462,62 @@ function VerificationCoveragePanel({ coverage }: { coverage?: Adr100Verification
|
||||
{compactLabel(item.remediation_reason)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ minWidth: 0, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { void handleDryRun(item.work_item_id) }}
|
||||
disabled={actionState[item.work_item_id]?.status === 'loading'}
|
||||
title={t('dryRunButton')}
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
minHeight: 28,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '5px 9px',
|
||||
borderRadius: 6,
|
||||
border: '0.5px solid rgba(20,20,19,0.14)',
|
||||
background: actionState[item.work_item_id]?.status === 'loading'
|
||||
? 'rgba(135,134,127,0.08)'
|
||||
: 'rgba(34,197,94,0.08)',
|
||||
color: '#141413',
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 10,
|
||||
cursor: actionState[item.work_item_id]?.status === 'loading' ? 'wait' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<PlayCircle size={13} style={{ color: '#22C55E', flexShrink: 0 }} />
|
||||
<span>{actionState[item.work_item_id]?.status === 'loading' ? t('dryRunLoading') : t('dryRunButton')}</span>
|
||||
</button>
|
||||
{actionState[item.work_item_id]?.status === 'done' && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 5,
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 9,
|
||||
color: actionState[item.work_item_id].data?.allowed === false ? '#7c5a10' : '#166534',
|
||||
lineHeight: 1.4,
|
||||
overflowWrap: 'anywhere',
|
||||
}}>
|
||||
<SearchCheck size={12} style={{ flexShrink: 0, marginTop: 1 }} />
|
||||
<span>
|
||||
{actionState[item.work_item_id].data?.allowed === false
|
||||
? t('dryRunBlocked')
|
||||
: t('dryRunResult', {
|
||||
mode: actionState[item.work_item_id].data?.mode ?? '--',
|
||||
result: actionState[item.work_item_id].data?.verification_result_preview ?? '--',
|
||||
tools: actionState[item.work_item_id].data?.post_state_summary?.tool_count ?? 0,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{actionState[item.work_item_id]?.status === 'error' && (
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 9, color: '#FF3300', lineHeight: 1.4 }}>
|
||||
{t('dryRunError')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user