spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { fmtDuration, fmtInterval, fmtUntil } from '@/components/meta/connectors-table';4import { ATTRIBUTION, FIELD_PRIORITY } from '@/components/meta/legal';5import { Callout } from '@/components/meta/prose';6import { FreshnessBadge } from '@/components/ui/badges';7import { Container, PageHeader, Section } from '@/components/ui/section';8import { Unavailable } from '@/components/ui/unavailable';9import { api, safe } from '@/lib/api';10import { fmtAgo, fmtDateTime, fmtInt } from '@/lib/format';11import { SITE_URL, routes } from '@/lib/site';12import type { SourceRow } from '@/lib/types';1314export const metadata: Metadata = {15 title: 'Data sources — provenance, licenses and freshness',16 description: 'Every upstream source behind SatelliteIndex: CelesTrak GP and SATCAT today, planned government and scientific catalogs, with license, attribution, sync schedule and freshness.',17 alternates: { canonical: `${SITE_URL}/sources` },18 openGraph: { title: 'Data sources | SatelliteIndex', description: 'Provenance, licenses, attribution and live freshness of every source feeding SatelliteIndex.', url: `${SITE_URL}/sources` },19 twitter: { card: 'summary', title: 'Data sources | SatelliteIndex', description: 'Provenance, licenses, attribution and live freshness of every source feeding SatelliteIndex.' },20};2122const AUTHORITY_LABEL: Record<string, string> = { government: 'Government', intergovernmental: 'Intergovernmental', scientific: 'Scientific', secondary: 'Derived / secondary', commercial: 'Commercial' };23const AUTHORITY_TONE: Record<string, string> = { government: 'var(--accent-2)', intergovernmental: 'var(--accent-2)', scientific: 'var(--accent)', secondary: 'var(--ink-3)', commercial: 'var(--warn)' };2425function AuthorityBadge({ type, official }: { type: string | null; official: boolean }) {26 const t = type ?? 'unknown';27 const color = AUTHORITY_TONE[t] ?? 'var(--inactive)';28 return (29 <span className="inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[11px] font-medium" style={{ color, borderColor: `color-mix(in oklab, ${color} 35%, transparent)`, background: `color-mix(in oklab, ${color} 10%, transparent)` }}>30 {AUTHORITY_LABEL[t] ?? t}31 {official && <span className="text-ink-3">· official</span>}32 </span>33 );34}3536function SourceCard({ s, now }: { s: SourceRow; now: number }) {37 const planned = !s.enabled;38 return (39 <article id={s.id} className="scroll-mt-[calc(var(--header-h)+1rem)] border-t border-rule py-6 md:py-8">40 <div className="grid gap-6 md:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)]">41 <div className="min-w-0">42 <div className="flex flex-wrap items-center gap-2">43 <h2 className="text-lg font-semibold tracking-tight md:text-xl">{s.name}</h2>44 <AuthorityBadge type={s.authority_type} official={s.official} />45 <span className="rounded border border-rule px-1.5 py-0.5 text-[11px] uppercase tracking-wider text-ink-3">{s.type}</span>46 {planned ? <span className="rounded border border-warn/40 bg-warn-soft px-1.5 py-0.5 text-[11px] text-warn">Planned connector</span> : <FreshnessBadge status={s.freshness} />}47 </div>48 <dl className="mt-3 grid gap-2 text-sm sm:grid-cols-[120px_minmax(0,1fr)]">49 <dt className="text-xs uppercase tracking-wider text-ink-3 sm:pt-0.5">Website</dt>50 <dd className="min-w-0 break-all">51 {s.base_url ? (52 <a href={s.base_url} rel="noopener noreferrer" target="_blank" className="link">53 {s.base_url.replace(/^https?:\/\//, '')}54 </a>55 ) : (56 '—'57 )}58 </dd>59 <dt className="text-xs uppercase tracking-wider text-ink-3 sm:pt-0.5">License</dt>60 <dd className="text-ink-2">{s.license ?? 'Unavailable'}</dd>61 <dt className="text-xs uppercase tracking-wider text-ink-3 sm:pt-0.5">Attribution</dt>62 <dd className="text-ink-2">63 {s.attribution_text ? <q className="italic">{s.attribution_text}</q> : '—'}64 {s.attribution_required && <span className="ml-2 text-xs text-ink-3">(required)</span>}65 </dd>66 <dt className="text-xs uppercase tracking-wider text-ink-3 sm:pt-0.5">Cadence</dt>67 <dd className="text-ink-2">{s.update_frequency_seconds ? `Upstream refresh about every ${fmtInterval(s.update_frequency_seconds)}` : 'On demand (derived after each ingestion)'}</dd>68 <dt className="text-xs uppercase tracking-wider text-ink-3 sm:pt-0.5">Priority</dt>69 <dd className="tnum text-ink-2">{s.priority}</dd>70 </dl>71 {planned && <p className="mt-3 text-sm text-ink-3">Registered in the source catalog with its license and attribution, but no connector is running yet. Nothing on the site is sourced from it today.</p>}72 </div>7374 <div className="min-w-0">75 <p className="eyebrow">Connectors</p>76 {!s.connectors || s.connectors.length === 0 ? (77 <p className="mt-2 text-sm text-ink-3">No connector registered.</p>78 ) : (79 <ul className="mt-2 divide-y divide-rule border-y border-rule">80 {s.connectors.map((c) => (81 <li key={c.name} className="py-2.5">82 <div className="flex flex-wrap items-center justify-between gap-2">83 <span className="mono text-sm text-ink">{c.name}</span>84 <span className="text-xs text-ink-3">every {fmtInterval(c.interval_seconds)}</span>85 </div>86 {c.description && <p className="mt-0.5 text-xs text-ink-3">{c.description}</p>}87 <dl className="mt-1.5 grid grid-cols-2 gap-x-3 gap-y-0.5 text-xs sm:grid-cols-3">88 <dt className="text-ink-3">Last sync</dt>89 <dd className="sm:col-span-2" title={fmtDateTime(c.last_success_at)}>90 {fmtAgo(c.last_success_at, now)}91 </dd>92 <dt className="text-ink-3">Next run</dt>93 <dd className="sm:col-span-2">{c.enabled ? fmtUntil(c.next_run_at, now) : 'disabled'}</dd>94 <dt className="text-ink-3">Duration</dt>95 <dd className="sm:col-span-2">{fmtDuration(c.last_duration_ms)}</dd>96 {(c.consecutive_failures > 0 || c.circuit_open_until) && (97 <>98 <dt className="text-ink-3">Failures</dt>99 <dd className="text-warn sm:col-span-2">100 {fmtInt(c.consecutive_failures)} consecutive{c.circuit_open_until ? ` · circuit open until ${fmtDateTime(c.circuit_open_until)}` : ''}101 </dd>102 </>103 )}104 </dl>105 </li>106 ))}107 </ul>108 )}109 <div className="mt-4 grid grid-cols-3 gap-3">110 <div>111 <p className="eyebrow">Raw snapshots</p>112 <p className="tnum mt-0.5 text-lg font-semibold">{fmtInt(s.raw_snapshots)}</p>113 </div>114 <div>115 <p className="eyebrow">Provenance rows</p>116 <p className="tnum mt-0.5 text-lg font-semibold">{fmtInt(s.provenance_rows)}</p>117 </div>118 <div>119 <p className="eyebrow">Last snapshot</p>120 <p className="mt-0.5 text-sm" title={fmtDateTime(s.last_snapshot_at)}>121 {fmtAgo(s.last_snapshot_at, now)}122 </p>123 </div>124 </div>125 </div>126 </div>127 </article>128 );129}130131export default async function SourcesPage() {132 const res = await safe(api.sources());133 const now = Date.now();134 const sources = res?.data ?? null;135 const active = sources?.filter((s) => s.enabled) ?? [];136 const planned = sources?.filter((s) => !s.enabled) ?? [];137 return (138 <Container>139 <PageHeader eyebrow="Transparency" title="Data sources" lede="Where every number on SatelliteIndex comes from: the upstream catalogs, their licenses and attribution requirements, how often we sync them and how fresh each feed is right now. Nothing is presented without provenance.">140 <p className="mt-4 text-sm text-ink-3">141 Live connector health: <Link href={routes.statusData()} className="link">/status/data</Link> · How data is processed: <Link href={routes.methodology()} className="link">/methodology</Link>142 {res && <span className="ml-2">· generated {fmtDateTime(res.meta.generated_at)}</span>}143 </p>144 </PageHeader>145146 {sources === null ? (147 <Unavailable what="Source catalog" />148 ) : (149 <>150 <Section eyebrow="Active" title={`${active.length} source${active.length === 1 ? '' : 's'} feeding the index`} className="pt-0">151 <div>152 {active.map((s) => (153 <SourceCard key={s.id} s={s} now={now} />154 ))}155 </div>156 </Section>157158 <Section eyebrow="Source priority" title="Which source wins for each field">159 <p className="max-w-3xl text-sm leading-relaxed text-ink-2">160 Sources are ordered by priority; a higher-priority source overrides a lower one for the fields it covers, and every accepted value is recorded in <code className="mono rounded bg-plane-2 px-1.5 py-0.5 text-[13px]">field_provenance</code> with the source and observation time. Objects that disappear from a feed are never deleted.161 </p>162 <div className="mt-4 overflow-x-auto">163 <table className="data-table stack md:min-w-[760px]">164 <thead>165 <tr>166 <th>Field</th>167 <th>Source</th>168 <th>Note</th>169 </tr>170 </thead>171 <tbody>172 {FIELD_PRIORITY.map((f) => (173 <tr key={f.field}>174 <td className="primary text-sm text-ink">{f.field}</td>175 <td data-label="Source" className="text-sm">176 <a href={`#${f.sourceId}`} className="link">177 {f.source}178 </a>179 </td>180 <td data-label="Note" className="text-sm text-ink-2">181 {f.note}182 </td>183 </tr>184 ))}185 </tbody>186 </table>187 </div>188 </Section>189190 {planned.length > 0 && (191 <Section eyebrow="Planned" title={`${planned.length} planned connectors`}>192 <Callout className="mt-0 mb-2">These catalogs are registered with their license and attribution so the schema is ready for them, but they are not ingested yet. They are listed here for honesty about coverage — no field on the site is currently attributed to them.</Callout>193 {planned.map((s) => (194 <SourceCard key={s.id} s={s} now={now} />195 ))}196 </Section>197 )}198199 <Section eyebrow="Attribution" title="How we credit sources">200 <p className="max-w-3xl text-sm leading-relaxed text-ink-2">{ATTRIBUTION}</p>201 <p className="mt-3 max-w-3xl text-sm leading-relaxed text-ink-3">If you reuse data from this site or its API, carry the attribution texts above forward — see the <Link href={routes.developers()} className="link">API page</Link>.</p>202 </Section>203 </>204 )}205 </Container>206 );207}208