Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
permissions:
contents: read
pages: write
pull-requests: read
id-token: write

concurrency:
Expand All @@ -38,6 +39,17 @@ jobs:
uses: actions/configure-pages@v6
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Resolve build metadata
env:
BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
GH_TOKEN: ${{ github.token }}
run: |
echo "VITE_BUILD_TIMESTAMP=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV"
build_pr_url="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${BUILD_SHA}/pulls" --jq '.[0].html_url // empty')"
if [ -z "$build_pr_url" ]; then
build_pr_url="https://github.com/${GITHUB_REPOSITORY}/pulls?q=is%3Apr+is%3Aclosed"
fi
echo "VITE_BUILD_PR_URL=${build_pr_url}" >> "$GITHUB_ENV"
- name: Build site
run: bun run build
- name: Upload site artifact
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ Bike trainer control web app using Web Bluetooth. Tested with Wahoo KICKR Core 2
## Features

- Welcomes first-time visitors with a concise introduction, open-source and local-data privacy details, a direct source-code link, and an optional “Don't show again” preference stored in the browser; the welcome screen remains available from the Ride Control footer link.
- Manages the smart trainer, heart rate monitor, and both Zwift Click V2 controllers independently from one paired-devices panel, with a blue activity indicator while devices connect and a green indicator once every paired device is ready; keeps the `+` controller above the `−` controller, automatically identifies each physical side, connects both controllers concurrently, routes mirrored Bluetooth notifications only to that side, glows only its row as it is pressed, remembers its identity, and continuously retries saved Click connections after a refresh or controller sleep; stalled attempts can be retried immediately, and Click presses made while this panel is open stay in setup and do not shift the ride.
- Manages the smart trainer, heart rate monitor, and both Zwift Click V2 controllers independently from one paired-devices panel, with blue pulsing dots for controllers awaiting connection and a green indicator once every paired device is ready; keeps the `+` controller above the `−` controller, automatically identifies each physical side, connects both controllers concurrently, routes mirrored Bluetooth notifications only to that side, glows only its row as it is pressed, remembers its identity, continuously retries saved Click connections after a refresh or controller sleep, and keeps the sleeping-controller display stable between retry attempts; stalled attempts can be retried immediately, and Click presses made while this panel is open stay in setup and do not shift the ride.
- Detects browsers outside the currently tested Chrome environment and replaces the pairing controls with a compatibility notice, while showing reconnect-saving guidance alongside the pairing controls only in Chrome.
- Shows each deployment's build time in the viewer's local timezone and links it to the GitHub pull request that produced the build, falling back to the closed pull-request list when no associated PR is available.
- Connects to compatible bike trainers and standard Bluetooth heart rate monitors through Web Bluetooth, remembers authorized devices, and automatically reconnects when possible.
- Shows live speed, power, cadence, heart rate, elapsed time, distance, and estimated calories, with MPH and KM/H display modes.
- Provides direct resistance control with buttons, a slider, and keyboard shortcuts with matching button feedback, shows smoothing progress inside the slider thumb, and records resistance changes alongside the other ride metrics.
Expand Down Expand Up @@ -40,6 +42,9 @@ directory to GitHub Pages at [ridecontrol.xyz](https://ridecontrol.xyz).

## Automatic reconnect

Ride Control currently tests Web Bluetooth only in desktop Chrome. Bluetooth does not work in
Brave.

Persistent Web Bluetooth permissions are disabled by default in current Chromium builds. To allow the app to reconnect after a page reload:

1. Open `chrome://flags/#enable-web-bluetooth-new-permissions-backend`.
Expand Down
17 changes: 15 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useHeartRateMonitor } from './hooks/use-heart-rate-monitor';
import { useSession } from './hooks/use-session';
import { useTrainer } from './hooks/use-trainer';
import { useZwiftClick } from './hooks/use-zwift-click';
import { BUILD_PR_URL, BUILD_TIMESTAMP_UTC, formatBuildTimestamp } from './lib/build-info';
import { formatAggregateAverage, formatDuration } from './lib/format';
import { type AppShortcut, appShortcutForKey, gearingKeyboardShortcuts } from './lib/keyboard';
import {
Expand Down Expand Up @@ -344,7 +345,7 @@ export function App() {
const devicesConnecting = [
trainer.connectionBusy,
heartRate.busy,
click.busy,
click.reconnecting,
click.pairing,
].some(Boolean);
let sessionControlLabel = 'Auto paused';
Expand Down Expand Up @@ -551,7 +552,7 @@ export function App() {
</div>
</section>
</div>
<footer className="fixed bottom-3 left-4 z-20 flex items-center gap-1.5 text-[11px] text-slate-600">
<footer className="fixed right-4 bottom-3 left-4 z-20 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[11px] text-slate-600">
<button
className="font-semibold tracking-wide transition hover:text-slate-400"
onClick={() => setWelcomeOpen(true)}
Expand All @@ -577,6 +578,18 @@ export function App() {
>
Sponsor
</a>
<span aria-hidden="true">·</span>
<a
className="transition hover:text-slate-400"
href={BUILD_PR_URL}
rel="noreferrer"
target="_blank"
title={`Built from UTC timestamp ${BUILD_TIMESTAMP_UTC}`}
>
<time dateTime={BUILD_TIMESTAMP_UTC}>
{formatBuildTimestamp(BUILD_TIMESTAMP_UTC)}
</time>
</a>
</footer>
<Notification
connected={connected}
Expand Down
8 changes: 8 additions & 0 deletions src/build-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
interface ImportMetaEnv {
readonly RIDE_CONTROL_BUILD_PR_URL: string;
readonly RIDE_CONTROL_BUILD_TIMESTAMP_UTC: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
Loading