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,即可验证所有权并自行管理介绍。
一分钟完成验证Campeonato Brasileiro Série A companion app, live in production — React 19 · TypeScript · Express · AWS. Built end-to-end by directing Claude Code: 742 commits, 45 unit + 30 Playwright e2e tests, 7 GitHub Actions workflows (incl. a documented rollback drill) and keyless OIDC deploy.
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Companion app for the Brazilian football championship — live match detail, standings, and club data for the Campeonato Brasileiro Série A. React 19 · TypeScript · Express · AWS. Built end-to-end by directing the AI coding agent Claude Code. Live: https://brasileirao.mpbarbosa.com — a t3.micro in sa-east-1 running the bundle as a systemd service behind nginx, on a static Elastic IP, with an auto-renewing Let's Encrypt certificate.查看完整 README ↗
Companion app for the Brazilian football championship — live match detail, standings, and club data for the Campeonato Brasileiro Série A.
React 19 · TypeScript · Express · AWS. Built end-to-end by directing the AI coding agent Claude Code.
Live: https://brasileirao.mpbarbosa.com — a t3.micro in sa-east-1 running the bundle as a systemd service behind nginx, on a static Elastic IP, with an auto-renewing Let's Encrypt certificate.
Live Série A data comes from football-data.org when
FOOTBALL_DATA_TOKENis set; without a token the app serves a frozen snapshot, so a fresh clone runs with no signup.
Classificação, light and dark. The app follows your system setting and remembers an explicit choice; the control in the header switches between them. Campanha is the club's position after each round — the season behind a single row.
On a phone the five sections move to a navigation bar fixed at the bottom, icon above label,
the current one marked by a pill. Above sm they stay inline in the header — which is why the
desktop shots above look no different. Five is Material Design 3's ceiling for this pattern, and
the bar is now at it: the labels fit at 375dp because the items carry no horizontal padding, and
below 360dp the active indicator narrows rather than let the last destination fall off the edge.
Ao vivo, light and dark — what is being played now, what is next, and what just finished. Matches in progress get a card each rather than a row, so simultaneous kickoffs are all visible at once. These shots caught none being played, which is the ordinary state: between rounds "Agora" answers in a sentence rather than vanishing, which is the more common state and the reason it is written as a sentence at all. It is the only page that refetches on its own. There is deliberately no match minute: the provider reports a status and a score and never an elapsed clock, so the page says bola rolando rather than guessing a number.
On a phone the header card's link row wraps to its own lines and the five season figures split across two rows of pills instead of one — the frame ends at the Painel do clube band, well above where the desktop crop reaches Artilheiros. It ended at Últimos resultados until the X handle gave the link row one more line.
The header card's zone pill reads differently depending on where a club sits: green and naming the continental spot for a club inside the top eleven, grey and naming only the bare position for a club in the middle of the table, red and naming the rebaixamento for a club in the bottom four. Three more clubs, one per state:
Jogos, light and dark. Every round of the season is reachable from the picker, and each fixture carries the broadcasters showing it — ge, Globo, Premiere, SporTV, Cazé TV, YouTube and Prime Video, with anyone we have no mark for rendered as their own name.
Jogadores, light and dark — the elenco of all twenty clubs. The panels are native <details>,
closed on arrival: the division fields close to a thousand players, and rendered flat the second
club would begin twenty screens below the first, which is exactly the by-club structure the page
exists to show. Two clubs can be open at once, which a picker would not allow. Positions arrive
from the provider at two levels of detail in the same list — a broad line for most players, a
specific role for a few — so they are folded onto the line they belong to, and a player's own
position is printed under the name only when it says something the heading did not.
A match page, light and dark. Gols names who scored and for which club, from CBF's own match feed — the club a goal counts for rather than the scorer's club, which is the whole of what puts an own goal on the right side of a scoreboard. It is also what pushed the Escalações and the Campanha below this capture's crop — melhores momentos itself stays inside it — and goal coverage has since reached the second fixture too, where the Escalações and Campanha fall outside its own crop the same way. Neither capture shows either of the two. Both clubs' campanhas are stacked rather than overlaid, so they share one scale and their rounds line up — the app has no series palette, and inventing a hue pair would be the only place colour carried meaning no token defines. Melhores momentos plays the preferred package in the page, with the other broadcasters as links under it that swap the frame; where none is curated it falls back to a YouTube search and says so. It is also what pushed the Escalações out of both fixture captures: the player is taller than the row of channel marks it replaced.
A second match page. Where the Palmeiras capture shows four goals with a pênalti mark, this
one shows a foul-won free kick marked "(falta)" and one scorer a side — the two shapes
goalLabel prints. It no longer reaches the Estádio, Árbitro or Onde assistir sections either:
the taller centred header and the kickoff line under the score push the crop to end at the same
"Melhores momentos" band as the capture above, for the same reason. It was chosen, before that
header grew, for a fixture upstream names a referee on and the curated venue and broadcast pair
does not overlap with — 157 of the season's 380 fixtures carry a referee, 30 carry curated
venue and broadcast data for coming rounds, and no round sits in both sets.
A club page, in both themes. The Painel do clube row opens the club's campanha — the line tracing where it sat after every round, and beneath it a candle per rodada — and the marks under each fixture say which broadcaster showed it. The campanha is not drawn here: moving it let Jogos disputados into the frame, which is what these two images now reach. The marks keep a light backing in both themes on purpose — Globo's circle and the Premiere wordmark are dark artwork on a transparent ground, and they vanish against a dark page without it.*
The Painel do clube, in both themes — the page that row opens. The line at the top is the campanha as the Classificação draws it; the velas beneath read the same rounds one grain finer, because a round is one point on the line and a club can lose four places inside it. Colour carries the result and the geometry carries the direction, and the rounds worth looking at are the ones where they disagree. Destaques and Perfil sit below the frame: a capture is cropped at the last section fitting in 1080 CSS px, and the velas take most of it.
A stadium page, light and dark, reached from a match's Estádio line — it has no nav
entry, because a ground is somewhere you arrive at from a fixture rather than a section
you set out to browse. No feed we read has a stadium in it:
football-data carries no venue field at any tier, and CBF reports only Stadium - City - UF
per match, so the roster is derived by grouping fixtures on the slug of that string. That
slug is the identity, which is what makes CBF's ARENA MRV and Arena MRV one ground
rather than two. Mandantes falls out of who actually hosted there — nobody had to say
the Maracanã has two tenants. Capacity, the official name and the year are hand-curated, and
each is left out rather than guessed where the source is silent. So is the
photograph, which no feed carries either — it is named by its file title on
Wikimedia Commons and fetched from there, and it always ships with the photographer
and the licence, because every licence in use but one requires the credit wherever
the picture appears.
| Layer | Choice |
|---|---|
| UI | React 19 + TypeScript, Vite dev server and build |
| Styling | Tailwind CSS |
| API / SSR host | Express (TypeScript, bundled with esbuild for production) |
| Data | football-data.org v4, competition BSA |
| Hosting | AWS |
| Tests | node --test for unit logic, Playwright for end-to-end |
The Express server owns the data-sync layer (fetching and normalizing match, table, and club data) and serves the built React bundle, so development and production run the same single process.
npm install
Copy .env.example to .env. For live data, register a free token at
football-data.org and set
FOOTBALL_DATA_TOKEN. Skip it to run on seed fixtures.
Start the development server:
npm run dev
The dev server listens on port 3000, or the next free port if 3000 is taken.
npm run dev — start the Express + Vite development servernpm run build — build the frontend and bundle the servernpm start — run the production buildnpm run lint — type-check with TypeScriptnpm run test:unit — run the Node unit test suitenpm run test:e2e — run the Playwright end-to-end suite (boots its own server)npm run clean — remove distnpx tsx scripts/sync-seed-data.ts — regenerate the offline seed data from the live APInpm run screenshot [url] [light|dark] — refresh the README screenshot from the live sitenpm run deploy:preflight — build and verify the production payload locallyDEPLOY_HOST=ubuntu@host npm run deploy — deploy to EC2 (see scripts/README.md)Run a single test file with node --import tsx --test tests/standings-core.test.ts.
src/ React application, shared types, seed data
standings-core.ts pure table computation (no I/O)
matches-core.ts pure round filtering and feed ordering (no I/O)
football-data-core.ts pure football-data.org adapter: URLs + response mapping
cache-core.ts TTL cache: when a stored answer may be served
circuit-breaker-core.ts circuit breaker: when to stop asking a failing upstream
server.ts Express host: API routes, Vite in dev, static serving in prod
tests/ unit tests for the core modules
Calculation logic lives in root-level *-core.ts modules that do no I/O, so it can be
unit-tested without mocking HTTP. server.ts does any fetching and passes payloads in.
Every data endpoint returns { source, note, updatedAt, data }, where source is one of:
football-data — live upstream dataplaceholder — seed fixtures, because no token is configuredfallback — seed fixtures, because the upstream failed or was disabledThe UI banners the note for anything that isn't live. The free tier allows 10
requests/minute; standings cache for 60s and fixtures for 60s (15s while a match is live),
so the app makes at most ~5 upstream calls/minute no matter how much traffic it serves. A
circuit breaker opens after 3 consecutive failures and stays open for 60s.
GET /api/health — status, version, uptimeGET /api/clubs — the 20 Série A clubsGET /api/standings — the computed tableGET /api/matches[?round=N] — fixtures, defaulting to the whole feedThis repository is developed by directing Claude Code rather than by hand. CLAUDE.md carries the architecture and conventions; CONTEXT.md is the domain glossary — what each Portuguese term means in this codebase, and which near-synonyms were rejected and why. Read both before changing behaviour, and update them in the same commit when a convention or a term changes.
MIT — see LICENSE.