- Python 71.7%
- JavaScript 18.5%
- CSS 6.6%
- HTML 2.9%
- Dockerfile 0.3%
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NY4jVhZJeANRpBbmNzvCCD |
||
|---|---|---|
| tests | ||
| tracker | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pytest.ini | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
Flight Tracker
Records and analyses Starship flights from SpaceX's public vehicle tracker feed, for
bunko.me/tracker (UI and API come in later phases; design mockup lives in the proxy repo's
design lab at static/proto/lab/tracker/).
Services
| Container | What it does |
|---|---|
tracker-ingest |
Polls starship_tracker_public.json (both SpaceX hosts). Idle: once a minute, one snapshot archived per 15 min. Armed (T-0 −3 h…+6 h via starship-dashboard /api/next, anything airborne, or an open flight): waits for the next 30 s generation, then polls every 2 s until it lands. Per-host backoff on 429/403/5xx. Detects liftoff/landing. |
tracker-webcast |
Reads the X broadcast overlay: speed (km/h), altitude (km) and the T± clock, 2 frames/s at 720p. Waits for a flight (T-0 and watchUrl from starship-dashboard /api/next), reads from T−15 min to T+4 h, restarts on stream errors. Rows go to the webcast table keyed by broadcast id. ~10% of a core at real time; capped at 1 CPU / 512 MB. |
| tracker-web | FastAPI on the web network (port 8000), public at bunko.me/tracker. Serves recorded flights from their cached analysis and runs the live pipeline: polls the DB every second, grows the live flight fix by fix, streams events to clients. Also replays any recorded flight through that same pipeline on a simulated clock. Capped 1 CPU / 512 MB, max 2 simultaneous replays. |
API (tracker/web.py)
| Endpoint | Returns |
|---|---|
GET /health |
ok + ingest heartbeat age |
GET /api/status |
ingest + webcast health, T-0, live flight summary |
GET /api/flights |
recorded flights (positions: false = a past flight with the webcast only) |
GET /api/flights/{id} |
summary: liftoff, landing window, events (tracking / spacex / webcast), arcs, burns, webcast lag |
GET /api/flights/{id}/track |
fixes + derived speeds, dense coast paths, webcast series (lag-corrected) — ~60 KB gzipped for Flight 14 |
GET /api/flights/{id}/state?t= |
state at a moment: measured (last fix + age), estimated (orbit fit / between fixes / last fix), webcast, best readouts with their source, predicted path (stops at 120 km — ballistic is meaningless in the atmosphere), phase |
GET /api/live |
SSE: hello, track, then fix, summary (when events/arcs change), webcast, state (1/s), predicted (every 10 s) |
GET /api/live?replay=ID&speed=N&from=S |
the same stream for a recorded flight on a simulated clock (≤ 60×; S = seconds from liftoff) — a dress rehearsal of the live pipeline. 409 for a webcast-only flight (no fixes to stream), 503 while a flight is live |
Live processing is incremental (process.ArcGrower, model.LiveFlight): each fix costs < 1 s even mid-orbit, and a live-grown flight ends with exactly the batch analysis's events (tested).
UI (tracker/static/, served by tracker-web at /)
The console layout chosen in the design lab: globe (or map) pinned on the left, state + events on
the right, altitude/speed profile below, time controls at the bottom. Relative URLs, so it works at
/ and behind Caddy at /tracker/.
./— live when a flight is on (SSE); otherwise the latest recorded flight, with a banner saying so?flight=ID&t=S— replay, scrubbed in the browser (1×–300×); readouts, orbit and prediction come from/state?rehearse=ID&speed=N&from=S— a recorded flight through the live pipeline (≤ 60×)&view=map— flat map instead of the globe
Every readout names its source (webcast, orbit fit, between tracker fixes, last tracker fix). The big numbers use whichever is most current: the orbit fit in coast, the webcast when it's fresh or (live, arriving 15–30 s late) still newer than the last fix. Predictions stop at the 120 km entry interface. Events are marked SpaceX timeline / tracking / webcast; a SpaceX item that tracking confirms is shown once. Chart colours are the validated pair (measured #2B9FD6, predicted #C08428).
Webcast overlay reader (tracker/webcast.py, tracker/webcast_run.py)
- Frames normalised to 1280×720 grayscale. Clock bottom-centre (22 px digits) or top-left (16 px); four gauge slots L1 L2 / R1 R2 — first of a pair is SPEED, second ALTITUDE (icons can take a slot). "SUPER HEAVY" under the left pair / "STARSHIP" under the right while both vehicles are shown; otherwise it's the stack (before staging) or the ship.
- Flights 9–13 use the same overlay, with three differences handled: Flights 9–10 draw gauge digits a pixel shorter in a slightly different cut; Flights 10 and 12 show a G-FORCE gauge in R2 late in the flight (rejected by its one-letter unit "G" — "KM/H" is ~25 px wide, "KM" ~13, "G" ~6); Flight 9's coast (T+31–47 min) put the ship's gauges either side of the clock (slots C1/C2). Flights 1–8 used an older text-row overlay the reader doesn't know.
- Characters are cut out by connected components and matched to reference glyphs (
tracker/webcast_glyphs.npz, trained from 44 hand-read frames of Flights 9–14 intests/data/webcast-*; clock and gauge digits each matched only against their own; leave-one-out: 44/44 frames exact). - Frame mission time = clock + time since the clock last ticked (the video lags reality 10–30 s, but every reading carries its exact T+). A clock that disagrees with the video's own timing by > 2 s is held back unless three readings agree (a real jump).
- A reading is flagged
suspectwhen more of its neighbours (up to three each side) disagree with it than agree, or when it sits in a run of readings (≤ 20 s) that neither neighbouring run can physically reach — misreads come in agreeing pairs and, on Flight 9's centre layout, 12-second runs ("117" read as "11.7"). An altitude never has two decimals. - "SUPER HEAVY" read under a pair more than 10 min after staging, over 9,000 km/h or above 130 km is bright ground (the payload-bay camera), not the booster: those readings are the ship's.
webcast_run relabel <broadcast> --staging-met Sre-applies these rules to stored readings. - Backfill a replay:
docker compose run --rm --no-deps tracker-webcast python -m tracker.webcast_run backfill <x.com/i/broadcasts/…> --start S --end E(S/E = seconds into the video). - Retrain after a layout change: add hand-read frames to
tests/data/webcast-labels.json+ strips towebcast-frames.npz, thenwc.train(*wc.load_fixture(...)).
Past flights (tracker/past_flights.json, tracker/past.py)
Flights 9–13 flew before this tracker (and 9–11 before SpaceX's public tracker, which started with Flight 12), so they have no fixes: speed, altitude and events come from the webcast replay alone. The registry holds each flight's X broadcast, where T-0 falls in the replay, hot-staging time, liftoff (GCAT, J. McDowell) and SpaceX's published timeline (archived mission page). Events are read off the overlay: liftoff (T+0), engine cutoff (speed stops rising), highest point, entry interface (first reading under 120 km), touchdown — or where the overlay's data ends. The webcast delay is the one measured on Flight 14 (4.5 s). Checked on Flight 14 against its tracking: cutoff T+8:21 (tracking window 7:53–8:33), entry within 6 s, touchdown to the second.
# read a replay (≈ 8× real time on one core), then register it
docker compose run --rm --no-deps tracker-webcast python -m tracker.webcast_run backfill \
https://x.com/i/broadcasts/<id> --start <broadcast_t0_s − 60> --staging-met <staging_s>
docker compose run --rm --no-deps tracker-web python -m tracker.cli past [starship-flight-N …]
The UI shows these with the readouts, events and profile but no map track, orbit or rehearsal.
Data (./data, not in git)
raw/YYYY-MM-DD.jsonl[.gz]— lossless archive of every stored snapshot; everything else can be rebuilt from it.tracker.db— SQLite (WAL):fixes,flights,events(+detailJSON),webcast(overlay readings),status(ingest/webcast health).analysis/<flight>.json— cached analysis (orbit states included); written bycli analyzeand by tracker-web 20 min after a live flight lands.
What the feed actually is (learned on Flight 14)
- A new
generation_timeevery 30 s (sometimes 40 s); published 1–11 s later (sawtooth). spacex.com's own tracker reads the same file, so there's no faster public position source. current= one fix (ECEF + lat/lon/alt, GPS time,mission_time).speedis always 0.trajectoryis a 15-min ballistic forecast, not history.mission_timeis liftoff-relative only after liftoff → liftoff =gps_time − mission_time(±0.05 s).- Position time-tags have ~0.5 s noise, occasionally 1–1.6 s (km along-track): isolated misses are bad fixes, not manoeuvres.
Analysis (tracker/process.py)
Coast arcs are grown fix by fix against a two-body + J2 orbit fit (tracker/orbit.py, robust least squares). Clean arcs give velocity, orbit elements and predictions; where two arcs meet gives burn time and Δv (Flight 14 orbital insertion: T+25:20, 96 m/s — SpaceX: 25:17–25:36; physics: ~97 m/s). Elsewhere speed comes from local robust smoothing. Events are tagged tracking or spacex (CMS timeline); timeline items planned after the vehicle was already down are set aside, not shown as events.
Commands
docker compose up -d --build # run the recorder
docker compose run --rm --no-deps tracker-ingest python -m tracker.cli status
docker compose run --rm --no-deps -v /path:/imp:ro tracker-ingest python -m tracker.cli import /imp/capture.jsonl
docker compose run --rm --no-deps -v $PWD/tests/data:/td:ro tracker-ingest \
python -m tracker.cli analyze ship41 2026-09-28T09:00:00+00:00 2026-09-28T18:00:00+00:00 \
--id starship-flight-14 --name "Starship Flight 14" --timeline /td/flight-14-timeline.json
uv run --with-requirements requirements.txt --with-requirements requirements-dev.txt python -m pytest -q
Tests pin the Flight 14 results (tests/data/flight-14-raw.jsonl.gz is the merged raw recording).