TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import Link from 'next/link';2import { requireAdmin } from '@/lib/admin/auth';3import { dataQuality, matchCandidatesForRecord } from '@/lib/admin/queries';4import { manualMatchAction, saleStatusAction } from '@/lib/admin/actions';5import { AdminShell, ActionButton, Kpi, StatusPill, fmtTs, n } from '@/components/admin/shell';6import { Table, th, td, tdNum } from '@/components/ui/primitives';78export default async function DataQualityPage({ searchParams }: { searchParams: Promise<{ match?: string }> }) {9 await requireAdmin();10 const sp = await searchParams;11 const dq = await dataQuality();12 const q = (dq.quality ?? {}) as Record<string, unknown>;13 const matching = sp.match ? await matchCandidatesForRecord(sp.match) : null;14 return (15 <AdminShell current="/admin/data-quality" title="Data quality" subtitle="Flagged/excluded sales with audit reasons, unmatched records with manual matching, cross-listing duplicates. Nothing is deleted; every change is written to the audit log.">16 <div className="grid grid-cols-2 gap-3 md:grid-cols-4">17 <Kpi label="Sales data quality (avg)" value={n(q.sales_avg, 1)} sub={`p10 ${n(q.sales_p10, 1)}`} />18 <Kpi label="Assets data quality (avg)" value={n(q.assets_avg, 1)} />19 <Kpi label="Low-confidence sales" value={n(q.low_confidence_sales)} sub="confidence < 0.6" />20 <Kpi label="Flagged / excluded" value={dq.flagged.length} sub="latest 100 shown" />21 </div>2223 {matching?.record ? (24 <section className="card mt-6 border-index/40">25 <header className="flex items-center justify-between border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Manual match</h2><Link href="/admin/data-quality" className="text-xs text-muted hover:text-fg">close</Link></header>26 <div className="p-4 text-sm">27 <p className="font-medium">{String((matching.record.payload as { rawTitle?: string }).rawTitle ?? matching.record.id)}</p>28 <ul className="mt-3 divide-y divide-border text-xs">29 {matching.candidates.length === 0 ? <li className="py-2 text-subtle">No similar asset titles. Reject or leave for the resolver.</li> : null}30 {matching.candidates.map((c) => (31 <li key={String(c.id)} className="flex items-center justify-between gap-3 py-1.5">32 <span className="truncate"><Link href={`/asset/${String(c.slug)}`} className="hover:underline">{String(c.title)}</Link> <span className="text-subtle">{String(c.category_slug)} · {Math.round(Number(c.score) * 100)}%</span></span>33 <form action={manualMatchAction}><input type="hidden" name="recordId" value={String(matching.record!.id)} /><input type="hidden" name="assetId" value={String(c.id)} /><ActionButton label="match" tone="primary" /></form>34 </li>35 ))}36 </ul>37 <form action={manualMatchAction} className="mt-3 flex items-center gap-2 text-xs"><input type="hidden" name="recordId" value={String(matching.record.id)} /><input name="assetId" placeholder="or paste an asset id (rare_…)" className="flex-1 rounded-md border border-border bg-sunken px-2 py-1" /><ActionButton label="match id" /></form>38 <form action={manualMatchAction} className="mt-2"><input type="hidden" name="recordId" value={String(matching.record.id)} /><input type="hidden" name="assetId" value="" /><ActionButton label="reject record" tone="danger" /></form>39 </div>40 </section>41 ) : null}4243 <section className="card mt-6">44 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Flagged & excluded sales</h2></header>45 <Table>46 <thead><tr><th className={th}>Asset</th><th className={th}>Status</th><th className={th}>Flags</th><th className={`${th} text-right`}>Price</th><th className={th}>Date</th><th className={th}>Source</th><th className={th}>Last reason</th><th className={th}>Actions</th></tr></thead>47 <tbody>48 {dq.flagged.length === 0 ? <tr><td className={td} colSpan={8}><span className="text-subtle">No flagged or excluded sales.</span></td></tr> : null}49 {dq.flagged.map((s) => (50 <tr key={String(s.id)}>51 <td className={`${td} max-w-[260px] truncate`}><Link href={`/asset/${String(s.asset_slug)}`} className="hover:underline">{String(s.title)}</Link><div className="truncate text-[10px] text-subtle">{String(s.raw_title)}</div></td>52 <td className={td}><StatusPill status={String(s.status)} /></td>53 <td className={`${td} font-mono text-[10px]`}>{(s.flags as string[]).join(', ')}</td>54 <td className={tdNum}>{n(s.price, 2)} {String(s.currency)}<div className="text-[10px] text-subtle">${n(s.price_usd)}</div></td>55 <td className={td}>{fmtTs(s.sale_date).slice(0, 10)}</td>56 <td className={td}><a href={String(s.source_url)} target="_blank" rel="noreferrer noopener" className="hover:underline">{String(s.source_id)}</a></td>57 <td className={`${td} max-w-[220px] truncate`} title={String(s.last_reason ?? '')}>{String(s.last_reason ?? '')}</td>58 <td className={td}>59 <div className="flex gap-1">60 {(['valid', 'flagged', 'excluded'] as const).filter((x) => x !== s.status).map((x) => (61 <form key={x} action={saleStatusAction} className="flex items-center gap-1"><input type="hidden" name="id" value={String(s.id)} /><input type="hidden" name="status" value={x} /><input type="hidden" name="reason" value={`admin review → ${x}`} /><ActionButton label={x === 'valid' ? 'restore' : x} tone={x === 'excluded' ? 'danger' : 'neutral'} /></form>62 ))}63 </div>64 </td>65 </tr>66 ))}67 </tbody>68 </Table>69 </section>7071 <section className="card mt-6">72 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Unmatched / rejected normalized records</h2></header>73 <Table>74 <thead><tr><th className={th}>Title</th><th className={th}>Connector</th><th className={th}>Kind</th><th className={th}>Category</th><th className={`${th} text-right`}>Price</th><th className={th}>Reason</th><th className={th}>Created</th><th className={th}></th></tr></thead>75 <tbody>76 {dq.unmatched.length === 0 ? <tr><td className={td} colSpan={8}><span className="text-subtle">Nothing waiting for a manual decision.</span></td></tr> : null}77 {dq.unmatched.map((r) => (78 <tr key={String(r.id)}>79 <td className={`${td} max-w-[300px] truncate`}><a href={String(r.source_url ?? '#')} target="_blank" rel="noreferrer noopener" className="hover:underline">{String(r.raw_title ?? r.id)}</a></td>80 <td className={td}>{String(r.connector_id)}</td>81 <td className={td}>{String(r.kind)}</td>82 <td className={td}>{String(r.category_slug ?? '—')}</td>83 <td className={tdNum}>{r.price ? `${n(r.price, 2)} ${String(r.currency ?? '')}` : '—'}</td>84 <td className={`${td} max-w-[200px] truncate`} title={String(r.reject_reason ?? '')}>{String(r.reject_reason ?? '')}</td>85 <td className={td}>{fmtTs(r.created_at)}</td>86 <td className={td}><Link href={`/admin/data-quality?match=${String(r.id)}`} className="rounded-md border border-border px-2 py-0.5 text-[11px] font-medium hover:bg-inset">match</Link></td>87 </tr>88 ))}89 </tbody>90 </Table>91 </section>9293 <section className="card mt-6">94 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Cross-listing groups</h2></header>95 <Table>96 <thead><tr><th className={th}>Group</th><th className={`${th} text-right`}>Listings</th><th className={th}>Sources</th><th className={th}>Sample</th></tr></thead>97 <tbody>98 {dq.duplicates.length === 0 ? <tr><td className={td} colSpan={4}><span className="text-subtle">No cross-listing groups detected yet.</span></td></tr> : null}99 {dq.duplicates.map((d) => <tr key={String(d.cross_listing_group_id)}><td className={`${td} font-mono text-[11px]`}>{String(d.cross_listing_group_id)}</td><td className={tdNum}>{n(d.n)}</td><td className={td}>{(d.sources as string[]).join(', ')}</td><td className={`${td} max-w-[320px] truncate`}>{String(d.sample_title)}</td></tr>)}100 </tbody>101 </Table>102 </section>103 </AdminShell>104 );105}106