Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog.
[Unreleased]
Added
-
Landing page: a static page (Astro, in
landing/) for the main domain that explains OpenRadlr, what you need and how to host your own server, with FAQ, changelog, imprint and privacy policy. The app moves to theapp.subdomain. Optional: enable it withCOMPOSE_PROFILES=tls,landingandOPENRADLR_LANDING_DOMAIN(see the deploy guide); without it nothing changes. -
make setupkeeps.envup to date: settings that are new in.env.exampleare added to the end of an existing.env, with their explanation; your values stay as they are.make upandmake updaterun it too (updateafter pulling). -
Zwift Click, Play and Ride controllers: pair them on the start screen next to the trainer. Click +/− shifts the virtual gears; both buttons together open the menu. Play and Ride also change lanes, adjust ERG targets, switch cameras and take photos. In menus, dialogs and on the start screen the buttons move the focus and select, so a whole ride works without a keyboard. Controllers reconnect on their own after falling asleep.
-
One map for all routes: Riverside Flats, Rolling Hills and Col de la Forge now lie in one valley and start together at the bike café on a shared main road. Riding one route, you see the others branching off at the junctions, and the terrain, rivers, villages and farms of the whole map. The routes were laid out again (elevations agree where roads meet, the col climbs out of the valley to about 200 m) with natural bends and small rises; their lengths and character stay as before. Route files can join a map with the new
mapfield. The 3D world is now built in the background, so the ride never stutters while it loads. -
Start menu over the island: the menu’s background is a slow drone flight over the island. Picking a route glides the camera down to it, lights it up (climbs red, sprints blue, a beacon at the start) and shows its elevation profile and a map. The world built in the menu is reused when you join, so the 3D view is there right away. Low graphics, data saver and browsers without WebGL 2 get the 2D map; reduced motion gets cuts instead of flights.
-
The map is an island: the valley now ends in beaches and cliffs, with the open sea all around and a few far-off islands on the horizon instead of the mountain ring. Rivers that reach the coast flow into the sea. The coast is derived from the roads, so it grows with the map.
-
Spin studio: pick Spin Studio on the start screen to ride a group class instead of a road. A class starts every 30 minutes (5 minutes of lobby, then 25 minutes: Tempo Ride, HIIT 30/30, Climb Pyramid, Sweet Spot). Riders sit side by side in a 3D studio, Coach Lemon calls every block (on screen, and out loud with Coach voice), targets follow your FTP in ERG mode (±5 % intensity) or you ride them with your gears, and the room board ranks time on target. Rooms seat 16; bots fill the empty bikes, the 17th rider opens a second room.
OPENRADLR_STUDIO=falsecloses the studio. -
A road network for the island: the map is now built terrain first: a new island with bays, a long cape and cliffs under the mountains, a river valley and a col. Three cafés (Valley Café, Hilltop Café, the Forge Hut on the col) and six junctions are joined by eleven roads that follow the land, bend instead of zigzag and stay well apart. Routes are walks over the roads and turn on a short curve at junctions. The 3D world draws every road once, with a café at every café; the network file is served at
/api/map. The routes changed once: Riverside Flats (8.6 km, flat, two sprints), Rolling Hills (7.8 km from the Hilltop Café) and Col de la Forge (12.9 km, 2.5 km at about 6 % to the col). Rides recorded before still show on the map, on the old roads. -
One world for the whole map: riders on Riverside Flats, Rolling Hills and Col de la Forge now ride one world on the server. You see the riders of the other routes where your roads meet or share a stretch, and draft them when you ride the same road the same way. Races, laps, the HUD and the riders list stay per route; bots spread over the map instead of bunching up on shared roads, and you only receive the riders around you (protocol version 2; reload open tabs after updating).
-
Turn at junctions: 250 m before a junction the HUD shows the roads you can take, left to right; ← / → (or a tap) picks one, and you stay on your route if you pick nothing. A road another route rides takes you onto that route; any other road lets you ride off route and find your way back. The ride stays one recording with the GPS track of the roads you rode. Race entrants stay on their route.
Changed
- Renamed to OpenRadlr, with a new logo and app icons. Environment
variables are now
OPENRADLR_*(rename them in your.env), the Compose project isopenradlr(rundocker compose -p openride downonce beforemake up) and the session cookie changed, so riders connect Strava again. Saved profiles and settings in the browser are kept.
Fixed
-
Devices come back on their own. A trainer or heart rate strap that drops mid-ride now reconnects in the background, like controllers already did; the trainer gets its resistance back. On the next app start, remembered devices connect without a tap where the browser allows it, and otherwise behind a one-tap Reconnect button whose chooser lists only that device. See Staying connected.
-
Devices stay connected between rides. Back to menu and Reconnect after a lost connection no longer reload the page, so trainer, heart rate monitor and controllers stay paired: the next ride starts without the Bluetooth chooser. Leaving a ride puts the trainer back on a flat road.
-
Strava uploads are virtual rides with their climbing. Rides now go to Strava as a FIT file marked as a virtual activity. Strava didn’t let us turn an uploaded TCX ride into a Virtual Ride afterwards, and it replaced the ride’s elevation with its own map, which is flat sea where our world lies. New
.fitdownload next to.tcxand.gpx. -
Smoother trainer resistance: the grade no longer jumps at every point of a route (it is measured over 50 m of road), and the trainer eases into each change over a few seconds. Virtual gears follow your speed in the game instead of your cadence: each shift changes the resistance by one even step straight away, descents keep something to push against, and a hard gear no longer turns into a wall seconds after shifting.
-
ERG in the spin studio: the trainer now holds the class’s power target from the start. Before, it kept whatever resistance it had until ERG was switched off and on again (#41).
-
The 3D map no longer flickers in the ride or the menu’s drone view. The coarse terrain out to the horizon poked through the island’s hills and the sea-floor plane cut through it, other roads’ markings shone through the ridden road at junctions, and the cameras’ near planes wasted depth precision far away.
-
The menu’s drone view is clear again over the bigger island: the haze now grows with the island and the drone circles a little closer. The map stays on screen over the 3D view, small in the stage’s corner.
Added
- Rides on the map: recorded rides carry GPS positions in a virtual world
in the sea south of Mallorca, so Strava and other platforms show the route
on a map. New
.gpxdownload next to.tcx. Strava uploads are marked as Virtual Ride (keeps them off real-world segments) instead of trainer rides.
Added (ready for a public server)
- Ride photos: camera button or P takes a photo of the world with route, distance, time, power, speed and grade on it; the finish dialog lists them for download together with a summary card (totals, power and elevation of the ride), drawn automatically and listed first — ready to add to a Strava activity. Phones can share the images straight to another app.
- Riders online in the ride: the riders button shows how many people are riding on the server; its tooltip and the riders drawer add how many are on your route and how many bots.
- Start menu with server status (riders online, room left), a route picker with live rider counts, and the rider/device setup.
- Routes: every file in
routes/is its own world; new routes Riverside Flats and Col de la Forge; route descriptions. - Bike café at the start of every route: terrace, parasols, bike rack, sign.
- Strava: “Connect with Strava” (server-side OAuth), optional requirement to
join (
OPENRADLR_REQUIRE_STRAVA, one ride per account), upload after a ride. - Finish ride: summary (time, distance, power, climbing, work, heart rate),
.tcxdownload for any platform. - Installable app (PWA): manifest, icons, network-first service worker.
- Power-only trainers: badge, ERG and virtual shifting switched off, guided workouts (targets without resistance control).
- Protection: player cap across routes, connections per IP, message and chat rate limits, handshake/idle timeouts, allowed origins, client IPs from a trusted reverse proxy.
OPENRADLR_ALLOW_SIMULATED_TRAINERto hide the simulated trainer in production.- Deploying: running behind your own Caddy, Strava setup.
Changed
- The finish dialog scrolls on short screens and keeps Keep riding / Back to menu pinned at the bottom; leaving with an unsaved ride or photos asks first.
- Races are opt-in: the starter is entered, others join during a 30 s countdown (J); everyone else keeps riding and is never moved.
- Riders actually pedal: cranks and pedals turn around the bottom bracket, each foot stays clipped to its pedal (with a little ankling) and the knee bends by two-bone IK, instead of stiff legs swinging from the hip.
Changed (ride screen)
- The world now fills the whole window; the HUD floats on top of it instead of the 3D view sitting in a box between panels.
- Four HUD layouts: Cockpit (power gauge, minimal), Broadcast (metric bar, nearby riders, minimap, elevation strip), Training (workout chart, target vs. power, block countdown, average power) and Race (position, lap, race timeline, groups with time gaps, riders around you, sprint/KOM banner).
- Auto layout follows the ride: race → Race, workout → Training, otherwise Cockpit. Pick a fixed one with L, the layout button or the menu; the choice is remembered.
- Settings, training, race controls and diagnostics moved into a slide-out menu (M); riders and chat into a left drawer. Recent messages appear briefly on the HUD.
- Road-ahead preview (next 2 km) and “next KOM / sprint / finish” distance.
Changed (3D visuals, round 3: a lived-in world)
- Optional
zonesin route files (village, river, forest), passed through by the server; seeded defaults when a route has none. - A river carved into its own valley, crossed by a stone arch bridge; it ends in a pond wherever it would reach the road again.
- A village with varied houses, a church, pavements, street lamps (lit at dusk) and name boards; farmsteads with barns, silos, hay bales, cows and sheep.
- A row of spinning wind turbines on the highest ground nearby.
- Cheering spectators behind barriers at the finish and the climb summit.
- Guard rails on descents, warning signs around climbs, a flamme rouge 1 km before the finish; denser forest along forest zones.
- The world is drawn in 300 m chunks with view and shadow culling, and is built after joining while the ride starts in 2D.
Changed (3D visuals, round 2)
- Real-time sun shadows (shadow map with soft PCF filtering) from trees, arches and riders, replacing baked shadow discs.
- Time of day: day, golden hour, dusk, or auto by local clock (T).
- Camera modes: chase, close, drone and TV side, blended switching (C).
- Rider variety: skin, helmet, bike colour and size per rider, keeping the jersey colour code; spoked wheels that spin with speed.
- Grass tufts and wildflowers along the road.
- Graphics quality switch (High/Low) for weaker GPUs.
Changed (3D visuals)
- Gradient sky with sun glow, low-poly clouds and a ring of snow-capped mountains; coarse terrain out to the horizon; lakes in low valleys.
- Farmland-style terrain colours, road verge, roadside posts with kilometre markers, chequered start/finish line.
- Firs, leafy trees and bushes clustered into small woods, with soft shadows; soft shadows under riders.
- More detailed riders (tinted helmet and frame, rims, saddle, shoes) and better lighting with rim light, height fog and tone mapping.
Added (ERG and workouts)
- ERG mode via FTMS Set Target Power: manual target (±5 W), switchable per ride between road simulation and ERG.
- Structured workouts relative to FTP (new profile field): warm-up, steady blocks,
ramps, intervals, cool-down; four built-in presets and
.zwoimport; intensity ±5 %, skip block, pause while not pedalling; workout chart with zone colours.
Added (3D and shifting)
- 3D view with a dependency-free WebGL 2 renderer: low-poly terrain blended to the road, lane markings, trees and rocks from the route seed, arches at start/finish, climb and sprint, pedalling riders, chase camera and name labels. Toggle with V; falls back to 2D without WebGL 2.
- Virtual shifting over standard FTMS for Zwift Cog and single-gear setups: 24 gears, keyboard (E/Q, +/−) and on-screen buttons, gear shown in the HUD.
Fixed
- Trainers that report power via Cycling Power Service (or send zeros in FTMS Indoor Bike Data) now show power and cadence.
- Trainer data is subscribed before taking control, so a refused control request no longer blocks data.
- Overlapping FTMS grade writes (“GATT operation already in progress”) are prevented by a latest-wins write queue.
Added
-
Live power/cadence check on the setup screen, device diagnostics with a copyable report, and a “no trainer data” warning during rides.
-
Rust simulation core: route maths, cycling physics, drafting, lanes, bots with fitness profiles, races with countdown and results.
-
Game server: WebSocket protocol v1, fixed-rate game loop, trainer resistance updates, health check, environment configuration.
-
Browser client: FTMS trainers and heart rate straps via Web Bluetooth, simulated trainer, trainer difficulty, 2D lane view, elevation profile, minimap, nearby riders, race panel, chat.
-
Docker image, Docker Compose with optional HTTPS via Caddy, Makefile, CI.