# SatelliteIndex web — frontend guide (apps/web) Next 16 (App Router, React 19, TypeScript strict, Tailwind v4). Server components by default; client components only for interactivity (`'use client'`). The FastAPI backend runs on `http://127.0.0.1:8311`; the browser talks to the same origin (`/api/v1/*` is rewritten to the API in `next.config.ts`). ## Data access - **Server components**: `import { api, safe, ApiError } from '@/lib/api'` — typed wrappers for every endpoint (see `src/lib/api.ts`, types in `src/lib/types.ts`). `api.x()` throws `ApiError` (`.notFound`, `.unavailable`). Use `notFound()` from `next/navigation` on 404. Wrap optional panels with `safe()` and render `` when null. **Never crash a page because one panel failed.** - **Client components**: `import { clientApi } from '@/lib/client-api'` (positions, live position, track, search, view beacon). - Inspect any payload live: `curl -s localhost:8311/api/v1/ | python3 -m json.tool | head -80`. OpenAPI: `localhost:8311/api/v1/docs`. - Postgres aggregates can arrive as **strings** (`Num = number | string | null`): always go through `num()` / `fmtInt()` etc. from `@/lib/format`. - **No hardcoded statistics, counts, timestamps or fake data.** If something is missing, show "Unavailable" (``). ## Design system (dark, scientific, premium — not a crypto dashboard, not generic SaaS cards) Tokens in `src/app/globals.css` → Tailwind utilities: `bg-space | bg-plane | bg-plane-2 | bg-plane-3`, `text-ink | text-ink-2 | text-ink-3`, `border-rule | border-rule-strong`, `text-accent | bg-accent | text-accent-ink | bg-accent-soft`, `text-accent-2`, status `text-active | text-warn | text-danger | text-inactive` (+ `-soft` backgrounds), orbit classes `text-leo | text-meo | text-geo | text-heo | text-other`, chart series `series-1..8`. Utility classes: `.panel` (translucent bordered surface — use sparingly, prefer spatial composition and hairlines), `.eyebrow` (small caps label), `.display` (big headline), `.mono` / `.tnum` (telemetry, IDs, numbers), `.container-x`, `.data-table` (+ `.stack` to transform rows into stacked cards under 768 px — put `data-label="…"` on each ``, `className="primary"` on the name cell, `.num` for numbers), `.grid-bg`, `.dot` / `.pulse`, `.link`, `.scrollbar-thin`, `.no-scrollbar`. Shared components (do **not** modify; build local ones in your own folder if you need variants): - `@/components/ui/section` → `Container`, `PageHeader`, `Section` (eyebrow/title/action), `Stat` (big number tile) - `@/components/ui/badges` → `StatusBadge`, `OrbitBadge`, `TypeBadge`, `MissionLabel`, `FreshnessBadge` - `@/components/ui/pagination` → `Pagination` (URL-driven), `@/components/ui/unavailable` → `Unavailable` - `@/components/charts/charts` → `HBars`, `Bars`, `StackedBars`, `AreaChart`, `Donut`, `Sparkline`, `Histogram` (pure SVG, server-safe) - `@/components/map/world-map` → `WorldMap` (SVG equirectangular; `tracks`, `markers`; `splitTrack` for antimeridian), `MAP_SIZE` - `@/lib/site` → `routes.*`, `SITE_NAME/SITE_URL/TAGLINE`, `ORBIT_CLASS_COLORS`, `STATUS_COLORS`, `MISSION_LABELS`, `OBJECT_TYPE_LABELS`, `EVENT_TYPE_LABELS` - `@/lib/format` → `fmtInt fmt1 fmt2 fmtCompact fmtKm fmtDeg fmtMinutes fmtPct fmtDate fmtDateTime fmtAgo fmtYear titleCase num` - Layout (header, mobile tab bar, footer, ⌘K search dialog) is already mounted in `src/app/layout.tsx`. Pages render inside `
` **without** padding: wrap content in `` (max 1280) or `` (1600), or go full-bleed on purpose (globe). Typography: Inter (UI) + JetBrains Mono (IDs/telemetry). Big display numbers use `tnum`. Icons: `lucide-react`. ## Mobile rules (mandatory — every feature is tested at 390 px before it is complete) - No horizontal overflow at 320–430 px. Tables → `.data-table.stack` or purpose-built list rows. Long IDs wrap or truncate. - Tap targets ≥ 44 px. The fixed bottom tab bar (58 px + safe area) is reserved by `` padding; don't add another fixed bottom bar. - Desktop should use wide screens (two-column "terminal" layouts on detail pages: main visualisation + right telemetry panel). - DOM order = visual order on all breakpoints (no `order:` tricks). Heavy globe/3D features lazy-load (`next/dynamic`, `ssr: false`). ## SEO & sharing Every public page exports `generateMetadata` (title, description, `alternates.canonical`, `openGraph`, `twitter`). Detail pages should add JSON-LD where sensible (`