Building an Artemis II Tracker
How I tracked humanity's return to the Moon with Python, Streamlit, and a handful of free APIs

When NASA announced that Artemis II would carry the first crewed lunar mission since Apollo 17 in 1972, I wanted to do more than just watch the launch. I wanted to build something — a live dashboard that would let me (and anyone else) follow the mission the way Mission Control does, with real numbers updating in real time. No accounts, no API keys, no paywalls. Just open data and a Python script.
The Idea: What Would Mission Control Actually Show?
The first question I had to answer was: what data is even publicly available for a crewed NASA mission? The answer was more than I thought, but less than I hoped.
On the "more than I thought" side: NASA's Jet Propulsion Laboratory runs a system called Horizons, which is essentially a live database of the positions and velocities of every tracked object in the solar system, including active spacecraft. So cool! Artemis II's Orion capsule has an official JPL target ID: -1024. That means I could query its exact position, distance from Earth, and velocity at any moment, for free, with no authentication required.
On the "less than I hoped" side: crew biometrics, cabin pressure, CO₂ levels, and propellant remaining; all of that is monitored internally by Mission Control in Houston and none of it is released publicly during an active mission. Boo! I made peace with that early and decided to focus on what I could show and show it beautifully.
The final feature list came together around four pillars: orbital tracking from JPL Horizons, space weather and crew radiation data from NOAA, live imagery from GOES-16 and NASA's Solar Dynamics Observatory, and lastly I built a 3D WebGL scene just to make it all feel like a real mission control room.
Setting Up the Project
The stack is deliberately minimal. The whole application is a single Python file, app.py, with three dependencies: Streamlit for the web interface, Plotly for charts, and Requests for HTTP calls. That's it. No database, no background workers, no message queues.
I also wrote a small installer script to handle environment setup cleanly.
Running
python3 install.py
once creates a self-contained virtual environment and installs everything. After that, launching the dashboard is a single command. I wanted the setup experience to be frictionless for anyone who wanted to run it themselves.
The Heart of It: Querying JPL Horizons
The most technically interesting part of the project was getting live orbital data out of JPL Horizons. The system has a REST API, but its response format is designed for astronomers; it returns a structured text block with headers, separator lines, and a data section bracketed by
$$SOE
and
$$EOE
markers.
I queried two objects simultaneously on every refresh: the Orion capsule ( -1024
) and the Moon ( 301
). Getting the Moon's live position was important; the distance between Earth and the Moon varies by about 50,000 km over its orbit, and I wanted the "percentage of the way to the Moon" calculation to be accurate rather than using a fixed average.
The quantities
1
and
20
give me the spacecraft's right ascension and declination (its angular position in the sky) plus its range from Earth in astronomical units and its range-rate — the rate at which that distance is changing, which is the radial velocity. From those four numbers I could derive everything else: distance in km and miles, velocity in km/s and mi/s, altitude above Earth's surface, and light travel time.
Parsing the response required a small trick. Horizons sometimes inserts a variable number of
***
separator lines between the column header and the data block, which breaks naive parsing. I solved it by walking backwards from the
$$SOE
marker to find the first line with at least four commas — that's always the header:
With the position and angles in hand, I could convert the spherical coordinates (RA, Dec, distance) into Cartesian XYZ coordinates in kilometers — which I'd need later to place the spacecraft correctly in the 3D scene.
Derived Metrics: Making the Numbers Meaningful
Raw astronomical units and right ascension coordinates aren't what most people want to see on a dashboard. I built a layer of conversions to turn the Horizons output into human-readable metrics:
I also built a mission phase detector that uses the spacecraft's distance relative to the Moon's current distance to determine where in the journey it is:
Space Weather: Protecting the Crew
One thing that makes a crewed lunar mission genuinely different from robotic missions is radiation. Beyond Earth's magnetosphere, the crew is exposed to solar energetic particles and galactic cosmic rays with no natural shielding. Space weather isn't just an interesting sidebar — it's a crew safety issue.
NOAA's Space Weather Prediction Center publishes real-time data from the DSCOVR satellite (parked at the L1 Lagrange point, about 1.5 million km sunward of Earth) and the GOES geostationary satellites. All of it is free JSON, no API key required, updating every minute or two.
I pulled five endpoints on every refresh:
- Planetary Kp Index — a measure of geomagnetic disturbance, 0–9 scale
- Solar wind speed and density — from DSCOVR's Faraday cup
- Interplanetary Bz — the north-south component of the solar magnetic field; a sustained negative Bz drives geomagnetic storms
- X-ray flux — used to classify solar flares from A through X class
- Proton flux >10 MeV — the NOAA S-scale radiation storm indicator
The flare classification logic mirrors the official NOAA scale:
And the S-scale radiation storm levels map onto proton flux thresholds from S1 (Minor, >10 pfu) through S5 (Extreme, >100,000 pfu), each rendered with an appropriately alarming color.
Live Imagery
The dashboard pulls two live satellite images that update automatically — no intervention needed.
Earth comes from GOES-16, NOAA's geostationary weather satellite parked over the Americas at 35,786 km altitude. NOAA's NESDIS division publishes the GeoColor full-disk composite at a public URL that updates every ten minutes. It's a single
st.image()
call.
The Sun comes from NASA's Solar Dynamics Observatory, which has been imaging the Sun continuously since 2010. The 193-angstrom AIA channel shows the solar corona at about 1.5 million degrees Celsius — active regions appear bright, and solar flares are visible as sudden brightenings. NASA publishes the latest image at a stable URL that updates every fifteen minutes.
The 3D Scene: WebGL Inside Streamlit
The most visually striking part of the dashboard is a live 3D rendering of the Earth-Moon system with the Artemis spacecraft shown at its actual position. I built this using Three.js, the JavaScript WebGL library, and embedded it in Streamlit using
components.html()
.
The trick is passing live Python data into the JavaScript scene. I serialized the orbital positions into a JSON payload and injected it as a JavaScript constant:
Inside the Three.js scene, everything is built procedurally from Canvas 2D textures — no image files required. Earth gets a multi-layer texture with ocean gradients, painted continents, polar ice caps, a cloud layer, and a night-side emissive map showing city lights. The Moon gets a hand-painted texture with maria (the dark basaltic plains), highland regions, and impact craters. Both textures are generated fresh in the browser on every load.
The spacecraft itself is rendered as a triple-layer pulsing orange glow — an outer halo, a mid-layer, and a bright inner core — with a white point at the center and a yellow velocity vector pointing in the direction of travel. 7,000 stars surround the scene, distributed using the golden-angle spiral method for uniform coverage of a sphere, with vertex colors spanning blue-white, white, and warm yellow to approximate the real distribution of stellar spectral types.
Camera control is handled with spherical coordinates. The scene auto-orbits slowly when idle; dragging pauses auto-orbit and gives manual control; scrolling zooms. The camera always looks at the origin (Earth's center), and the zoom is clamped so you can't zoom inside Earth or past the Moon.
Caching: Keeping It Fast and Polite
The dashboard uses Streamlit's
@st.cache_data
decorator to avoid hammering the APIs on every page interaction. Different data sources have different refresh rates:
This means the dashboard feels instant for users — cached data is served immediately — while API calls happen at a sensible cadence in the background.
Deploying It
I wanted the dashboard to be always-on and publicly accessible, so I deployed it on a $6/month DigitalOcean Droplet running Ubuntu. The deployment stack is:
- systemd to run Streamlit as a background service that restarts automatically on crashes or reboots
- Nginx as a reverse proxy, forwarding port 80 to Streamlit's port 8501
- Certbot for automatic HTTPS via Let's Encrypt
The systemd service file is the critical piece — it's what makes the dashboard "always-on" rather than dependent on a terminal window staying open:
Total monthly cost: $6. No sleeping, no cold starts, no usage limits.
What's Not There (And Why)
The dashboard intentionally includes a section explaining what it can't show. Crew biometrics, cabin telemetry, propellant levels — all of that exists, but it flows through NASA's internal Mission Control systems and has never been made available via a public API. This isn't a gap I could fill with creativity; it would require a direct NASA partnership.
I think it's worth being honest about this on the dashboard itself. A lot of "live tracking" sites paper over data gaps with stale numbers or estimates presented as live data. I'd rather show a clear "not available" than fake precision.
Closing Thoughts
The whole project took a weekend to build, costs $6 a month to run, and requires no API keys or paid data sources. Everything it shows is genuinely live — not simulated, not estimated, not cached from hours ago.
The most satisfying moment was watching the spacecraft position update in real time on the 3D globe and realizing that the numbers on screen represent four actual human beings traveling through deep space, further from Earth than any person has been in over fifty years. That felt worth building.
If you want to run it yourself, the full source is available and setup takes about ten minutes. All you need is Python 3.10+ and a terminal.








