'use client'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'; import { PageHeader } from '@/components/navigation/page-header'; interface Candidate { ciId: string; confidence: string | null; hostname: string | null; serial: string | null; mac: string | null; companyId: string | null; companyName: string | null; isDeleted: boolean; } interface Review { id: string; detectedAt: string; xref: { id: string; source: string; sourceId: string; hostname: string | null; serial: string | null; mac: string | null; companyId: string | null; companyName: string | null; lastSeenAt: string | null; }; candidates: Candidate[]; } const SOURCES = ['all', 'datto_rmm', 'itglue', 's1', 'veeam'] as const; type SourceFilter = (typeof SOURCES)[number]; function confidenceColor(c: string | null): 'default' | 'secondary' | 'outline' { if (c === 'exact_serial') return 'default'; if (c === 'mac') return 'default'; if (c === 'hostname_in_company') return 'secondary'; return 'outline'; } export default function DeviceLinkConflictsPage() { const [items, setItems] = useState(null); const [total, setTotal] = useState(0); const [error, setError] = useState(null); const [source, setSource] = useState('all'); const [resolving, setResolving] = useState(null); async function load(): Promise { setError(null); setItems(null); try { const params = new URLSearchParams({ limit: '100' }); if (source !== 'all') params.set('source', source); const res = await fetch(`/api/admin/device-link-conflicts?${params}`); if (!res.ok) { const data = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(data.error ?? `Request failed: ${res.status}`); } const data = (await res.json()) as { items: Review[]; total: number }; setItems(data.items); setTotal(data.total); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } } useEffect(() => { void load(); }, [source]); async function resolve(reviewId: string, ciId: string): Promise { setResolving(`${reviewId}:${ciId}`); try { const res = await fetch(`/api/admin/device-link-conflicts/${reviewId}/resolve`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ciId }), }); const data = (await res.json().catch(() => ({}))) as { error?: string }; if (!res.ok) throw new Error(data.error ?? `Request failed: ${res.status}`); toast.success(`Linked to CI ${ciId}`); setItems((prev) => prev?.filter((r) => r.id !== reviewId) ?? null); setTotal((t) => Math.max(0, t - 1)); } catch (err) { toast.error(err instanceof Error ? err.message : 'Resolve failed'); } finally { setResolving(null); } } return ( <>
Device-link conflicts

Cases where the reconciler found two or more configuration_items matching one external device record. Pick the right CI to break the tie. Skipped rows stay unlinked until resolved.

Source: {total} unresolved {total === 1 ? 'conflict' : 'conflicts'}
{error && ( Failed to load {error} )} {items === null && !error && (
)} {items !== null && items.length === 0 && !error && ( No conflicts Nothing waiting for review on this filter. )} {items?.map((r) => (
{r.xref.source}:{r.xref.sourceId}
{r.xref.hostname ?? '(no hostname)'} {r.xref.companyName && ( @ {r.xref.companyName} )}
{r.candidates.length} candidates
{r.xref.serial && serial: {r.xref.serial}} {r.xref.mac && mac: {r.xref.mac}} {r.xref.lastSeenAt && ( last seen: {new Date(r.xref.lastSeenAt).toLocaleString()} )}
{r.candidates.map((c) => { const isResolving = resolving === `${r.id}:${c.ciId}`; return (
{c.hostname ?? '(no hostname)'} {c.isDeleted && ( deleted )} {c.confidence && ( {c.confidence} )}
CI {c.ciId} {c.serial && serial: {c.serial}} {c.companyName && @ {c.companyName}}
); })}
))}
); }