import type { Metadata } from 'next'; import Link from 'next/link'; import { ActionButton, AdminFilters, AdminTitle, JsonPre, KindChip, Mono, Notice, StatusChip, Trunc } from '@/components/admin/ui'; import { EntityBadge, TierBadge } from '@/components/ui/badges'; import { LiveAgo } from '@/components/ui/live'; import { Pagination, withParams } from '@/components/ui/pagination'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { keepConflictSideAction, reviewAction } from '@/lib/admin/actions'; import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api'; import type { ReviewItem } from '@/lib/admin/types'; import { fmtDateTime, fmtInt, fmtValue, num } from '@/lib/format'; import { propertyLabel, routes } from '@/lib/site'; export const metadata: Metadata = { title: 'Review queue', robots: { index: false, follow: false } }; export const dynamic = 'force-dynamic'; const LIMIT = 25; const STATUSES = ['pending', 'approved', 'rejected', 'edited', 'all']; const KINDS = ['conflict', 'merge_candidate', 'parser_breakage', 'blocked_source']; function host(url: unknown): string { if (typeof url !== 'string') return '—'; try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; } } /** Conflict payload `{ property, current, claimed, current_source, claimed_source, tier }` rendered as two columns, each with a "Keep" button. */ function ConflictView({ item, ret }: { item: ReviewItem; ret: string }) { const p = item.payload ?? {}; const property = typeof p.property === 'string' ? p.property : null; const entity = item.entities?.[0]; const sides = [ { key: 'current', title: 'Current value', value: p.current, source: p.current_source, tier: p.current_tier ?? null }, { key: 'claimed', title: 'Claimed by new source', value: p.claimed, source: p.claimed_source, tier: p.tier ?? p.claimed_tier ?? null }, ]; return (
{sides.map((s) => (

{s.title}

{fmtValue(s.value, property ?? undefined)}

{typeof s.source === 'string' ? ( {host(s.source)} ) : ( source unknown )} {num(s.tier) !== null && }

{item.status === 'pending' && property && entity && (
Keep this
)}
))}
); } export default async function AdminReviewPage({ searchParams }: { searchParams: Promise> }) { await requireAdmin(); const sp = await searchParams; const current: Record = { status: sp.status ?? 'pending' }; if (sp.kind) current.kind = sp.kind; if (sp.offset) current.offset = sp.offset; const offset = Math.max(0, Number(current.offset) || 0); const href = (patch: Record) => withParams('/admin/review', current, patch); const ret = href({}); const res = await load(adminApi.review({ status: current.status, kind: current.kind, limit: LIMIT, offset })); const byKind = res.ok ? (res.data.by_kind ?? []).filter((k) => k.status === (current.status === 'all' ? k.status : current.status)) : []; return ( <> {byKind.length > 0 && (
    {byKind.map((k) => (
  • {k.kind} {fmtInt(k.n)} {current.status === 'all' && }
  • ))}
)} ({ value: s, label: s })) }, { kind: 'select', name: 'kind', label: 'Kind', value: current.kind, options: KINDS.map((k) => ({ value: k, label: k })) }, ]} /> {!res.ok ? ( ) : res.data.items.length === 0 ? ( ) : ( <>
    {res.data.items.map((item) => { const p = item.payload ?? {}; const property = typeof p.property === 'string' ? p.property : null; return (
  • {property && {propertyLabel(property)}} {item.id}
    {item.reason &&

    } {item.entities && item.entities.length > 0 && (
      {item.entities.map((e) => (
    • {e.name} {e.slug} {e.status !== 'active' && }
    • ))}
    )} {item.kind === 'conflict' && } {item.kind === 'merge_candidate' && item.entities && item.entities.length > 1 && item.status === 'pending' && (

    Approve merges {item.entity_ids[0]} into {item.entity_ids[1]} (first two ids); pick directions explicitly under Duplicates when the order is wrong.

    )}
    Payload{item.resolution ? ' · resolution' : ''}
    {item.resolution && }
    {item.status === 'pending' && (
    Approve
    Reject
    )}
  • ); })}
href({ offset: o || undefined })} className="mt-4" /> )} ); }