个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A triathlon training planner that fits a season to the hours you actually have, and keeps it in your own browser. It is free, open source, and has no server of its own — no account is required, and nothing is uploaded unless you ask it to be.
Live at https://yootri.gzarruk.com
Nothing is edited in place. Any change — yours or the coach's — is built as a draft, checked, shown to you as a diff, and only written when you apply it.
The coach is optional and stays off until you configure it. It can read your plan and propose changes; it cannot write to one. Everything it does lands in the same draft-and-review gate as everything else, so the worst it can produce is a proposal you reject.
localStorage, is never
written into a plan, and is never exported or synced. Anything that can run
script on the page could read it, so use a key you are willing to rotate.Two providers are supported: Anthropic, and anything speaking the OpenAI Chat Completions shape — OpenAI itself, OpenRouter, Groq, Together, LM Studio or Ollama — through an editable base URL. A local endpoint works well here and keeps everything on your own machine.
Responses are generated by an AI model, not a human coach, and can be wrong.
localStorage and works
fully offline and signed out. This is a normal way to run yootri, not a
degraded one.updatedAt. Plans are readable only by
the account that wrote them — see firestore.rules.tools/garmin-bridge is a small program
you run on your own computer (make garmin-login, then make garmin). The
page asks it for recent activities on 127.0.0.1, matches them to your planned
sessions, and logs the ones you confirm. It is off unless you pair a browser
with it, and it uses an unofficial library, so a change on Garmin's side can
break it.YOOTRI_GARMIN_OVERNIGHT=1 make garmin and the box ticked in the Garmin
panel, a sync also reads minutes asleep, the overnight HRV average and resting
heart rate, night by night — none of Garmin's own scores. They stay in this
browser like heart rate: never on a plan, never synced or exported, never
given to the coach. The weekly review sets your last seven nights beside the
four weeks before them and, when they are outside your usual range, says so
beside the option to make one session of next week easy. See
the bridge README.Plan menu → Export this plan writes the file, and Import plan… reads it
back. That file is the only thing import accepts — there is no separate template
format. If you just want to see one,
assets/example-plan.json is a real 20-week 70.3
season, and the import panel will load it for you.
An export wraps the whole plan record in a small envelope:
{
"_type": "im703-plan",
"version": 1,
"exportedFrom": "yootri.gzarruk.com",
"plan": { "...": "the fields below" }
}
A bare plan object — the contents of plan, without the envelope — is accepted
too.
| Field | What it is |
|---|---|
schema | 3. Older files are upgraded on the way in; the file on disk is not touched. |
id, name | Identity. An import always mints a fresh id, so importing your own export sits next to the original rather than replacing it. |
start | "YYYY-MM-DD", the Monday week 0 begins on. Required. |
profile | Race date and type, per-day availability in minutes (the season's biggest week), constraints, and any per-block discipline splits. |
season | One entry per week: absWeek, block, recovery, load, hours. This is the target curve the chart plots against. |
weeks | The sessions themselves, keyed w0…w{n-1} to match season. |
done | Completion flags, keyed by session id. |
actuals | What actually happened, keyed by session id. |
weekBudgets | Per-week hour budgets you have pinned, keyed w0… |
chartmode, charthidden, view | Display state. |
Two things do not travel in a file. Your coach conversation is local to the browser it happened in and is never written out. Your API key is not part of a plan at all, so it is never exported or synced.
A plan is self-contained on purpose: the season only means anything against the
budget and the runway it was fitted to, so both come across with the sessions.
Import is the exact inverse of export — if a field survives the round trip it is
in the table above, and
tests/portable.test.js is what holds that true.
Assets use relative paths and ES modules need a real origin, so serve the folder
rather than opening index.html from disk:
make dev # http://localhost:8000/, opens a browser
make dev PORT=8001 # when 8000 is taken
make dev OPEN=0 # do not open a browser
make stop # free port 8000 again
make stop is there because a dev server outlives the terminal it was started
from — close the window instead of pressing Ctrl-C and it keeps the port, with
its parent shell gone. make dev says so when it hits that, and names the way
out rather than leaving you to remember the lsof incantation.
That runs tools/dev-server.mjs: one small Node script,
no dependencies. Any static server works — python3 -m http.server 8000 is fine — but this one is deliberately stricter than the folder deserves,
because each thing it refuses to do costs an afternoon otherwise:
.js as JavaScript, without which the browser silently refuses
every engine module and the page loads with nothing behind it,Cache-Control: no-store, so a saved edit is never a stale page,index.html,Season.js when the file is season.js. That one only matters on
macOS, where a case-wrong path opens happily and then 404s on Pages, which
runs on Linux.Open it as localhost, not 127.0.0.1. Google sign-in works on the first
and fails on the second: localhost is on Firebase's authorized-domain list by
default and the bare IP is not. Nothing else in the app can tell the difference,
which is what makes it a confusing ten minutes. make dev prints and opens the
spelling that works.
There is no build step and there are no dependencies. package.json exists only
so node --test can run the engine's unit tests:
make test # npm test
make check # the whole CI gate: unit tests + repo hygiene
Every pull request runs the same npm test on Node 20, 22 and 24, plus a check
that nothing private, credential-shaped or broken-linked has been committed —
this repo is a website, so anything merged here is published. See
.github/workflows/ci.yml.
The app is static — any web server or static host will do, and GitHub Pages serves it straight from the repo root.
To be aware: cloud sync will not work on your own domain until you point it at your own Firebase project and add your domain to that project's authorized list. Until then the app runs local-only and says so in the UI, with an explanation of what still works. That is a supported way to use it — if you do not want sync, leave it exactly like that and everything except cross-device syncing and Google sign-in works normally.
To set sync up, see FIREBASE_SETUP.md.
To deploy on GitHub Pages: Settings → Pages → Source = Deploy from a
branch, Branch = main, Folder = / (root). To serve from your own
subdomain, replace the CNAME file with your domain, add a CNAME record at
your DNS host pointing to <your-user>.github.io, and tick Enforce HTTPS
once DNS has propagated.
| Path | What it is |
|---|---|
index.html | Markup, styles, DOM, storage and cloud sync |
assets/coach/*.js | The engine: pure ES modules, no DOM and no I/O |
assets/nocturne.css | The "Nocturne" design-system base |
tests/ | npm test, plain node --test, no framework |
tools/ | The dev server, and an optional Strava relay that is off by default |
firestore.rules | Firestore security rules (owner-only, default deny) |
.github/workflows/ | CI: unit tests on three Node versions, repo hygiene, CodeQL |
CLAUDE.md is the contributor and architecture document. Read it
before changing the engine.
yootri is a training planner. It is not medical advice, and the developer is not a certified coach, personal trainer, or medical professional. Consult a physician before beginning any training programme. Endurance training carries inherent and significant risks of injury, illness and death, and you are solely responsible for your own training decisions.
The full text is in the app, under About & disclaimer in the footer.
yootri is free and MIT-licensed, and always will be. There is no paid tier, no licence key, and nothing to unlock.
If it is useful to you and you would like to support its development, there is a supporter page on Gumroad. That is a contribution to the work, not a purchase of the software.
MIT — see LICENSE.
SPDX-License-Identifier: MIT
YOOTRI_GARMIN_WORKOUTS=1 make garmin