# Earth Magnetic Monitor — handoff for Cursor

Project name: `earth-magnetic-monitor`.

This file is the working notes for the app as built. Drop the repo into Cursor and point the agent at this file. No API keys are required for the live data.

## Stack (read this first)

The app is **not** Next.js. It is **TanStack Start** (React, file routes) with **Vite**, **TypeScript**, and **Tailwind**.

- Dev server: `npm run dev` (Vite on port **8080**)
- Typecheck: `npm run typecheck`
- Entry route: `src/routes/index.tsx` renders the dashboard
- Shell: `src/routes/__root.tsx`
- Main UI: `src/components/dashboard.tsx`
- Shared SWPC types: `src/types/swpc.ts`
- Dark theme is the default (`src/styles.css`). A night toggle only changes how bright the dark theme is.

`src/routeTree.gen.ts` is generated. Do not edit it by hand. Adding a file under `src/routes/` regenerates it when the dev server runs.

Image hosts that the browser cannot load directly go through `src/routes/api/solar-image.ts`. Allowed hosts are listed in `src/lib/image-proxy.ts`. That route queues at most 3 upstream fetches. A missing image returns **404**, not 502. Do not fire a frame change for every tile on one global timer. Each tile waits until the current image has loaded.

## How the pages work

`dashboard.tsx` is a landing strip (status cards) plus two rows of tabs. Each tab is its own page. The hash in the URL opens a page (`#planets`, `#sun`, `#field`, `#wind`, and so on).

| Tab | What it is | Main files |
|---|---|---|
| Now | Kp, G-scale, solar wind, Bz, storm word, NOAA scales | `src/lib/swpc.ts`, `dashboard.tsx` |
| Earth field | INTERMAGNET one-minute stations. Stennis is the default (nearest the Florida coast). | `src/components/earth-field.tsx`, `src/lib/ground.ts` |
| Solar wind | L1 watch: SOLAR-1, IMAP, ACE, GOES, Parker spiral, Enlil | `src/components/l1-board.tsx`, `src/lib/l1.ts` |
| Aurora | OVATION forecast map | `src/components/aurora-map.tsx` |
| Ozone | NASA GIBS total column ozone, with a height note and numbers | `src/lib/ozone.ts`, `ozone-panel.tsx` |
| Quakes | USGS earthquakes, coastlines drawn locally (no map API key) | `src/lib/quakes.ts`, `quake-map.tsx` |
| Asteroids | JPL close approaches, Earth globe with a live sunlit face, Moon kept in the plot | `src/lib/nea.ts`, `nea-panel.tsx` |
| Planets | Heliocentric alignment map and current comets | `src/lib/orbits.ts`, `src/lib/comets.ts`, `alignment-map.tsx` |
| Charts | Kp, solar wind speed, IMF Bz | `live-charts.tsx` and the chart components |
| Sun | SDO, SUVI, LASCO stills, plus the Fleet tab | `src/lib/solar-images.ts`, `solar-images.tsx` |
| Movies | Animated SDO / SUVI sequences and a separate LASCO tab | `src/lib/solar-movies.ts`, `solar-movies.tsx`, `lasco-panel.tsx` |
| Feeds | Sources that are not already on the other pages | `src/lib/world-feeds.ts` |
| APOD | NASA picture of the day, today and the previous four | `src/lib/apod.ts` |
| Poles | Magnetic dip poles, 2000–2030 | `src/lib/poles.ts`, `pole-track.tsx` |
| Jerks | Published geomagnetic-jerk catalog, plus observatory Y secular variation | `src/lib/jerks.ts`, `jerks-panel.tsx` |
| Schumann | Tomsk resonance charts. Its own tab, not inside Motion. | `earth-motion.tsx` (`SchumannPanel`) |
| Motion | Chandler wobble versus daily polar motion | `src/lib/motion.ts`, `earth-motion.tsx` |
| SAA | South Atlantic Anomaly from WMM2025, plus GOES protons | `src/lib/saa.ts`, `saa-panel.tsx` |

Local quiet field (WMM2025 via the `magvar` package) and the phone magnetometer live on the Earth field page: `local-field.tsx`, `magnetometer-reading.tsx`. Default example location if geolocation is denied: about 29.47°N, 81.26°W (Bunnell / Flagler County).

## Data sources (no keys)

All of these are public. Cache them. Do not fan out parallel calls to JPL Horizons.

- NOAA SWPC JSON: `https://services.swpc.noaa.gov` — Kp, Kp forecast, RTSW mag and plasma, NOAA scales, OVATION, X-ray, protons, electrons, D-RAP, SUVI and LASCO animation JSON, CCOR-1 and CCOR-2, Enlil, GOES magnetometer.
- INTERMAGNET one-minute files via the BGS GIN.
- NASA SDO browse JPEGs: `https://sdo.gsfc.nasa.gov/assets/img/`
- SOHO realtime (LASCO, EIT): `https://soho.nascom.nasa.gov/data/realtime/`
- STEREO-A beacon only: `https://stereo-ssc.nascom.nasa.gov/beacon/latest/`. **STEREO-B ended in 2014.** Do not add it.
- JPL CAD and Horizons for near-Earth asteroids. Horizons must be **one request at a time**, with a retry. Parallel calls get blocked.
- COBS / MPC comet elements: `https://cobs.si/api/elements.api?format=mpc&mag=obs&is-observed=true`
- USGS earthquakes, NASA GIBS ozone, NASA APOD, SILSO sunspots, GFZ Kp, Bureau of Meteorology ionosphere GIF.
- World Magnetic Model 2025 through npm package `magvar`. Credit NOAA NCEI and the British Geological Survey.
- Tomsk State University for Schumann spectrograms. One station, local time UTC+7, not a global number.
- USNO / IERS for polar motion. Chandler wobble is separated from the daily motion.

## Planet and comet model

`src/lib/orbits.ts` uses JPL’s published approximate Kepler elements (the table meant for roughly 1800–2050).

- Mean longitude is integrated for the full date range.
- Orbit size, eccentricity, and inclination are only allowed to drift about ±500 years. Farther than that they are held near present values.
- A date thousands of years ago is a **long-range sketch**, not a precise ephemeris. Say so in the UI.

`src/lib/comets.ts` keeps up to 80 observed comets brighter than magnitude 16 and inside 6 AU. Trails are two-body tracks from the current elements, sampled every 8 days from −40 to +80 days. They are **not** valid 25,000 years ago. The map hides them when the selected time is more than about 500 days from now.

Planets page controls, in `alignment-map.tsx`:

- Motion on/off. Speeds are **1 month/s, 1 year/s, and 5 years/s**. Do not put the old 50–800 years/s speeds back.
- Backward and Forward. Forward stops at the present. Backward stops at 25,000 BCE.
- Era (BCE or CE), year, month, day, and **time in UTC**.
- Slider labeled 25,000 BCE on the left and Current on the right.
- Jump buttons: 25,000 BCE, 10,000 BCE, 5,000 BCE, 1 CE.
- **Current** sets the clock to real time and stops motion.

BCE years in the UI are historical years (25000 BCE), not the negative astronomical year. Internally, year 0 is 1 BCE.

## Geomagnetic jerks

There is no real-time jerk product. A jerk is a break in the slope of the core field, usually in Y, and it is named only after later years confirm it.

`RECORDED_JERKS` in `src/lib/jerks.ts` is the catalog from 1901 through a provisional window near 2022.8. Do not invent amplitudes. Sources are named on each row.

The chart is the year-to-year change in annual-mean Y from `https://wdcapi.bgs.ac.uk/monthly-means?obs_code=frd` (and `sjg`, `her`, `ngk`). The response is a zip, not JSON. Cache it. Scientific-use credit to the BGS World Data Centre, INTERMAGNET, and the papers listed in the panel. Do not import `node:zlib` at the top of a file the browser loads. A top-level import makes the Jerks tab render and then ignore clicks. Unzip inside the server function only.

## Sun imagery notes

- Sun tab groups: Corona, Chromosphere, Magnetogram, Coronagraph, **Fleet**, Animated.
- Fleet is only what was **not** already on Feeds: STEREO-A EUVI 171, 284, 304, COR1, HI1, plus GOES-19 **CCOR-1** and SWFO-L1 **CCOR-2**.
- Feeds already has STEREO-A EUVI 195, COR2, and HI2, plus SOHO EIT. Do not duplicate those onto Fleet.
- Movies: SDO/SUVI sequences have a loading bar and a speed control (0.5–2 fps). LASCO is its own tab inside Movies, with a loading state. Frames crossfade. Do not start the next frame until the current one has loaded.
- CCOR frame lists: `products/animations/ccor1/ccor1.json` and `ccor2/ccor2.json`. Latest stills: `images/animations/ccor1/latest.jpg` and the same for ccor2.

## Behavior that was fixed on purpose

- Refresh on the dashboard remounts the open page (`revision` in `dashboard.tsx`) so it is not only the top status cards that update.
- The asteroid map stays clickable. Earth is a corner globe, not the center of the NEA plot. The Moon stays in the plot. The sunlit face updates about once a minute from the subsolar longitude. The Sun marker is larger than the first version.
- Earthquake coasts are local GeoJSON (`src/data/world-coast.json`). Do not switch this to a tile service that needs a key.
- Schumann is not inside the Motion tab.
- The image proxy must stay concurrency-limited. Opening Sun used to 502 the preview because every tile requested a new frame every second.

## Credits to keep in the UI

NOAA SWPC (public domain), NASA SDO, SOHO/ESA/NASA and NRL, NASA STEREO, NOAA NCEI and the British Geological Survey for the WMM and IGRF, JPL CNEOS, COBS and the Minor Planet Center, USGS, NASA GIBS, NASA APOD, SILSO, GFZ, Bureau of Meteorology, Tomsk State University, INTERMAGNET.

## What not to build

- No API keys, no paid map tiles, no login required for the science pages.
- Do not add STEREO-B, and do not treat comet elements as valid across millennia.
- Do not describe the 25,000-year map as a precise ephemeris.
- Phone magnetometer readings include the handset and nearby metal. They will not match the quiet WMM value.
- Local field strength from the WMM is the slow core field. Real-time disturbance is the Kp and solar-wind section, not the WMM number.
