OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Score. Bet. Settle up.
An offline-first PWA for keeping a golf bet honest. Enter scores hole by hole,
pick a game, and the app tracks the money as you play — then shows exactly who
owes whom, in the fewest transactions, when you walk off 18.
Games: Wolf (with Hammer and Breakout), Nassau with presses, Skins, Match Play,
Stableford, Vegas, Snake, Bingo Bango Bongo, Sixes, Banker, and Trash/Dots.
There is no build step for the app. index.html is the whole thing — open it,
or serve the repository root over HTTP so the service worker registers:檢視完整 README ↗Score. Bet. Settle up.
An offline-first PWA for keeping a golf bet honest. Enter scores hole by hole, pick a game, and the app tracks the money as you play — then shows exactly who owes whom, in the fewest transactions, when you walk off 18.
Games: Wolf (with Hammer and Breakout), Nassau with presses, Skins, Match Play, Stableford, Vegas, Snake, Bingo Bango Bongo, Sixes, Banker, and Trash/Dots.
The shipped app is one file, index.html — open it, or serve the repository
root over HTTP so the service worker registers:
python3 -m http.server 8000
index.html is generated. The source is under src/: the markup in
src/index.html, the stylesheet in src/css/, and the script in src/js/,
one file per area of the app (players, scoring, games, storage, screens, live
rounds, …). Edit those, then rebuild and commit both:
npm run build # reassemble index.html from src/ and data/courses.json
npm run format # optional: format src/ with Prettier (CI checks it)
The build only concatenates: each <style>/<script> in src/index.html
is filled with its folder's files in filename order, so the numeric prefixes
are the load order. Nothing is transformed, and the shipped code is exactly
the source. npm test fails if index.html is out of date.
Much of the script was recovered from minified code, so many functions still
have single-letter parameter names (function (e, t)). Rename them as you
touch them.
The money math is the product, so it has the most tests.
| Command | What it covers |
|---|---|
npm test | Money math, Wolf/Hammer settlement, the check that index.html is built from the current src/ and course data, and a static design-system audit. A few seconds, no dependencies. |
npm run test:flows | Drives a real browser (Playwright) through complete rounds — the only check that sees bugs in the seam between two screens. |
npm run test:rules | Runs firestore.rules through the real rules engine in the Firestore emulator. Requires Java. |
npm run test:live | Two real browsers sharing one round against the Firestore and Auth emulators and the real security rules — publishing, joining by code, scores and Breakouts crossing the wire, the watch link, and account deletion. Requires Java. |
All three run in CI on every push.
data/courses.json is the single source of truth for the built-in course list;
it is inlined into index.html at build time because the app reads COURSE_DB
synchronously and has to work with no signal. After editing it, run
npm run build and commit both files — npm test fails if they drift.
See data/README.md for the schema and validation rules.
Diagrams of the system, the money engine, live rounds, and CI live in
docs/architecture.md.
index.html — the entire application, built from src/: markup,
styles, and logic in one file, so the service worker caches one shell and the app opens on a course
with no signal. Nothing on the launch path goes to the network. The Firebase
SDK in particular is not a blocking <script src>: it is listed in
FIREBASE_SDK and fetched by ensureFirebase() on idle, after the first
screen is up — as three blocking tags it put 528KB of uncacheable
cross-origin JavaScript in front of the scorecard, and in a dead zone the app
never opened at all. Anything that genuinely needs the SDK (sign-in, joining
a live round, a ?watch= link) awaits ensureFirebase().sw.js — the service worker. Caches the shell and static assets
individually (never addAll, which fails atomically) and announces updates
through the cache so a reloaded page still sees them.src/ — the readable source index.html is built from.data/, scripts/ — the course database, build.js (which assembles
index.html), and the preview builds the browser tests use.test/ — money math, Wolf/Hammer settlement, security rules, browser
flows, and a static design-system audit.Local storage is the source of truth. Everything in Firestore is a mirror of what is already on the device, which is why a signed-out player loses nothing.
scripts/build-preview.py produces the two builds the browser tests drive. Both
work by rewriting the one FIREBASE_SDK array — emptied for flows.mjs, so
ensureFirebase() resolves to "no Firebase" without a request; repointed at
node_modules for live-round.test.mjs, which then drives the real SDK against
the emulators. Rewriting the list rather than deleting the loader keeps the
loader, its retry, and every await ensureFirebase() call site under test.
Google Sign-In (Firebase Auth) is optional and unlocks cross-device sync, live rounds, and the shared course library. Three Firestore collections:
| Collection | Contents | Who can read it |
|---|---|---|
users/{uid} | A private mirror of one player's profiles, courses, and round history | That player only |
liveRounds/{code} | A round in progress. The document ID is the six-character share code. | Anyone holding the code |
sharedCourses/{slug} | The community course library — course data only, no personal information | Any signed-in player |
The share code being the document ID is load-bearing. A round can be fetched by its code and the collection can never be listed by anyone but its own owner, so there is no query that returns rounds you were not invited to.
firestore.rules is the deployed policy, not a copy of it: it is tested in CI
(test/rules.test.mjs, 34 cases against the real rules engine) and deployed
from .github/workflows/deploy-rules.yml on every push to main that touches
it. Editing rules in the Firebase console is how the repository and production
drift apart — don't.
Deploying requires a FIREBASE_SERVICE_ACCOUNT repository secret (a service
account JSON key with the Firebase Rules Admin role). Without it the workflow
still tests the rules and warns instead of deploying.
privacy.html and terms.html are served from
the site root so they have stable public URLs — app stores require a reachable
privacy policy link, and a policy that only exists inside a modal has no URL.
The in-app Settings → Legal screens summarise the same text.LICENSE — proprietary, all rights reserved.Before publishing: both policy pages carry a CONTACT EMAIL placeholder and
name "The Press" rather than a legal entity. Replace both with the real contact
address and the company's registered name once it exists.
The Press is a scorekeeper and a calculator. It never accepts, holds, or transfers money — the settle-up buttons hand off to Venmo, Cash App, or PayPal with an amount pre-filled, and the payment happens between the players in those apps. Keeping it that way is what keeps this a utility rather than a regulated money transmitter or gambling operator.