SPB Git

spb/rareindex

Public
10.0 KB · 191 lines tsx
1import type { Metadata } from 'next';2import Link from 'next/link';3import { readFile } from 'node:fs/promises';4import path from 'node:path';5import { PageHeader } from '@/components/ui/page-header';6import { Card, CardHeader, Table, th, td, tdNum, Badge, EmptyState, Stat } from '@/components/ui/primitives';7import { getSiteStats } from '@/lib/queries/site';8import { getConnectorCoverage, getFxCoverage } from '@/lib/queries/market-lists';9import { getMarketRows } from '@/lib/queries/markets';10import { fmtNum, fmtRelative, cn } from '@/lib/format';11import { catName, humanize } from '@/lib/taxonomy';1213export const metadata: Metadata = { title: 'Data & methodology', description: 'Coverage by category, source and connector; RareIndex methodology; exports and API access.' };14export const revalidate = 300;1516async function loadMethodology(): Promise<string> {17  const candidates = [path.resolve(process.cwd(), '../../docs/METHODOLOGY.md'), path.resolve(process.cwd(), 'docs/METHODOLOGY.md'), path.resolve(process.cwd(), '../docs/METHODOLOGY.md')];18  for (const p of candidates) {19    try {20      return await readFile(p, 'utf8');21    } catch {22      /* try next */23    }24  }25  return '';26}2728/** Minimal markdown → JSX for headings, paragraphs and bullet lists (methodology doc is controlled content). */29function Markdown({ text }: { text: string }) {30  const blocks = text.split(/\n{2,}/);31  return (32    <div className="prose-ri space-y-3 text-[13px] leading-relaxed text-muted">33      {blocks.map((b, i) => {34        const t = b.trim();35        if (!t) return null;36        if (t.startsWith('# ')) return <h2 key={i} className="text-lg font-semibold text-fg">{t.slice(2)}</h2>;37        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>;38        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>;39        return <p key={i} dangerouslySetInnerHTML={{ __html: inline(t.replace(/\n/g, ' ')) }} />;40      })}41    </div>42  );43}44function inline(s: string): string {45  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').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>');46}4748export default async function DataPage() {49  const [stats, connectors, fx, rows, md] = await Promise.all([getSiteStats(), getConnectorCoverage(), getFxCoverage(), getMarketRows(), loadMethodology()]);50  const active = rows.filter((r) => r.counts.assets > 0);51  return (52    <div>53      <PageHeader title="Data" description="What RareIndex covers today, where it comes from, and how the numbers are computed. Coverage figures are live counts." />54      <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">55        {[56          ['Assets', fmtNum(stats.assets)],57          ['Sales', fmtNum(stats.sales)],58          ['Observations', fmtNum(stats.observations)],59          ['Active listings', fmtNum(stats.activeListings)],60          ['Sources', fmtNum(stats.sources)],61          ['Connectors', fmtNum(stats.connectors)],62          ['Categories with data', `${active.length}/${rows.length}`],63          ['FX rates', fx.rows ? `${fmtNum(fx.rows)} · ${fx.first}→${fx.last}` : '—'],64        ].map(([k, v]) => (65          <div key={k} className="bg-elevated px-3 py-2.5">66            <Stat label={k} value={v} />67          </div>68        ))}69      </section>7071      <Card className="mt-4 overflow-hidden">72        <CardHeader title="Connectors & sources" subtitle="Every source, what it contributes and its health. Trust scores weight sources in valuations." />73        {connectors.length ? (74          <Table>75            <thead>76              <tr>77                <th className={th}>Connector</th>78                <th className={th}>Source</th>79                <th className={th}>Type</th>80                <th className={th}>Contributes</th>81                <th className={th}>Categories</th>82                <th className={cn(th, 'text-right')}>Trust</th>83                <th className={cn(th, 'text-right')}>Raw</th>84                <th className={cn(th, 'text-right')}>Sales</th>85                <th className={cn(th, 'text-right')}>Listings</th>86                <th className={cn(th, 'text-right')}>Observations</th>87                <th className={th}>Health</th>88                <th className={th}>Last success</th>89              </tr>90            </thead>91            <tbody>92              {connectors.map((c) => (93                <tr key={c.id} className="hover:bg-sunken">94                  <td className={cn(td, 'font-medium')}>{c.displayName}</td>95                  <td className={td}>96                    {c.sourceUrl ? (97                      <a href={c.sourceUrl} target="_blank" rel="noopener nofollow" className="text-muted hover:text-fg">98                        {c.sourceName} ↗99                      </a>100                    ) : (101                      c.sourceName102                    )}103                  </td>104                  <td className={cn(td, 'text-muted')}>{humanize(c.sourceType)}</td>105                  <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>106                  <td className={cn(td, 'max-w-[220px] truncate text-[11px] text-muted')} title={c.categories.map(catName).join(', ')}>107                    {c.categories.map(catName).join(', ')}108                  </td>109                  <td className={tdNum}>{Math.round(c.trustScore * 100)}%</td>110                  <td className={tdNum}>{fmtNum(c.rawRecords)}</td>111                  <td className={tdNum}>{fmtNum(c.sales)}</td>112                  <td className={tdNum}>{fmtNum(c.listings)}</td>113                  <td className={tdNum}>{fmtNum(c.observations)}</td>114                  <td className={td}>115                    <Badge tone={c.healthStatus === 'healthy' ? 'gain' : c.healthStatus === 'degraded' ? 'alert' : c.healthStatus === 'failing' ? 'loss' : 'neutral'}>{c.healthStatus ?? c.status}</Badge>116                  </td>117                  <td className={cn(td, 'text-muted')}>{c.lastSuccessAt ? fmtRelative(c.lastSuccessAt) : 'never'}</td>118                </tr>119              ))}120            </tbody>121          </Table>122        ) : (123          <EmptyState title="No connectors registered yet" description="Connectors are declared in the registry and mirrored here after seeding." />124        )}125      </Card>126127      <Card className="mt-4 overflow-hidden">128        <CardHeader title="Coverage by category" subtitle="Live counts from canonical tables" />129        <Table>130          <thead>131            <tr>132              <th className={th}>Category</th>133              <th className={cn(th, 'text-right')}>Assets</th>134              <th className={cn(th, 'text-right')}>Priced</th>135              <th className={cn(th, 'text-right')}>Sales</th>136              <th className={cn(th, 'text-right')}>Listings</th>137              <th className={cn(th, 'text-right')}>Observations</th>138              <th className={th}>Last sale</th>139            </tr>140          </thead>141          <tbody>142            {[...rows].sort((a, b) => b.counts.assets - a.counts.assets).map((r) => (143              <tr key={r.node.slug} className={cn('hover:bg-sunken', r.counts.assets === 0 && 'text-subtle')}>144                <td className={td}>145                  <Link href={`/markets/${r.node.slug}`} className="hover:underline">146                    {r.node.name}147                  </Link>148                  <Badge className="ml-2">P{r.node.phase}</Badge>149                </td>150                <td className={tdNum}>{fmtNum(r.counts.assets)}</td>151                <td className={tdNum}>{fmtNum(r.counts.priced)}</td>152                <td className={tdNum}>{fmtNum(r.counts.sales)}</td>153                <td className={tdNum}>{fmtNum(r.counts.listings)}</td>154                <td className={tdNum}>{fmtNum(r.counts.observations)}</td>155                <td className={cn(td, 'text-muted')}>{r.counts.lastSaleAt ? fmtRelative(r.counts.lastSaleAt) : '—'}</td>156              </tr>157            ))}158          </tbody>159        </Table>160      </Card>161162      <div className="mt-4 grid gap-4 lg:grid-cols-[2fr_1fr]">163        <Card className="p-5" as="article">164          <div id="methodology" />165          {md ? <Markdown text={md} /> : <EmptyState title="Methodology document not found" />}166        </Card>167        <Card className="p-5 self-start">168          <h2 className="text-sm font-semibold">Exports & API</h2>169          <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>170          <ul className="mt-3 space-y-1 text-[12px]">171            <li>172              <code className="rounded-sm bg-inset px-1">GET /v1/assets/:id/sales?format=csv</code>173            </li>174            <li>175              <code className="rounded-sm bg-inset px-1">GET /v1/assets/:id/history?format=csv</code>176            </li>177            <li>178              <code className="rounded-sm bg-inset px-1">GET /v1/indices/:ticker/history?format=json</code>179            </li>180          </ul>181          <Link href="/api-docs" className="mt-3 inline-block text-xs font-medium text-fg underline">182            API documentation →183          </Link>184          <h3 className="mt-5 text-sm font-semibold">Corrections</h3>185          <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>186        </Card>187      </div>188    </div>189  );190}191
Files · 5925a21568bf6709687297c36d2900f34fc3f57a
Menu

Theme