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%
7.4 KB · 138 lines tsx
Raw Blame History
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