Satellite Flight Dynamics Dashboard
Build a live satellite tracker with SGP4 orbital mechanics and FastAPI.
Introduction
30 Second Summary
Every time you use GPS, check a weather forecast, or watch satellite imagery on the news, you're relying on objects hurtling through space at over 7 km/s. Their exact positions are computed using the same orbital mechanics math you're about to implement.
In this project, you will build a satellite flight dynamics dashboard that fetches real orbital data from CelesTrak, propagates satellite positions using the SGP4 algorithm, and displays live tracking on an interactive map.
What You'll Build
A full-stack Python application with a FastAPI backend performing orbital mechanics computations and a browser-based frontend rendering real-time satellite positions on a Leaflet.js map.
By the end of this project, you'll have:
- A FastAPI backend that fetches live Two-Line Element data from CelesTrak and propagates satellite orbits using SGP4.
- A TEME-to-geodetic coordinate conversion pipeline implemented from scratch in Python, covering GMST computation, frame rotation, and WGS-84 ellipsoid math.
- An interactive satellite tracking dashboard displaying real-time position, ground track, and orbital elements on a world map with auto-refreshing data.
- Secret Mission: Add satellite pass prediction that computes when a satellite will be visible above your ground station location.
Are there any prerequisites?
You should have Python 3.9 installed and either Cursor or VS Code ready to go. Basic familiarity with Python syntax and HTTP concepts (like what an API endpoint is) will help you follow along.
Before We Start
You are about to build a satellite flight dynamics dashboard that fetches real orbital data, propagates it using the SGP4 algorithm, and displays results on an interactive map.
In this step, you will set up your project folder, install the core Python libraries, and create the file structure your dashboard will live in.
In this step, get ready to:
- Create the project directory and a Python virtual environment.
- Install FastAPI, sgp4, and httpx.
- Verify the SGP4 library and create the project file structure.
Create the project directory and virtual environment
Every Python project should live in its own folder with its own virtual environment. This keeps your dependencies isolated from other projects on your machine.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir satellite-dashboard
cd satellite-dashboard
- Create and activate a Python virtual environment by running the commands for your operating system:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activation command (the second line), not just the creation command. On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned first, then try activating again.
help me activate my Python virtual environment
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Packages you install while the venv is active only exist inside this project, so they cannot conflict with other Python work on your machine.
Install Python dependencies
With your virtual environment active, you can now install the libraries your dashboard needs.
- Install all three project dependencies by running:
pip install "fastapi[standard]==0.128.5" sgp4==2.24 httpx
What are these packages?
- fastapi[standard] installs FastAPI (your web framework) along with uvicorn (the development server that runs it).
- sgp4 is the orbital propagation library that computes satellite positions from TLE data.
- httpx is an async HTTP client your backend will use to fetch live orbital data from CelesTrak.
You should see a series of download and install messages ending with Successfully installed followed by the package names.
Seeing a permission error or network timeout?
If you see a permissions error, confirm you see (venv) in your prompt. Installing without an active venv may require admin access. If downloads are timing out, check your internet connection and try again.
help me install Python packages in my virtual environment
Verify installation and create file structure
The sgp4 library includes an optional C++ acceleration path that makes propagation much faster. You need to confirm this compiled extension loaded correctly.
- Verify the SGP4 C++ fast path is active by running:
python -c "from sgp4.api import Satrec, accelerated; print(f'SGP4 accelerated: {accelerated}')"
You should see SGP4 accelerated: True printed in your terminal.
Seeing SGP4 accelerated: False?
This means the C++ extension did not compile during installation. The library will still work using pure Python, just slower. You can continue with the project either way.
help me get SGP4 accelerated mode working
- Create the project files and folder structure by running:
macOS
touch main.py orbital.py
mkdir static
touch static/index.html
Windows
New-Item main.py, orbital.py -ItemType File
mkdir static
New-Item static/index.html -ItemType File
- Confirm the files exist by running:
ls
You should see main.py, orbital.py, static, and venv in the output.
What are these files for?
- main.py will hold your FastAPI web server and API endpoints.
- orbital.py will contain the orbital mechanics engine (SGP4 propagation and coordinate conversion).
- static/index.html will be your browser-based dashboard with the interactive map.
- Open the satellite-dashboard folder in Cursor.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it.
- In Cursor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the satellite-dashboard folder, then click Open.
Reactivating your venv in Cursor
When you open a terminal in Cursor (Cmd+` on macOS or Ctrl+` on Windows), Cursor often auto-detects and activates the venv for you. If you do not see (venv) in the prompt, run the activation command again from the platform tab above.
Your environment is ready. Next up, you will build the orbital mechanics module that computes satellite positions from raw TLE data.
Set Up the Project Environment
A satellite tracking dashboard needs several specialized libraries working together. FastAPI serves the web interface and API endpoints, the sgp4 library handles orbital propagation math, and httpx fetches live satellite data from CelesTrak.
Your project folder and virtual environment are ready with all dependencies installed. In this step, you'll verify that everything is configured correctly before writing any code.
In this step, get ready to:
- Confirm your virtual environment is active and packages are installed.
- Verify the SGP4 library's C++ accelerated path is working.
- Review your project file structure.
Confirm your virtual environment and packages
Your virtual environment isolates this project's packages from your system Python. A quick check confirms everything installed correctly.
- In the Cursor terminal (with your satellite-dashboard folder open), confirm the virtual environment is active by checking your terminal prompt for a (venv) prefix.
- List your installed packages by running:
pip list
You should see fastapi version 0.128.5, sgp4 version 2.24, httpx, and uvicorn in the output list.
Not seeing (venv) in your prompt?
Your virtual environment may have deactivated if you closed and reopened the terminal. Reactivate it by running venv\Scripts\activate (Windows) or source venv/bin/activate (macOS/Linux).
Help me troubleshoot my virtual environment
Verify the SGP4 accelerated path
The sgp4 library includes an optional C++ extension that dramatically speeds up orbital propagation. Confirming it's active means your satellite position calculations will run at compiled speed rather than interpreted Python.
- Verify the C++ accelerated path is active by running this command:
python -c "from sgp4.api import Satrec, accelerated; print(f'SGP4 accelerated: {accelerated}')"
You should see SGP4 accelerated: True printed in your terminal.
What does accelerated mean here?
The sgp4 package ships with both a pure Python implementation and a compiled C++ version of the same algorithm. When accelerated is True, the C++ version loaded successfully.
For a dashboard that recalculates satellite positions every 5 seconds, the compiled path keeps things snappy.
Seeing SGP4 accelerated: False?
This means the C++ extension didn't compile during installation. The library will still work using pure Python, just slower. You can continue with the project either way.
Help me get the C++ extension working
Review your project file structure
Your project has three files that you'll build out over the next steps. Each handles a distinct layer of the application.
- Confirm your file structure by running:
ls
You should see main.py, orbital.py, static/, and venv/ in the output.
What is each file for?
- orbital.py is the flight dynamics engine. It handles SGP4 propagation and coordinate conversion math.
- main.py is the FastAPI backend. It serves the REST API and connects the orbital module to the web.
- static/index.html is the dashboard frontend. It renders the interactive map and orbital elements panel.
Your environment is locked and loaded. Next up, you'll build the orbital mechanics module that turns raw TLE data into latitude, longitude, and altitude.
Build the Orbital Mechanics Module
Your environment is ready and all dependencies are installed. Now it's time to build the core engine that powers your dashboard.
A satellite's position is encoded in Two-Line Element (TLE) data, which is a compact text format containing orbital parameters. To turn that data into a latitude and longitude on a map, you need a multi-stage math pipeline: propagate the orbit with SGP4, rotate from an inertial reference frame to an Earth-fixed frame, then convert to geodetic coordinates. In this step, you will build that entire pipeline in Python.
In this step, get ready to:
- Implement SGP4 propagation that computes satellite position and velocity from TLE data.
- Implement the TEME-to-geodetic coordinate conversion pipeline using GMST rotation and WGS-84 ellipsoid math.
- Implement ground track generation that traces a satellite's path over one full orbit.
Implement SGP4 propagation
The SGP4 algorithm takes a TLE and a target time, then computes where the satellite is in the TEME (True Equator Mean Equinox) inertial reference frame. The result is a position vector in kilometers and a velocity vector in km/s.
- Open orbital.py in Cursor and add the imports, constants, and propagate_satellite function by pasting the following code:
import math
from datetime import datetime, timezone, timedelta
from sgp4.api import Satrec, jday
EARTH_RADIUS_KM = 6378.137
EARTH_E2 = 0.00669437999014
def propagate_satellite(tle_line1, tle_line2, dt=None):
# Create a satellite record from the two TLE lines
satellite = Satrec.twoline2rv(tle_line1, tle_line2)
if dt is None:
dt = datetime.now(timezone.utc)
# Convert datetime to Julian Date (split into integer day + fraction)
jd, fr = jday(dt.year, dt.month, dt.day, dt.hour, dt.minute,
dt.second + dt.microsecond / 1e6)
# Propagate: returns error code, position (km), velocity (km/s) in TEME frame
e, r, v = satellite.sgp4(jd, fr)
if e != 0:
return None
# Convert TEME position to latitude, longitude, altitude
lat, lon, alt = teme_to_geodetic(r, jd + fr)
# Compute speed magnitude from velocity vector
speed = math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2)
# Derive orbital period from mean motion (radians per minute)
period_minutes = (2 * math.pi / satellite.no_kozai) / 60.0 if satellite.no_kozai != 0 else 90.0
return {
"latitude": lat,
"longitude": lon,
"altitude_km": alt,
"velocity_km_s": speed,
"inclination_deg": math.degrees(satellite.inclo),
"eccentricity": satellite.ecco,
"period_minutes": period_minutes,
"epoch": dt.isoformat(),
}
What does this code do?
Satrec.twoline2rv(line1, line2) parses a TLE into a satellite object that the SGP4 algorithm can propagate. The jday function converts a calendar date into a Julian Date, which is the time format SGP4 expects.
satellite.sgp4(jd, fr) returns position and velocity in the TEME inertial frame. The function then calls teme_to_geodetic (which you will write next) to convert that into Earth-relative latitude, longitude, and altitude.
You won't see a change yet. The next function (greenwich_mean_sidereal_time) provides the rotation angle this function needs.
Implement TEME-to-geodetic coordinate conversion
SGP4 gives you a position in the TEME frame, which is fixed relative to the stars. To place the satellite on a map, you need to rotate that position into an Earth-fixed frame (ECEF), then convert to geodetic coordinates (latitude, longitude, altitude).
The key to this rotation is Greenwich Mean Sidereal Time (GMST). GMST tells you how far the Earth has rotated relative to the stars at a given moment.
- Add the greenwich_mean_sidereal_time function below propagate_satellite by pasting this code:
def greenwich_mean_sidereal_time(jd_ut1):
# Compute centuries since J2000.0 epoch
t_ut1 = (jd_ut1 - 2451545.0) / 36525.0
# Vallado formula: GMST in seconds of time
gmst_sec = (67310.54841
+ (876600 * 3600 + 8640184.812866) * t_ut1
+ 0.093104 * t_ut1 ** 2
- 6.2e-6 * t_ut1 ** 3)
# Convert seconds to radians (43200 seconds = pi radians)
gmst_rad = math.fmod(gmst_sec * math.pi / 43200.0, 2 * math.pi)
if gmst_rad < 0:
gmst_rad += 2 * math.pi
return gmst_rad
What does this code do?
This function computes how far the Earth has rotated at a given Julian Date. The formula comes from Vallado's "Fundamentals of Astrodynamics and Applications" and is the standard method used in flight dynamics software.
The result is an angle in radians that you will use to rotate the satellite's position from the inertial frame (TEME) to the Earth-fixed frame (ECEF).
You won't see a change yet. The next function (teme_to_geodetic) uses this rotation angle to convert coordinates.
- Add the teme_to_geodetic function below greenwich_mean_sidereal_time by pasting this code:
def teme_to_geodetic(r_teme, jd_ut1):
x, y, z = r_teme
gmst = greenwich_mean_sidereal_time(jd_ut1)
# Rotate TEME to ECEF using GMST (z-axis rotation)
x_ecef = x * math.cos(gmst) + y * math.sin(gmst)
y_ecef = -x * math.sin(gmst) + y * math.cos(gmst)
z_ecef = z
# Compute longitude from ECEF x,y
lon = math.degrees(math.atan2(y_ecef, x_ecef))
p = math.sqrt(x_ecef ** 2 + y_ecef ** 2)
# Iterative latitude computation using WGS-84 ellipsoid
lat = math.atan2(z_ecef, p)
for _ in range(5):
n = EARTH_RADIUS_KM / math.sqrt(1 - EARTH_E2 * math.sin(lat) ** 2)
lat = math.atan2(z_ecef + EARTH_E2 * n * math.sin(lat), p)
# Compute altitude above the ellipsoid
n = EARTH_RADIUS_KM / math.sqrt(1 - EARTH_E2 * math.sin(lat) ** 2)
alt = p / math.cos(lat) - n
lat = math.degrees(lat)
return lat, lon, alt
What does this code do?
The first step rotates from TEME to ECEF by applying the GMST angle as a z-axis rotation matrix. This effectively accounts for the Earth's rotation beneath the satellite.
The second step converts ECEF (x, y, z) cartesian coordinates to geodetic (lat, lon, alt). Longitude is straightforward, but latitude requires an iterative approach because the Earth is an oblate ellipsoid, not a perfect sphere. The WGS-84 constants (EARTH_RADIUS_KM and EARTH_E2) define that ellipsoid shape.
Why implement this manually instead of using a library like Skyfield?
Libraries like Skyfield abstract away the coordinate transformations entirely. By using the sgp4 library directly and writing the GMST and geodetic conversion yourself, you can see exactly how the rotation matrix and ellipsoid math work.
This is the same pipeline you would implement in MATLAB for an astrodynamics course. The transferable math is the point.
- Save orbital.py.
- Test the coordinate pipeline by running this command in your terminal:
python -c "from orbital import teme_to_geodetic; lat, lon, alt = teme_to_geodetic([6778, 0, 0], 2460000.5); print('Test:', round(lat, 2), round(lon, 2), round(alt, 1))"
- You should see three numbers printed representing a test latitude, longitude, and altitude. The exact values depend on the GMST at that Julian Date, but you should see no errors.
Seeing a NameError or ImportError?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
- Check that greenwich_mean_sidereal_time is defined above teme_to_geodetic in the file. Python reads top to bottom, so the function must exist before it is called.
- Confirm you are running the command from inside the satellite-dashboard folder where orbital.py lives.
Why is my teme_to_geodetic function throwing an error?
Implement ground track generation
A ground track is the path a satellite traces over the Earth's surface during one full orbit. You compute it by propagating the satellite forward in time across its orbital period and recording the lat/lon at each step.
- Add the compute_ground_track function at the bottom of orbital.py by pasting this code:
def compute_ground_track(tle_line1, tle_line2, steps=90):
# Create satellite record and compute orbital period
satellite = Satrec.twoline2rv(tle_line1, tle_line2)
period_min = (2 * math.pi / satellite.no_kozai) / 60.0 if satellite.no_kozai != 0 else 90.0
now = datetime.now(timezone.utc)
track = []
# Propagate across one full orbit in evenly spaced time steps
for i in range(steps + 1):
dt = now + timedelta(minutes=period_min * i / steps)
jd, fr = jday(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
e, r, v = satellite.sgp4(jd, fr)
if e == 0:
lat, lon, alt = teme_to_geodetic(r, jd + fr)
track.append({"lat": lat, "lon": lon})
return track
What does this code do?
The function derives the orbital period from no_kozai (the satellite's mean motion in radians per minute). It then propagates forward across that full period in 90 evenly-spaced steps, converting each position to lat/lon.
The result is an array of coordinate points that your frontend will later draw as a line on the map.
- Save orbital.py.
- Test the full module by fetching live ISS data and propagating it. Run this command:
python -c "import httpx; from orbital import propagate_satellite; r = httpx.get('https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=TLE'); lines = r.text.strip().splitlines(); result = propagate_satellite(lines[1], lines[2]); print('ISS Lat:', round(result['latitude'], 2), 'Lon:', round(result['longitude'], 2), 'Alt:', round(result['altitude_km'], 1), 'km')"
- You should see the ISS current position printed as latitude, longitude, and altitude values. The numbers will vary depending on when you run it, but altitude should be around 410-430 km.
Seeing a connection error or empty result?
- Check your internet connection. The command fetches live data from CelesTrak.
- If you see None instead of coordinates, the SGP4 propagation returned an error code. Double-check that the teme_to_geodetic function is defined before propagate_satellite calls it (it must appear below propagate_satellite in the file, which is fine because Python only resolves names at call time, not definition time).
- Ensure httpx is installed in your virtual environment. Run pip list and confirm httpx appears in the output.
Why is my ISS propagation test failing?
✔️ Awesome, I've got everything!
Great. Double check you have saved orbital.py and that your test printed real ISS coordinates.
ⓧ I'd like to double check the full code
Here is the complete orbital.py file. Make sure yours matches exactly.
import math
from datetime import datetime, timezone, timedelta
from sgp4.api import Satrec, jday
EARTH_RADIUS_KM = 6378.137
EARTH_E2 = 0.00669437999014
def propagate_satellite(tle_line1, tle_line2, dt=None):
satellite = Satrec.twoline2rv(tle_line1, tle_line2)
if dt is None:
dt = datetime.now(timezone.utc)
jd, fr = jday(dt.year, dt.month, dt.day, dt.hour, dt.minute,
dt.second + dt.microsecond / 1e6)
e, r, v = satellite.sgp4(jd, fr)
if e != 0:
return None
lat, lon, alt = teme_to_geodetic(r, jd + fr)
speed = math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2)
period_minutes = (2 * math.pi / satellite.no_kozai) / 60.0 if satellite.no_kozai != 0 else 90.0
return {
"latitude": lat,
"longitude": lon,
"altitude_km": alt,
"velocity_km_s": speed,
"inclination_deg": math.degrees(satellite.inclo),
"eccentricity": satellite.ecco,
"period_minutes": period_minutes,
"epoch": dt.isoformat(),
}
def greenwich_mean_sidereal_time(jd_ut1):
t_ut1 = (jd_ut1 - 2451545.0) / 36525.0
gmst_sec = (67310.54841
+ (876600 * 3600 + 8640184.812866) * t_ut1
+ 0.093104 * t_ut1 ** 2
- 6.2e-6 * t_ut1 ** 3)
gmst_rad = math.fmod(gmst_sec * math.pi / 43200.0, 2 * math.pi)
if gmst_rad < 0:
gmst_rad += 2 * math.pi
return gmst_rad
def teme_to_geodetic(r_teme, jd_ut1):
x, y, z = r_teme
gmst = greenwich_mean_sidereal_time(jd_ut1)
x_ecef = x * math.cos(gmst) + y * math.sin(gmst)
y_ecef = -x * math.sin(gmst) + y * math.cos(gmst)
z_ecef = z
lon = math.degrees(math.atan2(y_ecef, x_ecef))
p = math.sqrt(x_ecef ** 2 + y_ecef ** 2)
lat = math.atan2(z_ecef, p)
for _ in range(5):
n = EARTH_RADIUS_KM / math.sqrt(1 - EARTH_E2 * math.sin(lat) ** 2)
lat = math.atan2(z_ecef + EARTH_E2 * n * math.sin(lat), p)
n = EARTH_RADIUS_KM / math.sqrt(1 - EARTH_E2 * math.sin(lat) ** 2)
alt = p / math.cos(lat) - n
lat = math.degrees(lat)
return lat, lon, alt
def compute_ground_track(tle_line1, tle_line2, steps=90):
satellite = Satrec.twoline2rv(tle_line1, tle_line2)
period_min = (2 * math.pi / satellite.no_kozai) / 60.0 if satellite.no_kozai != 0 else 90.0
now = datetime.now(timezone.utc)
track = []
for i in range(steps + 1):
dt = now + timedelta(minutes=period_min * i / steps)
jd, fr = jday(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
e, r, v = satellite.sgp4(jd, fr)
if e == 0:
lat, lon, alt = teme_to_geodetic(r, jd + fr)
track.append({"lat": lat, "lon": lon})
return track
Your orbital mechanics engine is complete. It can take raw TLE data and produce real-time lat/lon/alt coordinates for any satellite in the catalog. Next up, you will wire this module into a FastAPI backend so the browser can request satellite positions over HTTP.
Build the FastAPI Backend
Your orbital mechanics engine can now propagate satellite positions and generate ground tracks. The missing piece is a way for a web browser to access those calculations.
In this step, you will wire orbital.py into a REST API using FastAPI. The API exposes endpoints that the frontend dashboard will call to fetch satellite positions, ground tracks, and pass predictions.
In this step, get ready to:
- Create the FastAPI app with static file serving and a satellite catalog.
- Build API endpoints for satellite position, ground track, and pass predictions.
- Add TLE caching to avoid redundant requests to CelesTrak.
Create the FastAPI app with static file serving
Every FastAPI project starts with an app instance and the configuration it needs. For this dashboard, that includes a list of satellites to track and a cache dictionary for TLE data.
- In Cursor, open main.py (it should be empty) and paste the following setup code:
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import httpx
from orbital import propagate_satellite, compute_ground_track, compute_pass_predictions
# Create the FastAPI application instance
app = FastAPI(title="Satellite Flight Dynamics Dashboard")
# Predefined list of satellites to track (NORAD catalog numbers)
SATELLITES = [
{"name": "ISS (ZARYA)", "catalog_number": 25544},
{"name": "HST (Hubble)", "catalog_number": 20580},
{"name": "NOAA-20", "catalog_number": 43013},
]
# Dictionary to cache fetched TLE data per satellite
TLE_CACHE = {}
What does this code do?
The imports bring in FastAPI for routing, HTTPException for error responses, StaticFiles for serving the HTML dashboard, and httpx for making async HTTP requests to CelesTrak.
The SATELLITES list defines three well-known satellites by their NORAD catalog numbers. These are the objects your dashboard will be able to track.
- Below the TLE_CACHE = {} line, leave some blank space. You will add the API endpoints there shortly.
- At the bottom of main.py, paste the static file serving code:
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def root():
return FileResponse("static/index.html")
Why is the static mount at the bottom?
FastAPI matches routes in definition order. The app.mount call acts as a catch-all for anything under /static. Placing it after the API endpoints ensures your /api/ routes get matched first.
The root / route serves static/index.html directly so the browser loads the dashboard when you visit the server URL.
- Save main.py.
Your file now has the top setup section and the bottom static-serving section. You will fill in the API endpoints between them next.
Create the API endpoints
The dashboard needs three types of data from the backend: the list of available satellites, the current position of a selected satellite, and the ground track for one full orbit. Each becomes its own endpoint.
All endpoints depend on a shared helper function that fetches TLE data from CelesTrak. You will add that first.
- In main.py, find the line TLE_CACHE = {} and paste the following function below it:
async def fetch_tle(catalog_number: int):
# Return cached TLE if available
if catalog_number in TLE_CACHE:
return TLE_CACHE[catalog_number]
# Fetch fresh TLE from CelesTrak GP API
url = f"https://celestrak.org/NORAD/elements/gp.php?CATNR={catalog_number}&FORMAT=TLE"
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=15.0)
if response.status_code != 200:
raise HTTPException(status_code=502, detail="Failed to fetch TLE from CelesTrak")
lines = response.text.strip().splitlines()
if len(lines) < 3:
raise HTTPException(status_code=404, detail="TLE not found")
# Parse the three-line TLE format (name, line1, line2)
tle_data = {"name": lines[0].strip(), "line1": lines[1].strip(), "line2": lines[2].strip()}
TLE_CACHE[catalog_number] = tle_data
return tle_data
What does this code do?
This async function fetches Two-Line Element data from the CelesTrak GP API using the satellite's NORAD catalog number. The TLE format gives back three lines: the satellite name, then two lines of orbital parameters that the SGP4 algorithm needs.
The function also stores fetched TLEs in TLE_CACHE so repeated requests for the same satellite skip the network call. TLEs remain valid for days, so caching avoids hitting CelesTrak's rate limits.
- Below the fetch_tle function, paste the satellite catalog and position endpoints:
@app.get("/api/satellites")
async def list_satellites():
return SATELLITES
@app.get("/api/satellite/{catalog_number}")
async def get_satellite_position(catalog_number: int):
tle = await fetch_tle(catalog_number)
result = propagate_satellite(tle["line1"], tle["line2"])
if result is None:
raise HTTPException(status_code=500, detail="Propagation error")
result["name"] = tle["name"]
return result
What does this code do?
The /api/satellites endpoint returns the predefined list of tracked satellites so the frontend can populate its dropdown menu.
The /api/satellite/{catalog_number} endpoint fetches the TLE, calls your propagate_satellite() function from orbital.py, and returns the current latitude, longitude, altitude, velocity, and orbital elements as JSON.
- Below the get_satellite_position function, paste the ground track and pass prediction endpoints:
@app.get("/api/satellite/{catalog_number}/track")
async def get_ground_track(catalog_number: int):
tle = await fetch_tle(catalog_number)
track = compute_ground_track(tle["line1"], tle["line2"])
return {"name": tle["name"], "track": track}
@app.get("/api/satellite/{catalog_number}/passes")
async def get_passes(catalog_number: int, lat: float = 0.0, lon: float = 0.0):
tle = await fetch_tle(catalog_number)
passes = compute_pass_predictions(tle["line1"], tle["line2"], lat, lon)
return {"name": tle["name"], "passes": passes}
What does this code do?
The /track endpoint propagates the satellite across one full orbit and returns an array of latitude/longitude points for drawing the ground track on the map.
The /passes endpoint accepts observer latitude and longitude as query parameters. It computes when the satellite will pass above that location in the next 24 hours. This endpoint powers the Secret Mission feature you will build later.
- Save main.py.
- Confirm your endpoint code sits between the TLE_CACHE = {} line and the app.mount line at the bottom.
Seeing a red underline on compute_pass_predictions?
Your editor may show a warning because compute_pass_predictions does not exist in orbital.py yet. That function is part of the Secret Mission step. The import will not cause a runtime error until that endpoint is actually called, so you can safely ignore this for now.
If you want to silence the warning immediately, add an empty placeholder function to orbital.py:
def compute_pass_predictions(tle_line1, tle_line2, obs_lat, obs_lon, obs_alt_km=0.0, hours=24, step_seconds=30):
return []
help me fix the compute_pass_predictions import warning
Add TLE caching and verify the server
The caching logic is already built into fetch_tle. Every time an endpoint calls fetch_tle, it checks TLE_CACHE first. If the satellite's TLE has already been downloaded, the cached version is returned instantly without another network request.
Why cache TLE data?
CelesTrak provides TLE data for free, but it enforces rate limits. If 50 failed requests come from your IP within 2 hours, your connection gets blocked by the firewall.
TLEs describe an orbit that stays accurate for days. Caching them in memory means the auto-refreshing frontend (updating every 5 seconds) only hits CelesTrak once per satellite instead of thousands of times per session.
- Start the development server by running this command in your terminal:
uvicorn main:app --reload
- Open your browser and navigate to http://127.0.0.1:8000/docs to see the FastAPI Swagger UI.
- In the Swagger UI, expand the GET /api/satellite/{catalog_number} section.
- Click Try it out, enter 25544 for the catalog number, and click Execute.
You should see a JSON response with the ISS current position including latitude, longitude, altitude, velocity, and orbital elements.
Seeing a connection error or empty response?
Make sure your virtual environment is active in the terminal where you ran uvicorn. If it is not, activate it first with venv\Scripts\activate (Windows) or source venv/bin/activate (macOS/Linux).
If you see a ModuleNotFoundError, confirm all packages are installed by running pip install "fastapi[standard]==0.128.5" sgp4==2.24 httpx again.
help me debug my FastAPI server startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and the server is still running.
ⓧ I'd like to double check the full code
Here is the complete main.py file at this point:
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import httpx
from orbital import propagate_satellite, compute_ground_track, compute_pass_predictions
app = FastAPI(title="Satellite Flight Dynamics Dashboard")
SATELLITES = [
{"name": "ISS (ZARYA)", "catalog_number": 25544},
{"name": "HST (Hubble)", "catalog_number": 20580},
{"name": "NOAA-20", "catalog_number": 43013},
]
TLE_CACHE = {}
async def fetch_tle(catalog_number: int):
if catalog_number in TLE_CACHE:
return TLE_CACHE[catalog_number]
url = f"https://celestrak.org/NORAD/elements/gp.php?CATNR={catalog_number}&FORMAT=TLE"
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=15.0)
if response.status_code != 200:
raise HTTPException(status_code=502, detail="Failed to fetch TLE from CelesTrak")
lines = response.text.strip().splitlines()
if len(lines) < 3:
raise HTTPException(status_code=404, detail="TLE not found")
tle_data = {"name": lines[0].strip(), "line1": lines[1].strip(), "line2": lines[2].strip()}
TLE_CACHE[catalog_number] = tle_data
return tle_data
@app.get("/api/satellites")
async def list_satellites():
return SATELLITES
@app.get("/api/satellite/{catalog_number}")
async def get_satellite_position(catalog_number: int):
tle = await fetch_tle(catalog_number)
result = propagate_satellite(tle["line1"], tle["line2"])
if result is None:
raise HTTPException(status_code=500, detail="Propagation error")
result["name"] = tle["name"]
return result
@app.get("/api/satellite/{catalog_number}/track")
async def get_ground_track(catalog_number: int):
tle = await fetch_tle(catalog_number)
track = compute_ground_track(tle["line1"], tle["line2"])
return {"name": tle["name"], "track": track}
@app.get("/api/satellite/{catalog_number}/passes")
async def get_passes(catalog_number: int, lat: float = 0.0, lon: float = 0.0):
tle = await fetch_tle(catalog_number)
passes = compute_pass_predictions(tle["line1"], tle["line2"], lat, lon)
return {"name": tle["name"], "passes": passes}
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def root():
return FileResponse("static/index.html")
Your backend is live and serving real satellite data. Next up, you will build the interactive frontend dashboard that visualizes this data on a map.
Build the Dashboard Frontend
Your backend API is live and returning real satellite position data from orbit. Now you need a way to actually see where these satellites are.
In this step, you will build an interactive browser dashboard that plots the satellite on a world map, draws its ground track, and refreshes its position every 5 seconds. The frontend uses Leaflet.js for the map rendering and plain JavaScript to call the API endpoints you created in Step 3.
In this step, get ready to:
- Build the HTML structure with a Leaflet.js map and dark-themed sidebar.
- Implement satellite tracking logic that fetches position data and draws the ground track.
- Add real-time auto-refresh so the satellite marker updates every 5 seconds.
Build the HTML structure with Leaflet.js map and sidebar
The dashboard layout is a two-panel design. A narrow sidebar on the left displays satellite telemetry data, and the map fills the remaining screen space.
- Open static/index.html in Cursor. This file is currently empty.
- Paste the following HTML structure into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Satellite Flight Dynamics Dashboard</title>
<!-- Leaflet.js CSS for map styling -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
/* Reset and full-height flexbox layout */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: monospace; display: flex; height: 100vh; background: #1a1a2e; color: #e0e0e0; }
/* Sidebar panel for telemetry display */
#sidebar { width: 300px; padding: 20px; overflow-y: auto; background: #16213e; border-right: 1px solid #0f3460; }
#sidebar h1 { font-size: 14px; margin-bottom: 16px; color: #4fc3f7; letter-spacing: 2px; }
#sidebar h2 { font-size: 12px; margin-bottom: 8px; color: #4fc3f7; border-bottom: 1px solid #0f3460; padding-bottom: 4px; }
#sidebar label { font-size: 11px; display: block; margin-bottom: 4px; color: #90a4ae; }
#sidebar select { width: 100%; padding: 8px; margin-bottom: 16px; background: #0f3460; color: #e0e0e0; border: 1px solid #4fc3f7; border-radius: 4px; font-family: monospace; }
/* Data rows for orbital elements */
.data-group { margin-bottom: 16px; }
.data-row { display: flex; justify-content: space-between; font-size: 12px; padding: 4px 0; }
.data-row .label { color: #90a4ae; }
.data-row .value { color: #e0e0e0; }
/* Map fills remaining width */
#map { flex: 1; }
.status { font-size: 11px; margin-top: 12px; color: #66bb6a; }
</style>
</head>
<body>
<div id="sidebar">
<h1>SATELLITE FLIGHT DYNAMICS</h1>
<label>SELECT SATELLITE</label>
<select id="sat-select"></select>
<div class="data-group">
<h2>GEODETIC POSITION</h2>
<div class="data-row"><span class="label">Latitude</span><span class="value" id="lat">--</span></div>
<div class="data-row"><span class="label">Longitude</span><span class="value" id="lon">--</span></div>
<div class="data-row"><span class="label">Altitude</span><span class="value" id="alt">--</span></div>
</div>
<div class="data-group">
<h2>ORBITAL ELEMENTS</h2>
<div class="data-row"><span class="label">Velocity</span><span class="value" id="vel">--</span></div>
<div class="data-row"><span class="label">Inclination</span><span class="value" id="inc">--</span></div>
<div class="data-row"><span class="label">Period</span><span class="value" id="period">--</span></div>
<div class="data-row"><span class="label">Eccentricity</span><span class="value" id="ecc">--</span></div>
</div>
<div class="status" id="status">Initializing...</div>
</div>
<div id="map"></div>
<!-- Leaflet.js library -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Initialize the map centered on the equator at zoom level 2
const map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// State: satellite marker, ground track line, and currently selected catalog number
let marker = null;
let trackLine = null;
let currentCatalog = 25544;
// Red circle icon to represent the satellite on the map
const satIcon = L.divIcon({
html: '<div style="background:#ff5252;width:12px;height:12px;border-radius:50%;border:2px solid #fff;"></div>',
iconSize: [12, 12],
className: ''
});
What does this code do?
The HTML creates a two-panel layout: a 300px sidebar on the left for telemetry data, and a full-width map on the right. Leaflet.js 1.9.4 is loaded from the unpkg CDN for both CSS and JavaScript.
The JavaScript at the bottom initializes the Leaflet map with OpenStreetMap tiles and declares three state variables. currentCatalog defaults to 25544 (the ISS) so something loads immediately.
Do not save yet. You will add the JavaScript functions next before closing the file.
Implement satellite tracking logic in JavaScript
The tracking logic needs four functions: one to populate the satellite dropdown, one to update the marker position, one to draw the ground track, and one to orchestrate a full dashboard refresh.
- In static/index.html, directly below the satIcon definition you just pasted, add the following four functions:
// Fetch satellite list from backend and populate the dropdown
async function loadSatellites() {
const res = await fetch('/api/satellites');
const sats = await res.json();
const select = document.getElementById('sat-select');
sats.forEach(function(s) {
const opt = document.createElement('option');
opt.value = s.catalog_number;
opt.textContent = s.name;
select.appendChild(opt);
});
select.value = currentCatalog;
select.addEventListener('change', function(e) {
currentCatalog = parseInt(e.target.value);
updateDashboard();
});
}
// Fetch current position and update marker + sidebar data
async function updatePosition() {
const res = await fetch('/api/satellite/' + currentCatalog);
const data = await res.json();
if (!data.latitude) return;
const latLng = [data.latitude, data.longitude];
if (marker) { marker.setLatLng(latLng); }
else { marker = L.marker(latLng, {icon: satIcon}).addTo(map); }
document.getElementById('lat').textContent = data.latitude.toFixed(4) + '\u00B0';
document.getElementById('lon').textContent = data.longitude.toFixed(4) + '\u00B0';
document.getElementById('alt').textContent = data.altitude_km.toFixed(1) + ' km';
document.getElementById('vel').textContent = data.velocity_km_s.toFixed(2) + ' km/s';
document.getElementById('inc').textContent = data.inclination_deg.toFixed(2) + '\u00B0';
document.getElementById('period').textContent = data.period_minutes.toFixed(1) + ' min';
document.getElementById('ecc').textContent = data.eccentricity.toFixed(6);
document.getElementById('status').textContent = 'Updated: ' + new Date().toLocaleTimeString();
}
// Fetch ground track and draw as polyline segments split at the antimeridian
async function updateTrack() {
const res = await fetch('/api/satellite/' + currentCatalog + '/track');
const data = await res.json();
if (trackLine) { map.removeLayer(trackLine); }
var segments = [];
var current = [];
data.track.forEach(function(p, i) {
if (i > 0 && Math.abs(p.lon - data.track[i-1].lon) > 180) {
segments.push(current);
current = [];
}
current.push([p.lat, p.lon]);
});
segments.push(current);
trackLine = L.layerGroup(segments.map(function(seg) {
return L.polyline(seg, {color: '#4fc3f7', weight: 2, opacity: 0.7});
})).addTo(map);
}
// Clear existing layers and redraw everything from scratch
async function updateDashboard() {
if (marker) { map.removeLayer(marker); marker = null; }
if (trackLine) { map.removeLayer(trackLine); trackLine = null; }
await updatePosition();
await updateTrack();
if (marker) { map.setView(marker.getLatLng(), 3); }
}
What does this code do?
- loadSatellites() fetches the satellite list from your /api/satellites endpoint and creates an <option> element for each one. It also attaches a change listener that triggers a full redraw when the user picks a different satellite.
- updatePosition() calls your propagation endpoint and updates both the map marker and all seven sidebar data fields (lat, lon, alt, velocity, inclination, period, eccentricity).
- updateTrack() fetches the ground track and splits it into segments at the antimeridian. Without this split, a polyline would draw a line straight across the map when longitude jumps from +179 to -179.
- updateDashboard() orchestrates a full refresh by clearing old layers and calling both update functions, then re-centering the map on the satellite.
Why split the polyline at the antimeridian?
Leaflet draws polylines as straight lines between coordinates. When a satellite crosses from longitude +179 to -179, that is a 358-degree jump in raw numbers. Leaflet would draw a line all the way across the map instead of crossing the date line.
By detecting jumps greater than 180 degrees and starting a new segment, each piece of the track renders correctly on its side of the map.
Add real-time auto-refresh
The ISS moves at roughly 7.7 km/s. To keep the marker visibly tracking across the map, the dashboard needs to re-fetch the position on a short interval.
- Below the updateDashboard() function, add the initialization and auto-refresh lines, then close the script and HTML tags:
// Load the satellite list, then trigger the first full render
loadSatellites().then(function() { updateDashboard(); });
// Re-fetch position every 5 seconds to animate the marker
setInterval(updatePosition, 5000);
</script>
</body>
</html>
What does this code do?
The first line calls loadSatellites() and chains updateDashboard() so the ISS loads immediately when the page opens. The setInterval then re-fetches position data every 5000 milliseconds (5 seconds) to keep the marker moving across the map.
- Save static/index.html.
- Make sure your server is still running in the terminal (it should be if you left uvicorn main:app --reload active from Step 3).
- Open http://127.0.0.1:8000 in your browser.
- You should see the dark-themed dashboard with a world map on the right. The ISS marker (a red dot) appears on the map, a cyan ground track line is drawn across the map, and the sidebar shows live latitude, longitude, altitude, velocity, inclination, period, and eccentricity values.
- Wait 5 seconds and watch the sidebar values update automatically. The status line at the bottom of the sidebar shows the last update timestamp.
- Try selecting a different satellite from the dropdown (Hubble or NOAA-20). The marker, ground track, and orbital elements should all update to reflect the new satellite.
Map not loading or seeing a blank page?
- Open your browser's developer console (F12 or right-click and select **Inspect**, then the **Console** tab). Check for any JavaScript errors. A common issue is a typo in the Leaflet CDN URL or a missing closing tag.
- If you see a CORS or network error when fetching /api/satellites, make sure your server is running. Check the terminal for the uvicorn process.
- If the map tiles appear but no satellite marker shows, check that the API is returning data by visiting http://127.0.0.1:8000/api/satellite/25544 directly in your browser. You should see a JSON response with latitude and longitude values.
Help me debug my satellite dashboard
✔️ Awesome, I've got everything!
Great. Double check you have saved static/index.html and can see the satellite moving on the map.
ⓧ I'd like to double check the full code
Here is the complete static/index.html file. Compare your code against this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Satellite Flight Dynamics Dashboard</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: monospace; display: flex; height: 100vh; background: #1a1a2e; color: #e0e0e0; }
#sidebar { width: 300px; padding: 20px; overflow-y: auto; background: #16213e; border-right: 1px solid #0f3460; }
#sidebar h1 { font-size: 14px; margin-bottom: 16px; color: #4fc3f7; letter-spacing: 2px; }
#sidebar h2 { font-size: 12px; margin-bottom: 8px; color: #4fc3f7; border-bottom: 1px solid #0f3460; padding-bottom: 4px; }
#sidebar label { font-size: 11px; display: block; margin-bottom: 4px; color: #90a4ae; }
#sidebar select { width: 100%; padding: 8px; margin-bottom: 16px; background: #0f3460; color: #e0e0e0; border: 1px solid #4fc3f7; border-radius: 4px; font-family: monospace; }
.data-group { margin-bottom: 16px; }
.data-row { display: flex; justify-content: space-between; font-size: 12px; padding: 4px 0; }
.data-row .label { color: #90a4ae; }
.data-row .value { color: #e0e0e0; }
#map { flex: 1; }
.status { font-size: 11px; margin-top: 12px; color: #66bb6a; }
</style>
</head>
<body>
<div id="sidebar">
<h1>SATELLITE FLIGHT DYNAMICS</h1>
<label>SELECT SATELLITE</label>
<select id="sat-select"></select>
<div class="data-group">
<h2>GEODETIC POSITION</h2>
<div class="data-row"><span class="label">Latitude</span><span class="value" id="lat">--</span></div>
<div class="data-row"><span class="label">Longitude</span><span class="value" id="lon">--</span></div>
<div class="data-row"><span class="label">Altitude</span><span class="value" id="alt">--</span></div>
</div>
<div class="data-group">
<h2>ORBITAL ELEMENTS</h2>
<div class="data-row"><span class="label">Velocity</span><span class="value" id="vel">--</span></div>
<div class="data-row"><span class="label">Inclination</span><span class="value" id="inc">--</span></div>
<div class="data-row"><span class="label">Period</span><span class="value" id="period">--</span></div>
<div class="data-row"><span class="label">Eccentricity</span><span class="value" id="ecc">--</span></div>
</div>
<div class="status" id="status">Initializing...</div>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
let marker = null;
let trackLine = null;
let currentCatalog = 25544;
const satIcon = L.divIcon({
html: '<div style="background:#ff5252;width:12px;height:12px;border-radius:50%;border:2px solid #fff;"></div>',
iconSize: [12, 12],
className: ''
});
async function loadSatellites() {
const res = await fetch('/api/satellites');
const sats = await res.json();
const select = document.getElementById('sat-select');
sats.forEach(function(s) {
const opt = document.createElement('option');
opt.value = s.catalog_number;
opt.textContent = s.name;
select.appendChild(opt);
});
select.value = currentCatalog;
select.addEventListener('change', function(e) {
currentCatalog = parseInt(e.target.value);
updateDashboard();
});
}
async function updatePosition() {
const res = await fetch('/api/satellite/' + currentCatalog);
const data = await res.json();
if (!data.latitude) return;
const latLng = [data.latitude, data.longitude];
if (marker) { marker.setLatLng(latLng); }
else { marker = L.marker(latLng, {icon: satIcon}).addTo(map); }
document.getElementById('lat').textContent = data.latitude.toFixed(4) + '\u00B0';
document.getElementById('lon').textContent = data.longitude.toFixed(4) + '\u00B0';
document.getElementById('alt').textContent = data.altitude_km.toFixed(1) + ' km';
document.getElementById('vel').textContent = data.velocity_km_s.toFixed(2) + ' km/s';
document.getElementById('inc').textContent = data.inclination_deg.toFixed(2) + '\u00B0';
document.getElementById('period').textContent = data.period_minutes.toFixed(1) + ' min';
document.getElementById('ecc').textContent = data.eccentricity.toFixed(6);
document.getElementById('status').textContent = 'Updated: ' + new Date().toLocaleTimeString();
}
async function updateTrack() {
const res = await fetch('/api/satellite/' + currentCatalog + '/track');
const data = await res.json();
if (trackLine) { map.removeLayer(trackLine); }
var segments = [];
var current = [];
data.track.forEach(function(p, i) {
if (i > 0 && Math.abs(p.lon - data.track[i-1].lon) > 180) {
segments.push(current);
current = [];
}
current.push([p.lat, p.lon]);
});
segments.push(current);
trackLine = L.layerGroup(segments.map(function(seg) {
return L.polyline(seg, {color: '#4fc3f7', weight: 2, opacity: 0.7});
})).addTo(map);
}
async function updateDashboard() {
if (marker) { map.removeLayer(marker); marker = null; }
if (trackLine) { map.removeLayer(trackLine); trackLine = null; }
await updatePosition();
await updateTrack();
if (marker) { map.setView(marker.getLatLng(), 3); }
}
loadSatellites().then(function() { updateDashboard(); });
setInterval(updatePosition, 5000);
</script>
</body>
</html>
Your satellite dashboard is live. You can track the ISS, Hubble, and NOAA-20 in real time with position updates every 5 seconds. Next up, you will extend this with satellite pass prediction in the Secret Mission.
Secret mission
Add Satellite Pass Prediction
Your dashboard tracks where a satellite is right now. But can you predict when it will fly over YOUR location? This mission adds topocentric elevation computation and a 24-hour forward scan to find visible passes above your ground station.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The satellite-dashboard project folder (contains main.py, orbital.py, static/index.html, and the venv virtual environment).
- The running uvicorn development server.
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment with additional satellites and features.
- Your uvicorn server continues running in the terminal.
- Visit http://127.0.0.1:8000 any time to use the dashboard.
- If you close the terminal, restart the server by activating the virtual environment and running uvicorn main:app --reload from inside the satellite-dashboard folder.
Pause - I'll come back to this later
Stop the running server to free up your terminal and system resources. Your project files stay on disk for next time.
- Switch to the terminal where uvicorn is running.
- Press Ctrl+C to stop the server.
- To resume later, navigate to satellite-dashboard, activate your virtual environment, and run uvicorn main:app --reload.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes the code, virtual environment, and all installed packages.
- Switch to the terminal where uvicorn is running and press Ctrl+C to stop the server.
- Deactivate the virtual environment by running:
deactivate
- Navigate to the parent directory (your Desktop) and delete the project folder.
cd ~/Desktop
rm -rf satellite-dashboard
On Windows, use this command instead:
cd ~\Desktop
Remove-Item -Recurse -Force satellite-dashboard
- Confirm the folder is gone by running:
ls
You should no longer see satellite-dashboard in the output.
Nice Work!
Nice Work!
You just built a full-stack satellite flight dynamics dashboard that tracks real objects in orbit using live data from CelesTrak.
You've learned how to:
- Implement SGP4 orbital propagation with a manual TEME-to-geodetic coordinate conversion pipeline in Python.
- Build a FastAPI backend that fetches live TLE data and serves real-time satellite position through REST API endpoints.
- Create an interactive Leaflet.js dashboard with real-time satellite tracking, ground track visualization, and auto-refreshing orbital elements.
- Secret Mission: Added satellite pass prediction using topocentric elevation angle computation from a ground station location.
Ready to quiz yourself?