import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getDiscoveredCertificates, getDiscoverySummary, getDiscoveryScans, claimDiscoveredCertificate, dismissDiscoveredCertificate, getAgents, } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { DiscoveredCertificate, DiscoveryScan } from '../api/types'; /** Map agent_id to a human-readable source type badge. */ function sourceTypeBadge(agentId: string): { label: string; style: string } { switch (agentId) { case 'server-scanner': return { label: 'Network', style: 'bg-blue-100 text-blue-800' }; case 'cloud-aws-sm': return { label: 'AWS SM', style: 'bg-orange-100 text-orange-800' }; case 'cloud-azure-kv': return { label: 'Azure KV', style: 'bg-sky-100 text-sky-800' }; case 'cloud-gcp-sm': return { label: 'GCP SM', style: 'bg-green-100 text-green-800' }; default: return { label: 'Filesystem', style: 'bg-gray-100 text-gray-800' }; } } function ClaimModal({ cert, onClose, onClaim }: { cert: DiscoveredCertificate; onClose: () => void; onClaim: (managedCertId: string) => void }) { const [managedCertId, setManagedCertId] = useState(''); return (
e.stopPropagation()}>

Claim Certificate

Link {cert.common_name} to a managed certificate

setManagedCertId(e.target.value)} placeholder="e.g., mc-api-prod" className="w-full border border-surface-border rounded px-3 py-2 text-sm text-ink bg-white font-mono focus:outline-none focus:ring-2 focus:ring-brand-500" />

Enter the ID of the managed certificate this discovered cert belongs to.

); } function ScanHistoryPanel({ scans }: { scans: DiscoveryScan[] }) { if (scans.length === 0) return

No scans recorded yet

; return (
{scans.map(s => ( ))}
Agent Directories Found New Errors Duration Started
{s.agent_id} {s.directories?.join(', ') || '—'} {s.certificates_found} {s.certificates_new} {s.errors_count > 0 ? {s.errors_count} : '0'} {s.scan_duration_ms}ms {formatDateTime(s.started_at)}
); } export default function DiscoveryPage() { const [statusFilter, setStatusFilter] = useState(''); const [agentFilter, setAgentFilter] = useState(''); const [claimingCert, setClaimingCert] = useState(null); const [showScans, setShowScans] = useState(false); const params: Record = {}; if (statusFilter) params.status = statusFilter; if (agentFilter) params.agent_id = agentFilter; const { data, isLoading, error, refetch } = useQuery({ queryKey: ['discovered-certificates', params], queryFn: () => getDiscoveredCertificates(params), refetchInterval: 30000, }); const { data: summary } = useQuery({ queryKey: ['discovery-summary'], queryFn: getDiscoverySummary, refetchInterval: 30000, }); const { data: scansData } = useQuery({ queryKey: ['discovery-scans'], queryFn: () => getDiscoveryScans(), enabled: showScans, }); const { data: agentsData } = useQuery({ queryKey: ['agents-for-filter'], queryFn: () => getAgents({ per_page: '200' }), }); // Phase 2 TQ-M3 closure: claim + dismiss with optimistic updates. // Each one flips the row's status in the ['discovered-certificates'] // cache immediately so the visual response is sub-100ms regardless // of network RTT. Rollback restores the snapshot + fires a Sonner // error toast. const queryClient = useQueryClient(); type DiscSnapshot = { prev?: { data: DiscoveredCertificate[]; total: number } | undefined; }; const claimMutation = useTrackedMutation({ mutationFn: ({ id, managedCertId }) => claimDiscoveredCertificate(id, managedCertId), invalidates: [['discovered-certificates'], ['discovery-summary']], onMutate: async ({ id }): Promise => { await queryClient.cancelQueries({ queryKey: ['discovered-certificates'] }); const prev = queryClient.getQueryData(['discovered-certificates']) as DiscSnapshot['prev']; if (prev) { queryClient.setQueryData(['discovered-certificates'], { ...prev, data: prev.data.map((c) => (c.id === id ? { ...c, status: 'Managed' as const } : c)), }); } return { prev }; }, onError: (err, _vars, snap) => { if (snap?.prev) queryClient.setQueryData(['discovered-certificates'], snap.prev); toast.error(`Claim failed: ${err.message}`); }, onSuccess: () => { toast.success('Certificate claimed'); setClaimingCert(null); }, }); const dismissMutation = useTrackedMutation({ mutationFn: dismissDiscoveredCertificate, invalidates: [['discovered-certificates'], ['discovery-summary']], onMutate: async (id): Promise => { await queryClient.cancelQueries({ queryKey: ['discovered-certificates'] }); const prev = queryClient.getQueryData(['discovered-certificates']) as DiscSnapshot['prev']; if (prev) { queryClient.setQueryData(['discovered-certificates'], { ...prev, data: prev.data.map((c) => (c.id === id ? { ...c, status: 'Dismissed' as const } : c)), }); } return { prev }; }, onError: (err, _id, snap) => { if (snap?.prev) queryClient.setQueryData(['discovered-certificates'], snap.prev); toast.error(`Dismiss failed: ${err.message}`); }, onSuccess: () => toast.success('Discovery dismissed'), }); const formatExpiry = (notAfter?: string) => { if (!notAfter) return '—'; const d = new Date(notAfter); const now = new Date(); const days = Math.floor((d.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); if (days < 0) return Expired {Math.abs(days)}d ago; if (days < 30) return {days}d left; return {days}d left; }; const discoveryStatusStyle: Record = { Unmanaged: 'badge badge-warning', Managed: 'badge badge-success', Dismissed: 'badge badge-neutral', }; const columns: Column[] = [ { key: 'common_name', label: 'Common Name', render: (c) => (
{c.common_name || '(no CN)'}
{c.sans?.length > 0 && (
{c.sans.slice(0, 2).join(', ')}{c.sans.length > 2 ? ` +${c.sans.length - 2}` : ''}
)}
), }, { key: 'status', label: 'Status', render: (c) => {c.status}, }, { key: 'source', label: 'Source', render: (c) => { const badge = sourceTypeBadge(c.agent_id); return (
{badge.label}
{c.source_path}
); }, }, { key: 'issuer', label: 'Issuer', render: (c) => {c.issuer_dn?.split(',')[0] || '—'}, }, { key: 'expiry', label: 'Expiry', render: (c) => {formatExpiry(c.not_after)}, }, { key: 'key_info', label: 'Key', render: (c) => (
{c.key_algorithm}{c.key_size ? ` ${c.key_size}` : ''} {c.is_ca && ( CA )}
), }, { key: 'fingerprint', label: 'Fingerprint', render: (c) => {c.fingerprint_sha256?.substring(0, 16)}..., }, { key: 'actions', label: '', render: (c) => ( c.status === 'Unmanaged' ? (
) : null ), }, ]; return ( <> {/* Summary stats bar */} {summary && (
{summary.Unmanaged || 0} Unmanaged
{summary.Managed || 0} Managed
{summary.Dismissed || 0} Dismissed
)} {/* Scan history collapsible */} {showScans && (

Recent Scans

)} {/* Filters */}
{/* Table */}
{error ? ( refetch()} /> ) : ( )}
{claimingCert && ( setClaimingCert(null)} onClaim={(managedCertId) => claimMutation.mutate({ id: claimingCert.id, managedCertId })} /> )} ); }