spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { Callout, DocSection, Prose } from '@/components/meta/prose';3import { Unavailable } from '@/components/ui/unavailable';4import { fmtDateTime, fmtInt } from '@/lib/format';5import { routes } from '@/lib/site';6import type { MethodologyPayload } from '@/lib/types';78/** Data-driven sections of /methodology rendered from `api.methodology()`: constellation rules, owner codes, metric definitions. */910export function ConstellationRulesSection({ rules }: { rules: MethodologyPayload['constellation_rules'] | null }) {11 return (12 <DocSection id="constellations" eyebrow="07" title="Constellation membership">13 <Prose>14 <p>15 Membership is assigned in two tiers. <strong>CelesTrak GP groups first</strong>: an object listed in a thematic group mapped to a constellation (for example <code>starlink</code>, <code>oneweb</code>, <code>kuiper</code>) is a source-backed member (<em>method: celestrak_group</em>). <strong>Documented name patterns second</strong>: payloads not covered by a group are matched against the regular expressions of the curated registry (<em>method: name_pattern</em>, derived). Membership history is kept with <code>since</code>/<code>until</code> dates; a member is never removed just because a group response was short.16 </p>17 <p>The table below is the live registry — the exact patterns and groups the classifier uses right now.</p>18 </Prose>19 {rules === null ? (20 <Unavailable what="Constellation rules" className="mt-4" />21 ) : (22 <div className="mt-4 overflow-x-auto">23 <table className="data-table stack md:min-w-[720px]">24 <thead>25 <tr>26 <th>Constellation</th>27 <th>Service</th>28 <th>CelesTrak groups</th>29 <th>Name patterns</th>30 </tr>31 </thead>32 <tbody>33 {rules.map((r) => (34 <tr key={r.slug}>35 <td className="primary">36 <Link href={routes.constellation(r.slug)} className="text-sm text-ink hover:text-accent">37 {r.name}38 </Link>39 </td>40 <td data-label="Service" className="text-sm text-ink-2">41 {r.service_type ?? '—'}42 </td>43 <td data-label="CelesTrak groups">44 {r.celestrak_groups.length === 0 ? (45 <span className="text-xs text-ink-3">none</span>46 ) : (47 <span className="flex flex-wrap gap-1">48 {r.celestrak_groups.map((g) => (49 <code key={g} className="mono rounded bg-accent-soft px-1.5 py-0.5 text-[11px] text-accent">50 {g}51 </code>52 ))}53 </span>54 )}55 </td>56 <td data-label="Name patterns">57 {r.match_patterns.length === 0 ? (58 <span className="text-xs text-ink-3">none</span>59 ) : (60 <span className="flex flex-wrap gap-1">61 {r.match_patterns.map((p) => (62 <code key={p} className="mono rounded bg-plane-2 px-1.5 py-0.5 text-[11px] text-ink">63 {p}64 </code>65 ))}66 </span>67 )}68 </td>69 </tr>70 ))}71 </tbody>72 </table>73 <p className="mt-2 text-xs text-ink-3">{fmtInt(rules.length)} constellations in the registry.</p>74 </div>75 )}76 </DocSection>77 );78}7980const KIND_LABELS: Record<string, string> = { country: 'Countries', organization: 'Organizations', joint: 'Joint programmes', intergovernmental: 'Intergovernmental', agency: 'Agencies', unknown: 'Unknown / unassigned' };81const KIND_ORDER = ['country', 'intergovernmental', 'agency', 'organization', 'joint', 'unknown'];8283export function OwnerCodesSection({ owners }: { owners: MethodologyPayload['owner_codes'] | null }) {84 const groups = new Map<string, MethodologyPayload['owner_codes']>();85 for (const o of owners ?? []) {86 const list = groups.get(o.kind) ?? [];87 list.push(o);88 groups.set(o.kind, list);89 }90 const kinds = [...groups.keys()].sort((a, b) => (KIND_ORDER.indexOf(a) === -1 ? 99 : KIND_ORDER.indexOf(a)) - (KIND_ORDER.indexOf(b) === -1 ? 99 : KIND_ORDER.indexOf(b)));91 return (92 <DocSection id="owner-codes" eyebrow="10" title="Owner codes">93 <Prose>94 <p>95 SATCAT identifies the responsible party of every object with a short <strong>owner code</strong>. We map each code to a country (ISO 3166) and, when the owner is an organization, agency or intergovernmental body, to an operator entity. Joint programmes map to several countries’ programmes but a single primary country when one is defined. Unmapped codes raise an <code>UNKNOWN_COUNTRY</code> quality flag instead of guessing.96 </p>97 </Prose>98 {owners === null ? (99 <Unavailable what="Owner codes" className="mt-4" />100 ) : (101 <div className="mt-4 space-y-2">102 {kinds.map((kind) => {103 const rows = groups.get(kind) ?? [];104 return (105 <details key={kind} className="group rounded-md border border-rule" open={kind === 'intergovernmental'}>106 <summary className="flex min-h-11 cursor-pointer list-none items-center justify-between gap-3 px-3 text-sm hover:bg-plane-2">107 <span className="font-medium">{KIND_LABELS[kind] ?? kind}</span>108 <span className="tnum text-xs text-ink-3">{fmtInt(rows.length)} codes</span>109 </summary>110 <div className="overflow-x-auto border-t border-rule">111 <table className="data-table">112 <thead>113 <tr>114 <th>Code</th>115 <th>Name</th>116 <th>Country</th>117 </tr>118 </thead>119 <tbody>120 {rows.map((o) => (121 <tr key={o.code}>122 <td className="mono text-sm">{o.code}</td>123 <td className="text-sm text-ink-2">{o.name}</td>124 <td className="mono text-xs text-ink-3">{o.country_code ?? '—'}</td>125 </tr>126 ))}127 </tbody>128 </table>129 </div>130 </details>131 );132 })}133 <p className="text-xs text-ink-3">{fmtInt(owners.length)} owner codes mapped.</p>134 </div>135 )}136 </DocSection>137 );138}139140export function MetricsSection({ metrics }: { metrics: MethodologyPayload['metrics'] | null }) {141 return (142 <DocSection id="metrics" eyebrow="11" title="Derived metrics">143 <Prose>144 <p>145 Every derived value on the site points at one of these definitions. The version changes whenever the rule changes; the inputs list the exact canonical columns the rule reads. None of them is a safety metric.146 </p>147 </Prose>148 {metrics === null ? (149 <Unavailable what="Metric definitions" className="mt-4" />150 ) : metrics.length === 0 ? (151 <Callout>No metric definitions are published yet.</Callout>152 ) : (153 <div className="mt-4 divide-y divide-rule border-y border-rule">154 {metrics.map((m) => (155 <article key={m.key} id={`metric-${m.key}`} className="scroll-mt-[calc(var(--header-h)+1rem)] grid gap-3 py-5 md:grid-cols-[220px_minmax(0,1fr)]">156 <div className="min-w-0">157 <h3 className="text-base font-semibold text-ink">{m.name}</h3>158 <p className="mono mt-1 text-xs text-ink-3">159 {m.key} · v{m.version}160 </p>161 <p className="mt-1 text-xs text-ink-3" title={fmtDateTime(m.updated_at)}>162 updated {fmtDateTime(m.updated_at)}163 </p>164 </div>165 <div className="min-w-0">166 <p className="text-sm leading-relaxed text-ink-2">{m.methodology}</p>167 <p className="eyebrow mt-3">Inputs</p>168 <ul className="mt-1 flex flex-wrap gap-1">169 {m.inputs.map((i) => (170 <li key={i}>171 <code className="mono rounded bg-plane-2 px-1.5 py-0.5 text-[11px] text-ink">{i}</code>172 </li>173 ))}174 </ul>175 </div>176 </article>177 ))}178 </div>179 )}180 </DocSection>181 );182}183