feat(drift): surface fingerprint state handoff
This commit is contained in:
@@ -14,7 +14,7 @@ import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Diff, RefreshCw, AlertTriangle, CheckCircle2,
|
||||
Clock, Terminal, GitMerge, Info,
|
||||
Clock, Terminal, GitMerge, Info, Fingerprint, GitPullRequest,
|
||||
} from 'lucide-react'
|
||||
|
||||
// =============================================================================
|
||||
@@ -30,7 +30,7 @@ interface DriftReport {
|
||||
medium_count: number
|
||||
info_count: number
|
||||
interpretation: string | null
|
||||
status: 'pending' | 'resolved' | 'ignored'
|
||||
status: 'pending' | 'acknowledged' | 'rolled_back' | 'adopted' | 'ignored'
|
||||
created_at: string
|
||||
resolved_at: string | null
|
||||
}
|
||||
@@ -45,6 +45,39 @@ interface ScanResult {
|
||||
interpretation: string | null
|
||||
}
|
||||
|
||||
interface DriftFingerprintState {
|
||||
namespace?: string
|
||||
fingerprint?: string
|
||||
latest_report_id?: string
|
||||
latest_status?: string
|
||||
summary?: string
|
||||
occurrences_12h?: number
|
||||
fsm_state?: string
|
||||
next_step?: string
|
||||
high_count?: number
|
||||
medium_count?: number
|
||||
info_count?: number
|
||||
open_pr?: {
|
||||
number?: number | string | null
|
||||
state?: string | null
|
||||
file_count?: number | null
|
||||
commit_count?: number | null
|
||||
is_zero_diff?: boolean | null
|
||||
lookup_error?: string | null
|
||||
} | null
|
||||
latest_handoff?: {
|
||||
handoff_status?: string | null
|
||||
} | null
|
||||
p0_escalation?: {
|
||||
suppresses_repeated_p0?: boolean | null
|
||||
dedup_window_hours?: number | null
|
||||
} | null
|
||||
writes_drift_status?: boolean | null
|
||||
writes_incident_state?: boolean | null
|
||||
writes_auto_repair_result?: boolean | null
|
||||
writes_ticket?: boolean | null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helpers
|
||||
// =============================================================================
|
||||
@@ -107,7 +140,9 @@ function DriftLevelBadge({ high, medium, info, t }: {
|
||||
function StatusBadge({ status, t }: { status: DriftReport['status']; t: (k: string) => string }) {
|
||||
const styles: Record<string, string> = {
|
||||
pending: 'bg-status-warning/10 text-status-warning border-status-warning/20',
|
||||
resolved: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20',
|
||||
acknowledged: 'bg-neutral-100 text-neutral-500 border-neutral-200',
|
||||
rolled_back: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20',
|
||||
adopted: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20',
|
||||
ignored: 'bg-neutral-100 text-neutral-400 border-neutral-200',
|
||||
}
|
||||
return (
|
||||
@@ -120,6 +155,63 @@ function StatusBadge({ status, t }: { status: DriftReport['status']; t: (k: stri
|
||||
)
|
||||
}
|
||||
|
||||
function DriftFingerprintStateCard({
|
||||
state,
|
||||
t,
|
||||
}: {
|
||||
state: DriftFingerprintState | null
|
||||
t: (k: string, values?: Record<string, string | number>) => string
|
||||
}) {
|
||||
if (!state) return null
|
||||
return (
|
||||
<div className="mx-6 mt-4 border border-neutral-200 bg-neutral-50 px-4 py-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<Fingerprint size={15} className="mt-0.5 shrink-0 text-neutral-500" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[12px] font-semibold text-neutral-800">
|
||||
{t('fingerprintState.title')}
|
||||
</p>
|
||||
<p className="mt-1 truncate font-mono text-[11px] text-neutral-500">
|
||||
{state.fingerprint ?? '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="border border-status-warning/20 bg-status-warning/10 px-2 py-0.5 text-[11px] font-medium text-status-warning">
|
||||
{t('fingerprintState.occurrences', { count: state.occurrences_12h ?? 0 })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 text-[11px] text-neutral-500 md:grid-cols-2">
|
||||
<p>{t('fingerprintState.report', { report: state.latest_report_id ?? '--' })}</p>
|
||||
<p>{t('fingerprintState.state', { state: state.fsm_state ?? '--' })}</p>
|
||||
<p>{t('fingerprintState.next', { step: state.next_step ?? '--' })}</p>
|
||||
<p>
|
||||
{t('fingerprintState.writes', {
|
||||
drift: String(state.writes_drift_status ?? false),
|
||||
incident: String(state.writes_incident_state ?? false),
|
||||
repair: String(state.writes_auto_repair_result ?? false),
|
||||
ticket: String(state.writes_ticket ?? false),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-[11px] text-neutral-500">
|
||||
<span className="inline-flex items-center gap-1 border border-neutral-200 bg-white px-2 py-0.5">
|
||||
<GitPullRequest size={11} />
|
||||
{t('fingerprintState.pr', {
|
||||
pr: state.open_pr?.number ?? '--',
|
||||
zeroDiff: String(state.open_pr?.is_zero_diff ?? false),
|
||||
})}
|
||||
</span>
|
||||
<span className="border border-neutral-200 bg-white px-2 py-0.5">
|
||||
{t('fingerprintState.p0Dedup', {
|
||||
hours: state.p0_escalation?.dedup_window_hours ?? 24,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DriftPanel
|
||||
// =============================================================================
|
||||
@@ -130,6 +222,7 @@ export function DriftPanel() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [scanning, setScanning] = useState(false)
|
||||
const [scanResult, setScanResult] = useState<ScanResult | null>(null)
|
||||
const [fingerprintState, setFingerprintState] = useState<DriftFingerprintState | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchReports = useCallback(async () => {
|
||||
@@ -139,7 +232,17 @@ export function DriftPanel() {
|
||||
const res = await fetch(`${getApiBase()}/api/v1/drift/reports?limit=20`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const data = await res.json()
|
||||
setReports(data.items ?? [])
|
||||
const nextReports = data.items ?? []
|
||||
setReports(nextReports)
|
||||
const latestReportId = nextReports[0]?.report_id
|
||||
if (latestReportId) {
|
||||
const stateRes = await fetch(
|
||||
`${getApiBase()}/api/v1/drift/fingerprints/state?report_id=${encodeURIComponent(latestReportId)}`
|
||||
)
|
||||
setFingerprintState(stateRes.ok ? await stateRes.json() : null)
|
||||
} else {
|
||||
setFingerprintState(null)
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to fetch')
|
||||
} finally {
|
||||
@@ -252,6 +355,8 @@ export function DriftPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DriftFingerprintStateCard state={fingerprintState} t={t} />
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 px-6 py-4">
|
||||
{loading && reports.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user