SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
8.7 KB · 183 lines tsx
Raw Blame History
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&rsquo; 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