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。 以下为已保存的摘录。
Une plateforme MMA en français qui agrège les cartes, les résultats, les combattants et l'actualité de 13 organisations, et qui publie son propre classement : le FightScore. C'est une note de combattant calculée chaque semaine à partir des statistiques réelles de chaque combat UFC, sans vote ni avis éditorial. Le projet comprend un site Next.js, une app mobile Expo, un pipeline de scraping automatisé par GitHub Actions et un moteur de notation (Glicko + score de dominance + régression logistique), écrit en TypeScript de bout en bout et sans bibliothèque de ML.查看完整 README ↗
Une plateforme MMA en français qui agrège les cartes, les résultats, les combattants et l'actualité de 13 organisations, et qui publie son propre classement : le FightScore. C'est une note de combattant calculée chaque semaine à partir des statistiques réelles de chaque combat UFC, sans vote ni avis éditorial.
Le projet comprend un site Next.js, une app mobile Expo, un pipeline de scraping automatisé par GitHub Actions et un moteur de notation (Glicko + score de dominance + régression logistique), écrit en TypeScript de bout en bout et sans bibliothèque de ML.
| Page | Contenu |
|---|---|
/classement-calcule | FightScore : classement calculé par catégorie et pound-for-pound, avec tendance hebdomadaire (▲ ▼ New) et page méthodologie |
/simulateur | Simulateur de combat : probabilité de victoire entre deux combattants à partir de leurs notes et de leur incertitude ; URL partageable, main event pré-rempli, face-à-face |
/fighters/[slug] | Fiche combattant : palmarès complet (y compris hors des organisations suivies), taille, allonge, âge, nationalité, courbe FightScore, statistiques UFCStats, profil de style, probabilité de victoire ; URL lisibles (/fighters/islam-makhachev), anciennes URL numériques redirigées en 308 |
| Menu | Recherche globale (combattants, événements) accessible depuis le menu |
/events | Événements à venir et passés, cartes complètes, horaires de diffusion UFC |
/rankings | Classements officiels UFC |
/actualites | Actualités MMA agrégées par flux RSS (FR et EN) |
/mes-pronostics | Pick'em : pronostics sur les combats, avec un compte utilisateur (Clerk) |
mobile/ | App iOS / Android (Expo Router) qui consomme les routes app/api/mobile |
flowchart LR
subgraph Sources
S[Sherdog]
U[UFCStats.com]
W[ufc.com]
R[Flux RSS]
end
subgraph GA["GitHub Actions (cron)"]
D[daily-sync<br/>tous les jours]
E[event-day-sync<br/>toutes les 30 min, ven-dim]
F[weekly-fightscore<br/>lundi]
N[news-sync<br/>toutes les 2 h]
end
J[(data/scraped/*.json<br/>versionnés dans git)]
DB[(Postgres Neon)]
S --> D & E
W --> D
U --> F
R --> N
D & E & F --> J
J --> DB
N --> DB
F -->|calcul FightScore| DB
DB --> WEB[Site Next.js]
DB --> API[API mobile] --> MOB[App Expo]
Chaque scraper écrit d'abord un fichier JSON versionné dans data/scraped/, puis une étape de synchronisation séparée pousse ce fichier dans Postgres. Ce découpage a trois avantages :
chore(scrape): daily data refresh), donc l'historique git montre ce qui a changé et quand.data/scraped/.cache/ et repartent de là s'ils sont interrompus.| Source | Données | Technique |
|---|---|---|
| Sherdog | Événements, cartes et résultats de 13 organisations : UFC, PFL, Bellator, ONE, Cage Warriors, Rizin, KSW, ACA, Invicta, LFA, Hexagone, Ares, Oktagon. Historique complet de chaque combattant, taille, date de naissance, nationalité. | axios + cheerio, requêtes espacées (fetch-throttled.ts) |
| UFCStats.com | Statistiques de chaque combat UFC, round par round : frappes significatives (tête/corps/jambes, distance/clinch/sol), takedowns, temps de contrôle, knockdowns, tentatives de soumission. Allonge, catégorie du combat, type de décision, bonus de la soirée. | Le site est protégé par un challenge anti-bot en JavaScript que l'on ne passe pas avec un simple client HTTP. On utilise donc Playwright (Chromium headless) avec un seul contexte navigateur réutilisé pendant tout le crawl (fetch-playwright.ts). |
| ufc.com | Classements officiels (champions inclus), horaires de diffusion prelims / main card | axios + cheerio |
| RSS | Actualités : Sherdog, MMA Fighting, L'Équipe MMA | rss-parser (sources.config.ts) |
Les parseurs sont testés sur des pages HTML réelles enregistrées dans __fixtures__/, pour qu'un changement de structure côté source fasse échouer un test plutôt que de corrompre la base sans bruit.
| Workflow | Fréquence | Étapes |
|---|---|---|
daily-sync.yml | Tous les jours, 09:00 UTC | Re-scrape des événements à venir (13 orgs) → mise à jour de l'historique Sherdog des combattants des derniers combats → commit des JSON → synchro Neon → horaires de diffusion UFC → classements officiels UFC → historique et physique (taille, allonge, date de naissance) des nouveaux combattants |
event-day-sync.yml | Toutes les 30 min, vendredi à dimanche | Détecte les organisations qui ont un événement le jour même et ne re-scrape que celles-là, pour publier les résultats pendant la soirée |
weekly-fightscore.yml | Lundi, 12:00 UTC | Scrape UFCStats (stats + bonus) via Playwright → synchro Neon → recalcul du FightScore |
news-sync.yml | Toutes les 2 h | Ingestion des flux RSS |
Rapprocher les combattants entre sources est la partie difficile : Sherdog, UFCStats et ufc.com n'écrivent pas les noms de la même façon (accents, surnoms, translittérations). La correspondance se fait par normalisation de nom, limitée à l'organisation concernée (ranking-name-match.ts), et elle est testée.
Le FightScore classe les combattants UFC sur une échelle de 0 à 100 par catégorie, plus un classement pound-for-pound. Il est recalculé chaque lundi par compute-fighter-ratings.ts. Le calcul se fait en quatre étapes.
dominance-score.ts note chaque victoire entre 0 et 1 :
| Composante | Poids |
|---|---|
| Part des rounds gagnés | 0,35 |
| Bonus de finish (KO/TKO, soumission) | 0,30 |
| Écart de frappes significatives | 0,20 |
| Écart de temps de contrôle | 0,15 |
Le vainqueur de chaque round est estimé à partir des statistiques du round (frappes sig. + 0,5 × minutes de contrôle + 5 × knockdowns), car les cartes des juges ne sont pas publiées sous une forme exploitable. Les rounds gagnés pèsent autant que le finish : une décision où le vainqueur domine tous les rounds compte presque autant qu'un KO au premier round. Une version antérieure de la formule sous-évaluait ce type de victoire, ce que j'appelais le « problème Khabib ».
Deux ajustements volontairement modestes viennent ensuite : une décision partagée retire 0,10 et une décision majoritaire 0,05, tandis qu'un bonus Performance de la Nuit ajoute 0,10 et un Combat de la Nuit 0,05.
glicko-rating.ts et simulate-career.ts rejouent toute l'histoire de l'UFC dans l'ordre chronologique :
winScoreFloor). Ainsi, un combattant ne reste pas classé devant celui qui vient de le battre.Le score affiché (display-scores.ts) vaut deux fois la probabilité estimée de battre le n°1 de la catégorie : le n°1 vaut 100, et un combattant qui a environ une chance sur quatre de le battre vaut à peu près 50. Le champion en titre est toujours affiché en première position (la ceinture est un fait sportif), et en pound-for-pound aucun challenger ne passe devant le champion de sa propre catégorie.
style-archetype.ts attribue à chaque combattant un style parmi les suivants : lutteur/contrôleur, spécialiste soumission, frappeur de distance, puncheur, clincheur/pression ou polyvalent. Le style est calculé à partir de ses taux par 15 minutes, avec un poids plus fort pour les combats récents, en comparaison avec les combattants actifs de sa catégorie. Un style n'est attribué que si le trait est à la fois nettement au-dessus de la catégorie et élevé dans l'absolu.
Une première version utilisait un k-means. Je l'ai remplacée parce que les clusters changeaient d'un recalcul à l'autre et que les étiquettes n'étaient pas fiables.
logistic-regression.ts est une régression logistique écrite à la main (descente de gradient, normalisation des features), entraînée par train-win-predictor.ts sur 15 écarts entre les deux combattants : score de carrière, série en cours, statut d'ancien champion, inactivité, dominance moyenne sur les 3 derniers combats, et style sur les 5 derniers (frappes par zone, takedowns, contrôle, soumissions). Les poids sont exportés dans data/ml-models/win-predictor.json.
Les paramètres sont choisis sur les combats antérieurs à mars 2023, puis évalués sur les 1 468 combats suivants, que le modèle n'a jamais vus (tune-glicko.ts, glicko-params.json) :
| Prédicteur | Précision sur le jeu de test |
|---|---|
| Hasard | 50 % |
| Note de carrière seule | ~57 % |
| Régression logistique | ~58 % |
Ces chiffres restent modestes, et c'est attendu : un combat de MMA est très incertain, et les meilleurs modèles publics se situent dans les mêmes eaux. La page méthodologie le dit clairement aux visiteurs.
En plus de cette validation, sanity-checks.ts contrôle à chaque calcul que le classement reste cohérent, par exemple qu'aucun n°1 de catégorie n'a seulement une poignée de combats et aucune victoire contre le top 10. Ces contrôles ne forcent jamais un nom à une position donnée.
@neondatabase/serverless)node:test via tsx (*.test.ts dans data/)app/ Pages Next.js + routes API (web, mobile, cron)
components/ Composants React
mobile/ App Expo (iOS / Android)
data/
scrapers/ Scrapers Sherdog, UFCStats, ufc.com + synchros vers Neon
shared/ Fetch (throttle / Playwright), checkpoints, normalisation
__fixtures__/ Pages HTML réelles pour les tests de parsing
scraped/ Données brutes scrapées (JSON versionnés)
lib/rating/ Moteur FightScore : dominance, Glicko, scores, styles, régression
scripts/ Calcul, calibrage, réglage et entraînement du FightScore
ml-models/ Paramètres réglés et poids du modèle entraîné
news/ Ingestion RSS
docs/superpowers/ Specs de conception et plans d'implémentation de chaque fonctionnalité
.github/workflows/ Pipelines planifiés
Chaque fonctionnalité importante a d'abord fait l'objet d'une spec et d'un plan écrits dans docs/superpowers/, avant la moindre ligne de code. Par exemple : conception du FightScore, passage à Glicko.
Prérequis : Node 24 et une base Postgres (Neon).
npm install
Créer un fichier .env.local :
DATABASE_URL=postgres://...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
CLERK_SECRET_KEY=...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
npm run dev
Commandes utiles :
| Commande | Rôle |
|---|---|
npm test | Lance tous les tests unitaires |
npm run seed:db | Crée le schéma de base et l'alimente depuis data/scraped/ (base vide uniquement) |
npm run scrape:ufc (ou scrape:all) | Scrape une organisation (ou toutes) depuis Sherdog |
npm run scrape:ufcstats | Scrape les stats round par round depuis UFCStats (nécessite npx playwright install chromium) |
npm run sync:fighter-stats | Pousse les stats UFCStats dans Neon |
npm run compute:ratings | Recalcule le FightScore |
npm run tune:glicko | Règle les paramètres Glicko (entraînement / test temporel) |
npm run train:win-predictor | Entraîne la régression logistique |
npm run check:ratings | Lance les contrôles de cohérence sur le classement |
Les données proviennent de sources publiques (Sherdog, UFCStats.com, ufc.com) et sont récupérées avec des requêtes espacées. Ce projet n'est affilié à aucune organisation de MMA.