Skip to content

coffee-converter/seeksat

Repository files navigation

SeekSat

▶ Live: seeksat.com

SeekSat: a 3D globe showing a live ISS pass over a Chicago observer, with a ranked list of upcoming visibility windows and a polar sky chart

Satellite pass forecasts for ground stations. Place one or more observer locations on the globe, pick a satellite + a mode (visual naked-eye or radio), and SeekSat finds the windows when every observer can see (or reach) the satellite at once. Click a window to open a fullscreen polar sky chart showing the trajectory with sun / moon / planet / star context for that observer.

A pass sky chart: the ISS trajectory arc across a polar star map, with sun, moon, planet, and constellation context for the observer

A multi-observer ray-triangulation tool also lives in this repo (components/TriangulateApp.tsx + lib/triangulate-scene.js) for verifying the math against recorded sightings. It's a local dev tool, not wired up as a public route. To run it locally: add a one-line app/triangulate/page.tsx that imports TriangulateApp, and supply your own observation sample data under _data/ (gitignored).

Built on Next.js (App Router) + React + Zustand, with CesiumJS loaded from CDN. SGP4 propagation via satellite.js.

Run

npm install
npm run dev      # http://localhost:3000

Routes:

  • / - pass finder (the only public route)

Production build:

npm run build && npm start

Tests + type check

npm test         # 270+ Node-native unit tests (no jest)
npm run typecheck

Both run on every push + PR via .github/workflows/ci.yml.

Pass-finder math

  • Window search bisects observer visibility predicates (per-mode) on a coarse grid + refines to <1s resolution.
  • Visual rating combines twilight, target altitude, and cloud-cover probability with explicit correlation-aware combiners (MIN across observers for sky-darkness + clouds, PRODUCT for altitude).
  • Radio rating is peak-elevation × duration sigmoid.
  • Polar plots draw the trajectory arc with magnitude-derived stroke opacity in visual mode, dashed where the satellite would be in Earth's shadow or the sky is too bright. Stars from a ~250-entry catalog, planets via truncated Keplerian elements, moon via low-precision Meeus lunar ephemeris.

Triangulation math (offline tool)

  • Each observation → a ray in ECEF (Earth-Centered, Earth-Fixed).
  • RA/Dec ray directions: ECI → ECEF via GMST rotation.
  • Alt/Az ray directions: ENU → ECEF via the observer's local frame.
  • Bennett refraction correction applied to apparent altitudes.
  • Closest point to all rays = least-squares minimizer of Σ ‖(I - dᵢ dᵢᵀ)(x - pᵢ)‖², solved via a direct 3×3 inverse.
  • Per-ray residuals (meters) reported per observation.
  • Optional SGP4-propagated TLE truth comparison with miss-distance.

Adding observers

The home page accepts:

  • Lat/lon pair - DMS (40°30'30.0"N, 75°15'45.0"W) or decimal (40.5083, -88.1999)
  • Place name - any free text; geocoded via the OSM/Nominatim API
  • Use my location - browser geolocation
  • Click on globe - toggle the place-by-click mode, then click anywhere on the Cesium canvas

Observer set is persisted to localStorage; a Share button copies a URL that encodes the current set + the selected pass window.

Architecture

The two routes share the same Cesium viewer setup pattern:

  • Page (app/[route]/page.tsx) - server component, just renders the app's client root.
  • App composition root (components/{Triangulate,PassFinder}App.tsx)
    • "use client", owns the container ref, calls useCesiumViewer, dynamically imports the scene module once the viewer is ready, and wraps children in <CesiumViewerProvider> so descendants can read the viewer via useViewer() without prop-drilling.
  • Scene module (lib/{triangulate,pass-finder}-scene.js) - imperative Cesium island. Builds entities, subscribes to the relevant Zustand store, registers a typed SceneBridge (see lib/scene-bridge.ts) so React components can call back into the scene for one-shot commands like addObserver or renderPolarModal. Returns a teardown function.
  • Store - Zustand. Triangulate uses lib/store.ts; the pass finder uses lib/pass-finder-store.ts. Each owns its own slice shape; nothing is shared across pages.
  • Pure modules (lib/pass-finder/*.js) - about a dozen files of pure math + SVG painters extracted out of what used to be a single ~4k-line scene file. All have explicit deps (no scene-state reach-throughs); test coverage is in test/.

Cesium loading

The CDN script is loaded by <CesiumLoader> (a client-only wrapper around next/script). It fires onReady once and resolves a shared promise in lib/cesium-loaded.ts, which useCesiumViewer awaits. No polling. A <link rel="preload"> in the layout starts the network fetch in parallel with React hydration so the script lands as fast as possible.

MCP server env vars

  • CRON_SECRET - random string; Vercel Cron sends it as a Bearer token to /api/refresh-tle.
  • EDGE_CONFIG - read connection string (auto-set when an Edge Config store is linked to the project).
  • EDGE_CONFIG_ID - the Edge Config id (for writes).
  • VERCEL_API_TOKEN - token with Edge Config write scope (for the cron write).
  • VERCEL_TEAM_ID - only if the project lives under a team.

License

Copyright (c) 2026 Aaron Hanson. All rights reserved.

This source is published for viewing and reference. It is not licensed for reuse, redistribution, or deployment. No permission is granted to copy, modify, or run this code as a service. If you'd like to use any of it, reach out.

About

3D satellite pass forecasting for ground stations. Multi-observer visibility windows, polar sky charts, and an MCP server. SGP4, CesiumJS, Next.js.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Contributors