TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import { unstable_cache } from 'next/cache';34import Link from 'next/link';5import { readFile } from 'node:fs/promises';6import path from 'node:path';7import { PageHeader } from '@/components/ui/page-header';8import { Card, CardHeader, Table, th, td, tdNum, Badge, EmptyState, Stat } from '@/components/ui/primitives';9import { getSiteStats } from '@/lib/queries/site';10import { getConnectorCoverage, getFxCoverage } from '@/lib/queries/market-lists';11import { getMarketRows } from '@/lib/queries/markets';12import { fmtNum, fmtRelative, cn } from '@/lib/format';13import { catName, humanize } from '@/lib/taxonomy';1415export const metadata: Metadata = { title: 'Data & methodology', description: 'Coverage by category, source and connector; RareIndex methodology; exports and API access.' };16// Coverage aggregates over millions of rows take ~70 s: prerender at build (staticPageGenerationTimeout raised), then hourly ISR.17// Rendered on demand and cached for an hour at runtime: the coverage aggregates run over millions of18// rows and exceeded the 240 s prerender budget at build time, which broke deployments.19export const dynamic = 'force-dynamic';20const loadDataPage = unstable_cache(async () => Promise.all([getSiteStats(), getConnectorCoverage(), getFxCoverage(), getMarketRows()]), ['data-page-v1'], { revalidate: 3600 });2122async function loadMethodology(): Promise<string> {23 const candidates = [path.resolve(process.cwd(), '../../docs/METHODOLOGY.md'), path.resolve(process.cwd(), 'docs/METHODOLOGY.md'), path.resolve(process.cwd(), '../docs/METHODOLOGY.md')];24 for (const p of candidates) {25 try {26 return await readFile(p, 'utf8');27 } catch {28 /* try next */29 }30 }31 return '';32}3334/** Minimal markdown → JSX for headings, paragraphs and bullet lists (methodology doc is controlled content). */35function Markdown({ text }: { text: string }) {36 const blocks = text.split(/\n{2,}/);37 return (38 <div className="prose-ri space-y-3 text-[13px] leading-relaxed text-muted">39 {blocks.map((b, i) => {40 const t = b.trim();41 if (!t) return null;42 if (t.startsWith('# ')) return <h2 key={i} className="text-lg font-semibold text-fg">{t.slice(2)}</h2>;43 if (t.startsWith('## ')) return <h3 key={i} id={t.slice(3).toLowerCase().replace(/[^a-z0-9]+/g, '-')} className="pt-2 text-sm font-semibold text-fg">{t.slice(3)}</h3>;44 if (t.startsWith('### ')) return <h4 key={i} id={t.slice(4).toLowerCase().replace(/[^a-z0-9]+/g, '-')} className="pt-1 text-[12px] font-semibold uppercase tracking-wider text-subtle">{t.slice(4)}</h4>;45 if (t.startsWith('- ')) return <ul key={i} className="list-disc space-y-1 pl-5">{t.split('\n').map((l, j) => <li key={j} dangerouslySetInnerHTML={{ __html: inline(l.replace(/^- /, '')) }} />)}</ul>;46 return <p key={i} dangerouslySetInnerHTML={{ __html: inline(t.replace(/\n/g, ' ')) }} />;47 })}48 </div>49 );50}51function inline(s: string): string {52 return s.replace(/&/g, '&').replace(/</g, '<').replace(/\*\*(.+?)\*\*/g, '<strong class="text-fg">$1</strong>').replace(/\*(.+?)\*/g, '<em>$1</em>').replace(/`(.+?)`/g, '<code class="rounded-sm bg-inset px-1">$1</code>');53}5455export default async function DataPage() {56 const [[stats, connectors, fx, rows], md] = await Promise.all([loadDataPage(), loadMethodology()]);57 const active = rows.filter((r) => r.counts.assets > 0);58 return (59 <div>60 <PageHeader kicker="Transparency" title="Data" description="What RareIndex covers today, where it comes from, and how the numbers are computed. Coverage figures are live counts." />61 <section className="grid grid-cols-2 gap-px overflow-hidden rounded-md border border-border bg-border sm:grid-cols-4 lg:grid-cols-8">62 {[63 ['Assets', fmtNum(stats.assets)],64 ['Sales', fmtNum(stats.sales)],65 ['Observations', fmtNum(stats.observations)],66 ['Active listings', fmtNum(stats.activeListings)],67 ['Sources', fmtNum(stats.sources)],68 ['Connectors', fmtNum(stats.connectors)],69 ['Categories with data', `${active.length}/${rows.length}`],70 ['FX rates', fx.rows ? `${fmtNum(fx.rows)} · ${fx.first}→${fx.last}` : '—'],71 ].map(([k, v]) => (72 <div key={k} className="bg-elevated px-3 py-2.5">73 <Stat label={k} value={v} />74 </div>75 ))}76 </section>7778 <Card className="mt-4 overflow-hidden">79 <CardHeader title="Connectors & sources" subtitle="Every source, what it contributes and its health. Trust scores weight sources in valuations." />80 {connectors.length ? (81 <Table>82 <thead>83 <tr>84 <th className={th}>Connector</th>85 <th className={th}>Source</th>86 <th className={th}>Type</th>87 <th className={th}>Contributes</th>88 <th className={th}>Categories</th>89 <th className={cn(th, 'text-right')}>Trust</th>90 <th className={cn(th, 'text-right')}>Raw</th>91 <th className={cn(th, 'text-right')}>Sales</th>92 <th className={cn(th, 'text-right')}>Listings</th>93 <th className={cn(th, 'text-right')}>Observations</th>94 <th className={th}>Health</th>95 <th className={th}>Last success</th>96 </tr>97 </thead>98 <tbody>99 {connectors.map((c) => (100 <tr key={c.id} className="hover:bg-sunken">101 <td className={cn(td, 'font-medium')}>{c.displayName}</td>102 <td className={td}>103 {c.sourceUrl ? (104 <a href={c.sourceUrl} target="_blank" rel="noopener nofollow" className="text-muted hover:text-fg">105 {c.sourceName} ↗106 </a>107 ) : (108 c.sourceName109 )}110 </td>111 <td className={cn(td, 'text-muted')}>{humanize(c.sourceType)}</td>112 <td className={cn(td, 'text-[11px] text-muted')}>{[c.supportsSold && 'sales', c.supportsListings && 'listings', c.supportsCatalog && 'catalog', c.supportsAuctions && 'auctions', c.supportsPopulation && 'population'].filter(Boolean).join(' · ') || '—'}</td>113 <td className={cn(td, 'max-w-[220px] truncate text-[11px] text-muted')} title={c.categories.map(catName).join(', ')}>114 {c.categories.map(catName).join(', ')}115 </td>116 <td className={tdNum}>{Math.round(c.trustScore * 100)}%</td>117 <td className={tdNum}>{fmtNum(c.rawRecords)}</td>118 <td className={tdNum}>{fmtNum(c.sales)}</td>119 <td className={tdNum}>{fmtNum(c.listings)}</td>120 <td className={tdNum}>{fmtNum(c.observations)}</td>121 <td className={td}>122 <Badge tone={c.healthStatus === 'healthy' ? 'gain' : c.healthStatus === 'degraded' ? 'alert' : c.healthStatus === 'failing' ? 'loss' : 'neutral'}>{c.healthStatus ?? c.status}</Badge>123 </td>124 <td className={cn(td, 'text-muted')}>{c.lastSuccessAt ? fmtRelative(c.lastSuccessAt) : 'never'}</td>125 </tr>126 ))}127 </tbody>128 </Table>129 ) : (130 <EmptyState title="No connectors registered yet" description="Connectors are declared in the registry and mirrored here after seeding." />131 )}132 </Card>133134 <Card className="mt-4 overflow-hidden">135 <CardHeader title="Coverage by category" subtitle="Live counts from canonical tables" />136 <Table>137 <thead>138 <tr>139 <th className={th}>Category</th>140 <th className={cn(th, 'text-right')}>Assets</th>141 <th className={cn(th, 'text-right')}>Priced</th>142 <th className={cn(th, 'text-right')}>Sales</th>143 <th className={cn(th, 'text-right')}>Listings</th>144 <th className={cn(th, 'text-right')}>Observations</th>145 <th className={th}>Last sale</th>146 </tr>147 </thead>148 <tbody>149 {[...rows].sort((a, b) => b.counts.assets - a.counts.assets).map((r) => (150 <tr key={r.node.slug} className={cn('hover:bg-sunken', r.counts.assets === 0 && 'text-subtle')}>151 <td className={td}>152 <Link href={`/markets/${r.node.slug}`} className="hover:underline">153 {r.node.name}154 </Link>155 <Badge className="ml-2">P{r.node.phase}</Badge>156 </td>157 <td className={tdNum}>{fmtNum(r.counts.assets)}</td>158 <td className={tdNum}>{fmtNum(r.counts.priced)}</td>159 <td className={tdNum}>{fmtNum(r.counts.sales)}</td>160 <td className={tdNum}>{fmtNum(r.counts.listings)}</td>161 <td className={tdNum}>{fmtNum(r.counts.observations)}</td>162 <td className={cn(td, 'text-muted')}>{r.counts.lastSaleAt ? fmtRelative(r.counts.lastSaleAt) : '—'}</td>163 </tr>164 ))}165 </tbody>166 </Table>167 </Card>168169 <div className="mt-4 grid gap-4 lg:grid-cols-[2fr_1fr]">170 <Card className="p-5" as="article">171 <div id="methodology" />172 {md ? <Markdown text={md} /> : <EmptyState title="Methodology document not found" />}173 </Card>174 <Card className="p-5 self-start">175 <h2 className="text-sm font-semibold">Exports & API</h2>176 <p className="mt-1 text-[12px] leading-relaxed text-muted">Professional and research users can export comparable sales, price histories and index series as CSV or JSON through the public API. Each export carries source attribution and the timestamp of the underlying observations.</p>177 <ul className="mt-3 space-y-1 text-[12px]">178 <li>179 <code className="rounded-sm bg-inset px-1">GET /v1/assets/:id/sales?format=csv</code>180 </li>181 <li>182 <code className="rounded-sm bg-inset px-1">GET /v1/assets/:id/history?format=csv</code>183 </li>184 <li>185 <code className="rounded-sm bg-inset px-1">GET /v1/indices/:ticker/history?format=json</code>186 </li>187 </ul>188 <Link href="/api-docs" className="mt-3 inline-block text-xs font-medium text-fg underline">189 API documentation →190 </Link>191 <h3 className="mt-5 text-sm font-semibold">Corrections</h3>192 <p className="mt-1 text-[12px] leading-relaxed text-muted">Spotted a misidentified item, a duplicate or a wrong price? Every record links to its source; write to data@rareindex.io with the asset URL and we will review it against the audit log.</p>193 </Card>194 </div>195 </div>196 );197}198