Gym Log
개인 맞춤 근력 트레이닝 추적기(PWA)
StickToThePlan이 Vätternrundan 시간을 계산하는 방식: 일정한 노력, 가변 속도, 바람·드래프트 포함, 모델을 설명합니다
Så räknar StickToThePlan ut din tid för Vätternrundan: jämn ansträngning, variabel fart, vind och drafting. Här förklarar vi modellen.
스포츠·피트니스TypeScript · CSSGitHub @timcv온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Computes per-control split times and downloadable bike-computer / watch files for a long road cycling event (Vätternrundan 315 km is the bundled route) from a route GPX, a stop schedule, and a target finish time. The plan holds an even rider effort (constant normalized power) and lets speed vary with gradient and wind, capped so front pulls stay sustainable. The same pure calculation core (@stp/core) runs in the browser app and the Node CLI, so the web result and the local result are identical. • Live app: https://sticktotheplan.vercel.app • Model and validation numbers: MODELL.mdREADME 전문 보기 ↗
Computes per-control split times and downloadable bike-computer / watch files for a long road cycling event (Vätternrundan 315 km is the bundled route) from a route GPX, a stop schedule, and a target finish time. The plan holds an even rider effort (constant normalized power) and lets speed vary with gradient and wind, capped so front pulls stay sustainable.
The same pure calculation core (@stp/core) runs in the browser app and the Node CLI, so the web result and the local result are identical.
This README is for developers who want to run the code locally, reuse the core, or open a pull request. If you just want to plan a ride, use the live app.
This is an npm-workspaces monorepo. ESM throughout, TypeScript, no build step for the libraries (source is imported directly).
| Path | What it is |
|---|---|
packages/core | The pure calculation core and output builders: physics, chaingang model, planner, segmentation, GPX/FIT ingest, and the FIT/GPX/JSON builders (incl. the course.fit course export). Browser-safe, no Node-only IO. This is the reusable part. |
packages/cli | Node CLI that does file IO, weather fetch + cache, and the monkeyc compile, calling @stp/core for all math. |
apps/web | Vite + React single-page app. Runs @stp/core in a Web Worker; serves the form, split table, tempokort, and downloads. |
api/ | Vercel serverless function for the server-side weather fetch. |
ciq/ | Garmin Connect IQ data field (Next Control Pace): a static, generic field compiled once with npm run build:ciq. |
Requires Node 18+ (Vite 6 / React 19).
git clone [email protected]:timcv/StickToThePlan.git
cd StickToThePlan
npm install # installs all workspaces
Run the web app:
npm run dev -w apps/web # Vite dev server, default http://localhost:5173
Run the CLI planner:
npm start -w @stp/cli # uses config.json at the repo root
npm start -w @stp/cli -- --offline # no network: cached forecast, else calm wind
The CLI reads config.json at the repo root (GPX path, optional FIT path, FTP, rider count, target time, stops). The data/ directory is gitignored, so point config.json at your own files, or pass --config path/to/other.json.
All three must pass before a PR is merged. CI and reviewers run them.
npm test # Vitest across all workspaces
npm run typecheck # tsc --noEmit (root, api, web)
npm run build:web # production build of apps/web
@stp/core is the calculation engine with no UI and no Node-only IO, so it runs anywhere an ES module bundler does (browser, worker, Node, Bun). It is published only inside this repo and ships TypeScript source, so consume it through a bundler/runtime that handles .ts (Vite, tsx, Bun), or compile it first.
import {
applyDefaults,
ingestGpxString,
solveThreeScenarios,
segment,
buildSplitTable,
} from '@stp/core';
const cfg = applyDefaults({
gpx_path: 'route.gpx',
ftp: 272,
n_riders: 12,
target_total_hm: '11:45',
start_time: '04:22',
stops: [{ control: 'Gränna', km: 77, minutes: 10 }],
});
const micro = ingestGpxString(gpxText, cfg); // string in, no fs
const scenarios = solveThreeScenarios(micro, field, cfg); // field = EnsembleField, or calm
const splits = buildSplitTable(scenarios.expected, cfg);
const display = segment(scenarios.expected, cfg);
field is the weather (an EnsembleField); for no wind, pass a calm field. The full wiring, including how the web app builds the calm and fetched fields, is in the two worked examples: the web worker pipeline (apps/web/src/lib/pipeline.ts) and the CLI. The public surface is the re-exports in packages/core/src/index.ts.
np_target) is read from an optional representative FIT ride (its rolling normalized power), or falls back to 0.60 × FTP.1/n_riders).Full detail and validation in MODELL.md.
main.packages/core browser-safe: no fs, path, or other Node-only APIs. IO belongs in packages/cli or api/.npm test, npm run typecheck, and npm run build:web pass.feat:, fix:, docs:, refactor:, test:, chore:).main with a short description of the change and why.The math is intentionally pure and deterministic. If you change a number that affects output, say which validation case in MODELL.md still holds.
Uploaded GPX and FIT files are processed entirely in the browser, never sent to a server. In server weather mode only the route's rounded sample coordinates and the date are sent to the weather function (SMHI, MET Norway, Open-Meteo). Calm and manual modes send nothing.
MIT. See LICENSE.