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 { Bars, StackedBars } from '@/components/charts/charts';4import { ChartBlock, Chips, Disclaimer, Note, StatGrid } from '@/components/stats/shared';5import { TypeBadge } from '@/components/ui/badges';6import { Pagination } from '@/components/ui/pagination';7import { Container, PageHeader, Section, Stat } from '@/components/ui/section';8import { Unavailable } from '@/components/ui/unavailable';9import { api, safe } from '@/lib/api';10import { fmt2, fmtAgo, fmtDate, fmtInt, fmtKm, num } from '@/lib/format';11import { OBJECT_TYPE_LABELS, routes, SITE_NAME, SITE_URL } from '@/lib/site';1213export const revalidate = 300;1415type Search = Promise<{ page?: string | string[]; object_type?: string | string[]; days?: string | string[] }>;16const PAGE_SIZE = 50;17const DAYS = [30, 90, 365, 3650] as const;18const TYPES = ['PAYLOAD', 'DEBRIS', 'ROCKET_BODY', 'UNKNOWN'] as const;19const pick = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v);2021const TITLE = 'Reentries — recently decayed objects and low-perigee watch';22const DESC = 'Objects that have reentered the atmosphere according to published SATCAT decay dates: last 7/30/365 days, monthly history, filterable table by object type, and a low-perigee watch list of objects under 250 km. No reentry predictions.';2324export async function generateMetadata(): Promise<Metadata> {25 const url = `${SITE_URL}${routes.reentries()}`;26 return {27 title: TITLE,28 description: DESC,29 alternates: { canonical: url },30 openGraph: { title: `${TITLE} | ${SITE_NAME}`, description: DESC, url, type: 'website', siteName: SITE_NAME },31 twitter: { card: 'summary_large_image', title: `${TITLE} | ${SITE_NAME}`, description: DESC },32 };33}3435function href(params: { page?: number; object_type?: string; days?: number }): string {36 const p = new URLSearchParams();37 if (params.object_type) p.set('object_type', params.object_type);38 if (params.days && params.days !== 90) p.set('days', String(params.days));39 if (params.page && params.page > 1) p.set('page', String(params.page));40 const s = p.toString();41 return s ? `${routes.reentries()}?${s}` : routes.reentries();42}43const daysLabel = (d: number) => (d === 30 ? 'Last 30 d' : d === 90 ? 'Last 90 d' : d === 365 ? 'Last year' : 'Last 10 years');4445export default async function ReentriesPage({ searchParams }: { searchParams: Search }) {46 const sp = await searchParams;47 const page = Math.max(1, Number(pick(sp.page) ?? 1) || 1);48 const object_type = TYPES.find((t) => t === pick(sp.object_type)) ?? undefined;49 const daysRaw = Number(pick(sp.days) ?? 90);50 const days = (DAYS as readonly number[]).includes(daysRaw) ? daysRaw : 90;5152 const res = await safe(api.reentries({ page, page_size: PAGE_SIZE, object_type, days }));53 const monthly = res ? [...res.monthly].sort((a, b) => a.month.localeCompare(b.month)).slice(-24) : [];54 const monthlyBars = monthly.map((m) => ({ x: m.month.slice(0, 7), y: num(m.decayed) ?? 0 }));55 const monthlyStack = monthly.map((m) => ({ x: m.month.slice(0, 7), payloads: num(m.payloads) ?? 0, debris: num(m.debris) ?? 0, rocket_bodies: num(m.rocket_bodies) ?? 0 }));5657 const typeChips = [{ href: href({ days }), label: 'All types', active: !object_type }, ...TYPES.map((t) => ({ href: href({ object_type: t, days }), label: OBJECT_TYPE_LABELS[t] ?? t, active: object_type === t }))];58 const dayChips = DAYS.map((d) => ({ href: href({ object_type, days: d }), label: daysLabel(d), active: days === d }));5960 return (61 <Container>62 <PageHeader eyebrow="Reentries" title="Recent reentries" lede="Objects whose decay date has been published in the CelesTrak SATCAT, i.e. confirmed atmospheric reentries. SatelliteIndex reports what the catalogue says — it does not forecast when or where anything will come down.">63 {res && <p className="mono mt-4 text-xs text-ink-3">Generated {fmtAgo(res.meta.generated_at)}</p>}64 </PageHeader>6566 {!res ? (67 <div className="py-6">68 <Unavailable what="Reentry statistics" />69 </div>70 ) : (71 <>72 <Section eyebrow="Summary" title="Confirmed decays" className="pt-0 md:pt-0">73 <StatGrid cols={3}>74 <Stat label="Last 7 days" value={fmtInt(res.summary.last_7d)} accent hint="Objects with a published decay date" />75 <Stat label="Last 30 days" value={fmtInt(res.summary.last_30d)} />76 <Stat label="Last 365 days" value={fmtInt(res.summary.last_365d)} />77 </StatGrid>78 </Section>7980 <Section eyebrow="Monthly" title="Decays by month" action={{ href: routes.debris(), label: 'Debris history' }}>81 <div className="grid gap-10 lg:grid-cols-2 lg:gap-12">82 <ChartBlock title="Objects decayed per month" hint={monthly.length ? `${monthly[0]?.month.slice(0, 7)} → ${monthly[monthly.length - 1]?.month.slice(0, 7)}` : undefined}>83 <Bars data={monthlyBars} title="Objects decayed per month, last 24 months" height={190} xTicks={6} color="var(--series-4)" highlightLast />84 <Note className="mt-2">The last bar is the current, incomplete month.</Note>85 </ChartBlock>86 <ChartBlock title="By object type" hint="Payloads · debris · rocket bodies">87 <StackedBars data={monthlyStack} keys={['payloads', 'debris', 'rocket_bodies']} labels={{ payloads: 'Payloads', debris: 'Debris', rocket_bodies: 'Rocket bodies' }} title="Objects decayed per month by object type" height={190} xTicks={6} />88 </ChartBlock>89 </div>90 </Section>9192 <Section eyebrow="Table" title={`Reentries — ${daysLabel(days).toLowerCase()}${object_type ? ` · ${OBJECT_TYPE_LABELS[object_type] ?? object_type}` : ''}`}>93 <div className="space-y-3 pb-5">94 <Chips items={typeChips} ariaLabel="Object type" />95 <Chips items={dayChips} ariaLabel="Time window" />96 </div>97 {res.data.length === 0 ? (98 <Unavailable what="Reentries matching these filters" />99 ) : (100 <table className="data-table stack text-sm">101 <thead>102 <tr>103 <th>Decay date</th>104 <th>Object</th>105 <th>NORAD</th>106 <th>Type</th>107 <th>Country</th>108 <th className="num">RCS (m²)</th>109 <th>Constellation</th>110 </tr>111 </thead>112 <tbody>113 {res.data.map((r) => (114 <tr key={r.id}>115 <td data-label="Decay date" className="mono text-xs">{fmtDate(r.decay_date)}</td>116 <td data-label="Object" className="primary">117 <Link href={routes.satellite(r.slug)} className="link font-medium">{r.name}</Link>118 {r.cospar_id && <span className="mono ml-2 text-xs text-ink-3">{r.cospar_id}</span>}119 </td>120 <td data-label="NORAD" className="mono text-xs">{r.norad_id ?? '—'}</td>121 <td data-label="Type"><TypeBadge type={r.object_type} /></td>122 <td data-label="Country">{r.country_name ?? r.country_code ?? '—'}</td>123 <td data-label="RCS (m²)" className="num tnum">{fmt2(r.rcs_m2)}</td>124 <td data-label="Constellation">{r.constellation_slug ? <Link href={routes.constellation(r.constellation_slug)} className="link">{r.constellation_name}</Link> : r.constellation_name ?? '—'}</td>125 </tr>126 ))}127 </tbody>128 </table>129 )}130 <Pagination className="mt-6" page={res.pagination.page} pages={res.pagination.pages} total={res.pagination.total} pageSize={res.pagination.page_size} makeHref={(p) => href({ page: p, object_type, days })} />131 </Section>132133 <Section eyebrow="Low-perigee watch" title="Objects currently under 250 km perigee">134 <div className="mb-5 space-y-3">135 <Disclaimer text={res.disclaimer} label="Not a prediction" />136 </div>137 {res.low_perigee_watch.length === 0 ? (138 <Unavailable what="Low-perigee watch" />139 ) : (140 <table className="data-table stack text-sm">141 <thead>142 <tr>143 <th>Object</th>144 <th className="num">Perigee</th>145 <th className="num">Apogee</th>146 <th>Element epoch</th>147 <th>Type</th>148 <th>Country</th>149 <th>Constellation</th>150 </tr>151 </thead>152 <tbody>153 {res.low_perigee_watch.map((o) => (154 <tr key={o.id}>155 <td data-label="Object" className="primary">156 <Link href={routes.satellite(o.slug)} className="link font-medium">{o.name}</Link>157 {o.norad_id && <span className="mono ml-2 text-xs text-ink-3">#{o.norad_id}</span>}158 </td>159 <td data-label="Perigee" className="num tnum">{fmtKm(o.perigee_km, 1)}</td>160 <td data-label="Apogee" className="num tnum">{fmtKm(o.apogee_km, 1)}</td>161 <td data-label="Element epoch" className="mono text-xs">{fmtAgo(o.epoch)}</td>162 <td data-label="Type"><TypeBadge type={o.object_type} /></td>163 <td data-label="Country" className="mono text-xs">{o.country_code ?? '—'}</td>164 <td data-label="Constellation">{o.constellation_name ?? '—'}</td>165 </tr>166 ))}167 </tbody>168 </table>169 )}170 <Note className="mt-3">Perigee and apogee come from the latest element set (SGP4 mean elements, km). Actively manoeuvring satellites (e.g. during orbit raising) routinely sit under 250 km without decaying.</Note>171 </Section>172173 <Section eyebrow="Predicted" title="Predicted / upcoming reentries">174 <Unavailable what="Predicted reentries — no reentry-prediction source connected yet;" />175 <Note className="mt-3">176 SatelliteIndex does not compute reentry windows or ground tracks itself and will only show predictions once an authoritative source is connected, with its own attribution. See{' '}177 <Link href={routes.methodology()} className="text-accent hover:underline">methodology</Link> and <Link href={routes.sources()} className="text-accent hover:underline">sources</Link>.178 </Note>179 </Section>180 </>181 )}182 </Container>183 );184}185