import type { Metadata } from 'next'; import Link from 'next/link'; import { Bars, StackedBars } from '@/components/charts/charts'; import { ChartBlock, Chips, Disclaimer, Note, StatGrid } from '@/components/stats/shared'; import { TypeBadge } from '@/components/ui/badges'; import { Pagination } from '@/components/ui/pagination'; import { Container, PageHeader, Section, Stat } from '@/components/ui/section'; import { Unavailable } from '@/components/ui/unavailable'; import { api, safe } from '@/lib/api'; import { fmt2, fmtAgo, fmtDate, fmtInt, fmtKm, num } from '@/lib/format'; import { OBJECT_TYPE_LABELS, routes, SITE_NAME, SITE_URL } from '@/lib/site'; export const revalidate = 300; type Search = Promise<{ page?: string | string[]; object_type?: string | string[]; days?: string | string[] }>; const PAGE_SIZE = 50; const DAYS = [30, 90, 365, 3650] as const; const TYPES = ['PAYLOAD', 'DEBRIS', 'ROCKET_BODY', 'UNKNOWN'] as const; const pick = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v); const TITLE = 'Reentries — recently decayed objects and low-perigee watch'; const 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.'; export async function generateMetadata(): Promise { const url = `${SITE_URL}${routes.reentries()}`; return { title: TITLE, description: DESC, alternates: { canonical: url }, openGraph: { title: `${TITLE} | ${SITE_NAME}`, description: DESC, url, type: 'website', siteName: SITE_NAME }, twitter: { card: 'summary_large_image', title: `${TITLE} | ${SITE_NAME}`, description: DESC }, }; } function href(params: { page?: number; object_type?: string; days?: number }): string { const p = new URLSearchParams(); if (params.object_type) p.set('object_type', params.object_type); if (params.days && params.days !== 90) p.set('days', String(params.days)); if (params.page && params.page > 1) p.set('page', String(params.page)); const s = p.toString(); return s ? `${routes.reentries()}?${s}` : routes.reentries(); } const daysLabel = (d: number) => (d === 30 ? 'Last 30 d' : d === 90 ? 'Last 90 d' : d === 365 ? 'Last year' : 'Last 10 years'); export default async function ReentriesPage({ searchParams }: { searchParams: Search }) { const sp = await searchParams; const page = Math.max(1, Number(pick(sp.page) ?? 1) || 1); const object_type = TYPES.find((t) => t === pick(sp.object_type)) ?? undefined; const daysRaw = Number(pick(sp.days) ?? 90); const days = (DAYS as readonly number[]).includes(daysRaw) ? daysRaw : 90; const res = await safe(api.reentries({ page, page_size: PAGE_SIZE, object_type, days })); const monthly = res ? [...res.monthly].sort((a, b) => a.month.localeCompare(b.month)).slice(-24) : []; const monthlyBars = monthly.map((m) => ({ x: m.month.slice(0, 7), y: num(m.decayed) ?? 0 })); 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 })); 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 }))]; const dayChips = DAYS.map((d) => ({ href: href({ object_type, days: d }), label: daysLabel(d), active: days === d })); return ( {res &&

Generated {fmtAgo(res.meta.generated_at)}

}
{!res ? (
) : ( <>
The last bar is the current, incomplete month.
{res.data.length === 0 ? ( ) : ( {res.data.map((r) => ( ))}
Decay date Object NORAD Type Country RCS (m²) Constellation
{fmtDate(r.decay_date)} {r.name} {r.cospar_id && {r.cospar_id}} {r.norad_id ?? '—'} {r.country_name ?? r.country_code ?? '—'} {fmt2(r.rcs_m2)} {r.constellation_slug ? {r.constellation_name} : r.constellation_name ?? '—'}
)} href({ page: p, object_type, days })} />
{res.low_perigee_watch.length === 0 ? ( ) : ( {res.low_perigee_watch.map((o) => ( ))}
Object Perigee Apogee Element epoch Type Country Constellation
{o.name} {o.norad_id && #{o.norad_id}} {fmtKm(o.perigee_km, 1)} {fmtKm(o.apogee_km, 1)} {fmtAgo(o.epoch)} {o.country_code ?? '—'} {o.constellation_name ?? '—'}
)} 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.
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{' '} methodology and sources.
)}
); }