TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import Link from 'next/link';2import { requireAdmin } from '@/lib/admin/auth';3import { dashboard } from '@/lib/admin/queries';4import { queueDepths } from '@/lib/admin/queue';5import { adminLogout } from '@/lib/admin/actions';6import { AdminShell, Kpi, StatusPill, fmtTs, n } from '@/components/admin/shell';7import { Table, th, td, tdNum } from '@/components/ui/primitives';8import { getRouter } from '@rareindex/ai';910export default async function AdminHome() {11 await requireAdmin();12 const [{ counts, recentRuns, recentEvents }, queues] = await Promise.all([dashboard(), queueDepths()]);13 const c = (counts ?? {}) as Record<string, unknown>;14 const ai = getRouter();15 return (16 <AdminShell17 current="/admin"18 title="Pipeline overview"19 subtitle="Counts, lag and freshness across ingestion, canonical data and analytics"20 actions={21 <form action={adminLogout}>22 <button type="submit" className="rounded-md border border-border px-2.5 py-1 text-xs hover:bg-inset">Sign out</button>23 </form>24 }25 >26 <div className="grid grid-cols-2 gap-3 md:grid-cols-4 xl:grid-cols-6">27 <Kpi label="Assets" value={n(c.assets)} sub={`${n(c.variants)} variants`} />28 <Kpi label="Sales" value={n(c.sales)} sub={`${n(c.sales_flagged)} flagged · ${n(c.sales_excluded)} excluded`} />29 <Kpi label="Live listings" value={n(c.listings_live)} />30 <Kpi label="Price observations" value={n(c.observations)} />31 <Kpi label="Raw records" value={n(c.raw_records)} sub={`${n(c.raw_unprocessed)} unprocessed · ${n(c.raw_errors)} errors`} tone={Number(c.raw_errors) > 0 ? 'alert' : undefined} />32 <Kpi label="Normalized pending" value={n(c.normalized_pending)} sub={`${n(c.normalized_unmatched)} unmatched`} tone={Number(c.normalized_pending) > 1000 ? 'alert' : undefined} />33 <Kpi label="Connectors" value={`${n(c.connectors_active)} active`} sub={`${n(c.connectors_paused)} paused · ${n(c.connectors_unhealthy)} unhealthy`} tone={Number(c.connectors_unhealthy) > 0 ? 'loss' : undefined} />34 <Kpi label="Taxonomy proposals" value={n(c.proposals_pending)} sub="pending review" />35 <Kpi label="Latest fetch" value={fmtTs(c.latest_fetch).slice(0, 16)} />36 <Kpi label="Latest sale" value={fmtTs(c.latest_sale).slice(0, 10)} sub={`valuation ${fmtTs(c.latest_valuation).slice(0, 16)}`} />37 <Kpi label="Latest index date" value={c.latest_index ? String(c.latest_index).slice(0, 10) : '—'} />38 <Kpi label="Spend 24h" value={`$${n(c.cost_24h_usd, 2)}`} sub={`FC ${n(c.firecrawl_credits_24h)} · SF ${n(c.scrapfly_credits_24h)} credits`} />39 </div>4041 <div className="mt-6 grid gap-6 lg:grid-cols-3">42 <section className="card lg:col-span-2">43 <header className="flex items-center justify-between border-b border-border px-4 py-2.5">44 <h2 className="text-sm font-semibold">Recent connector runs</h2>45 <Link href="/admin/connectors" className="text-xs text-muted hover:text-fg">Control center →</Link>46 </header>47 <Table>48 <thead><tr><th className={th}>Connector</th><th className={th}>Status</th><th className={th}>Trigger</th><th className={th}>Started</th><th className={`${th} text-right`}>Pages</th><th className={`${th} text-right`}>Raw</th><th className={`${th} text-right`}>Norm.</th><th className={`${th} text-right`}>Dup.</th><th className={th}>Error</th></tr></thead>49 <tbody>50 {recentRuns.length === 0 ? <tr><td className={td} colSpan={9}><span className="text-subtle">No runs yet. Trigger one from the control center.</span></td></tr> : null}51 {recentRuns.map((r) => (52 <tr key={String(r.id)}>53 <td className={td}><Link href={`/admin/connectors/${String(r.connector_id)}`} className="font-medium hover:underline">{String(r.connector_id)}</Link></td>54 <td className={td}><StatusPill status={String(r.status)} /></td>55 <td className={td}>{String(r.trigger)}</td>56 <td className={td}>{fmtTs(r.started_at)}</td>57 <td className={tdNum}>{n(r.pages_success)}/{n(r.pages_attempted)}</td>58 <td className={tdNum}>{n(r.records_raw)}</td>59 <td className={tdNum}>{n(r.records_normalized)}</td>60 <td className={tdNum}>{n(r.records_duplicate)}</td>61 <td className={`${td} max-w-[240px] truncate text-loss`} title={String(r.error ?? '')}>{r.error ? String(r.error) : ''}</td>62 </tr>63 ))}64 </tbody>65 </Table>66 </section>67 <div className="space-y-6">68 <section className="card">69 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Queues (pg-boss)</h2></header>70 <ul className="divide-y divide-border text-xs">71 {queues.length === 0 ? <li className="px-4 py-3 text-subtle">Queue tables not initialised yet (the worker creates them on first start).</li> : null}72 {queues.map((q) => (73 <li key={q.name} className="flex items-center justify-between px-4 py-2"><span className="font-mono">{q.name}</span><span className="num text-muted">{q.queued} queued · {q.active} active · {q.failed} failed</span></li>74 ))}75 </ul>76 </section>77 <section className="card">78 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Events (24h)</h2></header>79 <ul className="divide-y divide-border text-xs">80 {recentEvents.length === 0 ? <li className="px-4 py-3 text-subtle">No events yet.</li> : null}81 {recentEvents.map((e) => (82 <li key={String(e.type)} className="flex items-center justify-between px-4 py-2"><span className="font-mono">{String(e.type)}</span><span className="num text-muted">{n(e.n)}</span></li>83 ))}84 </ul>85 </section>86 <section className="card">87 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">AI model routing</h2></header>88 <ul className="divide-y divide-border text-xs">89 {ai.table().map((r) => (90 <li key={r.role} className="flex items-center justify-between px-4 py-2"><span>{r.role}</span><span className="font-mono text-muted">{r.provider ? `${r.provider} · ${r.model}` : 'not configured'}</span></li>91 ))}92 </ul>93 </section>94 </div>95 </div>96 </AdminShell>97 );98}99