BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
Stage-by-stage GC, sprint and KOM analytics for the Tour de France, Giro d'Italia and Vuelta a España, plus 11 one-day monuments and classics and 6 American gravel and MTB races — 1892–2026.
Translation pending
Sports & fitnessPython · TypeScriptGitHub @eshifletOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Stage-by-stage cycling analytics: the Tour de France, Giro d'Italia and Vuelta a España, 11 one-day classics and 7 gravel races — 1892 to 2026, 786,787 results
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Stage-by-stage results for professional road cycling, charted — every edition of the Tour de France, the Giro d'Italia and the Vuelta a España, eleven one-day classics, and seven gravel and mountain-bike races. Every value is traced to a source in data_provenance. DATA_SOURCES.md lists each one — procyclingstats, bikeraceinfo, cyclingflash, Athlinks, sportmaniacs, tretzesports — with its URLs, how to reach it, and what it is and is not good for.Read the full README ↗
Stage-by-stage results for professional road cycling, charted — every edition of the Tour de France, the Giro d'Italia and the Vuelta a España, eleven one-day classics, and seven gravel and mountain-bike races.
Live: ericshiflet.com/tdf-analytics
21 races · 1,365 race-years · 1892–2026 · 19,023 riders · 786,870 stage results
Every value is traced to a source in data_provenance. DATA_SOURCES.md lists each one — procyclingstats, bikeraceinfo, cyclingflash, Athlinks, sportmaniacs, tretzesports — with its URLs, how to reach it, and what it is and is not good for.
| path | what it holds |
|---|---|
cycling-app/ | the site: TypeScript, D3, Vite. No framework. Deployed to GitHub Pages. |
cycling-app/src/data/ | exported JSON, one file per race-year — what the site actually loads |
pipeline/ | scrapers, ingest, validators and exporters (Python, standard library only) |
pipeline/*_scrapes/ | raw scraped JSON, tracked in git — the source of truth |
ai-context.md | the long-form record: every defect found, why it happened, what the fix was |
architecture.md | data model, module map, CI/CD flow |
cycling.db is not in git and not regenerable from scratch — some values
in it came from sources that no longer answer. Back it up with
python3 pipeline/db_backup.py.
cd cycling-app && npm install && npm run dev # the site, on :5173
cd pipeline && python3 -m unittest discover -p 'test_*.py'
The pipeline needs no dependencies at all — Python 3 and the standard library. The site needs Node 20.
A typical data change is scrape → ingest → export → validate:
python3 scrape_race.py --race tour 1985 # writes tour_scrapes/1985/
python3 preview_reingest.py --race tour 1985 # what the ingest WOULD change
python3 ingest_race.py --race tour 1985
python3 export_gc.py --race tdf --year 1985
python3 validate_db.py && python3 validate_exports.py
preview_reingest.py is worth the habit: it copies the database, ingests into
the copy and reports the diff, counting fills, overwrites and clears
separately. A rebuild that loses rows is the one to stop for.
Cycling data is wrong in quiet ways. A parser that stops matching returns nothing, which becomes a NULL column and a blank chart rather than an error — so the failures here are silent by default, and most of them look plausible. Some real examples, all fixed:
21:47.870 and the pattern allowed no fraction.ai-context.md records each one with the evidence, because the expensive part
is never the fix — it is working out that anything is wrong.
Results come from procyclingstats.com, with
Wikipedia and bikeraceinfo.com used to cross-check distances and classification
totals. Every stored value carries provenance: data_provenance records the
source and the URL for each field, and unknown is a real answer that means
nobody recorded where a number came from.