BikeControl (SwiftControl)
使用 Zwift Click、Zwift Play、Shimano Di2 或任何藍牙控制器,搭配 MyWhoosh、Rouvy、Zwift 等。iOS、Android、Windows、macOS 均可免費試用。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
A static, multi-race dashboard hub for ultra crew. Each race gets a live
dashboard, charts, printable report, and a phone-friendly pit board.
No server, no backend: every race is just a folder of files in this
repo, edited live via the GitHub Contents API.查看完整 README ↗A static, multi-race dashboard hub for ultra crew. Each race gets a live dashboard, charts, printable report, and a phone-friendly crew page. No server, no backend: every race is just a folder of files in this repo, edited live via the GitHub Contents API.
index.html: the marketing page at the site root. Static, no account,
and the only page that is about SendOff rather than about a race.app/index.html: the race hub. Lists every race from races/index.json.
It carries <base href="/"> because it was written at the site root and
every link in it is relative: the tag is what keeps the move to /app/
a move rather than a rewrite. Every other page stayed where it was, so
share links, invites and reset links sent before the move still work.setup.html: wizard. Asks for race name, runner(s), course type
(loops vs. point-to-point segments), aid stations, cutoffs, intake
targets, and a GPX file, then commits races/<slug>/config.json,
races/<slug>/data.json, optional races/<slug>/course.gpx, and
updates races/index.json.race.html?id=<slug>: live dashboard for one race, including a
course map (Leaflet + OpenStreetMap) when a course.gpx is present.pit.html?id=<slug>: sign in/out, intake editing, aid-station editing.charts.html?id=<slug>: leg times, pace, aid times, intake, cumulative.print-report.html?id=<slug>: printable summary.Two course shapes are supported from day one and share the same data
schema. Each "leg" has a startTime (left the previous aid/pit) and
endTime (arrived at the next).
course: { loopCount, loopDistanceMi, loopElevationGainFt? }.
A loop may optionally carry loopSegments: the aid-to-aid segments of
one lap (same shape as segments below). When present, the loop is
treated as that segment pattern repeated loopCount times, so each lap
is split into timed checkpoints; total legs = loopCount × loopSegments.length.course: { segments: [{ name, fromAid, toAid, distanceMi, elevationGainFt?, elevationLossFt?, arriveCutoffHours? }] }.In the wizard, the segments course type collects aid stations as
{ name, mileage, arriveCutoffHours? }, and saves them as segments[]
derived from consecutive aid pairs. Per-segment elevation gain/loss is
computed from the uploaded GPX. Aid stations can be edited later from
the crew page.
This repo is a GitHub Template. To get your own hub:
/ (root).https://<your-handle>.github.io/<your-repo>/. You'll land
on the empty hub.The hub supports two ways to authenticate writes:
To enable the login proxy, deploy worker/ to Cloudflare (see
worker/README.md) and set auth.proxyUrl in
hub.json at the repo root to the worker's URL. With proxyUrl unset
or null, the hub falls back to direct PAT mode, so the proxy can be
down without bricking race day, as long as an operator has a PAT.
Generate password hashes for the worker's USERS secret with
admin/hash.html (opens in any browser, runs
PBKDF2-SHA256 locally: passwords never leave the device).
When the login proxy is enabled, each race is created with a visibility and an ACL:
From the race page, the creator (and any editor) can:
v1 limitation: Private race files still live in this public repo.
The worker enforces ACLs on writes and on /get reads, but anyone with
the exact direct URL of a private race's config.json or data.json
could fetch it from GitHub Pages. The slug randomization makes this
impractical to guess, but it's obscurity, not encryption. The plan is
to move private race storage out of the public repo in a future
iteration.
/
index.html marketing landing page (site root)
app/index.html race hub (moved from / when the landing page took it)
setup.html new-race wizard
race.html per-race dashboard (?id=<slug>)
pit.html per-race crew page
charts.html per-race charts
print-report.html per-race printable
hub.json optional hub-wide config (login proxy URL)
DESIGN.md the design system: tokens, type, components, and the rules the tests hold
lib/
race-core.js shared helpers (compute, GitHub API, GPX, format, auth)
race-theme.css shared design tokens + base styles
signup.html invite-acceptance landing page (proxy mode only)
reset.html password-reset landing page (proxy mode only)
admin.html hub admin tools: issue password-reset links
admin/
hash.html local PBKDF2 password hasher for the proxy USERS list
worker/
src/worker.js Cloudflare Worker auth proxy (optional)
wrangler.toml worker config
README.md worker deploy instructions
races/
index.json hub manifest: { races: [...] }
<slug>/
config.json race definition (course, runners, cutoffs, targets)
data.json live state: { runners: [{ id, legs: [...] }] }
course.gpx optional course file
races/index.json{
"races": [
{
"slug": "sangre-de-cristo-100-2026",
"name": "Sangre de Cristo 100",
"location": "Westcliffe, CO",
"startTime": "2026-07-19T04:00:00-06:00",
"courseType": "segments",
"totalDistanceMi": 101.3,
"runnerNames": ["Jason"],
"cutoffHours": 36,
"createdBy": "[email protected]"
}
],
"lastUpdated": "2026-05-15T12:00:00Z"
}
races/<slug>/config.json (loops){
"name": "Bloodroot 100",
"location": "Pittsfield, VT",
"startTime": "2026-05-08T07:00:00-04:00",
"courseType": "loops",
"visibility": "public",
"createdBy": "[email protected]",
"editors": ["[email protected]"],
"viewers": [],
"cutoffs": { "totalHours": 38, "lastLegStartHours": 35 },
"course": {
"loopCount": 10,
"loopDistanceMi": 10,
"loopElevationGainFt": 1900
},
"runners": [
{ "id": "jasmine", "name": "Jasmine", "bib": "Jasmine · #1", "color": "#c44a18" }
],
"targets": { "caloriesPerHour": 250, "fluidOzPerHour": 20, "sodiumMgPerHour": 500 }
}
visibility, createdBy, editors, and viewers are only meaningful
when the login proxy is enabled. In direct PAT mode they're written but
ignored: any PAT-bearer can read or write any file.
races/<slug>/config.json (segments){
"name": "Sangre de Cristo 100",
"location": "Westcliffe, CO",
"startTime": "2026-09-26T05:00:00-06:00",
"courseType": "segments",
"cutoffs": { "totalHours": 36 },
"course": {
"segments": [
{
"name": "Start → Music Pass",
"fromAid": "Start", "toAid": "Music Pass",
"distanceMi": 7.2,
"elevationGainFt": 2400, "elevationLossFt": 100,
"arriveCutoffHours": 3.0
}
]
},
"runners": [ /* same shape */ ],
"targets": { /* same shape */ }
}
races/<slug>/data.json{
"lastUpdated": "2026-05-09T22:35:00Z",
"runners": [
{
"id": "jasmine",
"legs": [
{
"index": 1,
"startTime": "2026-05-08T07:00:00-04:00",
"endTime": "2026-05-08T13:30:57Z",
"calories": 800, "fluidOz": 30, "sodiumMg": 1356,
"gearChanges": "...", "meds": "...", "issues": "...", "notes": "..."
}
]
}
]
}
legs[].index is 1-based and references either the loop number (loops
mode) or the position in config.course.segments (segments mode).
Aid-station / pit time between legs is derived as
legs[n].startTime - legs[n-1].endTime.
This started as crew tooling for a single race (BloodRoot, a 100-mile loop event), then needed point-to-point support for the Sangre de Cristo 100. The hub generalizes both into one template anyone can fork and add their own races to.