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 { ConnectorsTable, FreshnessLegend } from '@/components/meta/connectors-table';4import { FreshnessBadge } from '@/components/ui/badges';5import { Container, PageHeader, Section, Stat } from '@/components/ui/section';6import { Unavailable } from '@/components/ui/unavailable';7import { api, safe } from '@/lib/api';8import { fmt1, fmtAgo, fmtDateTime, fmtInt, num } from '@/lib/format';9import { SITE_URL, routes } from '@/lib/site';1011export const metadata: Metadata = {12 title: 'Status — platform health and data freshness',13 description: 'Live health of the SatelliteIndex API, database, cache and orbit service, and the freshness of every data connector.',14 alternates: { canonical: `${SITE_URL}/status` },15 openGraph: { title: 'Status | SatelliteIndex', description: 'Platform health and data freshness, live.', url: `${SITE_URL}/status` },16 twitter: { card: 'summary', title: 'Status | SatelliteIndex', description: 'Platform health and data freshness, live.' },17 robots: { index: true, follow: true },18};1920export const dynamic = 'force-dynamic';2122function ComponentPill({ status }: { status: string }) {23 const ok = status === 'ok' || status === 'fresh';24 const warn = status === 'aging' || status === 'degraded';25 const color = ok ? 'var(--active)' : warn ? 'var(--warn)' : 'var(--danger)';26 return (27 <span className="mono inline-flex items-center gap-1.5 text-xs" style={{ color }}>28 <span className={`dot ${ok ? 'pulse' : ''}`} aria-hidden /> {status}29 </span>30 );31}3233export default async function StatusPage() {34 const [health, status] = await Promise.all([safe(api.health()), safe(api.sourcesStatus())]);35 const now = Date.now();36 const comps = health?.components ?? {};37 const overall = health?.status ?? 'unavailable';38 const overallColor = overall === 'ok' ? 'var(--active)' : overall === 'degraded' ? 'var(--warn)' : 'var(--danger)';39 return (40 <Container>41 <PageHeader eyebrow="Operations" title="Status" lede="Two distinct questions, answered separately: is the platform up, and is the data current? A healthy API can still serve aging orbital elements when an upstream feed pauses — that is shown here, not hidden.">42 <p className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-ink-3">43 <span>44 Rendered {fmtDateTime(new Date(now).toISOString())} · <Link href={routes.status()} className="link">Refresh</Link>45 </span>46 <Link href={routes.statusData()} className="link">47 Data-only view →48 </Link>49 </p>50 </PageHeader>5152 <Section eyebrow="Platform uptime" title="API and services" className="pt-0">53 {health === null ? (54 <Unavailable what="Health endpoint" />55 ) : (56 <>57 <div className="flex flex-wrap items-baseline gap-x-6 gap-y-2">58 <p className="display text-3xl md:text-5xl" style={{ color: overallColor }}>59 {overall === 'ok' ? 'Operational' : overall}60 </p>61 <p className="mono text-sm text-ink-3">62 version {health.version} · server time {fmtDateTime(health.time)}63 </p>64 </div>65 <div className="mt-6 grid grid-cols-2 gap-x-6 gap-y-5 md:grid-cols-4">66 <div>67 <p className="eyebrow">Database</p>68 <div className="mt-1">69 <ComponentPill status={String(comps.database?.status ?? 'unavailable')} />70 </div>71 <p className="tnum mt-1 text-sm text-ink-2">{comps.database?.satellites !== undefined ? `${fmtInt(comps.database.satellites as number)} satellites` : 'count unavailable'}</p>72 </div>73 <div>74 <p className="eyebrow">Redis cache</p>75 <div className="mt-1">76 <ComponentPill status={String(comps.redis?.status ?? 'unavailable')} />77 </div>78 <p className="mt-1 text-sm text-ink-2">response cache, locks, job queue</p>79 </div>80 <div>81 <p className="eyebrow">Orbit service</p>82 <div className="mt-1">83 <ComponentPill status={String(comps.orbit_service?.status ?? 'unavailable')} />84 </div>85 <p className="tnum mt-1 text-sm text-ink-2">{comps.orbit_service?.objects !== undefined ? `${fmtInt(comps.orbit_service.objects as number)} objects in the propagator` : 'object count unavailable'}</p>86 </div>87 <div>88 <p className="eyebrow">Data</p>89 <div className="mt-1">90 <ComponentPill status={String(comps.data?.status ?? 'unavailable')} />91 </div>92 <p className="mt-1 text-sm text-ink-2">worst connector freshness</p>93 </div>94 </div>95 </>96 )}97 </Section>9899 <Section eyebrow="Data freshness" title="Connectors" action={{ href: routes.sources(), label: 'Sources & licenses' }}>100 {status === null ? (101 <Unavailable what="Connector status" />102 ) : (103 <>104 <div className="mb-6 grid grid-cols-2 gap-x-6 gap-y-5 md:grid-cols-4">105 <Stat label="Latest element epoch" value={<span className="text-xl md:text-2xl">{fmtAgo(status.data.orbit.latest_epoch, now)}</span>} hint={fmtDateTime(status.data.orbit.latest_epoch)} />106 <Stat label="Median element age" value={status.data.orbit.median_element_age_hours === null ? 'Unavailable' : `${fmt1(status.data.orbit.median_element_age_hours)} h`} hint="across the latest element set of every object" />107 <Stat label="Propagator objects" value={fmtInt(status.data.orbit.propagator_objects)} hint="loaded for live positions" />108 <Stat109 label="Connectors fresh"110 value={`${fmtInt(status.data.connectors.filter((c) => c.freshness === 'fresh').length)} / ${fmtInt(status.data.connectors.filter((c) => c.enabled).length)}`}111 hint={112 <span className="inline-flex flex-wrap gap-2">113 {status.data.connectors.filter((c) => c.enabled && c.freshness !== 'fresh').map((c) => (114 <FreshnessBadge key={c.name} status={c.freshness} label={`${c.name}: ${c.freshness}`} />115 ))}116 {status.data.connectors.every((c) => !c.enabled || c.freshness === 'fresh') && 'all enabled connectors are fresh'}117 </span>118 }119 />120 </div>121 <ConnectorsTable connectors={status.data.connectors} now={now} />122 <details className="mt-4 text-sm">123 <summary className="cursor-pointer py-1 text-ink-3 hover:text-ink">Freshness thresholds</summary>124 <div className="mt-2">125 <FreshnessLegend connectors={status.data.connectors.filter((c) => c.enabled)} />126 </div>127 </details>128 <p className="mt-3 text-xs text-ink-3">129 generated {fmtDateTime(status.meta.generated_at)} · request {status.meta.request_id}130 {num(status.data.orbit.median_element_age_hours) !== null && (num(status.data.orbit.median_element_age_hours) ?? 0) > 48 && <span className="ml-2 text-warn">Element sets are older than 48 h on median — live positions carry more uncertainty than usual.</span>}131 </p>132 </>133 )}134 </Section>135 </Container>136 );137}138