尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 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.md檢視完整 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.
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.