A live progress tracker for two journeys at once. Log the distance you walk and the height you climb, then watch yourself walk along a long-distance trail while a second bar carries you up a famous mountain. Every walk counts twice, its horizontal distance against a route and its vertical ascent against a summit, on a full-screen map with running stats, records, a daily streak, and a finish-line celebration.
Pick from 15 trails (the Appalachian Trail, the Pacific Crest Trail, Te Araroa, the Tour du Mont Blanc, and more) and 15 mountains (Everest down to Toubkal). The route and mountain you choose are stored server-side, so the browser and the treadmill agent always agree on what your next walk counts toward.
Walks can be logged by hand or captured automatically from a treadmill with a small background agent that connects over Bluetooth and posts each walk for you, distance and ascent together. Either way, the map updates live, with no refresh.
The tracker ships as a single Rust binary that serves the JSON API and embeds the built React UI. Run it, then open it in your browser.
- Backend: Rust, axum, SQLite (via
sqlx), Server-Sent Events for live updates. - Frontend: React + TypeScript + Vite, MapLibre GL via react-map-gl, TanStack Query, Tailwind, Turf for progress-along-the-line math.
- Treadmill agent: Rust, btleplug for Bluetooth,
reqwestfor posting walks. Speaks standard FTMS. - Shared types: Rust structs are the single source of truth.
ts-rsgenerates the matching TypeScript definitions.
crates/
api-types/ Shared DTOs (source of the generated TS bindings)
storage/ SQLite persistence for walks and the current selection
server/ axum HTTP server that embeds the built UI
treadmill-agent/ Background agent that logs walks from a treadmill over Bluetooth
web/ React + Vite frontend
scripts/ One-time route geometry generator
setup.ps1 Registers the server + agent as Windows scheduled tasks
Two terminals:
# 1) API server on :8849
cargo run -p server
# 2) Vite dev server on :5173 (proxies /api to :8849)
cd web && pnpm install && pnpm devThen browse http://localhost:5173.
Build the UI, then run the release server, which serves everything on :8849:
cd web && pnpm build
cargo run --release -p serverThen browse http://localhost:8849. In release builds the UI is baked into the binary. In debug builds the
server reads web/dist from disk at runtime, so a UI rebuild shows up on refresh without a recompile.
Every walk stores a horizontal distance and a vertical ascent in meters. Distance accumulates against the
selected route, ascent against the selected mountain, and the two totals drive the two progress bars
independently. Routes and mountains live in web/public/data, each keyed by a stable id that the walk log
references. The map draws the selected route from its GeoJSON line and places the marker at the fraction of the
trail you have covered.
The database is seeded with a default route and mountain on first run, so there is no unset state to handle. Distances are stored as canonical integer meters and everything else (kilometers, percentages) is derived for display.
The treadmill-agent binary logs walks from an FTMS treadmill with no manual entry. A treadmill only advertises
over Bluetooth while it is powered on, so one power-on window is one connection. The agent scans, connects when
the treadmill appears, follows its cumulative distance and instantaneous inclination, and logs each walk. Ascent
is the inclination integrated over the distance covered. Pausing the belt keeps the walk open, and resetting the
console or powering off closes it out.
Run it alongside the server, on a machine with Bluetooth in range of the treadmill:
cargo run -p treadmill-agentBy default it matches treadmills whose Bluetooth name starts with FS- (a FitShow module). Override the prefix
for a different machine:
TREADMILL_NAME=MyTreadmill cargo run -p treadmill-agentIt posts to the same POST /api/walks endpoint the web form uses, leaving route and mountain unset so the server
stamps each walk with the stored selection. Captured walks show up in the history and on the map immediately. If
the server is not reachable, a walk is skipped and logged, and the manual form stays available as a fallback.
setup.ps1 registers the release binaries as scheduled tasks so the server and agent start at logon (the agent
one minute later, so Bluetooth is ready). Build the binaries first, then run the script once from an admin
PowerShell:
cargo build --release
.\setup.ps1| Method | Path | Description |
|---|---|---|
GET |
/api/progress |
Global totals plus per-route and per-mountain sums. |
POST |
/api/walks |
Log a walk (distance + ascent) and return the stored row. |
GET |
/api/walks |
List all logged walks (newest first). |
DELETE |
/api/walks/{id} |
Delete a single walk by id. |
GET |
/api/selection |
The current route + mountain selection. |
PUT |
/api/selection |
Replace the current selection. |
GET |
/api/events |
Server-Sent Events stream that pings on every change. |
A logged walk may omit route_id and mountain_id, in which case the server fills them from the stored
selection. This is what lets the treadmill agent post without knowing what you have selected.
After changing any #[derive(TS)] type in api-types, regenerate the bindings in web/src/types/:
cargo test -p api-typesThe trail lines in web/public/data/routes/ are pre-generated from OpenStreetMap. To rebuild or add one, edit the
relation ids in the script and run it. Existing files are skipped, so a flaky Overpass run can just be repeated:
node scripts/build-route-geometry.mjs