spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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