OpenAthlete
AGPLv3 기반 지구력 훈련 플랫폼—EU 호스팅, 투명한 로직, 자체 호스팅·전체 내보내기 지원
울트라 레이스 당일 팀 조정, 구호소에서 한 보드로 운영, 집에서는 실시간 구간 확인
Race-day crew coordination for ultra racers. Your crew works one board at the aid station, and everyone back home watches the splits land live.
스포츠·피트니스JavaScript · HTMLGitHub @SendOffHQ온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.