SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
7.4 KB · 123 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { RelativeTime } from "@/components/ui/freshness";4import { Page, PageHeader, Section } from "@/components/ui/section";5import { RightsBadge, StatusBadge } from "@/components/ui/status-badge";6import { api } from "@/lib/api";7import { ASSET_CLASS_LABEL } from "@/lib/format";8import type { Source } from "@/lib/types";910export const metadata: Metadata = { title: "Sources", description: "Directory of the exchanges, regulators, central banks and datasets Market Atlas observes, with rights and real-time classification.", alternates: { canonical: "/sources" } };11export const dynamic = "force-dynamic";1213const CATEGORY_LABEL: Record<string, string> = { EXCHANGE: "Exchanges", REGULATOR: "Regulators", CENTRAL_BANK: "Central banks", GOVERNMENT: "Government", ISSUER: "Issuer websites", PUBLIC_MARKET_SOURCE: "Public market sources", OFFICIAL_DATASET: "Official & licensed datasets", NEWS: "News", INTERNAL: "Market Atlas internal" };14const RT_LABEL: Record<string, string> = { REALTIME: "real time", DELAYED: "delayed", END_OF_DAY: "end of day", INDICATIVE: "indicative", UNKNOWN: "unknown", STALE: "stale" };1516export default async function SourcesPage() {17  const sources = (await api<Source[]>("/v1/sources")).filter((s) => s.category !== "INTERNAL");18  const cats = [...new Set(sources.map((s) => s.category))];19  return (20    <Page wide>21      <PageHeader kicker="Provenance" title="Sources" lead={`${sources.length} organizations feed the atlas through ${sources.reduce((a, s) => a + s.connectors, 0)} connectors. Each source declares how its data may be used and how fresh it is; endpoints are not published to discourage abuse.`} actions={22          <>23            <Link href="/coverage" className="text-sm text-accent hover:underline">24              Source coverage →25            </Link>26            <Link href="/connectors" className="text-sm text-accent hover:underline">27              Connector operations →28            </Link>29          </>30        } />31      {cats.map((cat) => (32        <Section key={cat} title={CATEGORY_LABEL[cat] ?? cat}>33          <div className="overflow-x-auto rounded-md border border-rule bg-surface">34            <table className="table-dense">35              <thead>36                <tr>37                  <th>Source</th>38                  <th>Status</th>39                  <th>Role</th>40                  <th>Type</th>41                  <th>Rights</th>42                  <th>Freshness</th>43                  <th className="text-right">Coverage</th>44                  <th className="text-right">Reliability</th>45                  <th>Last observation</th>46                </tr>47              </thead>48              <tbody>49                {sources50                  .filter((s) => s.category === cat)51                  .map((s) => (52                    <tr key={s.id} id={s.id}>53                      <td className="max-w-[360px] whitespace-normal">54                        <div className="font-medium">55                          {s.homepage ? (56                            <a href={s.homepage} target="_blank" rel="noopener noreferrer" className="hover:underline">57                              {s.name}58                            </a>59                          ) : (60                            s.name61                          )}62                        </div>63                        <div className="text-[11.5px] text-ink-3">64                          {s.organization}65                          {s.jurisdiction ? ` · ${s.jurisdiction}` : ""}66                          {s.asset_classes.length ? ` · ${s.asset_classes.map((c) => ASSET_CLASS_LABEL[c] ?? c).join(", ")}` : ""}67                        </div>68                        {s.description && <div className="mt-1 text-xs text-ink-2">{s.description}</div>}69                        {s.rights_notes && (70                          <div className="mt-1 text-[11px] text-ink-3">71                            {s.rights_notes}72                            {s.terms_url && (73                              <>74                                {" "}75                                <a href={s.terms_url} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">76                                  terms ↗77                                </a>78                              </>79                            )}80                          </div>81                        )}82                      </td>83                      <td>84                        <StatusBadge status={s.status} />85                      </td>86                      <td className="text-xs">87                        {s.role === "validator" ? (88                          <span className="inline-flex h-5 items-center rounded-[3px] bg-surface-3 px-1.5 text-[10.5px] font-medium uppercase tracking-wide text-ink-2" title="Restricted rights: compared to the consensus, never sets the price, never displayed">89                            validator90                          </span>91                        ) : (92                          <span className="inline-flex h-5 items-center rounded-[3px] bg-positive-soft px-1.5 text-[10.5px] font-medium uppercase tracking-wide text-positive" title="Votes in the consensus and is redistributed with attribution">93                            contributor94                          </span>95                        )}96                        {s.likely_shared_upstream_with && s.likely_shared_upstream_with.length > 0 && (97                          <div className="mt-1 max-w-[180px] whitespace-normal text-[10.5px] text-warning">98                            likely shares an upstream with {s.likely_shared_upstream_with.join(", ")} — counted as one family99                          </div>100                        )}101                      </td>102                      <td className="mono text-xs text-ink-2">{s.source_type}</td>103                      <td>104                        <RightsBadge status={s.rights_status} />105                      </td>106                      <td className="text-xs text-ink-2">{RT_LABEL[s.realtime_status] ?? s.realtime_status}</td>107                      <td className="num">{s.coverage.toLocaleString("en-US")}</td>108                      <td className="num">{s.reliability_score ?? "—"}</td>109                      <td className="text-xs text-ink-3">{s.last_observation ? <RelativeTime value={s.last_observation} /> : "—"}</td>110                    </tr>111                  ))}112              </tbody>113            </table>114          </div>115        </Section>116      ))}117      <p className="mt-6 text-xs text-ink-3">118        Role: <strong>contributors</strong> vote in the consensus and are redistributed with attribution; <strong>validators</strong> are sources whose data rights forbid redistribution — Market Atlas compares them to the canonical value to confirm or contest it, but they never set the price and are never displayed. Sources that statistically mirror each other (identical values within seconds) are counted as one family; see <Link href="/coverage" className="text-accent hover:underline">source coverage</Link>. Coverage = instruments this source has published a value for in the last 24 hours. Reliability = operational score (availability, latency, parse success, stability), see <Link href="/methodology" className="text-accent hover:underline">methodology</Link>. Rights statuses are explained on the <Link href="/licensing" className="text-accent hover:underline">data rights</Link> page.119      </p>120    </Page>121  );122}123