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 (seesrc/lib/api.ts, types insrc/lib/types.ts).api.x()throwsApiError(.notFound,.unavailable). UsenotFound()fromnext/navigationon 404. Wrap optional panels withsafe()and render<Unavailable/>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/<path> | python3 -m json.tool | head -80. OpenAPI:localhost:8311/api/v1/docs. - Postgres aggregates can arrive as strings (
Num = number | string | null): always go throughnum()/fmtInt()etc. from@/lib/format. - No hardcoded statistics, counts, timestamps or fake data. If something is missing, show "Unavailable" (
<Unavailable what="…"/>).
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 <td>, 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;splitTrackfor 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<main>without padding: wrap content in<Container>(max 1280) or<Container wide>(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.stackor 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
<body>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 (<script type="application/ld+json">). Titles follow ISS (ZARYA) — Live Orbit, NORAD 25544 | SatelliteIndex
(the layout template appends | SatelliteIndex automatically: pass only the first part).
Freshness & sources (transparency is a feature)
Show "Orbit updated X ago · epoch age" and "Metadata updated X ago" using fmtAgo; expose a Sources section on detail pages
(detail.sources, detail.provenance, detail.freshness). Derived values (orbit class, mission type, constellation membership,
activity score) must be labelled derived with a link to /methodology.
Verification before you report
cd apps/web && pnpm typecheckmust pass with zero errors (fix yours; if a shared type is wrong, say so in your report).- The dev server already runs at
http://localhost:8310(hot reload). Load each page you built at 390 px and 1440 px with Playwright:node -e "…"importing/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs(chromium), take screenshots intoapps/web/qa/screens/<your-area>/, and checkdocument.documentElement.scrollWidth <= innerWidthand zero console errors. - Do not run
next buildand do not start another dev server (port 8310 is taken). Do not edit files outside your ownership list.