SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
6.1 KB · 122 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Check } from 'lucide-react';4import { PageHeader, Section, Note } from '@/components/ui/section';5import { Badge, StatusBadge } from '@/components/ui/badge';6import { EmptyState } from '@/components/ui/empty-state';7import { Freshness } from '@/components/ui/freshness';8import { listSources, COVERAGE_DOMAINS, sourceDomains } from '@/lib/queries/sources';9import { fmtDate, fmtInt, humanize, relativeTime } from '@/lib/format';1011export const metadata: Metadata = { title: 'Sources', description: 'Public catalog of every data source: license status, access, coverage matrix and connector health.' };12export const revalidate = 600;1314export default async function SourcesPage() {15  const sources = await listSources();16  return (17    <div>18      <PageHeader kicker="Sources" title="Source registry" lede="Every fact on CancerIndex traces back to one of these providers. A connector goes live only after its license is reviewed; sources under review are listed but contribute no public records." />19      {sources.length === 0 ? (20        <EmptyState title="No source registered">Run `pnpm cix sources:sync` to seed the registry from connector manifests.</EmptyState>21      ) : (22        <div className="space-y-10">23          <Section id="coverage" kicker="Coverage matrix" title="Sources × data domains" description="A check means the source is declared to contribute to that domain (from its manifest). It does not imply records exist yet — see each source page for counts.">24            <div className="ci-table-wrap">25              <table className="ci-table">26                <thead>27                  <tr>28                    <th className="sticky-col">Source</th>29                    {COVERAGE_DOMAINS.map((d) => (30                      <th key={d} className="text-center">31                        {d}32                      </th>33                    ))}34                    <th>License</th>35                    <th>Status</th>36                  </tr>37                </thead>38                <tbody>39                  {sources.map((s) => {40                    const dom = sourceDomains(s);41                    return (42                      <tr key={s.slug}>43                        <td className="sticky-col min-w-[200px]">44                          <Link className="ci-link font-medium" href={`/source/${s.slug}`}>45                            {s.name}46                          </Link>47                          <span className="block text-[11.5px] text-ink-3">{s.organization ?? humanize(s.category)}</span>48                        </td>49                        {COVERAGE_DOMAINS.map((d) => (50                          <td key={d} className="text-center">51                            {dom.has(d) ? <Check className="mx-auto h-4 w-4 text-accent" aria-label={`covers ${d}`} /> : <span className="text-ink-4" aria-label={`does not cover ${d}`}>·</span>}52                          </td>53                        ))}54                        <td>55                          <Badge tone={s.license_status === 'approved' ? 'ok' : s.license_status === 'blocked' ? 'danger' : 'warn'} title={s.license ?? undefined}>56                            {s.license_status}57                          </Badge>58                        </td>59                        <td>60                          <StatusBadge status={s.status} />61                        </td>62                      </tr>63                    );64                  })}65                </tbody>66              </table>67            </div>68          </Section>6970          <Section id="catalog" kicker="Catalog" title="All sources" description={`${fmtInt(sources.length)} registered sources.`}>71            <div className="ci-table-wrap">72              <table className="ci-table">73                <thead>74                  <tr>75                    <th>Source</th>76                    <th>Category</th>77                    <th>Access</th>78                    <th>License</th>79                    <th>Redistribution</th>80                    <th>Health</th>81                    <th>Last success</th>82                    <th className="num">Records</th>83                    <th>Dataset version</th>84                  </tr>85                </thead>86                <tbody>87                  {sources.map((s) => (88                    <tr key={s.slug}>89                      <td className="min-w-[200px]">90                        <Link className="ci-link" href={`/source/${s.slug}`}>91                          {s.name}92                        </Link>93                        <span className="ci-mono ml-1 text-[10.5px] text-ink-4">{s.slug}</span>94                      </td>95                      <td>{humanize(s.category)}</td>96                      <td className="text-[12.5px]">97                        {s.access_type} · {humanize(s.access_auth)}98                      </td>99                      <td className="text-[12.5px]">{s.license ?? <span className="text-ink-4">—</span>}</td>100                      <td className="text-[12.5px]">{humanize(s.redistribution)}</td>101                      <td>102                        <StatusBadge status={s.health ?? 'unknown'} />103                      </td>104                      <td className="whitespace-nowrap text-[12.5px]" title={s.last_success_at ? fmtDate(s.last_success_at) : undefined}>105                        {s.last_success_at ? relativeTime(s.last_success_at) : '—'}106                      </td>107                      <td className="num">{fmtInt(s.record_count)}</td>108                      <td className="ci-mono text-[11.5px]">{s.last_dataset_version ?? '—'}</td>109                    </tr>110                  ))}111                </tbody>112              </table>113            </div>114            <Freshness dataUpdatedAt={sources.reduce<Date | null>((m, s) => (s.last_run_finished_at && (!m || s.last_run_finished_at > m) ? s.last_run_finished_at : m), null)} />115          </Section>116          <Note>License status meanings — approved: reviewed, may be ingested and displayed with attribution · review: terms under assessment, no public records · restricted: displayed with limits · blocked: not usable. See each source page for the plain-language summary.</Note>117        </div>118      )}119    </div>120  );121}122