{
  "generated_at": "2026-09-22 22:17:12",
  "generated_at_iso": "2026-09-22T22:17:12.007665",
  "repository": {
    "name": "revontulet-website",
    "branch": null,
    "url": "https://github.com/zRenard/revontulet-website"
  },
  "commits": [
    {
      "hash": "f16ceda04972c9cc2c21daa66b2df6dcf3ea511f",
      "short_hash": "f16ceda",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790115261,
      "message": "feat: SPOR-957 - Alimentation - Facteur (%) du besoin de base d'hydratation\n\nRetour utilisateur : le besoin de base (35/30/25 ml/kg x poids) semblait\ntrop eleve, independamment du supplement d'activite deja reglable\n(fourchette ml/h, SPOR-950). Ajoute un facteur pourcentage optionnel\n(Reglages -> Physiologie, 50-150%, vide = 100%) qui multiplie uniquement\nle besoin de base, sans jamais toucher au supplement d'activite.\n\nVerifie en local via un vrai POST du formulaire Reglages (CurlRunner,\nrestaure a l'identique apres verification) : 70 kg x 35 ml/kg x 0.8 =\n1960 ml, reflete correctement sur /hydration.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-957 - Alimentation - Facteur (%) du besoin de base d'hydratation",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/216_add_hydration_baseline_factor.sql",
        "docs/alimentation.md",
        "docs/profil.md",
        "src/Domain/Nutrition/Hydration/HydrationNeed.php",
        "src/Http/Nutrition/HydrationSummaryBuilder.php",
        "src/Http/Settings/SettingsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/settings.php",
        "tests/Domain/Nutrition/Hydration/HydrationNeedTest.php",
        "tests/Http/Nutrition/HydrationSummaryBuilderTest.php",
        "tests/Http/Settings/HydrationBaselineFactorSettingWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "db/migrations/216_add_hydration_baseline_factor.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "docs/profil.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Domain/Nutrition/Hydration/HydrationNeed.php",
          "insertions": 19,
          "deletions": 2,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Nutrition/HydrationSummaryBuilder.php",
          "insertions": 11,
          "deletions": 6,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 42,
          "deletions": 2,
          "lines_changed": 44
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/settings.php",
          "insertions": 17,
          "deletions": 1,
          "lines_changed": 18
        },
        {
          "path": "tests/Domain/Nutrition/Hydration/HydrationNeedTest.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "tests/Http/Nutrition/HydrationSummaryBuilderTest.php",
          "insertions": 31,
          "deletions": 1,
          "lines_changed": 32
        },
        {
          "path": "tests/Http/Settings/HydrationBaselineFactorSettingWiringTest.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        }
      ],
      "files_changed": 14,
      "insertions": 315,
      "deletions": 12,
      "tags": [
        "v1.7.72"
      ]
    },
    {
      "hash": "c8f511bacbbae9d4ae25610a62fc4d6740ac43e4",
      "short_hash": "c8f511b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790111856,
      "message": "fix: SPOR-956 - Sante - Tuile Frec affichait la derniere seance du jour, pas la meilleure\n\nrecoveryHrForUser() renvoyait feed()['latest'] (reaffecte a chaque ligne\ndans l'ordre started_at ASC = la toute derniere seance traitee), alors\nque le libelle \"meilleure de la semaine\" et le sens de la metrique (plus\nhaut = mieux) attendent le MAX. Deux seances le meme jour avec une pire\nFrec en fin de journee faisaient retomber la tuile dessus au lieu du pic\ndu matin. Corrige en lisant series[weeks-1] (deja agrege en 'max' par\nsemaine). Repro/regression : testRecoveryHrWeeklyBestNotLatestChronologicalSession.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-956 - Sante - Tuile Frec affichait la derniere seance du jour, pas la meilleure",
      "type": "fix",
      "modified_files": [
        "src/Http/Health/SessionPhysiologyTrends.php",
        "tests/Http/Health/PhysiologyTilesWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Http/Health/SessionPhysiologyTrends.php",
          "insertions": 16,
          "deletions": 3,
          "lines_changed": 19
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 58,
          "deletions": 1,
          "lines_changed": 59
        }
      ],
      "files_changed": 2,
      "insertions": 74,
      "deletions": 4,
      "tags": [
        "v1.7.71"
      ]
    },
    {
      "hash": "e19431bfaafefc3559a44583865ffb77c9029612",
      "short_hash": "e19431b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790102397,
      "message": "feat: SPOR-956 - Sante - Frec : backfill des seances FIT anterieures\n\nRetro-peuple sessions.recovery_hr_delta pour les seances FIT importees\navant cette story (meme patron que SessionStrideLengthBackfill : re-parse\ntcx_content par lot de 20, sentinelle recovery_hr_checked_at). Pas de\nfiltre sport, contrairement a la longueur de foulee : l'event recovery_hr\npeut apparaitre sur n'importe quelle discipline.\n\nVerifie en local : 186 seances FIT examinees, 81 mises a jour, 0 restante.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-956 - Sante - Frec : backfill des seances FIT anterieures",
      "type": "feat",
      "modified_files": [
        "TECHNICAL.md",
        "bin/backfill_session_recovery_hr.php",
        "db/migrations/215_add_session_recovery_hr_checked_at.sql",
        "public/backfill_session_recovery_hr.php",
        "src/Infra/Db/SessionRecoveryHrBackfill.php",
        "tests/Infra/Db/SessionRecoveryHrBackfillTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 24,
          "deletions": 6,
          "lines_changed": 30
        },
        {
          "path": "bin/backfill_session_recovery_hr.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "db/migrations/215_add_session_recovery_hr_checked_at.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "public/backfill_session_recovery_hr.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "src/Infra/Db/SessionRecoveryHrBackfill.php",
          "insertions": 119,
          "deletions": 0,
          "lines_changed": 119
        },
        {
          "path": "tests/Infra/Db/SessionRecoveryHrBackfillTest.php",
          "insertions": 194,
          "deletions": 0,
          "lines_changed": 194
        }
      ],
      "files_changed": 6,
      "insertions": 478,
      "deletions": 6
    },
    {
      "hash": "1ecd44d1e132dbc2b5a7206fe8d8db0c1f04ee42",
      "short_hash": "1ecd44d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790101929,
      "message": "feat: SPOR-956 - Sante - Frec (FC de recuperation) : event FIT recovery_hr + saisie manuelle + tuiles\n\nDecode le message FIT `event` (recovery_hr), jusqu'ici hors perimetre du\nparseur, pour calculer le delta bpm entre la FC d'effort et la FC 2 min\napres l'arret - verifie empiriquement sur un fichier reel (References/\ndataset/frec_40_..., anonymise). Saisie manuelle en fallback (TCX, seance\nmanuelle, FIT sans l'event). Tuiles sur /sessions/{id} et /health, modale\nd'explication dediee.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-956 - Sante - Frec (FC de recuperation) : event FIT recovery_hr + saisie manuelle + tuile",
      "type": "feat",
      "modified_files": [
        "References/dataset/frec_40_24455211316_ACTIVITY.fit",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/214_add_session_recovery_hr.sql",
        "docs/sante.md",
        "docs/sessions.md",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Import/SessionData.php",
        "src/Domain/Physiology/RecoveryHeartRate.php",
        "src/Domain/Training/SessionFieldOverrides.php",
        "src/Domain/Training/SessionInput.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/HealthTileOrderStore.php",
        "src/Http/Health/SessionPhysiologyTrends.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/health_indicator_modals.php",
        "templates/partials/session_modals.php",
        "templates/session_detail.php",
        "tests/Domain/FitImporterTest.php",
        "tests/Domain/Physiology/RecoveryHeartRateTest.php",
        "tests/Domain/SessionInputTest.php",
        "tests/Http/Health/HealthTileOrderStoreTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/dataset/frec_40_24455211316_ACTIVITY.fit",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "db/migrations/214_add_session_recovery_hr.sql",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "docs/sante.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "docs/sessions.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 64,
          "deletions": 8,
          "lines_changed": 72
        },
        {
          "path": "src/Domain/Import/SessionData.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "src/Domain/Physiology/RecoveryHeartRate.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Domain/Training/SessionFieldOverrides.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Training/SessionInput.php",
          "insertions": 21,
          "deletions": 2,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 37,
          "deletions": 4,
          "lines_changed": 41
        },
        {
          "path": "src/Http/Health/HealthTileOrderStore.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Health/SessionPhysiologyTrends.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 19,
          "deletions": 3,
          "lines_changed": 22
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 16,
          "deletions": 5,
          "lines_changed": 21
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "templates/partials/health_indicator_modals.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "templates/partials/session_modals.php",
          "insertions": 12,
          "deletions": 1,
          "lines_changed": 13
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        },
        {
          "path": "tests/Domain/Physiology/RecoveryHeartRateTest.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "tests/Domain/SessionInputTest.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "tests/Http/Health/HealthTileOrderStoreTest.php",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        }
      ],
      "files_changed": 25,
      "insertions": 569,
      "deletions": 30
    },
    {
      "hash": "ec66c411a1bf1c10c92a6c26460eb360d223106d",
      "short_hash": "ec66c41",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790030833,
      "message": "fix: SPOR-955 - Sessions - \"Frequents pour ce sport\" ignore le sous-sport (Zwift propose sur du Gravel)\n\nSessionTagSuggestionsMap regroupait les seances passees par sport seul,\njamais par subsport - un tag ultra-dominant sur un sous-sport (873 seances\nHome Trainer taguees Zwift) noyait tout autre sous-sport du meme sport\n(Gravel). Partition stricte par cle composite \"sport|subsport\", sans repli\nsur le pool sport entier quand un sous-sport est selectionne (verifie en\nlive que le repli initialement envisage reproduisait le bug).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-955 - Sessions - \"Frequents pour ce sport\" ignore le sous-sport (Zwift propose sur du Grav",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/js/interactions.js",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionTagSuggestionsMap.php",
        "tests/Http/Sessions/SessionTagSuggestionsMapTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 17,
          "deletions": 1,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Sessions/SessionTagSuggestionsMap.php",
          "insertions": 73,
          "deletions": 9,
          "lines_changed": 82
        },
        {
          "path": "tests/Http/Sessions/SessionTagSuggestionsMapTest.php",
          "insertions": 102,
          "deletions": 4,
          "lines_changed": 106
        }
      ],
      "files_changed": 7,
      "insertions": 244,
      "deletions": 18,
      "tags": [
        "v1.7.70"
      ]
    },
    {
      "hash": "2a20e9b9689b47d489c20bcd4b97f06fd45c26a2",
      "short_hash": "2a20e9b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790021104,
      "message": "feat: SPOR-954 - Dashboard - Ajoute l'age fete sur le bandeau anniversaire\n\nAddendum demande dans le meme echange que SPOR-954. Domain\\Health\\\nBirthday::age() (nouveau, pur, teste isolement) : simple difference\nd'annees entre birth_date et aujourd'hui - fiable uniquement le jour meme\n(docblock explicite : jamais l'ajustement \"anniversaire pas encore passe\ncette annee\", puisqu'on EST le jour meme des que la methode est appelee).\nDashboardController::show() ne l'appelle que si $isBirthdayToday (jamais\nquand le bandeau est masque - birthdayAge reste null).\n\ndashboard.birthday_text recrite avec un parametre :age (Translator::t()),\nfr/en. Aucune nouvelle story Jira - addendum sur SPOR-954 (deja Done,\ncommentaire Jira ajoute), meme feature demandee dans le meme echange.\n\nTest : tests/Domain/Health/BirthdayTest.php +3 cas (13 au total). Suite\ncomplete 5305 tests verte.\n\nVerifie en reel (meme methode que SPOR-954 : birth_date d'un compte de\ntest realignee temporairement sur le mois/jour du jour, restauree apres\ncoup) : bandeau affiche \"Vous fetez vos 52 ans aujourd'hui\" (fr) et\n\"You're turning 52 today\" (en, meme session, changement de locale a chaud).\n\nTECHNICAL.md + docs/dashboard.md a jour.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-954 - Dashboard - Ajoute l'age fete sur le bandeau anniversaire",
      "type": "feat",
      "modified_files": [
        "TECHNICAL.md",
        "docs/dashboard.md",
        "src/Domain/Health/Birthday.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Domain/Health/BirthdayTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 26,
          "deletions": 7,
          "lines_changed": 33
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Domain/Health/Birthday.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "tests/Domain/Health/BirthdayTest.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        }
      ],
      "files_changed": 8,
      "insertions": 71,
      "deletions": 11,
      "tags": [
        "v1.7.69"
      ]
    },
    {
      "hash": "9252c1b8c01b6a1133135acdcb00b0f4cf7df049",
      "short_hash": "9252c1b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790020338,
      "message": "feat: SPOR-954 - Dashboard - Bandeau anniversaire (birth_date) avec confettis au premier affichage de la session\n\nDemande explicite de l'utilisateur. Decisions actees via AskUserQuestion :\nconfettis une seule fois par session (pas par jour, pas persiste en base -\nla toute premiere fois que le Dashboard s'affiche apres connexion) ; bandeau\navec bouton croix (nouveau pattern, aucun bandeau Dashboard existant n'en a).\n\nInvestigation avant code : aucune animation de confettis nulle part dans\nl'app - effet canvas maison ecrit pour cette story, aucune lib tierce\n(CLAUDE.md : aucune ressource qui fuite l'IP). birth_date (migration 024,\ndeja utilisee pour l'age grading/metabolisme de base) est explicitement\nfacultative - pas de bandeau si absente, jamais de nudge depuis ici.\n\n- Domain\\Health\\Birthday::isToday() (pur, teste isolement) : compare\n  mois+jour uniquement (annee ignoree), 29 fevrier sur annee non bissextile\n  replie sur le 28.\n- Http\\Dashboard\\BirthdayBannerState : deux drapeaux session independants\n  (dismissed, confetti deja montres), aucune colonne BDD.\n- DashboardController::show() : birth_date ajoutee au SELECT existant.\n  Nouvelle action dismissBirthday() (POST /dashboard/dismiss-birthday,\n  CSRF, JSON, 204).\n- templates/dashboard.php : bandeau .rv-alert--success + bouton\n  .rv-alert__close (classe CSS deja presente dans le design system mais\n  jamais utilisee jusqu'ici).\n- public/assets/js/interactions.js : initBirthdayBanner() - bouton croix en\n  fetch() (pas de rechargement), effet confettis canvas\n  (prefers-reduced-motion respecte, canvas retire du DOM apres ~3,2s).\n- public/assets/js/icons.js : icone \"gift\" ajoutee (absente du set existant\n  et de la maquette - aucun ecran de reference pour cette feature).\n\nStory autonome, sans epic parent (aucun epic \"Dashboard\" n'existe dans ce\nprojet).\n\nTest : tests/Domain/Health/BirthdayTest.php (10 cas). Suite complete 5302\ntests verte.\n\nVerifie en reel (birth_date d'un compte de test temporairement alignee sur\naujourd'hui, restauree a l'identique apres coup) : bandeau + confettis au\npremier chargement ; confettis absents au second chargement de la meme\nsession (bandeau toujours affiche) ; POST dismiss-birthday -> 204, bandeau\nabsent ensuite ; requete sans CSRF -> 403.\n\nTECHNICAL.md + docs/dashboard.md + mindmap a jour. Jira SPOR-954 -> Done,\nfixVersion v1.7 (story creee ET implementee dans cette passe, sprint\ncourant).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-954 - Dashboard - Bandeau anniversaire (birth_date) avec confettis au premier affichage d",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "config/routes.php",
        "docs/dashboard.md",
        "public/assets/js/icons.js",
        "public/assets/js/interactions.js",
        "src/Domain/Health/Birthday.php",
        "src/Http/Dashboard/BirthdayBannerState.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Domain/Health/BirthdayTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "config/routes.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/dashboard.md",
          "insertions": 12,
          "deletions": 6,
          "lines_changed": 18
        },
        {
          "path": "public/assets/js/icons.js",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "src/Domain/Health/Birthday.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "src/Http/Dashboard/BirthdayBannerState.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 40,
          "deletions": 1,
          "lines_changed": 41
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "tests/Domain/Health/BirthdayTest.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        }
      ],
      "files_changed": 12,
      "insertions": 338,
      "deletions": 8
    },
    {
      "hash": "164ddb14e5a72d7c1ce586e132a17620c9e85614",
      "short_hash": "164ddb1",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790020194,
      "message": "docs: SPOR-953 - Objectifs - Synchronise le reste de docs/*.md avec le renommage Course -> Competition\n\nCorrection : le renommage initial n'avait synchronise que docs/objectifs.md,\nalors que le perimetre applicatif final touchait aussi Events/Records/\nTrends/Sessions/Dashboard/admin (voir commentaire SPOR-953). 13 fichiers\ndocs/*.md supplementaires restaient au mot \"course\" pour le meme concept.\n\nMeme methode (script Python + protection de phrases explicites, diff relu\nligne par ligne) mais protection etendue apres coup : \"tapis de course\" et\n\"dynamique de course\" en plus de \"course a pied\" (necessaire des le premier\nfichier traite, docs/sante.md).\n\nDeux classes d'erreur trouvees et corrigees a la main en relisant chaque\ndiff (jamais fait confiance aveuglement au script) :\n- Le mou-wrap Markdown casse la protection par chaine litterale : \"course a\n  pied\" ecrit sur deux lignes source (\"course a\\n  pied\") n'est pas trouve\n  par un match sur chaine complete - 3 faux positifs dans docs/sante.md.\n- Usage GENERIQUE de \"course\"/\"courses\" (une sortie, \"vos 90 derniers jours\n  de course\" du panneau DTL, un import mal categorise \"arrive en Cardio\",\n  le segment course a pied d'un triathlon) confondu avec l'entite \"race\" -\n  6 faux positifs dans docs/sessions.md/docs/profil.md, non recuperables\n  sans relecture du contexte.\n\ndocs/sante.md : aucune correction necessaire au final (toutes les\noccurrences etaient legitimement protegees une fois les faux positifs\ncorriges) - fichier redevenu identique a HEAD.\n\nTECHNICAL.md a jour (section SPOR-953). Commentaire ajoute sur SPOR-953\n(deja Done, pas de nouvelle story pour une correction de portee).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "docs: SPOR-953 - Objectifs - Synchronise le reste de docs/*.md avec le renommage Course -> Competiti",
      "type": "docs",
      "modified_files": [
        "TECHNICAL.md",
        "docs/README.md",
        "docs/admin.md",
        "docs/calendrier.md",
        "docs/events.md",
        "docs/notifications.md",
        "docs/pain-injuries.md",
        "docs/profil.md",
        "docs/records.md",
        "docs/sessions.md",
        "docs/site.md",
        "docs/statistiques.md",
        "docs/timeline.md"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 116,
          "deletions": 0,
          "lines_changed": 116
        },
        {
          "path": "docs/README.md",
          "insertions": 6,
          "deletions": 6,
          "lines_changed": 12
        },
        {
          "path": "docs/admin.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "docs/calendrier.md",
          "insertions": 5,
          "deletions": 5,
          "lines_changed": 10
        },
        {
          "path": "docs/events.md",
          "insertions": 36,
          "deletions": 36,
          "lines_changed": 72
        },
        {
          "path": "docs/notifications.md",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        },
        {
          "path": "docs/pain-injuries.md",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        },
        {
          "path": "docs/profil.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "docs/records.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "docs/sessions.md",
          "insertions": 17,
          "deletions": 17,
          "lines_changed": 34
        },
        {
          "path": "docs/site.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 5,
          "deletions": 5,
          "lines_changed": 10
        },
        {
          "path": "docs/timeline.md",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        }
      ],
      "files_changed": 13,
      "insertions": 201,
      "deletions": 85
    },
    {
      "hash": "9151333851d016cd6ba793f09301c7618aaa26c4",
      "short_hash": "9151333",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790009635,
      "message": "trad: SPOR-953 - Objectifs - Renommer \"Course\" en \"Competition\" partout (couvre plusieurs sports)\n\nDemande explicite de l'utilisateur, point de depart /races : \"Course\" en\nfrancais implique la course a pied, alors que l'entite \"race\" de l'app\ncouvre tous les sports (velo, natation, triathlon...). Renomme en\n\"Competition\"/\"Competitions\", francais UNIQUEMENT (l'anglais \"Race(s)\" n'a\npas cette ambiguite).\n\nInvestigation avant tout code (agent Explore) : templates/races*.php ne\nportent aucun texte francais en dur, tout passe par Translator::t('races.*')\n- changement 100% i18n (fr.php), aucun template touche.\n\nPortee elargie en 2 temps (AskUserQuestion) : d'abord limitee au bloc\nraces.* + nav.races, puis etendue partout ou le concept \"race\" apparait\n(Events, Records, Trends, detail de seance) une fois confirme que c'est la\nmeme entite montree ailleurs.\n\nCe qui reste volontairement \"course\" (relecture ligne par ligne du diff,\npas de remplacement aveugle) :\n- sports.run = \"Course\" et \"course a pied\" (nom du sport Run, copie\n  marketing) - jamais l'entite \"race\".\n- \"dynamique de course\"/\"vitesse de course\" des tuiles Sante (VO2max,\n  foulee, equilibre TCS, DTL) - biomecanique de la course a pied, aucun\n  rapport avec l'entite \"race\".\n- field_is_race (\"Est une course\") inclus finalement dans le renommage :\n  meme flag sur TOUTE seance quel que soit le sport.\n\ndocs/objectifs.md : renommage integral via script Python (protection\nexplicite de \"course a pied\"), diff relu integralement.\n\nAucun test dedie (copie texte pure, aucune logique touchee). Suite complete\n5290 tests verte.\n\nVerifie en reel (session forgee locale=fr) : /races, /sessions, /trends,\n/records, /events. Un \"Course\" restant sur /records ecarte apres verification\ndu contexte : titre de seance saisi par l'utilisateur (\"Simulation de\ncourse\"), pas une cle i18n - jamais touche, a raison.\n\nTECHNICAL.md + mindmap + docs/objectifs.md a jour. Jira SPOR-953 -> Done,\nfixVersion v1.7 (story creee ET implementee dans cette passe, sprint\ncourant).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "trad: SPOR-953 - Objectifs - Renommer \"Course\" en \"Competition\" partout (couvre plusieurs sports)",
      "type": "trad",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/objectifs.md",
        "src/Infra/I18n/lang/fr.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 101,
          "deletions": 101,
          "lines_changed": 202
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 88,
          "deletions": 88,
          "lines_changed": 176
        }
      ],
      "files_changed": 4,
      "insertions": 266,
      "deletions": 189
    },
    {
      "hash": "26d1aa7b611f18d215e584239dc00b9dce51df26",
      "short_hash": "26d1aa7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1790001200,
      "message": "feat: SPOR-952 - Materiel - Picker \"Gear used\" filtre au sport de la seance + bouton \"Voir plus\"\n\nDemande explicite de l'utilisateur. Decisions actees via AskUserQuestion\navant tout code (aucune notion de \"categorie\" de materiel dans ce schema,\nseulement sport/sous-sport) : filtre SPORT uniquement (pas sous-sport),\nbouton \"Voir plus\" pour le reste, item deja coche toujours visible (jamais\nde perte de selection en edition), applique aux DEUX formulaires (creation\nET edition) puisque c'est un filtre purement visuel.\n\n- Http\\Gear\\GearSportMembershipMap::forUser() : requete groupee simple\n  gear_sports JOIN gear (materiel actif). Pas de logique de correspondance/\n  classement a isoler (contrairement a GearAutoAssignMatcher/GearPickerOrder)\n  -> pas de classe Domain pure, pas de test dedie, meme situation que\n  GearAutoAssignMap/GearPickerOrderMap/GearSuggestions (aucune des trois\n  n'en a).\n- templates/partials/session_modals.php + templates/session_detail.php :\n  attribut data-gear-sport-membership (JSON) sur le conteneur du picker (les\n  DEUX instances : modale Edit + modale Add session partagee) + bouton\n  data-gear-show-more (.rv-linklike, sessions.gear_show_more).\n- public/assets/js/interactions.js : initGearCategoryFilter(), meme scoping\n  par <form> qu'initGearAutoAssign()/initGearPickerOrder() mais filtre sport\n  uniquement, jamais coche/decoche, garde toujours visibles les items\n  coches, se referme au changement de sport suivant.\n\nSuite complete 5290 tests verte (aucune regression).\n\nVerifie en reel (session admin forgee, compte reel avec materiel sur\nplusieurs sports) : data-gear-sport-membership correspond exactement a\ngear_sports en base. Comportement JS confirme en executant le VRAI\ninitGearCategoryFilter() (fichier vendorise reel) sous jsdom contre le HTML\nreel renvoye par le serveur : filtre correct au chargement et au changement\nde sport, \"Voir plus\" revele tout puis se cache, se referme au sport\nsuivant, item coche hors-filtre reste visible. Premier essai du script de\nverification bogue (requetes non scopees au <form> du picker, la page\n/sessions porte un AUTRE formulaire name=\"sport\" sans rapport) - corrige en\nscopant via container.closest('form'), comme le vrai code.\n\nTECHNICAL.md + docs/materiel.md + mindmap 🟢. Jira SPOR-952 -> Done,\nfixVersion v1.7 (story creee ET implementee dans cette passe, sprint\ncourant).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-952 - Materiel - Picker \"Gear used\" filtre au sport de la seance + bouton \"Voir plus\"",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/materiel.md",
        "public/assets/js/interactions.js",
        "src/Http/Gear/GearSportMembershipMap.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/session_modals.php",
        "templates/session_detail.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "docs/materiel.md",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "src/Http/Gear/GearSportMembershipMap.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/session_modals.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        }
      ],
      "files_changed": 11,
      "insertions": 264,
      "deletions": 2
    },
    {
      "hash": "fdb0a7b9f8fd058ab2d31b9bc5d964ceef6d448b",
      "short_hash": "fdb0a7b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789999016,
      "message": "feat: SPOR-583 - Archives - Local Archive : dump SQL complet de la base, telechargeable en ZIP depuis /admin/archive\n\nEpic SPOR-670 \"Archives and backup\". Description Jira initiale (\"fichiers +\nassets + BDD\") anterieure a l'architecture actuelle : verifie avant tout\ncode, l'app ne persiste aucun fichier utilisateur sur disque (TCX/medias en\nBLOB) -- la story se reduit a un dump SQL complet. Decisions actees avec\nl'utilisateur (AskUserQuestion) : BDD uniquement, dump SQL portable (mysql\n<), genere par etapes bornees plutot qu'en une requete synchrone (pas de\nworker/queue/cron sur mutualise -- seule reprise possible : polling client\ncontre un etat stage sur disque, meme idiome que ZipStagingStore).\n\n- Infra\\Archive\\SqlDumpFormatter (pur, teste) : echappement identique a\n  mysql_real_escape_string via une table strtr() explicite -- PAS\n  addcslashes() (son repli octal sur Ctrl+Z produit \\032, que MySQL ne\n  reconnait pas, seul \\Z l'est -- piege trouve en ecrivant les tests).\n  Colonnes BLOB hex-encodees (0x...), INSERT multi-lignes.\n- Infra\\Archive\\DatabaseDumper : SHOW TABLES/CREATE TABLE/SELECT en pur PDO,\n  curseur sur cle primaire (stable sous ecritures concurrentes). Bug reel\n  trouve en verification live : un lot de 50 lignes sur une table BLOB a\n  fait planter memory_limit 128M (sessions.tcx_content ~816 Ko/ligne, jusqu'a\n  10 Mo) -- exactement le constat deja documente dans Infra\\Db\\\n  ElevationBackfill pour la meme table. Fix identique : une ligne a la fois\n  sur toute table a colonne BLOB (detection generique via information_schema,\n  qui a aussi revele que users.avatar_content et sessions.polyline_public\n  sont BLOB).\n- Http\\Admin\\DbArchiveStore : etat sur disque (var/cache/db-archive/, TTL 6h),\n  SQL accumule en morceaux de 8 Mo max (jamais un fichier unique, jamais tout\n  le dump en memoire a l'assemblage du ZIP -- ZipWriter::add() charge une\n  entree entiere en memoire). Token de progression = token de telechargement,\n  consommation unique.\n- Http\\Admin\\AdminArchiveController : /admin/archive (show/step/download),\n  budget 12s ou 6 Mo par requete. AdminGuard sur les 3 actions -- artefact le\n  plus sensible que l'app puisse produire (password_hash, secrets 2FA, sante,\n  GPS de tous les utilisateurs).\n- templates/admin_archive.php : nouvel onglet admin, aucune maquette (comme\n  /admin/storage) -- barre de progression via fetch() recursif.\n\nTest : DatabaseDumperTest (13 cas SqlDumpFormatter -- apostrophe,\nbackslash+apostrophe, Ctrl+Z, hex-blob binaire aleatoire, INSERT\nmulti-lignes). Aucune connexion PDO dans cette suite -- DatabaseDumper\n(requetage) verifie en reel uniquement. Suite complete 5289 tests verte.\n\nVerifie en reel (session admin forgee) : generation bout en bout (75\nrequetes, 79/79 tables, ~616 Mo SQL) ; ZIP telecharge (43 Mo compresses)\nvalide, 47 morceaux, aucun ne s'arrete au milieu d'une instruction ; contenu\n= exactement 79 CREATE TABLE + 79 DROP TABLE, FOREIGN_KEY_CHECKS en bracket ;\nround-trip reel (table sports, 20 lignes rejouees dans une table renommee du\nmeme schema) restaure BYTE POUR BYTE ; second telechargement du meme token\n-> 404 (consommation unique) ; les 3 actions refusees a un non-admin.\n\nTECHNICAL.md + mindmap 🟢. Jira SPOR-583 -> Done, fixVersion v1.7 ; epic\nSPOR-670 -> Done (tous ses enfants termines).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-583 - Archives - Local Archive : dump SQL complet de la base, telechargeable en ZIP depui",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "src/Http/Admin/AdminArchiveController.php",
        "src/Http/Admin/DbArchiveStore.php",
        "src/Infra/Archive/DatabaseDumper.php",
        "src/Infra/Archive/SqlDumpFormatter.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/admin_archive.php",
        "templates/partials/admin_tabs.php",
        "tests/Infra/Archive/DatabaseDumperTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 127,
          "deletions": 0,
          "lines_changed": 127
        },
        {
          "path": "config/routes.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Admin/AdminArchiveController.php",
          "insertions": 233,
          "deletions": 0,
          "lines_changed": 233
        },
        {
          "path": "src/Http/Admin/DbArchiveStore.php",
          "insertions": 191,
          "deletions": 0,
          "lines_changed": 191
        },
        {
          "path": "src/Infra/Archive/DatabaseDumper.php",
          "insertions": 169,
          "deletions": 0,
          "lines_changed": 169
        },
        {
          "path": "src/Infra/Archive/SqlDumpFormatter.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "templates/admin_archive.php",
          "insertions": 149,
          "deletions": 0,
          "lines_changed": 149
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "tests/Infra/Archive/DatabaseDumperTest.php",
          "insertions": 122,
          "deletions": 0,
          "lines_changed": 122
        }
      ],
      "files_changed": 12,
      "insertions": 1124,
      "deletions": 3
    },
    {
      "hash": "96df0b82a85dfa77e3764491452abd31056bc78b",
      "short_hash": "96df0b8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789979854,
      "message": "feat: SPOR-801 - i18n - Pages Routes : planificateur /routes/plan et couverture /routes/coverage (FR/EN)\n\nSuite de SPOR-800 (alors hors perimetre) : les deux dernieres pages Routes\nrestees 100% en anglais en dur. ~40 cles ajoutees au meme bloc routes.*\n(prefixes plan_*/coverage_*) dans src/Infra/I18n/lang/{fr,en}.php.\n\n- templates/route_planner.php : tout le balisage + script inline (pas\n  vendorise, interactions.js reste fige) via un objet JS i18n serialise en\n  json_encode(Translator::t(...)) (meme pattern que 2fa_stepup.php/login.php).\n- RoutePlannerController::presentCandidate() : elevationLabel traduit via\n  Translator::t('routes.elevation_appetite_'.code) plutot que\n  ElevationAppetite::label() (Domaine pur, aucune dependance Infra\\I18n -\n  meme raisonnement que RouteSortOption::label() cote Http).\n- templates/route_coverage.php : titre, legende rouge/vert/jaune et etat\n  vide traduits ; coverage_intro (pluriel) et coverage_legend interpolent\n  :title (echappement Html::e() apres interpolation, comme err_route_exists).\n\nTest : RoutePlannerCatalogTest (cles traduisibles FR != EN, interpolation\n:sport/:count/:title, RoutePlannerController elevationLabel). Parite/non-vide\ndeja couvertes par RoutesCatalogTest (meme bloc routes). Suite complete 5271\ntests verte. Verifie en reel (sessions forgees locale=fr/en) : /routes/plan\net /sessions/{id}/route-coverage, HTTP 200 dans les deux locales, aucune cle\nroutes.* residuelle dans le HTML rendu.\n\nTECHNICAL.md + mindmap 🟢. Jira SPOR-801 -> Done, fixVersion v1.7.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-801 - i18n - Pages Routes : planificateur /routes/plan et couverture /routes/coverage (FR",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Domain/Training/ElevationAppetite.php",
        "src/Http/Routes/RoutePlannerController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/route_coverage.php",
        "templates/route_planner.php",
        "tests/Infra/I18n/RoutePlannerCatalogTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 59,
          "deletions": 0,
          "lines_changed": 59
        },
        {
          "path": "src/Domain/Training/ElevationAppetite.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Routes/RoutePlannerController.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "templates/route_coverage.php",
          "insertions": 9,
          "deletions": 5,
          "lines_changed": 14
        },
        {
          "path": "templates/route_planner.php",
          "insertions": 60,
          "deletions": 37,
          "lines_changed": 97
        },
        {
          "path": "tests/Infra/I18n/RoutePlannerCatalogTest.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        }
      ],
      "files_changed": 9,
      "insertions": 312,
      "deletions": 44
    },
    {
      "hash": "65ae149a8b3153d007d4fd3143015bc298fbdf7b",
      "short_hash": "65ae149",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789921759,
      "message": "fix: SPOR-951 - Notifications - Seances deja editees affichees a tort sur /sessions/unedited\n\nDeux causes : GearAutoAssignController::applyRequestedLinks() (page de\nrevue retroactive) liait du materiel sans jamais toucher\nsessions.updated_at, et le backfill de la migration 210 ne couvrait que\nles 6 champs field_overrides - une seance editee avant SPOR-944 dont\nl'edition n'a touche que les tags/incidents/partenaires/course/medias/\nSensations restait a tort \"jamais modifiee\". Corrige le controleur et\nelargit le backfill (session_gear volontairement exclu, auto-assignable\na l'import sans etre une edition).\n\n[SPOR-951]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-951 - Notifications - Seances deja editees affichees a tort sur /sessions/unedited",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/213_backfill_sessions_updated_at_from_side_tables.sql",
        "src/Http/Gear/GearAutoAssignController.php",
        "tests/Http/Sessions/UneditedFalsePositivesFixWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "db/migrations/213_backfill_sessions_updated_at_from_side_tables.sql",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Http/Gear/GearAutoAssignController.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "tests/Http/Sessions/UneditedFalsePositivesFixWiringTest.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        }
      ],
      "files_changed": 5,
      "insertions": 181,
      "deletions": 0,
      "tags": [
        "v1.7.68"
      ]
    },
    {
      "hash": "0df64138d453592d3ae374f4e6fde5225f1c509b",
      "short_hash": "0df6413",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789900281,
      "message": "feat: SPOR-950 - Alimentation - Hydratation - Personnaliser la fourchette de perte hydrique\n\nReglages -> Physiologie gagne un parametre \"Perte hydrique\" (min/max en\nml/h, optionnel mais les deux vont ensemble) qui remplace les constantes\ngeneriques 750-2000 ml/h du supplement d'activite d'hydratation pour\nl'utilisateur qui le renseigne (l'interpolation selon l'intensite de la\nseance reste la meme). Un lien depuis la modale d'explication de\n/hydration (SPOR-948) pointe vers ce reglage ; la fourchette par defaut\nutilisee reste affichee si les champs sont vides.\n\n[SPOR-950]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-950 - Alimentation - Hydratation - Personnaliser la fourchette de perte hydrique",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/212_add_hydration_loss_rate.sql",
        "docs/alimentation.md",
        "docs/profil.md",
        "src/Domain/Nutrition/Hydration/HydrationNeed.php",
        "src/Http/Nutrition/HydrationSummaryBuilder.php",
        "src/Http/Settings/SettingsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/hydration.php",
        "templates/settings.php",
        "tests/Domain/Nutrition/Hydration/HydrationNeedTest.php",
        "tests/Http/Nutrition/HydrationSummaryBuilderTest.php",
        "tests/Http/Settings/HydrationLossRateSettingWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "db/migrations/212_add_hydration_loss_rate.sql",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "docs/profil.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Domain/Nutrition/Hydration/HydrationNeed.php",
          "insertions": 21,
          "deletions": 2,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Nutrition/HydrationSummaryBuilder.php",
          "insertions": 19,
          "deletions": 5,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "templates/hydration.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/settings.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "tests/Domain/Nutrition/Hydration/HydrationNeedTest.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "tests/Http/Nutrition/HydrationSummaryBuilderTest.php",
          "insertions": 28,
          "deletions": 1,
          "lines_changed": 29
        },
        {
          "path": "tests/Http/Settings/HydrationLossRateSettingWiringTest.php",
          "insertions": 112,
          "deletions": 0,
          "lines_changed": 112
        }
      ],
      "files_changed": 15,
      "insertions": 407,
      "deletions": 10,
      "tags": [
        "v1.7.67"
      ]
    },
    {
      "hash": "eb5101e1a7115761921033f92e7692f5ed79abb8",
      "short_hash": "eb5101e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789898466,
      "message": "fix: SPOR-949 - Alimentation - Hydratation - interactions.js jamais charge sur /hydration\n\nLe contournement app_foot.php (SPOR-935, Chart.js vendorise) copiait\nicons.js + le script du graphique mais oubliait interactions.js\nlui-meme : tout data-modal-open de la page restait inerte (logout, et\nle lien \"Comment est-ce calcule ?\" de SPOR-948). Confirme via Playwright\n(vrai navigateur) avant/apres le correctif.\n\n[SPOR-949]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-949 - Alimentation - Hydratation - interactions.js jamais charge sur /hydration",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "templates/hydration.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "templates/hydration.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        }
      ],
      "files_changed": 4,
      "insertions": 59,
      "deletions": 0
    },
    {
      "hash": "08ccf1eb8cb8db75d8dcf499db9dcc5574b0ce40",
      "short_hash": "08ccf1e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789897805,
      "message": "feat: SPOR-948 - Alimentation - Hydratation - Formule de calcul et sources scientifiques\n\nAjoute un lien \"Comment est-ce calcule ?\" sous la carte objectif de\n/hydration, ouvrant une modale detaillant la formule (besoin de base en\nml/kg selon l'age, supplement d'activite 750-2000 ml/h) et les sources\ncitees (EFSA 2010, ESPEN 2019, ACSM/Sawka 2007) - meme mecanique que les\nmodales VO2max/TSS/EPOC de /health. Aucun changement de la formule\nelle-meme : sert a evaluer si un facteur de reduction doit etre applique.\n\n[SPOR-948]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-948 - Alimentation - Hydratation - Formule de calcul et sources scientifiques",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/alimentation.md",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/hydration.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "templates/hydration.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        }
      ],
      "files_changed": 7,
      "insertions": 167,
      "deletions": 0
    },
    {
      "hash": "ccafff0fd5b801bc7d0154d7e27aaf76a8a8e3bb",
      "short_hash": "ccafff0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789897252,
      "message": "feat: SPOR-947 - Alimentation - Hydratation - Ajouter/consulter un apport sur un autre jour\n\n/hydration affiche toujours aujourd'hui par defaut, mais chaque pastille de\nl'historique 7 jours devient un lien navigable (?day=YYYY-MM-DD) qui bascule\nl'objectif/journal/formulaires sur ce jour-la. Le graphique reste ancre sur\naujourd'hui ; jour absent/malforme/hors fenetre retombe silencieusement dessus.\n\n[SPOR-947]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-947 - Alimentation - Hydratation - Ajouter/consulter un apport sur un autre jour",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/alimentation.md",
        "src/Http/Nutrition/HydrationController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/hydration.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 13,
          "deletions": 2,
          "lines_changed": 15
        },
        {
          "path": "src/Http/Nutrition/HydrationController.php",
          "insertions": 78,
          "deletions": 8,
          "lines_changed": 86
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/hydration.php",
          "insertions": 42,
          "deletions": 3,
          "lines_changed": 45
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 86,
          "deletions": 3,
          "lines_changed": 89
        }
      ],
      "files_changed": 8,
      "insertions": 297,
      "deletions": 16
    },
    {
      "hash": "430b6183ba3810aed1409d7f2b4b1f798f0d9565",
      "short_hash": "430b618",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789895895,
      "message": "feat: SPOR-946 - Notifications - Boutons Modifier/Ignorer sur les seances non modifiees\n\nSur /sessions/unedited (SPOR-944), Modifier reouvre la modale d'edition\ncomplete existante (mecanisme ?edit=<onglet> deja utilise par SPOR-616)\net revient a la liste apres Save (return_to whitelist). Ignorer confirme\npuis appelle SessionUpdateTracker::touch(), reutilisant le signal deja\nexistant de la fonctionnalite plutot qu'une nouvelle colonne de dismissal.\n\nCorrige au passage le parent Jira de SPOR-944 (epic SPOR-38 au lieu de\nSPOR-228 Alertes).\n\n[SPOR-946]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-946 - Notifications - Boutons Modifier/Ignorer sur les seances non modifiees",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/notifications.md",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/UneditedImportsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/session_ignore_modal.php",
        "templates/session_detail.php",
        "templates/sessions_unedited.php",
        "tests/Http/Sessions/UneditedImportsActionsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 48,
          "deletions": 2,
          "lines_changed": 50
        },
        {
          "path": "config/routes.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "docs/notifications.md",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "src/Http/Sessions/UneditedImportsController.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/session_ignore_modal.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/sessions_unedited.php",
          "insertions": 13,
          "deletions": 2,
          "lines_changed": 15
        },
        {
          "path": "tests/Http/Sessions/UneditedImportsActionsWiringTest.php",
          "insertions": 100,
          "deletions": 0,
          "lines_changed": 100
        }
      ],
      "files_changed": 12,
      "insertions": 296,
      "deletions": 5
    },
    {
      "hash": "566942166a5d7285892c4556fd7f76af47ed4fba",
      "short_hash": "5669421",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789893727,
      "message": "feat: SPOR-944 - Notifications - Alerte configurable pour les seances importees jamais mises a jour\n\nNouvelle notification \"import_unmodified\" : signale les seances\nimportees (TCX/GPX/FIT) jamais touchees depuis l'import (aucun tag,\nmateriel, media, resultat de course ou autre changement), sur les 30\nderniers jours. Configurable en Reglages > Notifications : immediatement\napres import (defaut, evalue des la prochaine page authentifiee - pas de\ncron en prod) ou a une heure donnee, comme les rappels sante existants.\n\nsessions.updated_at (nouvelle colonne, NULL = jamais touchee) est mise a\njour par les 3 UPDATE sessions existants (edition principale, tours,\nfusion d'import en double) et par un nouveau SessionUpdateTracker pour\nles medias (table distincte). Le modele de notification ne supportant\nqu'un seul lien, celui-ci pointe vers une nouvelle page dediee\n(/sessions/unedited) qui liste toutes les seances concernees, chacune\navec son propre lien - reutilise le composant .rv-session-card existant.\n\nVerifie manuellement de bout en bout (import, edition, media, page\ndediee, heure d'alerte) avec un compte de test dedie ; suite complete\nverte (5205 tests).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-944 - Notifications - Alerte configurable pour les seances importees jamais mises a jour",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/210_add_sessions_updated_at.sql",
        "db/migrations/211_add_import_unmodified_notification.sql",
        "docs/notifications.md",
        "docs/profil.md",
        "docs/sessions.md",
        "src/Domain/Notifications/NotificationLink.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionMediaController.php",
        "src/Http/Sessions/SessionUpdateTracker.php",
        "src/Http/Sessions/UneditedImportsController.php",
        "src/Http/Sessions/UneditedSessionsStore.php",
        "src/Http/Settings/SettingsController.php",
        "templates/sessions_unedited.php",
        "templates/settings.php",
        "tests/Domain/Notifications/NotificationLinkTest.php",
        "tests/Http/Sessions/ImportUnmodifiedNotificationWiringTest.php",
        "tests/Http/Sessions/UneditedSessionsStoreTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 109,
          "deletions": 0,
          "lines_changed": 109
        },
        {
          "path": "config/routes.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "db/migrations/210_add_sessions_updated_at.sql",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "db/migrations/211_add_import_unmodified_notification.sql",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "docs/notifications.md",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "docs/profil.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/sessions.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Notifications/NotificationLink.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 11,
          "deletions": 3,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Sessions/SessionMediaController.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Sessions/SessionUpdateTracker.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Sessions/UneditedImportsController.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "src/Http/Sessions/UneditedSessionsStore.php",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 34,
          "deletions": 1,
          "lines_changed": 35
        },
        {
          "path": "templates/sessions_unedited.php",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "templates/settings.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "tests/Domain/Notifications/NotificationLinkTest.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "tests/Http/Sessions/ImportUnmodifiedNotificationWiringTest.php",
          "insertions": 147,
          "deletions": 0,
          "lines_changed": 147
        },
        {
          "path": "tests/Http/Sessions/UneditedSessionsStoreTest.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        }
      ],
      "files_changed": 21,
      "insertions": 828,
      "deletions": 6
    },
    {
      "hash": "5aa8e3812210a06a0408a2553b06cc40e4d1c845",
      "short_hash": "5aa8e38",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789853955,
      "message": "fix: SPOR-945 - Médias - Traduit les textes générés par le JS de la modale d'ajout de média\n\nLe titre et Annuler/Envoyer étaient déjà traduits côté PHP, mais media-upload.js\ngénérait \"Added\"/\"Done\"/\"X files selected\"/le résumé de fin en anglais en dur.\nNouvelles clés i18n injectées en data-t-* par media_upload_modal.php (même\nconvention que health-pulse-camera.js), lues par le script avec repli anglais.\n\nVérifié via jsdom en fr et en (vrai script sur le vrai HTML rendu) + suite\nPHPUnit complète (5187 tests, verte).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-945 - Médias - Traduit les textes générés par le JS de la modale d'ajout de média",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/media-upload.js",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/media_upload_modal.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "public/assets/js/media-upload.js",
          "insertions": 26,
          "deletions": 7,
          "lines_changed": 33
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "templates/partials/media_upload_modal.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        }
      ],
      "files_changed": 6,
      "insertions": 135,
      "deletions": 8
    },
    {
      "hash": "676364aa7ae9d11b4570153f81e2790582d1f63b",
      "short_hash": "676364a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789853229,
      "message": "fix: SPOR-943 - Médias - Boutons Annuler/Envoyer masqués une fois l'upload terminé\n\nLa modale \"Ajouter un média\" (Sessions/Matériel/Événements/Notes d'objectifs)\nlaissait Annuler et Envoyer visibles à côté de \"Done\" après la fin de l'envoi.\nfinish() masque désormais .rv-modal__actions, ne laissant que \"Done\".\n\nVérifié via jsdom (script réel media-upload.js sur le balisage réel de\nmedia_upload_modal.php) : reproduit avant correctif, résolu après.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-943 - Médias - Boutons Annuler/Envoyer masqués une fois l'upload terminé",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/media-upload.js"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "public/assets/js/media-upload.js",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        }
      ],
      "files_changed": 3,
      "insertions": 45,
      "deletions": 0
    },
    {
      "hash": "341638c7f45232c0d2bf47352e481b722eea4521",
      "short_hash": "341638c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789852260,
      "message": "feat: SPOR-942 - Objectifs - Photos attachees aux notes du Reve/but/sous-but\n\nGalerie multi-photos par note (confirme par l'utilisateur), reutilisant\nintegralement le pattern deja etabli par le materiel (GearPhoto pour la\nvalidation/re-encodage GD, meme forme de table que gear_media) plutot\nqu'un nouveau mecanisme. Nouveau NoteMediaController (separe de\nGoalsController, deja volumineux) + DreamNoteMediaStore/GoalNoteMediaStore.\nImages uniquement (jamais video/PDF, contrairement a la galerie de\nmateriel) : GearPhoto::process() directement, jamais SessionMediaUpload.\n\ntemplates/partials/media_upload_modal.php generalise (id de modale/accept\nparametrables) pour supporter plusieurs instances sur une meme page\ndediee (/goals/reve, /goals/{id}) - aucune modification necessaire a\nmedia-upload.js, deja ecrit pour gerer plusieurs formulaires.\n\nToujours strictement privees, comme le texte des notes.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-942 - Objectifs - Photos attachees aux notes du Reve/but/sous-but",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/208_add_dream_note_media.sql",
        "db/migrations/209_add_goal_note_media.sql",
        "docs/objectifs.md",
        "src/Http/Goals/DreamNoteMediaStore.php",
        "src/Http/Goals/DreamNoteRepository.php",
        "src/Http/Goals/GoalNoteMediaStore.php",
        "src/Http/Goals/GoalNoteRepository.php",
        "src/Http/Goals/NoteMediaController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dream_detail.php",
        "templates/goal_node_detail.php",
        "templates/partials/journal_notes_section.php",
        "templates/partials/media_upload_modal.php",
        "tests/Http/Goals/DreamNoteMediaStoreTest.php",
        "tests/Http/Goals/DreamNoteRepositoryTest.php",
        "tests/Http/Goals/GoalNodeDetailPagesWiringTest.php",
        "tests/Http/Goals/GoalNoteMediaStoreTest.php",
        "tests/Http/Goals/GoalNoteRepositoryTest.php",
        "tests/Http/Goals/NoteMediaWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "config/routes.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "db/migrations/208_add_dream_note_media.sql",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "db/migrations/209_add_goal_note_media.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Goals/DreamNoteMediaStore.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        },
        {
          "path": "src/Http/Goals/DreamNoteRepository.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Goals/GoalNoteMediaStore.php",
          "insertions": 121,
          "deletions": 0,
          "lines_changed": 121
        },
        {
          "path": "src/Http/Goals/GoalNoteRepository.php",
          "insertions": 12,
          "deletions": 4,
          "lines_changed": 16
        },
        {
          "path": "src/Http/Goals/NoteMediaController.php",
          "insertions": 243,
          "deletions": 0,
          "lines_changed": 243
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/dream_detail.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "templates/goal_node_detail.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "templates/partials/journal_notes_section.php",
          "insertions": 72,
          "deletions": 1,
          "lines_changed": 73
        },
        {
          "path": "templates/partials/media_upload_modal.php",
          "insertions": 14,
          "deletions": 3,
          "lines_changed": 17
        },
        {
          "path": "tests/Http/Goals/DreamNoteMediaStoreTest.php",
          "insertions": 122,
          "deletions": 0,
          "lines_changed": 122
        },
        {
          "path": "tests/Http/Goals/DreamNoteRepositoryTest.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "tests/Http/Goals/GoalNodeDetailPagesWiringTest.php",
          "insertions": 32,
          "deletions": 1,
          "lines_changed": 33
        },
        {
          "path": "tests/Http/Goals/GoalNoteMediaStoreTest.php",
          "insertions": 115,
          "deletions": 0,
          "lines_changed": 115
        },
        {
          "path": "tests/Http/Goals/GoalNoteRepositoryTest.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "tests/Http/Goals/NoteMediaWiringTest.php",
          "insertions": 103,
          "deletions": 0,
          "lines_changed": 103
        }
      ],
      "files_changed": 23,
      "insertions": 1158,
      "deletions": 13
    },
    {
      "hash": "e261900418eac70e601edecf947426a7ea87222e",
      "short_hash": "e261900",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789844350,
      "message": "fix: SPOR-941 - Objectifs - Corrige le vrai bloquant du glisser-depose inter-but (acceptsForeign se refusait des le 2e survol)\n\nL'utilisateur a signale que le precedent correctif ne suffisait pas.\nReproduit avec un vrai navigateur pilote par souris (Playwright) - une\nsimulation d'evenements isoles ne le revelait pas : le deplacement d'un\nsous-but vers un autre but ne declenchait JAMAIS de `drop` du tout, quel\nque soit le correctif du handler drop.\n\nCause : acceptsForeign() verifiait que l'element deplace n'etait pas\nencore enfant du conteneur etranger. Le premier dragover sur ce\nconteneur l'accepte et l'y insere (apercu en direct) - mais tous les\ndragover suivants le trouvent alors deja present, acceptsForeign()\ndevient definitivement false, et preventDefault() n'est plus rappele.\nUn drop natif ne se declenche que si le DERNIER dragover avant le\nrelachement de la souris a appele preventDefault() - avec un vrai geste\n(plusieurs dragover), ce cas est quasi systematique.\n\nCorrige en acceptant aussi un conteneur qui contient DEJA l'element\ndeplace (seule la premiere acceptation verifie la capacite MAX_SUB).\nVerifie par un vrai geste de souris (Playwright + Chromium reel) :\ndeplacement d'un sous-but plein vers un but vide desormais persiste\ncorrectement (une seule requete, bon order[]/scope/move), le\nplaceholder \"+\" reapparait dans la rangee de depart apres rechargement.\nReordonnancement plein et reordonnancement des buts re-testes sans\nregression.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-941 - Objectifs - Corrige le vrai bloquant du glisser-depose inter-but (acceptsForeign se ",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/js/interactions.js",
        "tests/Http/Goals/GoalTreeRoutesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 91,
          "deletions": 53,
          "lines_changed": 144
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 19,
          "deletions": 5,
          "lines_changed": 24
        },
        {
          "path": "tests/Http/Goals/GoalTreeRoutesTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        }
      ],
      "files_changed": 3,
      "insertions": 144,
      "deletions": 58
    },
    {
      "hash": "6045135d5fa8d02aebd0beee923ca3f46617f904",
      "short_hash": "6045135",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789842330,
      "message": "fix: SPOR-941 - Objectifs - Corrige le premier correctif du drag-and-drop, qui cassait le deplacement inter-but\n\nLe garde ajoute au handler drop (reprenant acceptsForeign() de dragover)\nrefusait a tort le VRAI conteneur destinataire d'un deplacement inter-but\n(SPOR-721) : au moment du drop, l'element deplace est deja enfant du\nconteneur cible (insere par les dragover precedents), donc\nacceptsForeign() - qui verifie l'ABSENCE de l'element - echouait\nsystematiquement, et persist() n'etait plus jamais appele pour ce cas.\n\nRemplace par un test qui reflete la realite au moment du drop : `dragging`\nest-il un enfant DIRECT de ce conteneur ? Vrai pour l'origine (reordonnancement\ninchangé) et pour un conteneur etranger valide (dragover l'y a deja insere),\nfaux pour un conteneur ancetre incompatible qui ne le contient jamais\ndirectement. Verifie par execution reelle du script (jsdom) sur les trois\nscenarios (reordonnancement plein, deplacement inter-but, reordonnancement\ndes buts) : chacun produit desormais exactement un appel fetch avec le bon\norder[]/scope/move.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-941 - Objectifs - Corrige le premier correctif du drag-and-drop, qui cassait le deplacemen",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/js/interactions.js",
        "tests/Http/Goals/GoalTreeRoutesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 43,
          "deletions": 16,
          "lines_changed": 59
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 12,
          "deletions": 8,
          "lines_changed": 20
        },
        {
          "path": "tests/Http/Goals/GoalTreeRoutesTest.php",
          "insertions": 17,
          "deletions": 7,
          "lines_changed": 24
        }
      ],
      "files_changed": 3,
      "insertions": 72,
      "deletions": 31
    },
    {
      "hash": "51630e95cc689904375537850648e5d3225d06c1",
      "short_hash": "51630e9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789841392,
      "message": "fix: SPOR-941 - Objectifs - Glisser-deposer d'un sous-but non persisté (drop bulle vers le conteneur des buts)\n\nLe handler drop d'initDragReorder() n'avait pas le même garde-fou que\ndragover pour ignorer un conteneur ancêtre incompatible : un drop de\nsous-but (conteneur imbriqué scope=sub) continuait de bulle jusqu'au\nconteneur des buts (scope=main), qui le retraitait comme un second\ndéplacement et rechargeait la page sans condition - une course pouvant\nfaire disparaître le réordonnancement qu'on venait de faire.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-941 - Objectifs - Glisser-deposer d'un sous-but non persisté (drop bulle vers le conteneur",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/interactions.js",
        "tests/Http/Goals/GoalTreeRoutesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 12,
          "deletions": 1,
          "lines_changed": 13
        },
        {
          "path": "tests/Http/Goals/GoalTreeRoutesTest.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        }
      ],
      "files_changed": 4,
      "insertions": 73,
      "deletions": 1
    },
    {
      "hash": "72c8347f221d5e7f157f01371855cca7e4892ac6",
      "short_hash": "72c8347",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789834740,
      "message": "feat: SPOR-940 - Objectifs - Retire Editer/Supprimer des cartes de l'arbre sur /goals\n\nRedondant avec la page dédiée du nœud (lien \"Détails\") introduite dans\nle même lot : une carte de but/sous-but sur /goals ne garde plus que\n\"Détails\" et le menu \"Ranger...\", Editer/Supprimer se font désormais\nuniquement sur la page dédiée.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-940 - Objectifs - Retire Editer/Supprimer des cartes de l'arbre sur /goals",
      "type": "feat",
      "modified_files": [
        "TECHNICAL.md",
        "docs/objectifs.md",
        "templates/goals.php",
        "tests/Http/Goals/GoalNodeDetailPagesWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 8,
          "deletions": 5,
          "lines_changed": 13
        },
        {
          "path": "templates/goals.php",
          "insertions": 0,
          "deletions": 8,
          "lines_changed": 8
        },
        {
          "path": "tests/Http/Goals/GoalNodeDetailPagesWiringTest.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        }
      ],
      "files_changed": 4,
      "insertions": 36,
      "deletions": 14
    },
    {
      "hash": "3b9fc957b2cf73d1f5336b997e66b01b940af276",
      "short_hash": "3b9fc95",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789834562,
      "message": "feat: SPOR-940 - Objectifs - Page dédiée par nœud (Rêve/but/sous-but) + notes horodatées liées à des séances\n\nPage de détail dédiée pour le Rêve (/goals/reve) et chaque but/sous-but\nde l'arbre (/goals/{id}), regroupant infos, historique de notes et\ntoutes les actions (Éditer, Supprimer, Ranger..., Ajouter/Éditer/\nSupprimer une note) en modales - demande explicite plutôt qu'une modale\nNotes isolée sur /goals. Inclut la fonctionnalité de notes horodatées\nelle-même [SPOR-939] : texte libre sur le Rêve et les buts/sous-buts,\nliaison à une ou plusieurs séances, toujours strictement privées.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-940 - Objectifs - Page dédiée par nœud (Rêve/but/sous-but) + notes horodatées liées à des",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/206_add_dream_notes.sql",
        "db/migrations/207_add_goal_notes.sql",
        "docs/objectifs.md",
        "src/Domain/Training/JournalNote.php",
        "src/Http/Goals/DreamNoteRepository.php",
        "src/Http/Goals/GoalNoteRepository.php",
        "src/Http/Goals/GoalsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dream_detail.php",
        "templates/goal_node_detail.php",
        "templates/goals.php",
        "templates/partials/journal_notes_section.php",
        "tests/Domain/Training/JournalNoteTest.php",
        "tests/Http/Goals/DreamNoteRepositoryTest.php",
        "tests/Http/Goals/GoalNodeDetailPagesWiringTest.php",
        "tests/Http/Goals/GoalNoteRepositoryTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 94,
          "deletions": 0,
          "lines_changed": 94
        },
        {
          "path": "config/routes.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "db/migrations/206_add_dream_notes.sql",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "db/migrations/207_add_goal_notes.sql",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Domain/Training/JournalNote.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "src/Http/Goals/DreamNoteRepository.php",
          "insertions": 136,
          "deletions": 0,
          "lines_changed": 136
        },
        {
          "path": "src/Http/Goals/GoalNoteRepository.php",
          "insertions": 179,
          "deletions": 0,
          "lines_changed": 179
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 381,
          "deletions": 3,
          "lines_changed": 384
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "templates/dream_detail.php",
          "insertions": 221,
          "deletions": 0,
          "lines_changed": 221
        },
        {
          "path": "templates/goal_node_detail.php",
          "insertions": 203,
          "deletions": 0,
          "lines_changed": 203
        },
        {
          "path": "templates/goals.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "templates/partials/journal_notes_section.php",
          "insertions": 154,
          "deletions": 0,
          "lines_changed": 154
        },
        {
          "path": "tests/Domain/Training/JournalNoteTest.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "tests/Http/Goals/DreamNoteRepositoryTest.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        },
        {
          "path": "tests/Http/Goals/GoalNodeDetailPagesWiringTest.php",
          "insertions": 158,
          "deletions": 0,
          "lines_changed": 158
        },
        {
          "path": "tests/Http/Goals/GoalNoteRepositoryTest.php",
          "insertions": 134,
          "deletions": 0,
          "lines_changed": 134
        }
      ],
      "files_changed": 20,
      "insertions": 2086,
      "deletions": 3
    },
    {
      "hash": "7ad698d013e9baa45e2119b79a3e302bdb60a6e3",
      "short_hash": "7ad698d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789813150,
      "message": "feat: SPOR-938 - Sensations - Coche sur l'onglet d'edition si un axe est renseigne\n\nContrairement aux onglets Tags/Partenaires/Equipement/Blessure/Incidents qui\ncomptent (pastille numerique .rv-tab__badge), l'onglet Sensations affiche une\nsimple coche des qu'un des 8 axes (feeling/motivation/pain x avant/pendant/\napres) est renseigne - demande explicite, pas de nombre pertinent ici.\n\nSessionEditTabBadges::hasAnySensation() (domaine pur) + fonction JS dediee\ninitSensationsTabCheck() (rendu different d'une pastille textuelle, recalcul\nlive sans rechargement a chaque changement de note).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-938 - Sensations - Coche sur l'onglet d'edition si un axe est renseigne",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/js/interactions.js",
        "src/Domain/Training/SessionEditTabBadges.php",
        "templates/session_detail.php",
        "tests/Domain/SessionEditTabBadgesTest.php",
        "tests/Http/Sessions/SensationsTabCheckWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "docs/sessions.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Domain/Training/SessionEditTabBadges.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "tests/Domain/SessionEditTabBadgesTest.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "tests/Http/Sessions/SensationsTabCheckWiringTest.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        }
      ],
      "files_changed": 8,
      "insertions": 192,
      "deletions": 1,
      "tags": [
        "v1.7.66"
      ]
    },
    {
      "hash": "e6adc5ef4347d5f54f87102293d67b0923acd479",
      "short_hash": "e6adc5e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789741219,
      "message": "feat: SPOR-937 - Alimentation - Hydratation : note en texte libre sur la quantite personnalisee\n\nLe formulaire \"Quantite personnalisee\" de /hydration gagne une note libre\noptionnelle (meme patron que health.field_note/note_ph), affichee ensuite\ndans le journal du jour. Les boutons presets (page et widget Dashboard) ne\nportent pas ce champ. HydrationEntry::$note sans validation de longueur\n(comme HealthMeasure), HydrationController::log() normalise la chaine vide\nen NULL.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-937 - Alimentation - Hydratation : note en texte libre sur la quantite personnalisee",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/205_add_hydration_entries_note.sql",
        "docs/alimentation.md",
        "src/Domain/Nutrition/Hydration/HydrationEntry.php",
        "src/Http/Nutrition/HydrationController.php",
        "src/Http/Nutrition/HydrationRepository.php",
        "src/Http/Nutrition/HydrationSummaryBuilder.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "templates/hydration.php",
        "tests/Domain/Nutrition/Hydration/HydrationEntryTest.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php",
        "tests/Http/Nutrition/HydrationSummaryBuilderTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "db/migrations/205_add_hydration_entries_note.sql",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Nutrition/Hydration/HydrationEntry.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Nutrition/HydrationController.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Nutrition/HydrationRepository.php",
          "insertions": 7,
          "deletions": 5,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Nutrition/HydrationSummaryBuilder.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/hydration.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "tests/Domain/Nutrition/Hydration/HydrationEntryTest.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "tests/Http/Nutrition/HydrationSummaryBuilderTest.php",
          "insertions": 18,
          "deletions": 1,
          "lines_changed": 19
        }
      ],
      "files_changed": 15,
      "insertions": 142,
      "deletions": 14,
      "tags": [
        "v1.7.65"
      ]
    },
    {
      "hash": "009672e8f3afca811dadfadedf591f55f97707ae",
      "short_hash": "009672e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789740152,
      "message": "feat: SPOR-936 - Alimentation - Hydratation : boutons rapides sur le widget Dashboard + icone alignee\n\nWidget restructure (un <a> ne peut pas contenir de <form>) : le lien vers\n/hydration n'entoure plus que l'en-tete + la barre, 4 formulaires presets\nCSRF separes en dessous. HydrationController::log() accepte return_to\n(liste blanche stricte /dashboard, jamais une valeur arbitraire) pour rester\nsur le Dashboard apres un ajout rapide.\n\nLibelle plus explicite (\"Objectif d'hydratation du jour\", partage avec\n/hydration) et correctif CSS transverse : .icon dans un h3 de\n.rv-section-head et dans un .rv-chip tombait au-dessus du libelle\n(svg{display:block} jamais contre) - meme defaut latent sur\nrecords.php/badges.php/trends.php, corrige une fois pour toutes cote\npublic/assets ET References/design (CSS synchronise).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-936 - Alimentation - Hydratation : boutons rapides sur le widget Dashboard + icone aligne",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/alimentation.md",
        "public/assets/css/app.css",
        "public/assets/css/components.css",
        "src/Http/Nutrition/HydrationController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Nutrition/HydrationController.php",
          "insertions": 13,
          "deletions": 4,
          "lines_changed": 17
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 23,
          "deletions": 7,
          "lines_changed": 30
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        }
      ],
      "files_changed": 12,
      "insertions": 198,
      "deletions": 15
    },
    {
      "hash": "8c25618e3b84795d9fabcd1c4f36c9671740e070",
      "short_hash": "8c25618",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789737428,
      "message": "style: SPOR-935 - Alimentation - Hydratation : coche/croix directement sur la courbe objectif\n\nRetour utilisateur : la rangee d'icones separee sous le graphique\nn'apportait rien. Le point de la ligne objectif est remplace par un glyphe\ndessine a la volee (canvas passe en pointStyle Chart.js) : coche verte si\nl'objectif est atteint/depasse, croix rouge sinon. Rangee d'icones et\ngraphique separe supprimes.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "style: SPOR-935 - Alimentation - Hydratation : coche/croix directement sur la courbe objectif",
      "type": "style",
      "modified_files": [
        "TECHNICAL.md",
        "docs/alimentation.md",
        "public/assets/js/hydration-history-chart.js",
        "templates/hydration.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 8,
          "deletions": 3,
          "lines_changed": 11
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "public/assets/js/hydration-history-chart.js",
          "insertions": 60,
          "deletions": 7,
          "lines_changed": 67
        },
        {
          "path": "templates/hydration.php",
          "insertions": 8,
          "deletions": 13,
          "lines_changed": 21
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        }
      ],
      "files_changed": 5,
      "insertions": 84,
      "deletions": 25
    },
    {
      "hash": "b396015e0de5fdee1d6f175333a42508a0d6d94d",
      "short_hash": "b396015",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789736651,
      "message": "feat: SPOR-935 - Alimentation - Hydratation : historique 7 jours glissants (objectif atteint ou non)\n\nSous la carte objectif de /hydration, graphique Chart.js mixte (barres\ncolorees vert/rouge selon HydrationSummaryBuilder::sevenDayHistory + ligne\npointillee de l'objectif du jour, qui varie selon les seances) et doublon\naccessible (icone check/croix par jour). Poids/age charges une seule fois\npour toute la fenetre (daySummary() extrait de forDay()).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-935 - Alimentation - Hydratation : historique 7 jours glissants (objectif atteint ou non)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/alimentation.md",
        "public/assets/js/hydration-history-chart.js",
        "src/Http/Nutrition/HydrationController.php",
        "src/Http/Nutrition/HydrationSummaryBuilder.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/hydration.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php",
        "tests/Http/Nutrition/HydrationSummaryBuilderTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "public/assets/js/hydration-history-chart.js",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "src/Http/Nutrition/HydrationController.php",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Nutrition/HydrationSummaryBuilder.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/hydration.php",
          "insertions": 46,
          "deletions": 1,
          "lines_changed": 47
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 24,
          "deletions": 1,
          "lines_changed": 25
        },
        {
          "path": "tests/Http/Nutrition/HydrationSummaryBuilderTest.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        }
      ],
      "files_changed": 11,
      "insertions": 373,
      "deletions": 3
    },
    {
      "hash": "54db66884b38fffbe42c1c3cfd0a5dcf1e39fe5e",
      "short_hash": "54db668",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789735785,
      "message": "feat: SPOR-933 - Alimentation - Hydratation : besoin quotidien estime + journal des apports en eau\n\nNouveau groupe menu \"Alimentation\" (epic SPOR-932) avec Hydratation cablee\net Nutrition reservee (v2, SPOR-934, backlog). Page /hydration + widget\nDashboard : objectif du jour (base 35/30/25 ml/kg selon l'age + supplement\nd'activite via Physiology\\IntensityFraction, formules sourcees EFSA/ESPEN/\nACSM plutot que le 50 ml/kg non date de la mindmap), boutons presets\n250/500/750/1000 ml + quantite personnalisee, journal du jour.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-933 - Alimentation - Hydratation : besoin quotidien estime + journal des apports en eau",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/204_add_hydration_entries.sql",
        "docs/README.md",
        "docs/alimentation.md",
        "src/Domain/Nutrition/Hydration/HydrationEntry.php",
        "src/Domain/Nutrition/Hydration/HydrationNeed.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Nutrition/HydrationController.php",
        "src/Http/Nutrition/HydrationRepository.php",
        "src/Http/Nutrition/HydrationSummaryBuilder.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "templates/hydration.php",
        "templates/partials/app_sidebar.php",
        "tests/Domain/Nutrition/Hydration/HydrationEntryTest.php",
        "tests/Domain/Nutrition/Hydration/HydrationNeedTest.php",
        "tests/Http/Nutrition/HydrationPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "config/routes.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "db/migrations/204_add_hydration_entries.sql",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "docs/README.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/alimentation.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Domain/Nutrition/Hydration/HydrationEntry.php",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "src/Domain/Nutrition/Hydration/HydrationNeed.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Nutrition/HydrationController.php",
          "insertions": 137,
          "deletions": 0,
          "lines_changed": 137
        },
        {
          "path": "src/Http/Nutrition/HydrationRepository.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Http/Nutrition/HydrationSummaryBuilder.php",
          "insertions": 131,
          "deletions": 0,
          "lines_changed": 131
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "templates/hydration.php",
          "insertions": 134,
          "deletions": 0,
          "lines_changed": 134
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 13,
          "deletions": 3,
          "lines_changed": 16
        },
        {
          "path": "tests/Domain/Nutrition/Hydration/HydrationEntryTest.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "tests/Domain/Nutrition/Hydration/HydrationNeedTest.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        },
        {
          "path": "tests/Http/Nutrition/HydrationPageWiringTest.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        }
      ],
      "files_changed": 20,
      "insertions": 1039,
      "deletions": 3
    },
    {
      "hash": "2578262859cbe6eef414e28936521a4f6aa561b2",
      "short_hash": "2578262",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789719416,
      "message": "docs: SPOR-931 - Sante - Documente le rappel du backfill FTP apres deploiement\n\n[SPOR-931]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "docs: SPOR-931 - Sante - Documente le rappel du backfill FTP apres deploiement",
      "type": "docs",
      "modified_files": [
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        }
      ],
      "files_changed": 1,
      "insertions": 9,
      "deletions": 0,
      "tags": [
        "v1.7.64"
      ]
    },
    {
      "hash": "7bd438e226d75b6aa120d78f39a536e299291735",
      "short_hash": "7bd438e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789719376,
      "message": "ops: SPOR-931 - Sante - Backfill sessions.ftp_estimate rappele apres la migration 203\n\n[SPOR-931]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "ops: SPOR-931 - Sante - Backfill sessions.ftp_estimate rappele apres la migration 203",
      "type": "ops",
      "modified_files": [
        "bin/backfill_session_physiology.php",
        "public/backfill_session_physiology.php"
      ],
      "modified_files_details": [
        {
          "path": "bin/backfill_session_physiology.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "public/backfill_session_physiology.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        }
      ],
      "files_changed": 2,
      "insertions": 11,
      "deletions": 2
    },
    {
      "hash": "6303aa793ff1a25bf6d97ea0f3b293a033fa8ef4",
      "short_hash": "6303aa7",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789717017,
      "message": "feat: SPOR-931 - Sante - FTP loggable/importable comme la VO2max (import Garmin inclus)\n\n[SPOR-931]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-931 - Sante - FTP loggable/importable comme la VO2max (import Garmin inclus)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/203_add_ftp_measure.sql",
        "docs/sante.md",
        "src/Domain/Health/HealthMeasureType.php",
        "src/Domain/Health/Import/DateParser.php",
        "src/Domain/Health/Import/MeasureColumn.php",
        "src/Domain/Health/Import/NumberParser.php",
        "src/Domain/Health/Import/ScaleCsvParser.php",
        "src/Domain/Physiology/SessionPhysiology.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/HealthImportController.php",
        "src/Http/Health/HealthTileOrderStore.php",
        "src/Http/Health/SessionPhysiologyTrends.php",
        "src/Http/Physiology/BikeFtpSettingsSync.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SessionPhysiologyBackfill.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/health_indicator_modals.php",
        "templates/session_detail.php",
        "tests/Domain/Health/HealthMeasureTypeTest.php",
        "tests/Domain/Health/Import/DateParserTest.php",
        "tests/Domain/Health/Import/MeasureColumnTest.php",
        "tests/Domain/Health/Import/NumberParserTest.php",
        "tests/Domain/Health/Import/ScaleCsvParserTest.php",
        "tests/Http/Health/HealthTileOrderStoreTest.php",
        "tests/Http/Health/PhysiologyTilesWiringTest.php",
        "tests/Http/Physiology/BikeFtpSettingsSyncTest.php",
        "tests/Http/Sessions/SessionPhysiologyCalculatorTest.php",
        "tests/Infra/Db/SessionPhysiologyBackfillTest.php",
        "tests/fixtures/health/garmin_ftp.csv"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 114,
          "deletions": 2,
          "lines_changed": 116
        },
        {
          "path": "db/migrations/203_add_ftp_measure.sql",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "docs/sante.md",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "src/Domain/Health/HealthMeasureType.php",
          "insertions": 13,
          "deletions": 1,
          "lines_changed": 14
        },
        {
          "path": "src/Domain/Health/Import/DateParser.php",
          "insertions": 56,
          "deletions": 6,
          "lines_changed": 62
        },
        {
          "path": "src/Domain/Health/Import/MeasureColumn.php",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "src/Domain/Health/Import/NumberParser.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Domain/Health/Import/ScaleCsvParser.php",
          "insertions": 36,
          "deletions": 10,
          "lines_changed": 46
        },
        {
          "path": "src/Domain/Physiology/SessionPhysiology.php",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 39,
          "deletions": 1,
          "lines_changed": 40
        },
        {
          "path": "src/Http/Health/HealthImportController.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Health/HealthTileOrderStore.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Health/SessionPhysiologyTrends.php",
          "insertions": 16,
          "deletions": 4,
          "lines_changed": 20
        },
        {
          "path": "src/Http/Physiology/BikeFtpSettingsSync.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 7,
          "deletions": 5,
          "lines_changed": 12
        },
        {
          "path": "src/Infra/Db/SessionPhysiologyBackfill.php",
          "insertions": 21,
          "deletions": 11,
          "lines_changed": 32
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "templates/partials/health_indicator_modals.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "tests/Domain/Health/HealthMeasureTypeTest.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/Health/Import/DateParserTest.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "tests/Domain/Health/Import/MeasureColumnTest.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "tests/Domain/Health/Import/NumberParserTest.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "tests/Domain/Health/Import/ScaleCsvParserTest.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "tests/Http/Health/HealthTileOrderStoreTest.php",
          "insertions": 5,
          "deletions": 5,
          "lines_changed": 10
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 64,
          "deletions": 5,
          "lines_changed": 69
        },
        {
          "path": "tests/Http/Physiology/BikeFtpSettingsSyncTest.php",
          "insertions": 61,
          "deletions": 0,
          "lines_changed": 61
        },
        {
          "path": "tests/Http/Sessions/SessionPhysiologyCalculatorTest.php",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "tests/Infra/Db/SessionPhysiologyBackfillTest.php",
          "insertions": 32,
          "deletions": 7,
          "lines_changed": 39
        },
        {
          "path": "tests/fixtures/health/garmin_ftp.csv",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        }
      ],
      "files_changed": 33,
      "insertions": 768,
      "deletions": 66
    },
    {
      "hash": "73196174c9f0fbb85988da5cab62c98a277ce2fc",
      "short_hash": "7319617",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789682157,
      "message": "feat: SPOR-930 - Tendances - Tri \"Plus recent\" du panneau Derniere seance par sport\n\n[SPOR-930]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-930 - Tendances - Tri \"Plus recent\" du panneau Derniere seance par sport",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "public/assets/js/trends-recency-sort.js",
        "src/Http/Trends/TrendsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/trends.php",
        "tests/Http/Trends/SportRecencySortTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/trends-recency-sort.js",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "templates/trends.php",
          "insertions": 16,
          "deletions": 4,
          "lines_changed": 20
        },
        {
          "path": "tests/Http/Trends/SportRecencySortTest.php",
          "insertions": 88,
          "deletions": 0,
          "lines_changed": 88
        }
      ],
      "files_changed": 9,
      "insertions": 213,
      "deletions": 5
    },
    {
      "hash": "7b0af48b397eca1f655d4fe5697ae2df16896dad",
      "short_hash": "7b0af48",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789675617,
      "message": "fix: SPOR-929 - Import - Charger merge-splice.js sur toutes les pages avec le bouton Ajouter une seance\n\nSPOR-834 n'avait ajoute merge-splice.js (definit window.rvInitMergeSplice,\nrequis par ensureLeaflet() dans interactions.js pour la fusion \"manuelle\"\nsplice) qu'a sessions.php et session_detail.php. Le bouton global\n\"+ Ajouter une seance\" (session_modals.php) est pourtant inclus sur 6\npages : dashboard/gear/goals/records.php en manquaient, d'ou le\nTypeError signale en production lors d'un import multiple depuis le\nDashboard.\n\nLe test de regression (BatchMergeWiringTest) codait en dur la liste des\n2 pages \"connues\" - remplace par un scan generique de tous les templates\nincluant session_modals.php, pour ne plus reproduire ce type d'oubli\nd'etendue sur une future page.\n\n[SPOR-929]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-929 - Import - Charger merge-splice.js sur toutes les pages avec le bouton Ajouter une sea",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "templates/dashboard.php",
        "templates/gear.php",
        "templates/goals.php",
        "templates/records.php",
        "tests/Http/Sessions/BatchMergeWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/gear.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/goals.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/records.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "tests/Http/Sessions/BatchMergeWiringTest.php",
          "insertions": 27,
          "deletions": 3,
          "lines_changed": 30
        }
      ],
      "files_changed": 7,
      "insertions": 105,
      "deletions": 3,
      "tags": [
        "v1.7.63"
      ]
    },
    {
      "hash": "8a006d02c8cf37bbe2d5523c57fb2dfceca75b0b",
      "short_hash": "8a006d0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789670952,
      "message": "feat: SPOR-928 - Tendances - Etendre l'image du bilan a partager a la vue Annee\n\nLeve le dernier garde par periode dans TrendsController::period() :\nle bouton \"Image du bilan...\" (avec carte combinee des parcours et\nlibelles traduits) est desormais disponible sur /trends/year en plus\nde Semaine (SPOR-907) et Mois (SPOR-925). Payload et rendu Canvas\netaient deja generiques par periode (spark mensuel via\nTrendPeriodWindow::sparkBuckets() pour Annee) - aucun changement de\nfond necessaire au-dela de la levee du garde et du nettoyage des\nconditions devenues tautologiques dans trends_period.php.\n\n[SPOR-928]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-928 - Tendances - Etendre l'image du bilan a partager a la vue Annee",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "public/assets/js/week-share-image.js",
        "src/Http/Trends/TrendsController.php",
        "src/Http/Trends/WeekShareImageData.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/week_share_image_modal.php",
        "templates/trends_period.php",
        "tests/Http/Trends/TrendsPeriodViewTest.php",
        "tests/Http/Trends/WeekShareImageDataTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/week-share-image.js",
          "insertions": 10,
          "deletions": 10,
          "lines_changed": 20
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 16,
          "deletions": 19,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Trends/WeekShareImageData.php",
          "insertions": 12,
          "deletions": 12,
          "lines_changed": 24
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/week_share_image_modal.php",
          "insertions": 6,
          "deletions": 7,
          "lines_changed": 13
        },
        {
          "path": "templates/trends_period.php",
          "insertions": 9,
          "deletions": 15,
          "lines_changed": 24
        },
        {
          "path": "tests/Http/Trends/TrendsPeriodViewTest.php",
          "insertions": 14,
          "deletions": 11,
          "lines_changed": 25
        },
        {
          "path": "tests/Http/Trends/WeekShareImageDataTest.php",
          "insertions": 7,
          "deletions": 7,
          "lines_changed": 14
        }
      ],
      "files_changed": 12,
      "insertions": 135,
      "deletions": 88,
      "tags": [
        "v1.7.62"
      ]
    },
    {
      "hash": "2d7288174a86a6a88842b54234549be55223c8d6",
      "short_hash": "2d72881",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789667041,
      "message": "fix: SPOR-927 - Tendances - Traduire les libelles de sport dans l'image du bilan\n\nsports.label est une seule valeur en base (pas un libelle par langue),\ndonc l'image du bilan affichait \"Run\"/\"Strength\" meme en interface\nfrancaise. WeekShareImageData::build() resout desormais chaque libelle\nvia Translator::t('sport.' . $code, [], $locale) - meme pattern deja\nutilise par PainInjuriesController/TimelineController - avec repli sur\nle libelle du catalogue pour les sports personnalises sans traduction\ndediee (ex. \"Tennis de table\").\n\n[SPOR-927]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-927 - Tendances - Traduire les libelles de sport dans l'image du bilan",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Http/Trends/TrendsController.php",
        "src/Http/Trends/WeekShareImageData.php",
        "tests/Http/Trends/WeekShareImageDataTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Trends/WeekShareImageData.php",
          "insertions": 25,
          "deletions": 2,
          "lines_changed": 27
        },
        {
          "path": "tests/Http/Trends/WeekShareImageDataTest.php",
          "insertions": 42,
          "deletions": 15,
          "lines_changed": 57
        }
      ],
      "files_changed": 5,
      "insertions": 122,
      "deletions": 18
    },
    {
      "hash": "fa3e54e1c8a4100b3e7025aa263390828163c270",
      "short_hash": "fa3e54e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789666419,
      "message": "fix: SPOR-926 - Tendances - Masquer le point de reference sur la carte combinee\n\nLa pastille marquant le point de depart le plus utilise (centre de\nla projection) est retiree du rendu Canvas - seules les traces\ncolorees par sport restent visibles sur la carte combinee.\n\n[SPOR-926]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-926 - Tendances - Masquer le point de reference sur la carte combinee",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/js/week-share-image.js"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "public/assets/js/week-share-image.js",
          "insertions": 0,
          "deletions": 10,
          "lines_changed": 10
        }
      ],
      "files_changed": 2,
      "insertions": 6,
      "deletions": 13
    },
    {
      "hash": "50da8c0c8d20a26bfd1c8f74eb6c857dc044883c",
      "short_hash": "50da8c0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789662338,
      "message": "feat: SPOR-926 - Tendances - Image du bilan : carte combinee des parcours\n\nDans le \"Detail par sport\" de l'image du bilan (Semaine + Mois), une\nnouvelle option remplace le graphique en barres par une carte\ncombinant les traces GPS de la periode, colorees par sport et\ncentrees sur le point de depart le plus utilise (clustering sur les\npoints de depart reels, jamais le champ profil home_lat/home_lon -\nplus sensible). Un rayon (km) filtre les seances incluses (depart OU\narrivee dans le rayon) sans jamais rogner une trace retenue.\n\nReutilise sessions.route_thumb_json deja backfille (aucun re-parsing\nTCX) et projette tout cote serveur (GeoProjection::localMeters) pour\nne jamais transmettre les coordonnees brutes du centre au client -\nseules des distances relatives en metres le sont.\n\n[SPOR-926]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-926 - Tendances - Image du bilan : carte combinee des parcours",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "public/assets/js/week-share-image.js",
        "src/Domain/Training/MostUsedStartPoint.php",
        "src/Http/Trends/PeriodCombinedMap.php",
        "src/Http/Trends/TrendsController.php",
        "src/Http/Trends/WeekShareImageData.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/week_share_image_modal.php",
        "tests/Domain/Training/MostUsedStartPointTest.php",
        "tests/Http/Trends/PeriodCombinedMapTest.php",
        "tests/Http/Trends/TrendsPeriodViewTest.php",
        "tests/Http/Trends/WeekShareImageDataTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 129,
          "deletions": 0,
          "lines_changed": 129
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/week-share-image.js",
          "insertions": 98,
          "deletions": 5,
          "lines_changed": 103
        },
        {
          "path": "src/Domain/Training/MostUsedStartPoint.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "src/Http/Trends/PeriodCombinedMap.php",
          "insertions": 99,
          "deletions": 0,
          "lines_changed": 99
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Trends/WeekShareImageData.php",
          "insertions": 10,
          "deletions": 2,
          "lines_changed": 12
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/partials/week_share_image_modal.php",
          "insertions": 17,
          "deletions": 1,
          "lines_changed": 18
        },
        {
          "path": "tests/Domain/Training/MostUsedStartPointTest.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "tests/Http/Trends/PeriodCombinedMapTest.php",
          "insertions": 127,
          "deletions": 0,
          "lines_changed": 127
        },
        {
          "path": "tests/Http/Trends/TrendsPeriodViewTest.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "tests/Http/Trends/WeekShareImageDataTest.php",
          "insertions": 22,
          "deletions": 6,
          "lines_changed": 28
        }
      ],
      "files_changed": 15,
      "insertions": 668,
      "deletions": 15
    },
    {
      "hash": "2f82597e0af43d272b8c83c9d2ff3bbebfd3211c",
      "short_hash": "2f82597",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789596186,
      "message": "feat: SPOR-925 - Tendances - Etendre l'image du bilan a partager a la vue Mois\n\nLe bouton \"Image du bilan...\" (SPOR-907, jusqu'ici Semaine seule) est\ndesormais aussi disponible sur /trends/month, sur simple extension du\ngarde de TrendsController::period() : le payload (WeekShareImageData)\net le rendu Canvas (week-share-image.js) etaient deja generiques par\nperiode (rangeLabel, spark journalier jusqu'a 31 valeurs). Toujours\nabsent sur /trends/year. Libelles i18n \"semaine\" genericises.\n\n[SPOR-925]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-925 - Tendances - Etendre l'image du bilan a partager a la vue Mois",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "public/assets/js/week-share-image.js",
        "src/Http/Trends/TrendsController.php",
        "src/Http/Trends/WeekShareImageData.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/week_share_image_modal.php",
        "templates/trends_period.php",
        "tests/Http/Trends/TrendsPeriodViewTest.php",
        "tests/Http/Trends/WeekShareImageDataTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/week-share-image.js",
          "insertions": 12,
          "deletions": 7,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Trends/WeekShareImageData.php",
          "insertions": 13,
          "deletions": 8,
          "lines_changed": 21
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "templates/partials/week_share_image_modal.php",
          "insertions": 11,
          "deletions": 9,
          "lines_changed": 20
        },
        {
          "path": "templates/trends_period.php",
          "insertions": 8,
          "deletions": 8,
          "lines_changed": 16
        },
        {
          "path": "tests/Http/Trends/TrendsPeriodViewTest.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "tests/Http/Trends/WeekShareImageDataTest.php",
          "insertions": 8,
          "deletions": 5,
          "lines_changed": 13
        }
      ],
      "files_changed": 12,
      "insertions": 148,
      "deletions": 50,
      "tags": [
        "v1.7.61"
      ]
    },
    {
      "hash": "db64227b3adee578d9f6891bf25003d90a1d32b3",
      "short_hash": "db64227",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789590379,
      "message": "fix: SPOR-923 - Materiel - Barre de progression disparait au filtre sport\n\nRetour utilisateur : dans la vue Compacte, la barre d'usure disparaissait\ndes qu'un filtre sport/sous-categorie etait active. initFilters()\n(interactions.js) reecrit style.display sur chaque ligne au clic d'un chip\n(y compris \"Tous\"), effacant le display:block inline pose sur chaque ligne\ncompacte et la faisant retomber sur le display:flex de .rv-gear-row (rangee\nhorizontale) - la barre sans largeur propre s'y retrouvait quasi invisible.\n\nFix : piloter l'empilement vertical via flex-direction/align-items plutot\nque display, proprietes que le filtre ne touche jamais.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-923 - Materiel - Barre de progression disparait au filtre sport",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "templates/gear.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "templates/gear.php",
          "insertions": 16,
          "deletions": 2,
          "lines_changed": 18
        }
      ],
      "files_changed": 2,
      "insertions": 37,
      "deletions": 2,
      "tags": [
        "v1.7.60"
      ]
    },
    {
      "hash": "30a2a58cb3abbbf6be279344ab18fce3c0ef5750",
      "short_hash": "30a2a58",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789589153,
      "message": "feat: SPOR-924 - Materiel - Notes en texte libre multi-lignes\n\nDemande explicite de l'utilisateur : \"ajoute des notes libre sur un\nmateriel\". Le champ Notes existait deja (gear.notes VARCHAR(500)) mais en\ninput mono-ligne. Passe en textarea (meme pattern que seances/evenements/\nblessures/citations), affichage avec white-space:pre-wrap pour conserver\nles retours a la ligne. Aucune migration necessaire.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-924 - Materiel - Notes en texte libre multi-lignes",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/materiel.md",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/partials/gear_form_fields.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 31,
          "deletions": 0,
          "lines_changed": 31
        },
        {
          "path": "docs/materiel.md",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "templates/gear.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        }
      ],
      "files_changed": 6,
      "insertions": 56,
      "deletions": 3,
      "tags": [
        "v1.7.59"
      ]
    },
    {
      "hash": "4d3eee63c39216023d0aa0eaaae031ac711b3ea7",
      "short_hash": "4d3eee6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789588866,
      "message": "feat: SPOR-923 - Materiel - Vue compacte de l'utilisation (bascule Cartes/Compact)\n\nDemande explicite de l'utilisateur (inspiration Grafana) : deux boutons\nau-dessus de la liste \"Active gear\" basculent entre les cartes existantes et\nune vue compacte (une ligne par materiel avec seuil configure, barre\nd'usure vs seuil de retraite, triee par % decroissant). Materiel sans seuil\nexclu de cette vue. Respecte les memes filtres sport/sous-categorie.\n\nPreference memorisee en localStorage (premiere utilisation dans l'appli),\nrepli silencieux sur la vue Cartes si le stockage est indisponible.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-923 - Materiel - Vue compacte de l'utilisation (bascule Cartes/Compact)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/materiel.md",
        "src/Http/Gear/GearController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/gear.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "docs/materiel.md",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/gear.php",
          "insertions": 116,
          "deletions": 1,
          "lines_changed": 117
        }
      ],
      "files_changed": 7,
      "insertions": 232,
      "deletions": 1
    },
    {
      "hash": "580c3bc09a89cf63253779c8e52c31f18b9de13c",
      "short_hash": "580c3bc",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789585205,
      "message": "fix: SPOR-921 - Sessions - Negative split sous la Charge + detail des km\n\nRetour utilisateur immediat : deplace le bloc \"Negative split\" de la grille\nMetrics vers un emplacement dedie juste sous le chiffre de Charge\nd'entrainement, et ajoute la distance de chaque moitie au libelle (ex.\n\"First half (2.5 km) at 10:41 /km, second half at 9:25 /km\").\n\nSessionPresenter::negativeSplit() retourne desormais aussi halfDistance\n(distanceWithUnit() existant, km ou metres pour la natation).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-921 - Sessions - Negative split sous la Charge + detail des km",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "docs/sessions.md",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 7,
          "deletions": 3,
          "lines_changed": 10
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 22,
          "deletions": 10,
          "lines_changed": 32
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 18,
          "deletions": 2,
          "lines_changed": 20
        }
      ],
      "files_changed": 7,
      "insertions": 80,
      "deletions": 19,
      "tags": [
        "v1.7.58"
      ]
    },
    {
      "hash": "f0d6f3bfa4fc58def0eec605ee6099b3e052df3f",
      "short_hash": "f0d6f3b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789581879,
      "message": "feat: SPOR-922 - Materiel - Champ Modele dans la fiche produit\n\nDemande explicite de l'utilisateur : \"Ajoute Modele dans le detail du\nproduit d'un materiel\". Nouveau champ texte optionnel (100 caracteres max),\nplace juste apres Brand dans l'onglet Product du formulaire Add/Edit gear\net sur la carte \"Product details\" de la fiche detail.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-922 - Materiel - Champ Modele dans la fiche produit",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/202_add_gear_model.sql",
        "docs/materiel.md",
        "src/Domain/Training/GearInput.php",
        "src/Http/Gear/GearController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/gear_form_fields.php",
        "tests/Domain/GearInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "db/migrations/202_add_gear_model.sql",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "docs/materiel.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 7,
          "deletions": 2,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 15,
          "deletions": 5,
          "lines_changed": 20
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        }
      ],
      "files_changed": 10,
      "insertions": 77,
      "deletions": 11
    },
    {
      "hash": "533fe85d1dd546f839617ecfe29da62db063daaa",
      "short_hash": "533fe85",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789581448,
      "message": "feat: SPOR-921 - Sessions - Badge Negative split (icone + libelle) quand la 2e moitie est plus rapide\n\nDemande explicite de l'utilisateur : \"Ajout sur une seance, un icon et un\nlabel si il y a un negative split\". Coupure par distance (moitie1/moitie2\negales), compare la duree de chaque moitie sur les trackpoints GPS/FIT\n(Run/Bike/Swim uniquement, jamais Triathlon/GPX/saisie manuelle).\n\nPrecalcule a l'import (comme le DTL, SPOR-845), jamais a la volee. Badge\npartage sur .rv-session-card (sessions/dashboard/timeline/evenements/trends)\n+ tuile dediee sur la fiche detail avec les 2 allures. Backfill fourni\n(bin/+public/backfill_session_negative_split.php) pour les seances\nexistantes.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-921 - Sessions - Badge Negative split (icone + libelle) quand la 2e moitie est plus rapid",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_session_negative_split.php",
        "db/migrations/201_add_session_negative_split.sql",
        "docs/sessions.md",
        "public/backfill_session_negative_split.php",
        "src/Domain/Training/NegativeSplitFinder.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Http/Sessions/SessionCardBadges.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionNegativeSplitCalculator.php",
        "src/Http/Sessions/SessionNegativeSplitStore.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SessionNegativeSplitBackfill.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/session_card.php",
        "templates/session_detail.php",
        "tests/Domain/NegativeSplitFinderTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php",
        "tests/Http/Trends/DayReviewTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 105,
          "deletions": 0,
          "lines_changed": 105
        },
        {
          "path": "bin/backfill_session_negative_split.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "db/migrations/201_add_session_negative_split.sql",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "docs/sessions.md",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "public/backfill_session_negative_split.php",
          "insertions": 90,
          "deletions": 0,
          "lines_changed": 90
        },
        {
          "path": "src/Domain/Training/NegativeSplitFinder.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Sessions/SessionCardBadges.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Sessions/SessionNegativeSplitCalculator.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        },
        {
          "path": "src/Http/Sessions/SessionNegativeSplitStore.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "src/Infra/Db/SessionNegativeSplitBackfill.php",
          "insertions": 85,
          "deletions": 0,
          "lines_changed": 85
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/partials/session_card.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/NegativeSplitFinderTest.php",
          "insertions": 118,
          "deletions": 0,
          "lines_changed": 118
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "tests/Http/Trends/DayReviewTest.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        }
      ],
      "files_changed": 22,
      "insertions": 778,
      "deletions": 5
    },
    {
      "hash": "1270a3f017a7f579d6895d8c9d912e26d46d8afe",
      "short_hash": "1270a3f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789577303,
      "message": "feat: SPOR-920 - Materiel - Categorie de materiel (Type en liste fermee) + type de lacage pour les chaussures\n\nDemande explicite de l'utilisateur : option pour chaussures a lacet\nautomatique. Le champ \"Type\" de la fiche produit passe de texte libre a des\nchips fermees (Shoes/Bike/Trainer/Wetsuit/Other, alignees sur la maquette),\nqui pilotent l'affichage de Drop (mm) et du nouveau champ \"Type de lacage\"\n(Classic/Automatic/Velcro/Boa/Elastic/Other) - reserves aux chaussures.\n\nAucune valeur \"Type\" existante n'est effacee : un input cache de secours\nresoumet le texte libre pre-existant tant qu'aucun chip n'est choisi\nexplicitement.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-920 - Materiel - Categorie de materiel (Type en liste fermee) + type de lacage pour les c",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/200_add_gear_lacing_type.sql",
        "docs/materiel.md",
        "src/Domain/Training/GearInput.php",
        "src/Http/Gear/GearController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/gear.php",
        "templates/gear_detail.php",
        "templates/partials/gear_form_fields.php",
        "tests/Domain/GearInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "db/migrations/200_add_gear_lacing_type.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "docs/materiel.md",
          "insertions": 20,
          "deletions": 8,
          "lines_changed": 28
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 47,
          "deletions": 4,
          "lines_changed": 51
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 35,
          "deletions": 25,
          "lines_changed": 60
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "templates/gear.php",
          "insertions": 18,
          "deletions": 8,
          "lines_changed": 26
        },
        {
          "path": "templates/gear_detail.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "templates/partials/gear_form_fields.php",
          "insertions": 52,
          "deletions": 6,
          "lines_changed": 58
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        }
      ],
      "files_changed": 12,
      "insertions": 386,
      "deletions": 52
    },
    {
      "hash": "c719ba3b61ec66ca4332a63328d5d9df089bb6ce",
      "short_hash": "c719ba3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789505949,
      "message": "fix: SPOR-919 - Notifications - Corps traduit a la lecture (tout le systeme) + retraduction de l'historique\n\nLe corps des notifications (badges, poids, materiel, charge, certificats,\nrecords, meteo) restait fige en anglais quelle que soit la langue de\nl'utilisateur, meme quand le chrome de la page etait deja traduit (SPOR-594).\nnotifications.title_key/message_key/params (JSON) remplacent title/message\ncomme source de verite, traduits a la LECTURE par NotificationsController\nplutot qu'a la generation. Infra\\Notifications\\NotificationTranslationBackfill\nretraduit retroactivement l'historique deja en base (82/82 lignes locales,\nfidelite verifiee bit a bit).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-919 - Notifications - Corps traduit a la lecture (tout le systeme) + retraduction de l'his",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_notification_translations.php",
        "db/migrations/199_add_notification_translation_keys.sql",
        "docs/notifications.md",
        "public/backfill_notification_translations.php",
        "src/Http/Notifications/NotificationGenerator.php",
        "src/Http/Notifications/NotificationsController.php",
        "src/Http/Notifications/RecordNotificationGenerator.php",
        "src/Http/Weather/WeatherNotification.php",
        "src/Http/Weather/WeatherPanel.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Notifications/NotificationTranslationBackfill.php",
        "tests/Http/Notifications/NotificationLinkWiringTest.php",
        "tests/Infra/Notifications/NotificationTranslationBackfillTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 111,
          "deletions": 0,
          "lines_changed": 111
        },
        {
          "path": "bin/backfill_notification_translations.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "db/migrations/199_add_notification_translation_keys.sql",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "docs/notifications.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/backfill_notification_translations.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "src/Http/Notifications/NotificationGenerator.php",
          "insertions": 92,
          "deletions": 59,
          "lines_changed": 151
        },
        {
          "path": "src/Http/Notifications/NotificationsController.php",
          "insertions": 29,
          "deletions": 6,
          "lines_changed": 35
        },
        {
          "path": "src/Http/Notifications/RecordNotificationGenerator.php",
          "insertions": 38,
          "deletions": 12,
          "lines_changed": 50
        },
        {
          "path": "src/Http/Weather/WeatherNotification.php",
          "insertions": 15,
          "deletions": 6,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 19,
          "deletions": 3,
          "lines_changed": 22
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "src/Infra/Notifications/NotificationTranslationBackfill.php",
          "insertions": 396,
          "deletions": 0,
          "lines_changed": 396
        },
        {
          "path": "tests/Http/Notifications/NotificationLinkWiringTest.php",
          "insertions": 5,
          "deletions": 5,
          "lines_changed": 10
        },
        {
          "path": "tests/Infra/Notifications/NotificationTranslationBackfillTest.php",
          "insertions": 164,
          "deletions": 0,
          "lines_changed": 164
        }
      ],
      "files_changed": 16,
      "insertions": 1177,
      "deletions": 92,
      "tags": [
        "v1.7.57"
      ]
    },
    {
      "hash": "6d78275e9c696c809425505bb7f337b01418d2c1",
      "short_hash": "6d78275",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789503052,
      "message": "fix: SPOR-918 - Mensurations - Traits torse/bras invisibles (mauvaise cle shoulder)\n\nRetour utilisateur (capture d'ecran) : les deux nouvelles mesures ne\ns'affichaient ni en trait ni meme en marqueur cliquable, quelle que\nsoit la presence d'une valeur. drawTorsoArmRules() cherchait\n[data-key=\"shoulder\"] en cle EXACTE, mais 'shoulder' est une region\nAPPARIEE (shoulder:L/shoulder:R, PAIRED_PARTS) - contrairement a 'abs'\n(CENTRALE, CENTRE_PARTS) qui a bien une cle exacte. La requete ne\ntrouvait donc jamais rien et la fonction abandonnait silencieusement.\nCorrige avec le meme repli que la boucle des marqueurs pour une region\nappariee (shoulder:L/shoulder:R interroges separement, le plus haut\ndes deux retenu).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-918 - Mensurations - Traits torse/bras invisibles (mauvaise cle shoulder)",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/js/interactions.js",
        "tests/Http/Body/MeasurementFigureWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 9,
          "deletions": 3,
          "lines_changed": 12
        },
        {
          "path": "tests/Http/Body/MeasurementFigureWiringTest.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        }
      ],
      "files_changed": 3,
      "insertions": 28,
      "deletions": 5
    },
    {
      "hash": "7faf4c131acd11841ce2559eaa5fc180d27ec646",
      "short_hash": "7faf4c1",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789500397,
      "message": "feat: SPOR-918 - Mensurations - Longueur du torse et longueur des bras\n\nDeux nouveaux types dans BodyMeasurementType (torso_length/arm_length,\nbornes [30,90]/[35,100] cm) : ce sont des LONGUEURS sans region unique\ndu body-map ou s'ancrer (contrairement aux tours) - meme principe que\nla stature (SPOR-687), sentinelles '__torso_length'/'__arm_length' ->\ntrait dedie sur la silhouette plutot qu'un point sur une region\nexistante. Cote DROIT (la stature reste a gauche), l'un au-dessus de\nl'autre, ancres sur les bboxes reelles shoulder/abs/hand:R du body-map.\n\nMigration 198 (extension ENUM, meme principe que 143/145). Zero\nchangement structurel controleur/vue/repository, deja entierement\npilotes par l'enum - seul ajout : latestValueCm() des 2 nouveaux types\npasse a la vue. interactions.js : drawHeightRule() factorisee en\nruleSvg() partagee (parametre side gauche/droite), nouvelle\ndrawTorsoArmRules().\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-918 - Mensurations - Longueur du torse et longueur des bras",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/198_add_torso_arm_measurements.sql",
        "docs/mensurations.md",
        "public/assets/js/interactions.js",
        "src/Domain/Body/BodyMeasurementType.php",
        "src/Http/Body/MeasurementsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/measurements.php",
        "templates/partials/measurement_figure.php",
        "tests/Domain/Body/BodyMeasurementTest.php",
        "tests/Domain/Body/BodyMeasurementTypeTest.php",
        "tests/Http/Body/MeasurementFigureWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "db/migrations/198_add_torso_arm_measurements.sql",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "docs/mensurations.md",
          "insertions": 13,
          "deletions": 4,
          "lines_changed": 17
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 90,
          "deletions": 7,
          "lines_changed": 97
        },
        {
          "path": "src/Domain/Body/BodyMeasurementType.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "src/Http/Body/MeasurementsController.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/measurements.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/measurement_figure.php",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/Body/BodyMeasurementTest.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "tests/Domain/Body/BodyMeasurementTypeTest.php",
          "insertions": 13,
          "deletions": 5,
          "lines_changed": 18
        },
        {
          "path": "tests/Http/Body/MeasurementFigureWiringTest.php",
          "insertions": 92,
          "deletions": 0,
          "lines_changed": 92
        }
      ],
      "files_changed": 14,
      "insertions": 378,
      "deletions": 18
    },
    {
      "hash": "4270e8ae6aecac2106530ae950bc46c704d78304",
      "short_hash": "4270e8a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789497582,
      "message": "feat: SPOR-917 - Sessions - Repere sur la carte GPS synchronise au clic sur un graphe\n\nSuite de SPOR-622 (scrubbing synchronise entre graphes du resume) :\nle meme point apparait desormais aussi sur la carte #route-map, quand\nelle est disponible. Reutilise SessionPresenter::routePointsWithTime()\n(deja existante) avec le meme maxPoints=500 que routePoints() - memes\npoints/ordre exacts (sampleGpsTrackpoints() partage), juste\nl'horodatage en plus. Nouvel attribut data-points-time additif (aucun\nchangement du tracage/decrochage/Known-New existants). Repere place\ncote client par recherche du point au temps le plus proche\n(window.rvSessionMapScrub, expose seulement si carte + trace\nhorodatee disponibles ; session-charts.js l'appelle sous garde\nd'existence, no-op sinon).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-917 - Sessions - Repere sur la carte GPS synchronise au clic sur un graphe",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/js/session-charts.js",
        "src/Http/Sessions/SessionDetailController.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/MapScrubMarkerWiringTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 49,
          "deletions": 1,
          "lines_changed": 50
        },
        {
          "path": "tests/Http/Sessions/MapScrubMarkerWiringTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 57,
          "deletions": 0,
          "lines_changed": 57
        }
      ],
      "files_changed": 8,
      "insertions": 284,
      "deletions": 2
    },
    {
      "hash": "9fbeca1ad2a9f8c60fb5b8ba775f21684c83753e",
      "short_hash": "9fbeca1",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789495599,
      "message": "feat: SPOR-916 - Meteo - Widget Dashboard : 2 sparklines temperature/pluie\n\nSous les cards de prevision 7 jours, a l'interieur du meme <details>\nrepliable (se replient/deplient ensemble, decision explicite) : courbe\ntemperature (jaune clair) au-dessus, tendance pluie/precipProb (bleu)\nen dessous. Aucune donnee nouvelle (WeatherPanel::dailyView() calculait\ndeja tempMaxC/precipProb par jour) - vue + petit script dedie\nweather-sparklines.js (Chart.js sans axes), initialisation paresseuse\nau premier depli du <details> (canvas 0x0 tant que ferme, meme\ncontrainte que la modale Progression de health-trends.js).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-916 - Meteo - Widget Dashboard : 2 sparklines temperature/pluie",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/meteo.md",
        "public/assets/js/weather-sparklines.js",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Http/Dashboard/WeatherSparklinesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 62,
          "deletions": 0,
          "lines_changed": 62
        },
        {
          "path": "docs/meteo.md",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "public/assets/js/weather-sparklines.js",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 38,
          "deletions": 3,
          "lines_changed": 41
        },
        {
          "path": "tests/Http/Dashboard/WeatherSparklinesTest.php",
          "insertions": 114,
          "deletions": 0,
          "lines_changed": 114
        }
      ],
      "files_changed": 8,
      "insertions": 286,
      "deletions": 5,
      "tags": [
        "v1.7.56"
      ]
    },
    {
      "hash": "8a54a62a983e31f20f91adb84790f803557465f2",
      "short_hash": "8a54a62",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789483275,
      "message": "docs: SPOR-849 - i18n - Mindmap a jour (Done)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "docs: SPOR-849 - i18n - Mindmap a jour (Done)",
      "type": "docs",
      "modified_files": [
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 1,
      "insertions": 1,
      "deletions": 1,
      "tags": [
        "v1.7.55"
      ]
    },
    {
      "hash": "5abd7f091b4d1cee7f36d942ae900de0f329a57c",
      "short_hash": "5abd7f0",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789483241,
      "message": "docs: SPOR-849 - i18n - Documentation technique de la migration\n\nNouvelle section TECHNICAL.md recapitulant les 19 classes migrees, la\nconvention (domaine renvoie une cle, la couche HTTP traduit), les cas\nparticuliers (parametres pool_length/dream max, cles reutilisees,\nEvents/Weather deja compatibles depuis SPOR-848), le second volet\n(libelles <option> admin) et les deux gaps connus, hors perimetre,\nidentifies au passage (GoalFormHelpers, throttle Settings). Note\n\"hors perimetre\" de la section SPOR-848 mise a jour en consequence.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "docs: SPOR-849 - i18n - Documentation technique de la migration",
      "type": "docs",
      "modified_files": [
        "TECHNICAL.md"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 77,
          "deletions": 5,
          "lines_changed": 82
        }
      ],
      "files_changed": 1,
      "insertions": 77,
      "deletions": 5
    },
    {
      "hash": "57f98887bc3d693eafa550b4cbf5fcb2dc8fac03",
      "short_hash": "57f9888",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789483172,
      "message": "test: SPOR-849 - i18n - Garde-fou CatalogAuditTest etendu a src/Domain\n\ntestNoHardcodedSentencesInFlashAndErrors() ne scannait que src/Http/ -\nc'est precisement pourquoi les ~95 chaines des validateurs *Input du\ndomaine ont pu s'accumuler sans etre detectees. Desormais le domaine\nest couvert lui aussi, rendant la migration auto-defendue : plus aucun\n$errors[...] = 'Phrase anglaise' ne peut y etre reintroduit sans faire\nechouer la suite.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "test: SPOR-849 - i18n - Garde-fou CatalogAuditTest etendu a src/Domain",
      "type": "test",
      "modified_files": [
        "tests/Infra/I18n/CatalogAuditTest.php"
      ],
      "modified_files_details": [
        {
          "path": "tests/Infra/I18n/CatalogAuditTest.php",
          "insertions": 25,
          "deletions": 15,
          "lines_changed": 40
        }
      ],
      "files_changed": 1,
      "insertions": 25,
      "deletions": 15
    },
    {
      "hash": "3550ec3bc190083d5b8f6ed40c079c7f57d6cae7",
      "short_hash": "3550ec3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789481297,
      "message": "feat: SPOR-849 - i18n - Domaine Weather + libelles admin restants -> cles i18n\n\nHomeLocationInput (lieu d'entrainement, /settings et /races) renvoie\ndesormais des cles i18n - dernier des 19 validateurs de la story.\nRacesController::validateRaceGoal() traduisait deja son point d'usage\n(SPOR-848), rien a y changer.\n\nDeuxieme volet de la story (libelles d'<option> codes en dur) :\nSettingsController::POWER_ZONE_SPORTS n'avait plus besoin de\ntraduction - ses libelles 'Bike'/'Run' etaient du code mort\n(templates/settings.php derive deja les siens via Translator::t()) -\nsimplifie en liste de codes plutot que const -> methode.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Weather + libelles admin restants -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Weather/HomeLocationInput.php",
        "src/Http/Settings/SettingsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/Weather/HomeLocationInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Weather/HomeLocationInput.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 12,
          "deletions": 4,
          "lines_changed": 16
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "tests/Domain/Weather/HomeLocationInputTest.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        }
      ],
      "files_changed": 5,
      "insertions": 29,
      "deletions": 8
    },
    {
      "hash": "e1c1e39c0c493bcb81fac25ed6f8fe891913d988",
      "short_hash": "e1c1e39",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789481016,
      "message": "feat: SPOR-849 - i18n - Domaine Auth : LoginChangeInput/PasswordChangeInput -> cles i18n\n\nLoginChangeInput/PasswordChangeInput (onglets Name/Security de\n/settings) renvoient desormais des cles i18n. SettingsController\ntraduit avant chaque re-rendu de formulaire. err_password_rules\nduplique le texte de RegistrationRules::PASSWORD_RULES_HINT_EN/FR\n(dette bilingue anterieure a l'infra i18n, register/reset non touches -\nhors perimetre de cette story) plutot que de coupler le catalogue i18n\nau domaine.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Auth : LoginChangeInput/PasswordChangeInput -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Auth/LoginChangeInput.php",
        "src/Domain/Auth/PasswordChangeInput.php",
        "src/Http/Settings/SettingsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/LoginChangeInputTest.php",
        "tests/Domain/PasswordChangeInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Auth/LoginChangeInput.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Auth/PasswordChangeInput.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Settings/SettingsController.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "tests/Domain/LoginChangeInputTest.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "tests/Domain/PasswordChangeInputTest.php",
          "insertions": 7,
          "deletions": 3,
          "lines_changed": 10
        }
      ],
      "files_changed": 7,
      "insertions": 55,
      "deletions": 14
    },
    {
      "hash": "5bc6163f3661bb9499da45f55f3076396b6e5b3f",
      "short_hash": "5bc6163",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789480660,
      "message": "feat: SPOR-849 - i18n - Domaine Badges (admin) : BadgeInput -> cles i18n\n\nBadgeInput::parse() (formulaire admin, ~19 points d'erreur repartis sur\nses helpers prives parseScope/parseType/parseTarget/parseThresholds/\nparseWindow/...) renvoie desormais des cles i18n sous le namespace\n'admin.badge_err_*', a cote des libelles d'<option> deja migres\n(admin.badge_type.*, SPOR-849 premiere passe). AdminBadgesController::\ntranslateErrors() traduit avant la vue ; $image['errors']/$categoryErrors\nrestaient deja traduits, non touches.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Badges (admin) : BadgeInput -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Training/BadgeInput.php",
        "src/Http/Admin/AdminBadgesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/BadgeInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/BadgeInput.php",
          "insertions": 19,
          "deletions": 19,
          "lines_changed": 38
        },
        {
          "path": "src/Http/Admin/AdminBadgesController.php",
          "insertions": 17,
          "deletions": 2,
          "lines_changed": 19
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "tests/Domain/BadgeInputTest.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        }
      ],
      "files_changed": 5,
      "insertions": 82,
      "deletions": 23
    },
    {
      "hash": "9ff112ea27cce1601cc2f3e0510a8b34b2fd4f4f",
      "short_hash": "9ff112e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789480315,
      "message": "feat: SPOR-849 - i18n - Domaine Races : validateurs *Input -> cles i18n\n\nRaceResultInput/ChecklistCustomItems renvoient desormais des cles i18n\n(le rang/total dynamique de RaceResultInput::parseCount() devient deux\ncles distinctes plutot qu'un label interpole). RacesController::\nvalidateRaceGoal() traduit les deux points d'ajout concernes (OptionalUrl,\nChecklistCustomItems) pour rester coherent avec le reste du tableau\n$errors, deja traduit inline depuis SPOR-848.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Races : validateurs *Input -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Training/ChecklistCustomItems.php",
        "src/Domain/Training/RaceResultInput.php",
        "src/Http/Goals/RacesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/ChecklistCustomItemsTest.php",
        "tests/Domain/RaceResultInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/ChecklistCustomItems.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Domain/Training/RaceResultInput.php",
          "insertions": 9,
          "deletions": 6,
          "lines_changed": 15
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/ChecklistCustomItemsTest.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "tests/Domain/RaceResultInputTest.php",
          "insertions": 10,
          "deletions": 6,
          "lines_changed": 16
        }
      ],
      "files_changed": 7,
      "insertions": 54,
      "deletions": 17
    },
    {
      "hash": "21da4e85a9e60527633a82fac0d903af4900f0f6",
      "short_hash": "21da4e8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789480025,
      "message": "feat: SPOR-849 - i18n - Domaine Goals/Records : validateurs *Input -> cles i18n\n\nGoalNodeInput/DreamInput (Objectifs, Reve) et DistanceMilestoneInput\n(Records, paliers de distance) renvoient desormais des cles i18n.\nGoalsController traduit juste avant chaque Flash (reset($errors) enveloppe\nde Translator::t(), fallback generique goals.err_check_* si jamais\nvide) ; goals.err_dream_required porte :max (DreamInput::MAX_LENGTH,\nfourni par le controleur). RecordsController traduit avant la vue.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Goals/Records : validateurs *Input -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Training/DistanceMilestoneInput.php",
        "src/Domain/Training/DreamInput.php",
        "src/Domain/Training/GoalNodeInput.php",
        "src/Http/Goals/GoalsController.php",
        "src/Http/Records/RecordsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/DistanceMilestoneInput.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Domain/Training/DreamInput.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "src/Domain/Training/GoalNodeInput.php",
          "insertions": 8,
          "deletions": 6,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Goals/GoalsController.php",
          "insertions": 14,
          "deletions": 6,
          "lines_changed": 20
        },
        {
          "path": "src/Http/Records/RecordsController.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        }
      ],
      "files_changed": 7,
      "insertions": 69,
      "deletions": 19
    },
    {
      "hash": "b2ae44cdbd94d189dc55cabac115ce9cfd619854",
      "short_hash": "b2ae44c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789479347,
      "message": "feat: SPOR-849 - i18n - Domaine Events : validateur EventInput -> cles i18n\n\nEventInput::parse() renvoie desormais des cles i18n. EventsController\nappelait deja Translator::t() sur ces erreurs (SPOR-848) - aucun\nchangement cote controleur, les cles se resolvent maintenant pour de\nvrai au lieu de retomber sur le fallback litteral de Translator::t().\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Events : validateur EventInput -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Training/EventInput.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/EventInput.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        }
      ],
      "files_changed": 3,
      "insertions": 9,
      "deletions": 2
    },
    {
      "hash": "efe231aeedff9a3dae9fd4ab4b44a9428a6c0cdd",
      "short_hash": "efe231a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789479236,
      "message": "feat: SPOR-849 - i18n - Domaine Gear : validateurs *Input -> cles i18n\n\nGearInput/GearBatteryEntryInput/GearServiceEntryInput/GearTagsInput/\nOptionalUrl renvoient desormais des cles i18n au lieu de phrases\nanglaises en dur. Traduction cote HTTP juste avant la vue/le Flash\n(GearController::translateErrors(), array_map(Translator::t(...)) dans\nGearBatteryController/GearServiceLogController). URL http(s) partagee\nentre GearInput et OptionalUrl (RacesController) -> namespace 'common'\nplutot que dupliquee.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Gear : validateurs *Input -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Training/GearBatteryEntryInput.php",
        "src/Domain/Training/GearInput.php",
        "src/Domain/Training/GearServiceEntryInput.php",
        "src/Domain/Training/GearTagsInput.php",
        "src/Domain/Training/OptionalUrl.php",
        "src/Http/Gear/GearBatteryController.php",
        "src/Http/Gear/GearController.php",
        "src/Http/Gear/GearServiceLogController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/GearInputTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/GearBatteryEntryInput.php",
          "insertions": 7,
          "deletions": 6,
          "lines_changed": 13
        },
        {
          "path": "src/Domain/Training/GearInput.php",
          "insertions": 28,
          "deletions": 25,
          "lines_changed": 53
        },
        {
          "path": "src/Domain/Training/GearServiceEntryInput.php",
          "insertions": 9,
          "deletions": 8,
          "lines_changed": 17
        },
        {
          "path": "src/Domain/Training/GearTagsInput.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Domain/Training/OptionalUrl.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Gear/GearBatteryController.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 19,
          "deletions": 2,
          "lines_changed": 21
        },
        {
          "path": "src/Http/Gear/GearServiceLogController.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "tests/Domain/GearInputTest.php",
          "insertions": 7,
          "deletions": 3,
          "lines_changed": 10
        }
      ],
      "files_changed": 11,
      "insertions": 165,
      "deletions": 53
    },
    {
      "hash": "2636c55400d2a69f7dbba45d2f5cff489b0da536",
      "short_hash": "2636c55",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789478801,
      "message": "feat: SPOR-849 - i18n - Domaine Sessions : validateurs *Input -> cles i18n\n\nSessionInput/SwimLapEdits/SessionPartnersInput/SessionTagsInput\nrenvoient desormais des cles i18n (le domaine reste pur, sans\nTranslator) au lieu de phrases anglaises en dur. Nouvelle classe\npartagee Http\\Sessions\\SessionErrorTranslator, appelee juste avant\nchaque vue (SessionsController::manual, SessionDetailController::edit/\neditLaps) - les templates ne traduisent jamais. sessions.err_pick_sport/\nerr_invalid_subsport (deja existantes, SPOR-848) reutilisees telles\nquelles.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-849 - i18n - Domaine Sessions : validateurs *Input -> cles i18n",
      "type": "feat",
      "modified_files": [
        "src/Domain/Training/SessionInput.php",
        "src/Domain/Training/SessionPartnersInput.php",
        "src/Domain/Training/SessionTagsInput.php",
        "src/Domain/Training/SwimLapEdits.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionErrorTranslator.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "tests/Domain/SessionInputTest.php",
        "tests/Http/Sessions/SwimLapEditWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/SessionInput.php",
          "insertions": 17,
          "deletions": 8,
          "lines_changed": 25
        },
        {
          "path": "src/Domain/Training/SessionPartnersInput.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "src/Domain/Training/SessionTagsInput.php",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Training/SwimLapEdits.php",
          "insertions": 6,
          "deletions": 5,
          "lines_changed": 11
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Sessions/SessionErrorTranslator.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "tests/Domain/SessionInputTest.php",
          "insertions": 9,
          "deletions": 5,
          "lines_changed": 14
        },
        {
          "path": "tests/Http/Sessions/SwimLapEditWiringTest.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        }
      ],
      "files_changed": 11,
      "insertions": 128,
      "deletions": 28
    },
    {
      "hash": "2b6c7aba943483a77675aba2fba7cb79aa7b4084",
      "short_hash": "2b6c7ab",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789477515,
      "message": "docs: SPOR-884 - Site vitrine - Cloture /features : notes obsoletes purgees, mindmap a jour\n\nLe code (route, menu 18 domaines, contenu FR/EN, vraies captures via\nSPOR-887) etait deja livre par les tickets de suivi 886->901 ; seul le\nticket parent restait ouvert avec un garde-fou \"Ne pas marquer Done\"\nperime depuis l'ajout des vraies captures. Contenu relu et confirme par\nl'utilisateur.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "docs: SPOR-884 - Site vitrine - Cloture /features : notes obsoletes purgees, mindmap a jour",
      "type": "docs",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/site.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 3,
          "deletions": 4,
          "lines_changed": 7
        },
        {
          "path": "docs/site.md",
          "insertions": 0,
          "deletions": 4,
          "lines_changed": 4
        }
      ],
      "files_changed": 3,
      "insertions": 9,
      "deletions": 10
    },
    {
      "hash": "88fa94fa5cabca1f74695aeb5eb004adb3a0a8c1",
      "short_hash": "88fa94f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789423764,
      "message": "feat: SPOR-914 - Natation - Allure par longueur, comme le SWOLF\n\nLe repli du graphe Allure (SPOR-914) prefere desormais la granularite par\nLONGUEUR de bassin (meme source que le graphe SWOLF, SPOR-915) plutot que\npar Tour : une longueur a une distance constante, son allure se calcule\ndirectement depuis sa duree. Le repli par Tour reste un second niveau de\nsecours quand aucune longueur individuelle n'est disponible.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-914 - Natation - Allure par longueur, comme le SWOLF",
      "type": "feat",
      "modified_files": [
        "TECHNICAL.md",
        "docs/sessions.md",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 32,
          "deletions": 15,
          "lines_changed": 47
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        }
      ],
      "files_changed": 6,
      "insertions": 132,
      "deletions": 20,
      "tags": [
        "v1.7.54"
      ]
    },
    {
      "hash": "26e744d7f6e8e96953ec06c9745a1c66317f4a2f",
      "short_hash": "26e744d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789423356,
      "message": "fix: SPOR-915 - Natation - Backfill swim_lengths pour les seances anterieures\n\nRetour utilisateur : carte SWOLF absente sur une seance deja importee avant\nl'ajout de swim_lengths, aucun bug de decode (reparsing manuel confirmait\ndes donnees exploitables). Infra\\Db\\SwimLengthsBackfill (bin/ + public/\nproteges ops_token, meme convention que SessionWeatherBackfill) avec\nsentinelle swim_lengths_checked_at (migration 197) pour converger vers\nremaining:0 meme sur une seance sans longueur exploitable. Execute en local :\n16 seances examinees, 14 mises a jour.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-915 - Natation - Backfill swim_lengths pour les seances anterieures",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "bin/backfill_swim_lengths.php",
        "db/migrations/197_add_swim_lengths_checked_at.sql",
        "public/backfill_swim_lengths.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SwimLengthsBackfill.php",
        "tests/Infra/Db/SwimLengthsBackfillTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "bin/backfill_swim_lengths.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "db/migrations/197_add_swim_lengths_checked_at.sql",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "public/backfill_swim_lengths.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        },
        {
          "path": "src/Infra/Db/SwimLengthsBackfill.php",
          "insertions": 122,
          "deletions": 0,
          "lines_changed": 122
        },
        {
          "path": "tests/Infra/Db/SwimLengthsBackfillTest.php",
          "insertions": 203,
          "deletions": 0,
          "lines_changed": 203
        }
      ],
      "files_changed": 8,
      "insertions": 543,
      "deletions": 3
    },
    {
      "hash": "5c48c30eba24a44e8616fe8206edd1ecf2a56451",
      "short_hash": "5c48c30",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789422273,
      "message": "feat: SPOR-915 - Natation - Mouvements et SWOLF par longueur depuis le FIT\n\nNouvelle table swim_lengths (migration 196), FitImporter::handleLength()\netendu (champs verifies empiriquement sur un fichier FIT reel), persiste\nau meme titre que les Tours (import + fusion). Graphique barres SWOLF avec\nmouvements en valeur secondaire du popin de scrubbing (session-charts.js).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-915 - Natation - Mouvements et SWOLF par longueur depuis le FIT",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/196_add_swim_lengths.sql",
        "docs/sessions.md",
        "public/assets/js/session-charts.js",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Import/LengthData.php",
        "src/Domain/Import/SessionData.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/FitImporterTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 97,
          "deletions": 0,
          "lines_changed": 97
        },
        {
          "path": "db/migrations/196_add_swim_lengths.sql",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 63,
          "deletions": 12,
          "lines_changed": 75
        },
        {
          "path": "src/Domain/Import/LengthData.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "src/Domain/Import/SessionData.php",
          "insertions": 10,
          "deletions": 1,
          "lines_changed": 11
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 60,
          "deletions": 5,
          "lines_changed": 65
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 57,
          "deletions": 1,
          "lines_changed": 58
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        }
      ],
      "files_changed": 16,
      "insertions": 501,
      "deletions": 20
    },
    {
      "hash": "f5b14aef788aebff69a518c5792868ce1cf687f7",
      "short_hash": "f5b14ae",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789421295,
      "message": "feat: SPOR-914 - Natation - Panel Allure alimente par les Tours sans trace GPS\n\nRepli en graphique barres (nouveau type dans session-charts.js) quand le\ngraphe base sur les trackpoints est vide - reutilise le meme calcul que la\ncolonne Allure du tableau Tours, tours de repos silencieusement omis.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-914 - Natation - Panel Allure alimente par les Tours sans trace GPS",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/js/session-charts.js",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 21,
          "deletions": 7,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 23,
          "deletions": 1,
          "lines_changed": 24
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 42,
          "deletions": 3,
          "lines_changed": 45
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        }
      ],
      "files_changed": 10,
      "insertions": 192,
      "deletions": 11
    },
    {
      "hash": "77e7cfd616af6d7b5b576619c1ec3d018d801d7e",
      "short_hash": "77e7cfd",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789419818,
      "message": "chore: SPOR-913 - Export - fixVersion V3.0 renseignee\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "chore: SPOR-913 - Export - fixVersion V3.0 renseignee",
      "type": "chore",
      "modified_files": [
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 1,
      "insertions": 1,
      "deletions": 1
    },
    {
      "hash": "cde9b513a425901ffa6b88d8a703e59c2416126a",
      "short_hash": "cde9b51",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789419659,
      "message": "chore: SPOR-913 - Export - Backlog fidelite multi-tours (visee v3.0)\n\nStory de suivi pour l'aplatissement en un seul Lap/segment de synthese\ndes exports regeneres (SPOR-911/912), limitation MVP assumee. Backlog,\npas de sprint (non implementee dans cette passe) ; fixVersion v3.0 a\ncreer manuellement dans Jira (echelle actuelle : v1.7/v1.8/v1.9).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "chore: SPOR-913 - Export - Backlog fidelite multi-tours (visee v3.0)",
      "type": "chore",
      "modified_files": [
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        }
      ],
      "files_changed": 1,
      "insertions": 1,
      "deletions": 0
    },
    {
      "hash": "cbf1e943ec9121f652d0489e6148b078875f8f27",
      "short_hash": "cbf1e94",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789416515,
      "message": "feat: SPOR-911 - Export - Writers purs FIT / GPX + totaux a jour pour TCX\n\nEncodeur FIT maison (PHP pur, 0 dependance), GpxWriter dedie a l'export de\nseance (distinct de GpxExport pour les parcours), et TcxWriter etendu pour\naccepter un resume de totaux autoritaires en override + supporter le\nresume seul (seance sans trace).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-911 - Export - Writers purs FIT / GPX + totaux a jour pour TCX",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/sessions.md",
        "src/Domain/Training/FitWriter.php",
        "src/Domain/Training/GpxWriter.php",
        "src/Domain/Training/SessionExportSummary.php",
        "src/Domain/Training/TcxWriter.php",
        "src/Http/Sessions/SessionExportController.php",
        "src/Http/Sessions/SessionExportPlan.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/TcxWriterTest.php",
        "tests/Domain/Training/FitWriterTest.php",
        "tests/Domain/Training/GpxWriterTest.php",
        "tests/Http/Sessions/SessionExportPlanTest.php",
        "tests/Http/Sessions/SessionExportWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 137,
          "deletions": 0,
          "lines_changed": 137
        },
        {
          "path": "config/routes.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "docs/sessions.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Training/FitWriter.php",
          "insertions": 358,
          "deletions": 0,
          "lines_changed": 358
        },
        {
          "path": "src/Domain/Training/GpxWriter.php",
          "insertions": 97,
          "deletions": 0,
          "lines_changed": 97
        },
        {
          "path": "src/Domain/Training/SessionExportSummary.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Domain/Training/TcxWriter.php",
          "insertions": 56,
          "deletions": 14,
          "lines_changed": 70
        },
        {
          "path": "src/Http/Sessions/SessionExportController.php",
          "insertions": 123,
          "deletions": 0,
          "lines_changed": 123
        },
        {
          "path": "src/Http/Sessions/SessionExportPlan.php",
          "insertions": 43,
          "deletions": 0,
          "lines_changed": 43
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "tests/Domain/TcxWriterTest.php",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "tests/Domain/Training/FitWriterTest.php",
          "insertions": 164,
          "deletions": 0,
          "lines_changed": 164
        },
        {
          "path": "tests/Domain/Training/GpxWriterTest.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        },
        {
          "path": "tests/Http/Sessions/SessionExportPlanTest.php",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "tests/Http/Sessions/SessionExportWiringTest.php",
          "insertions": 73,
          "deletions": 0,
          "lines_changed": 73
        }
      ],
      "files_changed": 18,
      "insertions": 1332,
      "deletions": 14
    },
    {
      "hash": "31d680d3ffe962fa90a01a568de650a47c3bc651",
      "short_hash": "31d680d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789407218,
      "message": "feat: SPOR-909 - Meteo - Widget Dashboard : horodatage de la derniere mise a jour meteo\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-909 - Meteo - Widget Dashboard : horodatage de la derniere mise a jour meteo",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/meteo.md",
        "src/Domain/Weather/WeatherForecast.php",
        "src/Http/Weather/WeatherPanel.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/dashboard.php",
        "tests/Domain/Weather/WeatherForecastTest.php",
        "tests/Http/Weather/WeatherPanelTempBandTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 38,
          "deletions": 1,
          "lines_changed": 39
        },
        {
          "path": "docs/meteo.md",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "src/Domain/Weather/WeatherForecast.php",
          "insertions": 18,
          "deletions": 1,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Weather/WeatherPanel.php",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/dashboard.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/Weather/WeatherForecastTest.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "tests/Http/Weather/WeatherPanelTempBandTest.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        }
      ],
      "files_changed": 10,
      "insertions": 139,
      "deletions": 5
    },
    {
      "hash": "f5d589d45c35f341b6e964673ea4029b411ead3c",
      "short_hash": "f5d589d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789395021,
      "message": "fix: Minor css bug fix.",
      "message_short": "fix: Minor css bug fix.",
      "type": "fix",
      "modified_files": [
        "public/assets/css/base.css"
      ],
      "modified_files_details": [
        {
          "path": "public/assets/css/base.css",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        }
      ],
      "files_changed": 1,
      "insertions": 1,
      "deletions": 0
    },
    {
      "hash": "fbefffc3da1e4f6e917300c3d0d117dfb4e448ef",
      "short_hash": "fbefffc",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789394941,
      "message": "fix: SPOR-903 - Observabilite - Quota de stockage genere par ConfigLocalRenderer plutot qu'edite a la main en prod\n\nconfig/config.local.php de production est REGENERE INTEGRALEMENT a chaque\ndeploiement (bin/render-config-local.php, depuis les secrets GitHub\nActions) - une modification manuelle par FTP est donc ecrasee au\ndeploiement suivant (constate : quota retombe a 0 apres redeploiement).\nstorage.disk_quota_bytes/db_quota_bytes sont desormais generes par\nConfigLocalRenderer lui-meme (defaut = plan actuel 100 Go/1000 Mo,\nsurchargeable par les variables d'environnement optionnelles\nDISK_QUOTA_BYTES/DB_QUOTA_BYTES, meme mecanisme que GPS_BLUR_RADIUS_M).\n\nMigration 195 : purge unique de storage_samples/storage_alert_state,\npollues par les mesures aberrantes des versions precedentes du\nDiskUsageMeasurer (disque physique de l'hebergeur, ~4,7 To) - retour\nutilisateur, la courbe /admin/storage doit repartir de zero.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-903 - Observabilite - Quota de stockage genere par ConfigLocalRenderer plutot qu'edite a l",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "db/migrations/195_reset_storage_samples.sql",
        "src/Infra/Config/ConfigLocalRenderer.php",
        "tests/Infra/Config/ConfigLocalRendererTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 29,
          "deletions": 8,
          "lines_changed": 37
        },
        {
          "path": "db/migrations/195_reset_storage_samples.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "src/Infra/Config/ConfigLocalRenderer.php",
          "insertions": 22,
          "deletions": 1,
          "lines_changed": 23
        },
        {
          "path": "tests/Infra/Config/ConfigLocalRendererTest.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        }
      ],
      "files_changed": 4,
      "insertions": 99,
      "deletions": 9,
      "tags": [
        "v1.7.53"
      ]
    },
    {
      "hash": "a91d93066d1a69c40a55fd645918dc9a7ad3e531",
      "short_hash": "a91d930",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789394058,
      "message": "style: SPOR-908 - CI/CD - Page de maintenance : effet aurore + logo sidebar sur icon-512.png + nettoyage d'images\n\nHalo aurore (dégradés + mix-blend-mode:screen) derrière le logo de la page\nde maintenance, pour masquer son fond noir plein sans canal alpha. Logo de\nla sidebar basculé sur icon-512.png (même raison que SPOR-894 : marque\nseule, plus lisible en petit format que revontulet-mark.png). Suppression\nde 6 images de marque orphelines (aucune référence restante) — vérifié\nqu'aucune autre image encore utilisée (splash-revontulet.jpg, page\nd'accueil) n'était incluse par erreur dans le lot.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "style: SPOR-908 - CI/CD - Page de maintenance : effet aurore + logo sidebar sur icon-512.png + netto",
      "type": "style",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/img/brand/app-icons.jpeg",
        "public/assets/img/brand/fox-cycling.jpeg",
        "public/assets/img/brand/fox-swimming.jpeg",
        "public/assets/img/brand/logo-colors-v2.png",
        "public/assets/img/brand/trading-card.jpg",
        "public/assets/img/logo/icon-revontulet.png",
        "public/maintenance-logo.jpg",
        "public/maintenance.html",
        "templates/partials/app_sidebar.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "public/assets/img/brand/app-icons.jpeg",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/brand/fox-cycling.jpeg",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/brand/fox-swimming.jpeg",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/brand/logo-colors-v2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/brand/trading-card.jpg",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/logo/icon-revontulet.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/maintenance-logo.jpg",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/maintenance.html",
          "insertions": 65,
          "deletions": 9,
          "lines_changed": 74
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        }
      ],
      "files_changed": 11,
      "insertions": 116,
      "deletions": 10,
      "tags": [
        "v1.7.52"
      ]
    },
    {
      "hash": "0177a1bb898a733933fc5645751a75f4e1fe60fb",
      "short_hash": "0177a1b",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789394028,
      "message": "feat: SPOR-907 - Tendances - Vue Semaine : bouton « Image du bilan » à partager\n\nMême mécanisme que l'image résumé d'une séance (SPOR-890) : modale de\ncomposition d'image téléchargeable rendue 100% côté navigateur (Canvas 2D),\nthème blanc/noir/Revontulet, format carré/story/bannière/personnalisé.\nPérimé à la vue Semaine de /trends (demande explicite de l'utilisateur).\n\nColonnes du tableau de sport (label/métriques/delta/spark) dimensionnées et\nalignées une seule fois pour toutes les lignes, comme la grille CSS de\nweek_review_card.php — le spark occupe tout l'espace restant jusqu'au bord\ndroit. Données santé (physiologie, jours blessé) volontairement exclues du\npayload : cette image est destinée à être partagée hors de l'application.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-907 - Tendances - Vue Semaine : bouton « Image du bilan » à partager",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/statistiques.md",
        "public/assets/js/week-share-image.js",
        "src/Http/Trends/TrendsController.php",
        "src/Http/Trends/WeekShareImageData.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/week_share_image_modal.php",
        "templates/trends_period.php",
        "tests/Http/Trends/PeriodReviewTest.php",
        "tests/Http/Trends/WeekShareImageDataTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 204,
          "deletions": 0,
          "lines_changed": 204
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/week-share-image.js",
          "insertions": 423,
          "deletions": 0,
          "lines_changed": 423
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 16,
          "deletions": 2,
          "lines_changed": 18
        },
        {
          "path": "src/Http/Trends/WeekShareImageData.php",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "templates/partials/week_share_image_modal.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "templates/trends_period.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "tests/Http/Trends/PeriodReviewTest.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "tests/Http/Trends/WeekShareImageDataTest.php",
          "insertions": 107,
          "deletions": 0,
          "lines_changed": 107
        }
      ],
      "files_changed": 12,
      "insertions": 986,
      "deletions": 3
    },
    {
      "hash": "b343f7aa38a0f2d7f9e2639e81157bc3768f03ad",
      "short_hash": "b343f7a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789388239,
      "message": "feat: SPOR-906 - Tendances - Resume \"derniere seance\" par sport et sous-sport en tete de /trends\n\nDomain\\Training\\SportRecency (pur, teste) regroupe les lignes brutes\n(sport, subsport, MAX(started_at)) en une entree par sport + sous-liste\ndes sous-sports reellement pratiques. Http\\Trends\\RecencyLabel formule\n\"Aujourd'hui\"/\"Hier\"/\"il y a N jours\" (localise). Panneau <details>\nreplie par defaut (rv-details, meme composant que badges.php/routes.php),\nplace DANS #trends-lists : chaque ligne porte data-sport, filtre donc\npar les MEMES chips que les sections plus bas, sans JS supplementaire.\n\nperf: SPOR-903 - DiskUsageMeasurer : baseline calculee au deploiement\n\nRetour utilisateur : disk_total_space() renvoyait en prod le disque\nPHYSIQUE de l'hebergeur (~4,7 To), pas le quota du compte. La quasi-\ntotalite de l'arbre deploye est statique entre deux releases - seuls\nvar/log, var/cache, var/seed, var/sessions sont ecrits par l'appli en\nprod (meme liste que les --exclude-glob du mirror FTP). Le poids du\ncode deploye est donc calcule UNE FOIS par la CI (etape \"Assembler\ndist/\", .github/workflows/deploy.yml) et lu depuis un fichier livre\navec le code ; seuls ces repertoires mutables sont mesures a chaud.\nRepli local (pas de CI) : parcours complet fait une fois, mis en cache.\n\ndocs: SPOR-906 - TECHNICAL.md + doc utilisateur du resume dernieres seances\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-906 - Tendances - Resume \"derniere seance\" par sport et sous-sport en tete de /trends",
      "type": "feat",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/194_add_storage_samples.sql",
        "docs/statistiques.md",
        "public/index.php",
        "src/Domain/Training/SportRecency.php",
        "src/Http/Trends/RecencyLabel.php",
        "src/Http/Trends/TrendsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Storage/DiskUsageMeasurer.php",
        "templates/trends.php",
        "tests/Domain/Training/SportRecencyTest.php",
        "tests/Http/Trends/RecencyLabelTest.php"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 88,
          "deletions": 2,
          "lines_changed": 90
        },
        {
          "path": "db/migrations/194_add_storage_samples.sql",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "docs/statistiques.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "public/index.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Training/SportRecency.php",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "src/Http/Trends/RecencyLabel.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "src/Http/Trends/TrendsController.php",
          "insertions": 77,
          "deletions": 1,
          "lines_changed": 78
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/Storage/DiskUsageMeasurer.php",
          "insertions": 91,
          "deletions": 22,
          "lines_changed": 113
        },
        {
          "path": "templates/trends.php",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "tests/Domain/Training/SportRecencyTest.php",
          "insertions": 85,
          "deletions": 0,
          "lines_changed": 85
        },
        {
          "path": "tests/Http/Trends/RecencyLabelTest.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        }
      ],
      "files_changed": 15,
      "insertions": 550,
      "deletions": 30
    },
    {
      "hash": "65d65ec33f05117862480053c9162a002f8432ea",
      "short_hash": "65d65ec",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789380750,
      "message": "fix: SPOR-901 - Site vitrine - captures d'ecran /features/blog manquantes + corruption CRLF du contenu\n\nRollout des captures d'ecran (deja pretes sur disque, jamais committees) :\n22 fichiers content/*.md + nouvelles images sous public/assets/img/{features,blog}/.\n\nCorruption CRLF decouverte en testant en prod (retour utilisateur : page\n/features/timeline mal rendue) : 21 des fichiers content modifies + le\npartial partage templates/partials/head.php avaient ete enregistres avec\ndes fins de ligne Windows (\\r\\n). Frontmatter::parseOne() exige un\nprefixe exact \"---\\n\" - avec \\r\\n, le parsing echoue silencieusement et\nle fichier entier (frontmatter compris) tombe en corps de page (titre\naffiche = le slug brut). Fichiers renormalises en LF.\n\nTests mis a jour pour refleter du contenu desormais termine (pas un bug) :\n- RoadmapPageTest : V2.1 (Materiel+Sante) est passe Done depuis l'ecriture\n  du test, V2.2 reste le tag pas-encore-fait de la meme entree.\n- BlogPageTest : \"Stocker le TCX brut dans MySQL\" n'avait qu'un\n  frontmatter (corps vide -> unpublished) a l'ecriture du test, son corps\n  a depuis ete redige -> le lien \"Suivant\" pointe desormais directement\n  dessus plutot que de replier sur /blog.\n\nSuite complete verifiee verte (4868 tests) apres coup.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-901 - Site vitrine - captures d'ecran /features/blog manquantes + corruption CRLF du conte",
      "type": "fix",
      "modified_files": [
        "content/blog/storing-raw-tcx-in-mysql.fr.md",
        "content/blog/why-hexagons.fr.md",
        "content/features/badges.fr.md",
        "content/features/calendar.fr.md",
        "content/features/dashboard.fr.md",
        "content/features/events.fr.md",
        "content/features/gear.fr.md",
        "content/features/goals.fr.md",
        "content/features/health.fr.md",
        "content/features/notifications.fr.md",
        "content/features/privacy.en.md",
        "content/features/privacy.fr.md",
        "content/features/profile.fr.md",
        "content/features/quotes.fr.md",
        "content/features/records.fr.md",
        "content/features/routes.fr.md",
        "content/features/sessions.fr.md",
        "content/features/timeline.fr.md",
        "content/features/training.fr.md",
        "content/features/trends.fr.md",
        "content/roadmap/timeline.en.md",
        "content/roadmap/timeline.fr.md",
        "public/assets/img/blog/why-hexagons/hexagons-live.png",
        "public/assets/img/blog/why-hexagons/hexagons.png",
        "public/assets/img/features/badges/badges-1.png",
        "public/assets/img/features/calendar/calendar-1.png",
        "public/assets/img/features/dashboard/dashboard-1.png",
        "public/assets/img/features/dashboard/dashboard-2.png",
        "public/assets/img/features/events/events-1.png",
        "public/assets/img/features/gear/gear-1.png",
        "public/assets/img/features/gear/gear-2.png",
        "public/assets/img/features/goals/goals-1.png",
        "public/assets/img/features/goals/goals-2.png",
        "public/assets/img/features/health/health-0.png",
        "public/assets/img/features/health/health-1.png",
        "public/assets/img/features/health/health-2.png",
        "public/assets/img/features/notifications/notifications-1.png",
        "public/assets/img/features/profile/profile-1.png",
        "public/assets/img/features/quotes/quotes-1.png",
        "public/assets/img/features/quotes/quotes-2.png",
        "public/assets/img/features/records/records-1.png",
        "public/assets/img/features/routes/routes-1.png",
        "public/assets/img/features/routes/routes-2.png",
        "public/assets/img/features/routes/routes-3.png",
        "public/assets/img/features/sessions/sessions-2.png",
        "public/assets/img/features/timeline/timeline-1.png",
        "public/assets/img/features/training/breathing.png",
        "public/assets/img/features/trends/trends-1.png",
        "public/assets/img/features/trends/trends-2.png",
        "public/assets/img/features/trends/trends-3.png",
        "public/assets/img/features/trends/trends-4.png",
        "templates/partials/head.php",
        "tests/Http/Site/BlogPageTest.php",
        "tests/Http/Site/RoadmapPageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "content/blog/storing-raw-tcx-in-mysql.fr.md",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        },
        {
          "path": "content/blog/why-hexagons.fr.md",
          "insertions": 7,
          "deletions": 5,
          "lines_changed": 12
        },
        {
          "path": "content/features/badges.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/calendar.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/dashboard.fr.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "content/features/events.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/gear.fr.md",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "content/features/goals.fr.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "content/features/health.fr.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "content/features/notifications.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/privacy.en.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "content/features/privacy.fr.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "content/features/profile.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/quotes.fr.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "content/features/records.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/routes.fr.md",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "content/features/sessions.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/timeline.fr.md",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "content/features/training.fr.md",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "content/features/trends.fr.md",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "content/roadmap/timeline.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/roadmap/timeline.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/img/blog/why-hexagons/hexagons-live.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/blog/why-hexagons/hexagons.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/badges/badges-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/calendar/calendar-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/dashboard/dashboard-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/dashboard/dashboard-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/events/events-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/gear/gear-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/gear/gear-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/goals/goals-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/goals/goals-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/health/health-0.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/health/health-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/health/health-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/notifications/notifications-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/profile/profile-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/quotes/quotes-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/quotes/quotes-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/records/records-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/routes/routes-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/routes/routes-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/routes/routes-3.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/sessions/sessions-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/timeline/timeline-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/training/breathing.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/trends/trends-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/trends/trends-2.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/trends/trends-3.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "public/assets/img/features/trends/trends-4.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "templates/partials/head.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "tests/Http/Site/BlogPageTest.php",
          "insertions": 7,
          "deletions": 2,
          "lines_changed": 9
        },
        {
          "path": "tests/Http/Site/RoadmapPageTest.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        }
      ],
      "files_changed": 54,
      "insertions": 106,
      "deletions": 35,
      "tags": [
        "v1.7.51"
      ]
    },
    {
      "hash": "752bbf5a1fc4b494ec005a4fcafbb05bb65fd503",
      "short_hash": "752bbf5",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789379224,
      "message": "perf: SPOR-903 - Observabilite - DiskUsageMeasurer : disk_total_space() plutot que parcours de fichiers\n\nLe parcours recursif de l'arbre du projet (RecursiveDirectoryIterator)\nmesurait 35s en local (Windows+Podman, ~7ms/fichier via le bind mount\nvirtualise sur ~4800 fichiers, dont tests/ et References/ jamais deployes)\n- une page admin qui bloque 10 a 35s a chaque echantillon est inacceptable.\n\nRetour a disk_total_space()/disk_free_space() (un seul appel systeme,\nquasi instantane) - decision utilisateur (AskUserQuestion) apres mesure\nreelle. A verifier une fois deploye : la plupart des mutualises a quota\npar compte font remonter le quota via ces fonctions, pas la capacite\nphysique du serveur, mais ce n'est pas garanti partout.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "perf: SPOR-903 - Observabilite - DiskUsageMeasurer : disk_total_space() plutot que parcours de fichi",
      "type": "perf",
      "modified_files": [
        "TECHNICAL.md",
        "db/migrations/194_add_storage_samples.sql",
        "src/Infra/Storage/DiskUsageMeasurer.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 28,
          "deletions": 16,
          "lines_changed": 44
        },
        {
          "path": "db/migrations/194_add_storage_samples.sql",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "src/Infra/Storage/DiskUsageMeasurer.php",
          "insertions": 21,
          "deletions": 34,
          "lines_changed": 55
        }
      ],
      "files_changed": 3,
      "insertions": 53,
      "deletions": 53,
      "tags": [
        "v1.7.50"
      ]
    },
    {
      "hash": "6917d03828f4df874342c4abcecf1e596b2a26f2",
      "short_hash": "6917d03",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789378773,
      "message": "fix: SPOR-903 - Observabilite - /admin/storage : $historyDays non capture dans la closure de rendu\n\n$renderQuotaCard ne capturait que $gaugeColor via use() ; $historyDays\n(utilise pour le texte \"pas de tendance sur les N derniers jours\") en\nsortait undefined. Trouve en verifiant la page en local.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-903 - Observabilite - /admin/storage : $historyDays non capture dans la closure de rendu",
      "type": "fix",
      "modified_files": [
        "templates/admin_storage.php"
      ],
      "modified_files_details": [
        {
          "path": "templates/admin_storage.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 1,
      "insertions": 1,
      "deletions": 1
    },
    {
      "hash": "efa6901e0829e3dd3fdc603974690e4d29fe7727",
      "short_hash": "efa6901",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789378551,
      "message": "feat: SPOR-903 - Observabilite - Quotas de stockage disque + BDD\n\nEchantillonnage sans cron (TTL 1h sur /dashboard + /admin/storage, meme\nmecanique que CachedWeatherProvider SPOR-632), page /admin/storage avec\njauges/courbes Chart.js/tendance de saturation (SPOR-904), et alerte mail\nadmin a 90% throttlee 1x/jour, production uniquement (SPOR-905). Epic\nSPOR-902. Quotas configures manuellement (100 Go disque / 1000 Mo BDD,\nconfig.local.php) : non deductibles d'un hebergement mutualise.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-903 - Observabilite - Quotas de stockage disque + BDD",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/config.local.php.dist",
        "config/routes.php",
        "db/migrations/194_add_storage_samples.sql",
        "docs/admin.md",
        "public/assets/js/admin-storage-charts.js",
        "public/index.php",
        "src/Domain/Storage/QuotaProjection.php",
        "src/Domain/Storage/QuotaUsage.php",
        "src/Http/Admin/StorageController.php",
        "src/Http/Dashboard/DashboardController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Storage/DatabaseUsageMeasurer.php",
        "src/Infra/Storage/DiskUsageMeasurer.php",
        "src/Infra/Storage/StorageLog.php",
        "src/Infra/Storage/StorageSampler.php",
        "templates/admin_storage.php",
        "templates/emails/storage_alert.php",
        "templates/emails/storage_alert_text.php",
        "templates/partials/admin_tabs.php",
        "tests/Domain/Storage/QuotaProjectionTest.php",
        "tests/Domain/Storage/QuotaUsageTest.php",
        "tests/Infra/View/StorageAlertEmailTemplateTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 122,
          "deletions": 0,
          "lines_changed": 122
        },
        {
          "path": "config/config.local.php.dist",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "config/routes.php",
          "insertions": 16,
          "deletions": 1,
          "lines_changed": 17
        },
        {
          "path": "db/migrations/194_add_storage_samples.sql",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "docs/admin.md",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "public/assets/js/admin-storage-charts.js",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "public/index.php",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "src/Domain/Storage/QuotaProjection.php",
          "insertions": 58,
          "deletions": 0,
          "lines_changed": 58
        },
        {
          "path": "src/Domain/Storage/QuotaUsage.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "src/Http/Admin/StorageController.php",
          "insertions": 151,
          "deletions": 0,
          "lines_changed": 151
        },
        {
          "path": "src/Http/Dashboard/DashboardController.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "src/Infra/Storage/DatabaseUsageMeasurer.php",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "src/Infra/Storage/DiskUsageMeasurer.php",
          "insertions": 54,
          "deletions": 0,
          "lines_changed": 54
        },
        {
          "path": "src/Infra/Storage/StorageLog.php",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "src/Infra/Storage/StorageSampler.php",
          "insertions": 148,
          "deletions": 0,
          "lines_changed": 148
        },
        {
          "path": "templates/admin_storage.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "templates/emails/storage_alert.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "templates/emails/storage_alert_text.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "templates/partials/admin_tabs.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "tests/Domain/Storage/QuotaProjectionTest.php",
          "insertions": 67,
          "deletions": 0,
          "lines_changed": 67
        },
        {
          "path": "tests/Domain/Storage/QuotaUsageTest.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "tests/Infra/View/StorageAlertEmailTemplateTest.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        }
      ],
      "files_changed": 25,
      "insertions": 1298,
      "deletions": 3
    },
    {
      "hash": "07f012f19ac64a5d01b5dc8c3bbdd8b2c96d907f",
      "short_hash": "07f012f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789371043,
      "message": "fix: SPOR-901 - Site vitrine - /features : FeaturesPageTest désynchronisé du contenu réel\n\nLes 18 domaines ont tous reçu leur(s) vraie(s) capture(s) d'écran depuis\n(sauf privacy, qui a délibérément retiré la sienne pour un texte \"Beta\nfermée\" — désormais confirmé symétrique FR/EN, pas une suppression\naccidentelle comme supposé dans SPOR-897). Les 5 tests qui comptaient des\nplaceholders dataient d'avant ce dépôt et échouaient depuis plusieurs\nstories (\"hors périmètre\") ; réécrits pour refléter l'état réel + un\nnouveau garde-fou paramétré (testNoDomainHasAPendingScreenshotPlaceholderAnymore)\nqui nommera domaine/locale si une future capture manque à l'appel. Aucun\ncode applicatif touché. Suite complète : 4843 tests, 0 échec.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-901 - Site vitrine - /features : FeaturesPageTest désynchronisé du contenu réel",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "tests/Http/Site/FeaturesPageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 57,
          "deletions": 17,
          "lines_changed": 74
        }
      ],
      "files_changed": 3,
      "insertions": 98,
      "deletions": 17
    },
    {
      "hash": "4d169e6c48b32222df02033b11b05d2cb37ce20c",
      "short_hash": "4d169e6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789335387,
      "message": "feat: SPOR-900 - Site vitrine - /blog et /roadmap : contenu externalisé en fichiers Markdown\n\nMême besoin que /features (SPOR-886), étendu au blog et à la feuille de\nroute. Infra\\Content\\Frontmatter et Infra\\Content\\MarkdownConverter\nextraits de FeatureContent (comportement inchangé, FeatureContentTest\nvert sans modification) et partagés par les nouvelles BlogContent/\nRoadmapContent. Le convertisseur gagne titres, citations, italique et\nliens en ligne, nécessaires à l'article \"Why hexagons?\".\n\nBlog : un fichier par article et par langue (content/blog/), comme\n/features. Les 4 articles annoncés mais pas encore écrits n'ont que le\nfrontmatter (published=false) : carte grisée sans lien, \"Bientôt\ndisponible\" au survol (même principe que SPOR-899), 404 brandé sur accès\ndirect. Le widget hexagonal codé en dur devient une image Markdown\nstandard (capture à fournir par l'utilisateur).\n\nRoadmap : un seul fichier par langue (content/roadmap/timeline.<locale>.md)\navec les 5 entrées à la suite (Frontmatter::parseAll()) — décision\nexplicite de l'utilisateur. Une étiquette de version déjà livrée est\npréfixée d'un *, une même étape pouvant avoir des versions shippées et\nd'autres non.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-900 - Site vitrine - /blog et /roadmap : contenu externalisé en fichiers Markdown",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "content/blog/designing-under-the-aurora.en.md",
        "content/blog/designing-under-the-aurora.fr.md",
        "content/blog/invitation-only-beta.en.md",
        "content/blog/invitation-only-beta.fr.md",
        "content/blog/one-year-of-dogfooding.en.md",
        "content/blog/one-year-of-dogfooding.fr.md",
        "content/blog/storing-raw-tcx-in-mysql.en.md",
        "content/blog/storing-raw-tcx-in-mysql.fr.md",
        "content/blog/why-hexagons.en.md",
        "content/blog/why-hexagons.fr.md",
        "content/roadmap/timeline.en.md",
        "content/roadmap/timeline.fr.md",
        "docs/site.md",
        "public/assets/css/site.css",
        "src/Infra/Content/BlogContent.php",
        "src/Infra/Content/FeatureContent.php",
        "src/Infra/Content/Frontmatter.php",
        "src/Infra/Content/MarkdownConverter.php",
        "src/Infra/Content/RoadmapContent.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/blog.php",
        "templates/blog_post.php",
        "templates/roadmap.php",
        "tests/Http/Site/BlogPageTest.php",
        "tests/Http/Site/RoadmapPageTest.php",
        "tests/Infra/Content/BlogContentTest.php",
        "tests/Infra/Content/FrontmatterTest.php",
        "tests/Infra/Content/MarkdownConverterTest.php",
        "tests/Infra/Content/RoadmapContentTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 143,
          "deletions": 0,
          "lines_changed": 143
        },
        {
          "path": "config/routes.php",
          "insertions": 14,
          "deletions": 4,
          "lines_changed": 18
        },
        {
          "path": "content/blog/designing-under-the-aurora.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/designing-under-the-aurora.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/invitation-only-beta.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/invitation-only-beta.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/one-year-of-dogfooding.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/one-year-of-dogfooding.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/storing-raw-tcx-in-mysql.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/storing-raw-tcx-in-mysql.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/blog/why-hexagons.en.md",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "content/blog/why-hexagons.fr.md",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "content/roadmap/timeline.en.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "content/roadmap/timeline.fr.md",
          "insertions": 33,
          "deletions": 0,
          "lines_changed": 33
        },
        {
          "path": "docs/site.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/site.css",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/Content/BlogContent.php",
          "insertions": 94,
          "deletions": 0,
          "lines_changed": 94
        },
        {
          "path": "src/Infra/Content/FeatureContent.php",
          "insertions": 19,
          "deletions": 160,
          "lines_changed": 179
        },
        {
          "path": "src/Infra/Content/Frontmatter.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "src/Infra/Content/MarkdownConverter.php",
          "insertions": 167,
          "deletions": 0,
          "lines_changed": 167
        },
        {
          "path": "src/Infra/Content/RoadmapContent.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 9,
          "deletions": 50,
          "lines_changed": 59
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 9,
          "deletions": 50,
          "lines_changed": 59
        },
        {
          "path": "templates/blog.php",
          "insertions": 34,
          "deletions": 28,
          "lines_changed": 62
        },
        {
          "path": "templates/blog_post.php",
          "insertions": 36,
          "deletions": 35,
          "lines_changed": 71
        },
        {
          "path": "templates/roadmap.php",
          "insertions": 28,
          "deletions": 35,
          "lines_changed": 63
        },
        {
          "path": "tests/Http/Site/BlogPageTest.php",
          "insertions": 116,
          "deletions": 0,
          "lines_changed": 116
        },
        {
          "path": "tests/Http/Site/RoadmapPageTest.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "tests/Infra/Content/BlogContentTest.php",
          "insertions": 117,
          "deletions": 0,
          "lines_changed": 117
        },
        {
          "path": "tests/Infra/Content/FrontmatterTest.php",
          "insertions": 60,
          "deletions": 0,
          "lines_changed": 60
        },
        {
          "path": "tests/Infra/Content/MarkdownConverterTest.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "tests/Infra/Content/RoadmapContentTest.php",
          "insertions": 108,
          "deletions": 0,
          "lines_changed": 108
        }
      ],
      "files_changed": 33,
      "insertions": 1475,
      "deletions": 363
    },
    {
      "hash": "8323c54677f9a88a2741c78a20692bb376dd24e5",
      "short_hash": "8323c54",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789333255,
      "message": "fix: SPOR-899 - Site vitrine - /donate : boutons de don grisés avec infobulle \"Bientôt disponible\"\n\nLes 3 boutons étaient des <a href=\"#\"> qui ne menaient nulle part (aucun\nprestataire de paiement branché). Passent en <span role=\"button\"\naria-disabled=\"true\" tabindex=\"-1\"> avec un title traduit\n(public.donate.coming_soon) — jamais un <button disabled> : certains\nnavigateurs suppriment le mouseover sur un contrôle de formulaire\nréellement désactivé, ce qui aurait empêché l'infobulle native de\ns'afficher. Nouvelle classe .rv-btn.is-disabled (opacité + curseur, sans\npointer-events: none pour la même raison) ; les règles :hover/:active des\nvariantes primary/secondary ignorent désormais aussi .is-disabled.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-899 - Site vitrine - /donate : boutons de don grisés avec infobulle \"Bientôt disponible\"",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/site.md",
        "public/assets/css/components.css",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/donate.php",
        "tests/Http/Site/DonatePageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "docs/site.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 8,
          "deletions": 3,
          "lines_changed": 11
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "templates/donate.php",
          "insertions": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "tests/Http/Site/DonatePageTest.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        }
      ],
      "files_changed": 8,
      "insertions": 130,
      "deletions": 9
    },
    {
      "hash": "2801c08cb8fb43f59556aa9587bf1d93f33cc07b",
      "short_hash": "2801c08",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789331794,
      "message": "feat: SPOR-898 - Site vitrine - /features : nouvelle entrée \"Courses\" séparée d'\"Objectifs\"\n\n/goals et /races sont deux entrées distinctes de la sidebar app (même\ngroupe de section, deux liens séparés) mais /features n'avait qu'un\ndomaine \"goals\" fusionnant les deux. Nouveau slug races (route +\n$domainSlugs) avec sa propre description (calendrier 12 mois, checklist,\ndocuments requis, prédiction de temps ajustée dénivelé/météo) et sa\ncapture réelle. goals.fr.md/.en.md : libellé simplifié en\n\"Objectifs\"/\"Goals\", phrase courses retirée du corps (plus de doublon\nentre les deux entrées).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-898 - Site vitrine - /features : nouvelle entrée \"Courses\" séparée d'\"Objectifs\"",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "content/features/goals.en.md",
        "content/features/goals.fr.md",
        "content/features/races.en.md",
        "content/features/races.fr.md",
        "docs/site.md",
        "public/assets/img/features/races/races-1.png",
        "templates/features.php",
        "tests/Http/Site/FeaturesPageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/goals.en.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "content/features/goals.fr.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "content/features/races.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/features/races.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "docs/site.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/img/features/races/races-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "templates/features.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 7,
          "deletions": 7,
          "lines_changed": 14
        }
      ],
      "files_changed": 11,
      "insertions": 75,
      "deletions": 16
    },
    {
      "hash": "5ae2144cbc051a2e851d908afd60ea4d4531ecf0",
      "short_hash": "5ae2144",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789331062,
      "message": "feat: SPOR-897 - Site vitrine - /features : synchronise les .en.md sur les captures ajoutées côté .fr.md\n\n16 des 17 fichiers content/features/<slug>.fr.md avaient reçu de nouvelles\ncaptures d'écran réelles (souvent promues en lightbox, SPOR-889) et parfois\ndu texte additionnel sans que leur .en.md suive. Traduction + même\nstructure d'images/légendes appliquée à chacun. Au passage : training.en.md\npointait encore vers training-1.png (remplacé par breathing.png côté FR,\nseul fichier présent sur disque) ; dashboard.en.md corrige le bug de lien\nimage `/dashboard-1.png` du .fr.md (le / de tête casse le lightbox une\nfois rawurlencode()) plutôt que de le reproduire.\n\nprivacy.en.md n'est délibérément PAS synchronisé : privacy.fr.md a\nsupprimé sa ligne d'image sans remplacement, contredisant la politique\n\"cadre pointillé en attendant\" de docs/site.md et cassant\nFeaturesPageTest côté FR — signalé à l'utilisateur, pas reproduit ni\ncorrigé unilatéralement (fichier en cours d'édition active par ailleurs).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-897 - Site vitrine - /features : synchronise les .en.md sur les captures ajoutées côté .f",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "content/features/badges.en.md",
        "content/features/calendar.en.md",
        "content/features/dashboard.en.md",
        "content/features/events.en.md",
        "content/features/gear.en.md",
        "content/features/goals.en.md",
        "content/features/health.en.md",
        "content/features/notifications.en.md",
        "content/features/profile.en.md",
        "content/features/quotes.en.md",
        "content/features/records.en.md",
        "content/features/routes.en.md",
        "content/features/sessions.en.md",
        "content/features/timeline.en.md",
        "content/features/training.en.md",
        "content/features/trends.en.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 45,
          "deletions": 0,
          "lines_changed": 45
        },
        {
          "path": "content/features/badges.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/calendar.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/dashboard.en.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "content/features/events.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/gear.en.md",
          "insertions": 6,
          "deletions": 2,
          "lines_changed": 8
        },
        {
          "path": "content/features/goals.en.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "content/features/health.en.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "content/features/notifications.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/profile.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/quotes.en.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "content/features/records.en.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "content/features/routes.en.md",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "content/features/sessions.en.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "content/features/timeline.en.md",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "content/features/training.en.md",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "content/features/trends.en.md",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        }
      ],
      "files_changed": 18,
      "insertions": 108,
      "deletions": 19
    },
    {
      "hash": "5e1bc89cd111a3c2f88bad8cf1abf6150f75f4b6",
      "short_hash": "5e1bc89",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789330402,
      "message": "feat: SPOR-896 - Site vitrine - /features : badge \"Beta\" dans le menu latéral\n\nEntraînement et Timeline sont encore incomplets (demande explicite de\nl'utilisateur, capture d'écran du menu) : nouveau champ `beta` dans le\nfrontmatter de content/features/<slug>.<locale>.md, comme title/menu\n(SPOR-886) — FeatureContent::load() l'expose, templates/features.php\naffiche un badge \"Beta\" à côté du libellé de menu concerné. Donnée par\ndomaine, pas une liste de slugs codée en dur.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-896 - Site vitrine - /features : badge \"Beta\" dans le menu latéral",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "content/features/timeline.en.md",
        "content/features/timeline.fr.md",
        "content/features/training.en.md",
        "content/features/training.fr.md",
        "docs/site.md",
        "public/assets/css/components.css",
        "src/Infra/Content/FeatureContent.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/features.php",
        "tests/Http/Site/FeaturesPageTest.php",
        "tests/Infra/Content/FeatureContentTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "content/features/timeline.en.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "content/features/timeline.fr.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "content/features/training.en.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "content/features/training.fr.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/site.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/Content/FeatureContent.php",
          "insertions": 7,
          "deletions": 2,
          "lines_changed": 9
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "templates/features.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "tests/Infra/Content/FeatureContentTest.php",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        }
      ],
      "files_changed": 14,
      "insertions": 112,
      "deletions": 5
    },
    {
      "hash": "8f536adf4953d24a3dfa7d17f61aa5a5d4bb8a8b",
      "short_hash": "8f536ad",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789325588,
      "message": "fix: SPOR-895 - Météo - precipitation_probability null en fin de fenêtre 16 j faisait échouer tout le parsing\n\nOpen-Meteo laisse precipitation_probability/precipitation_probability_max\nà null au-delà de son propre horizon (plus court que les 16 j de\ntempérature/vent demandés) : OpenMeteoResponseParser::num() traitait ce\nchamp comme obligatoire, faisant échouer TOUT le parsing (donc les 14\npremiers jours pourtant valides) dès qu'un seul index en queue de fenêtre\nétait null — le widget Dashboard restait bloqué sur \"indisponible\" une\nfois le cache périmé pour de bon. Repli à 0.0 sur ce champ, comme les\nautres champs tolérants (gustKph, feelsLikeC).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-895 - Météo - precipitation_probability null en fin de fenêtre 16 j faisait échouer tout l",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Infra/Weather/OpenMeteoResponseParser.php",
        "tests/Infra/Weather/OpenMeteoResponseParserTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "src/Infra/Weather/OpenMeteoResponseParser.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "tests/Infra/Weather/OpenMeteoResponseParserTest.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        }
      ],
      "files_changed": 4,
      "insertions": 72,
      "deletions": 2
    },
    {
      "hash": "40216a84549830f7fe94196b6c59e222bf8a593e",
      "short_hash": "40216a8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789324864,
      "message": "fix: SPOR-894 - Sessions - Image résumé + favicon app : icon-512.png plutôt que revontulet-mark.png\n\nrevontulet-mark.png embarque le texte \"REVONTULET\" sous la marque, illisible\nune fois compressé dans le petit badge du filigrane de l'image résumé\n(SPOR-890) ; même bascule sur le favicon des pages \"app\" (app_head.php),\npar cohérence avec head.php (site vitrine) qui utilisait déjà icon-512.png.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-894 - Sessions - Image résumé + favicon app : icon-512.png plutôt que revontulet-mark.png",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "templates/partials/app_head.php",
        "templates/partials/session_share_image_modal.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "templates/partials/app_head.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/session_share_image_modal.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        }
      ],
      "files_changed": 4,
      "insertions": 38,
      "deletions": 2
    },
    {
      "hash": "424df791f598594d1cef1be8f65061b1659521d1",
      "short_hash": "424df79",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789319061,
      "message": "fix: SPOR-893 - Sessions - Image résumé : courbes à côté de la trace en bannière\n\nLe carré letterboxé de SPOR-892 rendait la trace correctement proportionnée,\nmais en format bannière (large, bas) la hauteur restante après titre/trace/\ncourbes/métriques/pied de page ne laissait plus qu'un point. Mise en page\nadaptative : format large et bas + trace/photo + au moins une courbe →\ndeux colonnes (trace à gauche pleine hauteur, courbes empilées à droite)\nau lieu de l'empilement vertical, qui reste inchangé pour carré/story.\n\nVérifié en exécutant le vrai script dans jsdom (Canvas stubbé) contre le\nHTML réellement rendu par l'application — jsdom installé de façon isolée\ndans le scratchpad, jamais dans le projet.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-893 - Sessions - Image résumé : courbes à côté de la trace en bannière",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/session-share-image.js"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "public/assets/js/session-share-image.js",
          "insertions": 45,
          "deletions": 19,
          "lines_changed": 64
        }
      ],
      "files_changed": 3,
      "insertions": 87,
      "deletions": 19,
      "tags": [
        "v1.7.49"
      ]
    },
    {
      "hash": "25b240a66a347708a05f59f72bb33fa9c7742cf4",
      "short_hash": "25b240a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789317627,
      "message": "fix: SPOR-892 - Sessions - Image résumé : trace GPS déformée hors format carré\n\npaintTrace() étirait les points normalisés (carré [0,1]x[0,1], aspect ratio\ndéjà préservé par RouteThumbnail::toViewBoxPoints) par box.w et box.h\nindépendamment, déformant la trace dès que la zone de dessin n'est pas\ncarrée (format bannière notamment). Inscrit désormais un carré centré\n(letterbox) dans le rectangle de dessin avant de mapper les points.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-892 - Sessions - Image résumé : trace GPS déformée hors format carré",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/session-share-image.js"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "public/assets/js/session-share-image.js",
          "insertions": 13,
          "deletions": 3,
          "lines_changed": 16
        }
      ],
      "files_changed": 3,
      "insertions": 39,
      "deletions": 3
    },
    {
      "hash": "9c3e54e7b2c1d916eef56024684a52d670088cf7",
      "short_hash": "9c3e54e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789317238,
      "message": "feat: SPOR-890 - Sessions - Image résumé personnalisable à partager\n\nBouton « Image résumé… » sur le détail d'une séance : modale de sélection\n(trace GPS simplifiée, photo, courbes vitesse/FC/dénivelé, titre, lieu,\ndistance, durée, allure, FC, puissance ; thème blanc/noir/Revontulet ;\nformat prédéfini ou personnalisé), rendu 100% côté navigateur en Canvas\n(pas de génération serveur GD). Logo + URL du site sur toute image générée.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-890 - Sessions - Image résumé personnalisable à partager",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "public/assets/css/app.css",
        "public/assets/js/session-share-image.js",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionShareImageData.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/session_share_image_modal.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/SessionShareImageDataTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        },
        {
          "path": "docs/sessions.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 25,
          "deletions": 0,
          "lines_changed": 25
        },
        {
          "path": "public/assets/js/session-share-image.js",
          "insertions": 419,
          "deletions": 0,
          "lines_changed": 419
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "src/Http/Sessions/SessionShareImageData.php",
          "insertions": 126,
          "deletions": 0,
          "lines_changed": 126
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "templates/partials/session_share_image_modal.php",
          "insertions": 124,
          "deletions": 0,
          "lines_changed": 124
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "tests/Http/Sessions/SessionShareImageDataTest.php",
          "insertions": 170,
          "deletions": 0,
          "lines_changed": 170
        }
      ],
      "files_changed": 13,
      "insertions": 1093,
      "deletions": 0
    },
    {
      "hash": "18eb1a59854d6d255295bdb74bedb9b644d32c71",
      "short_hash": "18eb1a5",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789315970,
      "message": "fix: SPOR-891 - Sessions - Courbe Température invisible sur valeur constante (Chart.js min=max)\n\nSur une séance dont la température FIT est constante (ex. Yoga en\nintérieur), scales.y.min === max degénère le calcul de pixel de Chart.js\n(division par une portée nulle -> NaN, confirmé dans le vendor\nchart.umd.min.js) : la courbe ne se dessinait plus du tout. chartConfig()\nécarte désormais les bornes de ±1 dans ce cas, pour toutes les courbes du\nfichier.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-891 - Sessions - Courbe Température invisible sur valeur constante (Chart.js min=max)",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/js/session-charts.js"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 20,
          "deletions": 2,
          "lines_changed": 22
        }
      ],
      "files_changed": 3,
      "insertions": 59,
      "deletions": 2
    },
    {
      "hash": "881e733a7e50d4e02fe45c3842339109a69e34ec",
      "short_hash": "881e733",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789315511,
      "message": "feat: SPOR-889 - Site vitrine - /features : clic sur une capture d'écran ouvre une lightbox\n\nRemplace le nouvel onglet (SPOR-888) par une vraie lightbox : au clic, la\ncapture s'affiche en grand par-dessus la page (fermeture par le bouton\nfermer, un clic en dehors, ou Échap). Composant dédié et minimal au site\npublic (feature-shot-lightbox.js + .rv-shot-lightbox dans site.css) plutôt\nque de réutiliser le lightbox media de l'appli connectée\n(interactions.js/app.css, non chargés sur les pages marketing) — décision\nexplicite de l'utilisateur pour ne pas étendre leur chargement à tout le\nsite vitrine. Un lien externe garde le nouvel onglet.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-889 - Site vitrine - /features : clic sur une capture d'écran ouvre une lightbox",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/site.md",
        "public/assets/css/site.css",
        "public/assets/js/feature-shot-lightbox.js",
        "src/Infra/Content/FeatureContent.php",
        "templates/features.php",
        "tests/Infra/Content/FeatureContentTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 69,
          "deletions": 11,
          "lines_changed": 80
        },
        {
          "path": "docs/site.md",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "public/assets/css/site.css",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "public/assets/js/feature-shot-lightbox.js",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Infra/Content/FeatureContent.php",
          "insertions": 20,
          "deletions": 11,
          "lines_changed": 31
        },
        {
          "path": "templates/features.php",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "tests/Infra/Content/FeatureContentTest.php",
          "insertions": 34,
          "deletions": 3,
          "lines_changed": 37
        }
      ],
      "files_changed": 8,
      "insertions": 203,
      "deletions": 29
    },
    {
      "hash": "1f238269f89363752911840f49c78c29b8b2dfb5",
      "short_hash": "1f23826",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789314902,
      "message": "feat: SPOR-888 - Site vitrine - /features : image encadrée d'un lien (clic → pleine taille)\n\nSupporte [![alt](fichier)](cible) en plus de ![alt](fichier) : la cible est\nrésolue dans le même dossier public/assets/img/features/<slug>/ et ouverte\ndans un nouvel onglet. Corrige le rendu de sessions.fr.md où l'utilisateur\navait déjà utilisé cette syntaxe.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-888 - Site vitrine - /features : image encadrée d'un lien (clic → pleine taille)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "content/features/sessions.fr.md",
        "src/Infra/Content/FeatureContent.php",
        "tests/Http/Site/FeaturesPageTest.php",
        "tests/Infra/Content/FeatureContentTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 32,
          "deletions": 3,
          "lines_changed": 35
        },
        {
          "path": "content/features/sessions.fr.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/Content/FeatureContent.php",
          "insertions": 28,
          "deletions": 9,
          "lines_changed": 37
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 6,
          "deletions": 5,
          "lines_changed": 11
        },
        {
          "path": "tests/Infra/Content/FeatureContentTest.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        }
      ],
      "files_changed": 6,
      "insertions": 92,
      "deletions": 18
    },
    {
      "hash": "0a2644a16cbb0749dc992b80988e2d0381cbd9a5",
      "short_hash": "0a2644a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789313830,
      "message": "feat: SPOR-887 - Site vitrine - /features : vraies captures d'écran via syntaxe Markdown standard\n\nRemplace les champs shot1/shot2 du frontmatter par la syntaxe Markdown\nstandard ![légende](fichier) directement dans le corps : une vraie <img>\nsi public/assets/img/features/<slug>/<fichier> existe, sinon le même\nencadré pointillé qu'avant. 34 fichiers migrés ; ajoute la première vraie\ncapture d'écran (sessions-1.png, déposée par l'utilisateur).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-887 - Site vitrine - /features : vraies captures d'écran via syntaxe Markdown standard",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "content/features/badges.en.md",
        "content/features/badges.fr.md",
        "content/features/calendar.en.md",
        "content/features/calendar.fr.md",
        "content/features/dashboard.en.md",
        "content/features/dashboard.fr.md",
        "content/features/events.en.md",
        "content/features/events.fr.md",
        "content/features/gear.en.md",
        "content/features/gear.fr.md",
        "content/features/goals.en.md",
        "content/features/goals.fr.md",
        "content/features/health.en.md",
        "content/features/health.fr.md",
        "content/features/notifications.en.md",
        "content/features/notifications.fr.md",
        "content/features/privacy.en.md",
        "content/features/privacy.fr.md",
        "content/features/profile.en.md",
        "content/features/profile.fr.md",
        "content/features/quotes.en.md",
        "content/features/quotes.fr.md",
        "content/features/records.en.md",
        "content/features/records.fr.md",
        "content/features/routes.en.md",
        "content/features/routes.fr.md",
        "content/features/sessions.en.md",
        "content/features/sessions.fr.md",
        "content/features/timeline.en.md",
        "content/features/timeline.fr.md",
        "content/features/training.en.md",
        "content/features/training.fr.md",
        "content/features/trends.en.md",
        "content/features/trends.fr.md",
        "public/assets/css/components.css",
        "public/assets/img/features/sessions/sessions-1.png",
        "src/Infra/Content/FeatureContent.php",
        "templates/features.php",
        "tests/Http/Site/FeaturesPageTest.php",
        "tests/Infra/Content/FeatureContentTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "content/features/badges.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/badges.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/calendar.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/calendar.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/dashboard.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/dashboard.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/events.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/events.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/gear.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/gear.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/goals.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/goals.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/health.en.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "content/features/health.fr.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "content/features/notifications.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/notifications.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/privacy.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/privacy.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/profile.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/profile.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/quotes.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/quotes.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/records.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/records.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/routes.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/routes.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/sessions.en.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "content/features/sessions.fr.md",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "content/features/timeline.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/timeline.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/training.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/training.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/trends.en.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "content/features/trends.fr.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "public/assets/img/features/sessions/sessions-1.png",
          "insertions": 0,
          "deletions": 0,
          "lines_changed": 0
        },
        {
          "path": "src/Infra/Content/FeatureContent.php",
          "insertions": 102,
          "deletions": 23,
          "lines_changed": 125
        },
        {
          "path": "templates/features.php",
          "insertions": 10,
          "deletions": 16,
          "lines_changed": 26
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 21,
          "deletions": 8,
          "lines_changed": 29
        },
        {
          "path": "tests/Infra/Content/FeatureContentTest.php",
          "insertions": 143,
          "deletions": 57,
          "lines_changed": 200
        }
      ],
      "files_changed": 43,
      "insertions": 439,
      "deletions": 142
    },
    {
      "hash": "3c75e2fe7028d4a62034427b180220979b0b72ad",
      "short_hash": "3c75e2f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789304554,
      "message": "refactor: SPOR-886 - Site vitrine - Contenu de /features externalisé en fichiers Markdown\n\nLe contenu par domaine de /features (titre, menu, description, légendes de\ncapture) déménage des catalogues i18n vers content/features/<slug>.\n<locale>.md (34 fichiers), pour faciliter l'édition et le contrôle visuel.\nNouveau Infra\\Content\\FeatureContent (frontmatter minimal + Markdown\nparagraphes/**gras**, sans nouvelle dépendance Composer). deploy.yml\ncorrigé : ces .md doivent survivre à la purge de la doc technique avant\nl'upload FTP.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "refactor: SPOR-886 - Site vitrine - Contenu de /features externalisé en fichiers Markdown",
      "type": "refactor",
      "modified_files": [
        ".github/workflows/deploy.yml",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "content/features/badges.en.md",
        "content/features/badges.fr.md",
        "content/features/calendar.en.md",
        "content/features/calendar.fr.md",
        "content/features/dashboard.en.md",
        "content/features/dashboard.fr.md",
        "content/features/events.en.md",
        "content/features/events.fr.md",
        "content/features/gear.en.md",
        "content/features/gear.fr.md",
        "content/features/goals.en.md",
        "content/features/goals.fr.md",
        "content/features/health.en.md",
        "content/features/health.fr.md",
        "content/features/notifications.en.md",
        "content/features/notifications.fr.md",
        "content/features/privacy.en.md",
        "content/features/privacy.fr.md",
        "content/features/profile.en.md",
        "content/features/profile.fr.md",
        "content/features/quotes.en.md",
        "content/features/quotes.fr.md",
        "content/features/records.en.md",
        "content/features/records.fr.md",
        "content/features/routes.en.md",
        "content/features/routes.fr.md",
        "content/features/sessions.en.md",
        "content/features/sessions.fr.md",
        "content/features/timeline.en.md",
        "content/features/timeline.fr.md",
        "content/features/training.en.md",
        "content/features/training.fr.md",
        "content/features/trends.en.md",
        "content/features/trends.fr.md",
        "src/Infra/Content/FeatureContent.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/features.php",
        "tests/Http/Site/FeaturesPageTest.php",
        "tests/Infra/Content/FeatureContentTest.php"
      ],
      "modified_files_details": [
        {
          "path": ".github/workflows/deploy.yml",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "content/features/badges.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/badges.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/calendar.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/calendar.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/dashboard.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/dashboard.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/events.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/events.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/gear.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/gear.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/goals.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/goals.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/health.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/features/health.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/features/notifications.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/notifications.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/privacy.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/privacy.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/profile.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/profile.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/quotes.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/quotes.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/records.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/records.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/routes.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/routes.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/sessions.en.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/features/sessions.fr.md",
          "insertions": 7,
          "deletions": 0,
          "lines_changed": 7
        },
        {
          "path": "content/features/timeline.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/timeline.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/training.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/training.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/trends.en.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "content/features/trends.fr.md",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/Content/FeatureContent.php",
          "insertions": 113,
          "deletions": 0,
          "lines_changed": 113
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 4,
          "deletions": 89,
          "lines_changed": 93
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 6,
          "deletions": 87,
          "lines_changed": 93
        },
        {
          "path": "templates/features.php",
          "insertions": 29,
          "deletions": 29,
          "lines_changed": 58
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 73,
          "deletions": 21,
          "lines_changed": 94
        },
        {
          "path": "tests/Infra/Content/FeatureContentTest.php",
          "insertions": 165,
          "deletions": 0,
          "lines_changed": 165
        }
      ],
      "files_changed": 43,
      "insertions": 711,
      "deletions": 227
    },
    {
      "hash": "6d3dd49decc6dcdb24b4a2533ddebb1e649b3e17",
      "short_hash": "6d3dd49",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789301005,
      "message": "feat: SPOR-885 - Site vitrine - Nav publique adaptée à un visiteur déjà connecté\n\nUn visiteur déjà connecté qui revient sur une page publique voit \"Mon\ncarnet\" (-> /dashboard) à la place de \"Se connecter\" dans la nav partagée,\net \"Créer mon carnet\" devient \"Mon carnet\" sur le hero de l'accueil. État\nlu via AuthGuard::currentUserId() directement dans partials/head.php.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-885 - Site vitrine - Nav publique adaptée à un visiteur déjà connecté",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/site.md",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/home.php",
        "templates/partials/head.php",
        "tests/Http/Site/PublicNavLoginStateTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 56,
          "deletions": 0,
          "lines_changed": 56
        },
        {
          "path": "docs/site.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "templates/home.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/head.php",
          "insertions": 13,
          "deletions": 2,
          "lines_changed": 15
        },
        {
          "path": "tests/Http/Site/PublicNavLoginStateTest.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        }
      ],
      "files_changed": 8,
      "insertions": 166,
      "deletions": 2
    },
    {
      "hash": "e114be549d3b18aaf0e7fcf9ffcb12946a2472be",
      "short_hash": "e114be5",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789288834,
      "message": "refactor: SPOR-884 - Site vitrine - Fonctionnalités : menu latéral, une fonctionnalité par page\n\nRetour explicite : « place un menu a gauche et chaque fonctionnalite doit\netre presente seule, pas une page complete ». Remplace la page unique +\nmenu d'ancres par un vrai menu latéral (.rv-features-nav) et une route par\ndomaine (/features/{domain}, whitelist de 17 slugs, même patron que\n/trends/{period}) qui n'affiche jamais que ce seul domaine.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "refactor: SPOR-884 - Site vitrine - Fonctionnalités : menu latéral, une fonctionnalité par page",
      "type": "refactor",
      "modified_files": [
        "References/design/assets/css/components.css",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/site.md",
        "public/assets/css/components.css",
        "templates/features.php",
        "tests/Http/Site/FeaturesPageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 69,
          "deletions": 31,
          "lines_changed": 100
        },
        {
          "path": "config/routes.php",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        },
        {
          "path": "docs/site.md",
          "insertions": 8,
          "deletions": 7,
          "lines_changed": 15
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "templates/features.php",
          "insertions": 63,
          "deletions": 46,
          "lines_changed": 109
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 77,
          "deletions": 28,
          "lines_changed": 105
        }
      ],
      "files_changed": 7,
      "insertions": 270,
      "deletions": 114
    },
    {
      "hash": "477d8ac507244ff44905b430495bd9856107e258",
      "short_hash": "477d8ac",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789259191,
      "message": "feat: SPOR-884 - Site vitrine - Nouvelle page publique « Fonctionnalités » (menu + une entrée par domaine)\n\nPage /features (épic Communication SPOR-39) : un menu d'ancres + une\nsection par domaine applicatif (17, regroupés comme le menu latéral de\nl'app), bilingue FR/EN. Contenu et structure prêts, mais AUCUNE vraie\ncapture d'écran (aucun outil de capture/navigateur disponible ici) : chaque\nécran attendu est un encadré pointillé légendé (.rv-shot-placeholder), à\nremplacer avant de considérer la story terminée.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-884 - Site vitrine - Nouvelle page publique « Fonctionnalités » (menu + une entrée par do",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/site.md",
        "public/assets/css/components.css",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/features.php",
        "templates/partials/head.php",
        "tests/Http/Site/FeaturesPageTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "config/routes.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "docs/site.md",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 97,
          "deletions": 0,
          "lines_changed": 97
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 98,
          "deletions": 0,
          "lines_changed": 98
        },
        {
          "path": "templates/features.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "templates/partials/head.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "tests/Http/Site/FeaturesPageTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        }
      ],
      "files_changed": 11,
      "insertions": 493,
      "deletions": 1
    },
    {
      "hash": "4cdf31c30c31fc29f13a811a42df319203847e1e",
      "short_hash": "4cdf31c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789257337,
      "message": "feat: SPOR-883 - Santé - Focus : emplacements vides pointillés pour les slots non utilisés\n\nLa grille des grandes cartes \"focus\" montre désormais toujours ses 4\nemplacements (FocusMeasures::MAX) : un encadré pointillé \"+\" comble chaque\nplace libre, réutilisant tel quel le composant .rv-goal-slot--empty de\n/goals (généralisé pour couvrir un <a>, plus seulement un <button>).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-883 - Santé - Focus : emplacements vides pointillés pour les slots non utilisés",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sante.md",
        "public/assets/css/components.css",
        "src/Http/Health/HealthController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/health.php",
        "tests/Http/Health/HealthFocusEmptySlotsTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 8,
          "deletions": 4,
          "lines_changed": 12
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "docs/sante.md",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 8,
          "deletions": 4,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/health.php",
          "insertions": 12,
          "deletions": 3,
          "lines_changed": 15
        },
        {
          "path": "tests/Http/Health/HealthFocusEmptySlotsTest.php",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        }
      ],
      "files_changed": 10,
      "insertions": 144,
      "deletions": 15,
      "tags": [
        "v1.7.48"
      ]
    },
    {
      "hash": "a9d9dac749d74b762056316cfcd45fc9f7d366f0",
      "short_hash": "a9d9dac",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789255463,
      "message": "feat: SPOR-882 - Santé - Physiologie : poignée de glisser-déposer visible sur les tuiles et cartes focus\n\nBascule les tuiles physiologie (SPOR-879) et les cartes focus (SPOR-880) du\npattern \"carte entière draggable\" vers la poignée dédiée .rv-drag-handle\ndéjà utilisée sur /goals (icône grip, aucun nouveau CSS/JS). Évite au\npassage tout conflit drag/clic avec l'étoile d'épinglage et le bouton\nsparkline des cartes focus.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-882 - Santé - Physiologie : poignée de glisser-déposer visible sur les tuiles et cartes f",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sante.md",
        "public/assets/css/components.css",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/health.php",
        "tests/Http/Health/HealthFocusReorderRoutesTest.php",
        "tests/Http/Health/HealthTileReorderRoutesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "docs/sante.md",
          "insertions": 6,
          "deletions": 5,
          "lines_changed": 11
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/health.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "tests/Http/Health/HealthFocusReorderRoutesTest.php",
          "insertions": 13,
          "deletions": 1,
          "lines_changed": 14
        },
        {
          "path": "tests/Http/Health/HealthTileReorderRoutesTest.php",
          "insertions": 15,
          "deletions": 1,
          "lines_changed": 16
        }
      ],
      "files_changed": 10,
      "insertions": 121,
      "deletions": 9
    },
    {
      "hash": "e8c78330c6610932fe169b8a88113b86497405dc",
      "short_hash": "e8c7833",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789253706,
      "message": "feat: SPOR-881 - Santé - Focus : plafond porté de 3 à 4 mesures épinglées\n\nFocusMeasures::MAX passe de 3 à 4 (seul point de vérité, le flash et les\ngardes de fromString()/toggled() en dépendent déjà). Commentaires et doc\nutilisateur mis à jour en cohérence, y compris le CSS de la maquette côté\npublic/ et References/design/ qui reste synchronisé.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-881 - Santé - Focus : plafond porté de 3 à 4 mesures épinglées",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sante.md",
        "public/assets/css/components.css",
        "src/Domain/Health/FocusMeasures.php",
        "src/Http/Health/HealthController.php",
        "templates/health.php",
        "tests/Domain/Health/FocusMeasuresTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "docs/sante.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Health/FocusMeasures.php",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 4,
          "deletions": 3,
          "lines_changed": 7
        },
        {
          "path": "templates/health.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "tests/Domain/Health/FocusMeasuresTest.php",
          "insertions": 10,
          "deletions": 10,
          "lines_changed": 20
        }
      ],
      "files_changed": 9,
      "insertions": 56,
      "deletions": 25
    },
    {
      "hash": "a99e9e56a8ecc8075e8bdb827009e0d6da46dfdc",
      "short_hash": "a99e9e5",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789251554,
      "message": "feat: SPOR-880 - Santé - Cartes « focus » (mesures épinglées) réordonnables par glisser-déposer\n\nSuite immédiate de SPOR-879, sur les grandes cartes « focus » (jusqu'à 3\nmesures épinglées via l'étoile). L'ordre vit déjà dans users.focus_measures\n(liste de clés = ordre d'affichage) : pas de nouvelle table, juste\nFocusMeasures::reordered() et le même idiome initDragReorder() que\nreorderTiles().\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-880 - Santé - Cartes « focus » (mesures épinglées) réordonnables par glisser-déposer",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/sante.md",
        "src/Domain/Health/FocusMeasures.php",
        "src/Http/Health/HealthController.php",
        "templates/health.php",
        "tests/Domain/Health/FocusMeasuresTest.php",
        "tests/Http/Health/HealthFocusReorderRoutesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "docs/sante.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "src/Domain/Health/FocusMeasures.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 35,
          "deletions": 0,
          "lines_changed": 35
        },
        {
          "path": "templates/health.php",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "tests/Domain/Health/FocusMeasuresTest.php",
          "insertions": 34,
          "deletions": 0,
          "lines_changed": 34
        },
        {
          "path": "tests/Http/Health/HealthFocusReorderRoutesTest.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        }
      ],
      "files_changed": 9,
      "insertions": 189,
      "deletions": 3
    },
    {
      "hash": "3dd97fb77e33016928ffb5710d00cfc9e0380008",
      "short_hash": "3dd97fb",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789250718,
      "message": "feat: SPOR-879 - Santé - Physiologie : tuiles de /health réordonnables par glisser-déposer\n\nRéutilise initDragReorder() (interactions.js) et le patron\nTrendCardOrder/TrendCardOrderStore (SPOR-772) : les 13 tuiles de /health\nsont des sections de rendu, pas des lignes de BDD, l'ordre personnalisé se\nstocke donc comme une liste JSON de clés (table health_tile_orders).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-879 - Santé - Physiologie : tuiles de /health réordonnables par glisser-déposer",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/193_add_health_tile_orders.sql",
        "docs/sante.md",
        "src/Domain/Health/HealthTileOrder.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/HealthTileOrderStore.php",
        "templates/health.php",
        "tests/Domain/Health/HealthTileOrderTest.php",
        "tests/Http/Health/HealthTileOrderStoreTest.php",
        "tests/Http/Health/HealthTileReorderRoutesTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "db/migrations/193_add_health_tile_orders.sql",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "docs/sante.md",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Domain/Health/HealthTileOrder.php",
          "insertions": 53,
          "deletions": 0,
          "lines_changed": 53
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 53,
          "deletions": 1,
          "lines_changed": 54
        },
        {
          "path": "src/Http/Health/HealthTileOrderStore.php",
          "insertions": 75,
          "deletions": 0,
          "lines_changed": 75
        },
        {
          "path": "templates/health.php",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "tests/Domain/Health/HealthTileOrderTest.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "tests/Http/Health/HealthTileOrderStoreTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "tests/Http/Health/HealthTileReorderRoutesTest.php",
          "insertions": 38,
          "deletions": 0,
          "lines_changed": 38
        }
      ],
      "files_changed": 12,
      "insertions": 473,
      "deletions": 4
    },
    {
      "hash": "2d5c6da4107af36f9dc75bc0ee04667d5c70a445",
      "short_hash": "2d5c6da",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789242435,
      "message": "fix: SPOR-878 - Santé - Graphiques : les courbes ne prenaient pas leur couleur\n\nCapture d'écran de l'utilisateur : les deux courbes d'Équilibre TCS\ns'affichaient en noir. Cause : Canvas 2D ne résout pas var(--xxx) comme le\nferait un attribut CSS/SVG — ctx.strokeStyle = 'var(--aurora-cyan)' est\nsilencieusement ignoré et retombe sur le noir. Bug préexistant sur toute\ncourbe de health-trends.js, resté invisible tant qu'une seule courbe avec\nremplissage (rgba littéral, lui se résout nativement) masquait le trait\nnoir — le second dataset sans remplissage l'a rendu visible.\n\nNouvelle fonction resolveColor() (même bug de fond que resolveColor() dans\nsession-charts.js, SPOR-312, sous une forme différente) appliquée aux\ncouleurs de trait des deux séries et de la sparkline.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-878 - Santé - Graphiques : les courbes ne prenaient pas leur couleur",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/js/health-trends.js",
        "tests/Http/Health/PhysiologyTilesWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 23,
          "deletions": 0,
          "lines_changed": 23
        },
        {
          "path": "public/assets/js/health-trends.js",
          "insertions": 24,
          "deletions": 5,
          "lines_changed": 29
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        }
      ],
      "files_changed": 3,
      "insertions": 66,
      "deletions": 5
    },
    {
      "hash": "5b09b8313b43547a2ff908cb24e26ec76b92b1e3",
      "short_hash": "5b09b83",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789241303,
      "message": "fix: SPOR-878 - Santé - Équilibre TCS : 2 courbes, liste complète, axe sans bruit flottant\n\nRetour utilisateur (capture d'écran) : l'axe Y du graphique agrandi affichait\ndu bruit flottant Chart.js (\"49.800000000000004 %\"), et la demande d'avoir\nles deux courbes (gauche/droite) plutôt qu'une seule, avec les deux valeurs\ndans la liste chiffrée en dessous.\n\n- roundTick() (health-trends.js) arrondit les crans d'axe Y générés par\n  Chart.js — correctif générique, profite à toutes les tuiles à courbe.\n- Le graphique agrandi trace désormais 2 datasets (gauche + droite, légende\n  G/D ou L/R selon la locale) quand le payload porte `values2` — nouvelle\n  clé `chartSeriesLabels` côté HealthController, transmise via\n  data-series-labels.\n- La liste chiffrée affiche le texte composite complet par séance\n  (\"50,1 % G / 49,9 % D\") via une nouvelle clé optionnelle `valueLabels`,\n  sans affecter les autres tuiles.\n\nVérifié en conditions réelles : payload JSON gauche/droite complémentaires,\nlégende correcte, liste avec les deux valeurs.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-878 - Santé - Équilibre TCS : 2 courbes, liste complète, axe sans bruit flottant",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "docs/sante.md",
        "public/assets/js/health-trends.js",
        "src/Http/Health/HealthController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/health.php",
        "tests/Http/Health/PhysiologyTilesWiringTest.php",
        "tests/Http/Health/PrimaryTileSparklineWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 48,
          "deletions": 9,
          "lines_changed": 57
        },
        {
          "path": "docs/sante.md",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "public/assets/js/health-trends.js",
          "insertions": 54,
          "deletions": 18,
          "lines_changed": 72
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 35,
          "deletions": 1,
          "lines_changed": 36
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/health.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 47,
          "deletions": 0,
          "lines_changed": 47
        },
        {
          "path": "tests/Http/Health/PrimaryTileSparklineWiringTest.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        }
      ],
      "files_changed": 9,
      "insertions": 215,
      "deletions": 32
    },
    {
      "hash": "d4aef8751f5344f265cbd3b754eaa8f6b3a30f2d",
      "short_hash": "d4aef87",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789239795,
      "message": "fix: SPOR-878 - Santé - Équilibre TCS : afficher gauche ET droite sur la tuile\n\nLa tuile n'affichait que le pourcentage gauche + \"%\" (choix initial pour ne\npas casser le rappel chiffré de la modale Progression, qui attend un nombre\nsimple). Retour utilisateur : la tuile doit présenter les deux côtés.\n\nCorrigé en réutilisant le texte composite déjà utilisé par la carte\nPartners/Segments du détail de séance (SPOR-875) : \"49,7 % G / 50,3 % D\".\nLa courbe de la modale Progression reste sur le pourcentage gauche seul\n(série numérique simple) via une nouvelle clé de tile array `chartUnit`,\nconsommée uniquement par le canevas/la liste chiffrée — sans affecter les\nautres tuiles ni casser l'unité affichée sur le graphique.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-878 - Santé - Équilibre TCS : afficher gauche ET droite sur la tuile",
      "type": "fix",
      "modified_files": [
        "TECHNICAL.md",
        "docs/sante.md",
        "src/Http/Health/HealthController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/health.php",
        "tests/Http/Health/PhysiologyTilesWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 23,
          "deletions": 10,
          "lines_changed": 33
        },
        {
          "path": "docs/sante.md",
          "insertions": 5,
          "deletions": 4,
          "lines_changed": 9
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 25,
          "deletions": 7,
          "lines_changed": 32
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "templates/health.php",
          "insertions": 12,
          "deletions": 2,
          "lines_changed": 14
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        }
      ],
      "files_changed": 7,
      "insertions": 84,
      "deletions": 27
    },
    {
      "hash": "53f81c4deff905445ee832dd6daa61648facfe6a",
      "short_hash": "53f81c4",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789237437,
      "message": "fix: SPOR-878 - Santé - Physiologie : précision Longueur de foulée + tuile Équilibre TCS G/D\n\nCorrige la modale \"Progression\" de la tuile Longueur de foulée qui affichait\n\"1 m\" au lieu de \"1,00 m\" (un match($tile['key']) codait 0 décimale par\ndéfaut, sans entrée pour stride_length — l'axe du graphique et l'infobulle\nau survol n'étaient pas affectés).\n\nAjoute, de la même façon que la longueur de foulée, une tuile \"Équilibre TCS\n(gauche)\" sur /health : moyenne du champ FIT stance_time_balance sur les\ncourses à pied des 12 dernières semaines, colonne persistée à l'import/\nfusion de trace, backfill dédié (exécuté sur les 23 courses réelles), modale\nprécisant que la donnée vient des séances de course à pied avec capteur de\ndynamique de course.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-878 - Santé - Physiologie : précision Longueur de foulée + tuile Équilibre TCS G/D",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_session_gct_balance.php",
        "db/migrations/192_add_session_gct_balance.sql",
        "docs/sante.md",
        "public/backfill_session_gct_balance.php",
        "src/Domain/Training/GroundContactBalance.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/SessionPhysiologyTrends.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SessionGctBalanceBackfill.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/health.php",
        "templates/partials/health_indicator_modals.php",
        "tests/Domain/GroundContactBalanceTest.php",
        "tests/Http/Health/PhysiologyTilesWiringTest.php",
        "tests/Infra/Db/SessionGctBalanceBackfillTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "bin/backfill_session_gct_balance.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "db/migrations/192_add_session_gct_balance.sql",
          "insertions": 28,
          "deletions": 0,
          "lines_changed": 28
        },
        {
          "path": "docs/sante.md",
          "insertions": 13,
          "deletions": 7,
          "lines_changed": 20
        },
        {
          "path": "public/backfill_session_gct_balance.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "src/Domain/Training/GroundContactBalance.php",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 34,
          "deletions": 2,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Health/SessionPhysiologyTrends.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 6,
          "deletions": 1,
          "lines_changed": 7
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 8,
          "deletions": 2,
          "lines_changed": 10
        },
        {
          "path": "src/Infra/Db/SessionGctBalanceBackfill.php",
          "insertions": 121,
          "deletions": 0,
          "lines_changed": 121
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "templates/health.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/health_indicator_modals.php",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "tests/Domain/GroundContactBalanceTest.php",
          "insertions": 64,
          "deletions": 0,
          "lines_changed": 64
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 80,
          "deletions": 1,
          "lines_changed": 81
        },
        {
          "path": "tests/Infra/Db/SessionGctBalanceBackfillTest.php",
          "insertions": 174,
          "deletions": 0,
          "lines_changed": 174
        }
      ],
      "files_changed": 19,
      "insertions": 833,
      "deletions": 14
    },
    {
      "hash": "bbc87b32b8449d7773030f53b202f24fe5f89919",
      "short_hash": "bbc87b3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789235953,
      "message": "feat: SPOR-877 - Santé - Physiologie : tuile « Longueur de foulée » (course à pied)\n\nAjoute une tuile physiologie sur /health, juste après Métabolisme de base :\nmoyenne de la longueur de foulée (FIT record 85, SPOR-874) sur les séances\nde course à pied des 12 dernières semaines, avec courbe d'historique — même\nmodèle que VO2max/TSS/EPOC (colonne persistée à l'import/fusion de trace,\nagrégée par semaine), restreint à sport='run' à la lecture.\n\nNouvelle colonne sessions.avg_step_length_mm + sentinelle\nstep_length_checked_at, backfill dédié (réanalyse du blob FIT par lot de 20,\nmême pattern que SwimStrokeBackfill) exécuté sur les 23 courses réelles déjà\nimportées. Modale « Comment est-ce calculé ? » précisant explicitement que\nla donnée vient des séances de course à pied avec capteur de dynamique de\ncourse — jamais d'une saisie manuelle ni d'un autre sport.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-877 - Santé - Physiologie : tuile « Longueur de foulée » (course à pied)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "bin/backfill_session_stride_length.php",
        "db/migrations/191_add_session_stride_length.sql",
        "docs/sante.md",
        "public/backfill_session_stride_length.php",
        "src/Domain/Training/StrideLength.php",
        "src/Http/Health/HealthController.php",
        "src/Http/Health/SessionPhysiologyTrends.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/Db/SessionStrideLengthBackfill.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/health_indicator_modals.php",
        "tests/Domain/StrideLengthTest.php",
        "tests/Http/Health/PhysiologyTilesWiringTest.php",
        "tests/Infra/Db/SessionStrideLengthBackfillTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 90,
          "deletions": 0,
          "lines_changed": 90
        },
        {
          "path": "bin/backfill_session_stride_length.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "db/migrations/191_add_session_stride_length.sql",
          "insertions": 27,
          "deletions": 0,
          "lines_changed": 27
        },
        {
          "path": "docs/sante.md",
          "insertions": 16,
          "deletions": 4,
          "lines_changed": 20
        },
        {
          "path": "public/backfill_session_stride_length.php",
          "insertions": 96,
          "deletions": 0,
          "lines_changed": 96
        },
        {
          "path": "src/Domain/Training/StrideLength.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Health/HealthController.php",
          "insertions": 36,
          "deletions": 2,
          "lines_changed": 38
        },
        {
          "path": "src/Http/Health/SessionPhysiologyTrends.php",
          "insertions": 45,
          "deletions": 6,
          "lines_changed": 51
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 11,
          "deletions": 2,
          "lines_changed": 13
        },
        {
          "path": "src/Infra/Db/SessionStrideLengthBackfill.php",
          "insertions": 132,
          "deletions": 0,
          "lines_changed": 132
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "templates/partials/health_indicator_modals.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "tests/Domain/StrideLengthTest.php",
          "insertions": 61,
          "deletions": 0,
          "lines_changed": 61
        },
        {
          "path": "tests/Http/Health/PhysiologyTilesWiringTest.php",
          "insertions": 76,
          "deletions": 1,
          "lines_changed": 77
        },
        {
          "path": "tests/Infra/Db/SessionStrideLengthBackfillTest.php",
          "insertions": 177,
          "deletions": 0,
          "lines_changed": 177
        }
      ],
      "files_changed": 18,
      "insertions": 885,
      "deletions": 16
    },
    {
      "hash": "f8ac2b6b18f9295ea7784be9eed1bffb7d60debe",
      "short_hash": "f8ac2b6",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789232842,
      "message": "feat: SPOR-876 - Auth - Indicateur de complexité du mot de passe (barre de progression)\n\nAjoute une barre de progression sous le champ mot de passe sur les 3 écrans\nde création/changement (register, reset-password, settings > Sécurité).\nLe remplissage reflète exactement le nombre de règles satisfaites parmi les\n5 de RegistrationRules::isValidPassword() (13+ caractères, majuscule,\nminuscule, chiffre, spécial) — jamais un algorithme d'entropie tiers, pour\nrester honnête vis-à-vis de la vraie règle serveur. 3 paliers de couleur,\nmasquée tant que le champ est vide.\n\nVérifié avec le vrai interactions.js exécuté sur le vrai HTML de /register\nvia jsdom (installé temporairement dans le scratchpad, jamais dans le\ndépôt) : comportement correct à chaque étape de frappe.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-876 - Auth - Indicateur de complexité du mot de passe (barre de progression)",
      "type": "feat",
      "modified_files": [
        "References/design/assets/css/components.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/auth.md",
        "public/assets/css/components.css",
        "public/assets/js/interactions.js",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/register.php",
        "templates/reset_password.php",
        "templates/settings.php",
        "tests/Http/Auth/PasswordStrengthMeterWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/components.css",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 65,
          "deletions": 0,
          "lines_changed": 65
        },
        {
          "path": "docs/auth.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/css/components.css",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 49,
          "deletions": 0,
          "lines_changed": 49
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "templates/register.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "templates/reset_password.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "templates/settings.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "tests/Http/Auth/PasswordStrengthMeterWiringTest.php",
          "insertions": 158,
          "deletions": 0,
          "lines_changed": 158
        }
      ],
      "files_changed": 12,
      "insertions": 342,
      "deletions": 0
    },
    {
      "hash": "be7e66d7de03fd562f7ac8a28d13c754497b68ea",
      "short_hash": "be7e66d",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789230944,
      "message": "feat: SPOR-875 - Sessions - Partenaires : association fine à un segment de la trace GPS\n\nAjoute une capacité optionnelle et additive à session_partners (association\nà toute la séance) : associer un partenaire à une PORTION précise de la\ntrace (point de départ + point de fin), choisie via deux curseurs sur une\nmini-carte de l'onglet Partners (picker inspiré de merge-splice.js, SPOR-487).\nUn même partenaire peut avoir plusieurs segments distincts dans la même\nséance. Le segment est surligné sur #route-map (couleur par partenaire) et\naffiché en texte (plage en km) dans la carte Partners.\n\nNouvelle table session_partner_segments (migration 190), toujours GPS\n(contrairement aux Incidents, pas de mode manuel), sans contrainte\nd'unicité (segments multiples autorisés, décision explicite de\nl'utilisateur). Remplacement complet à la sauvegarde, même pattern que\nSessionIncidentsStore.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-875 - Sessions - Partenaires : association fine à un segment de la trace GPS",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/190_add_session_partner_segments.sql",
        "docs/sessions.md",
        "public/assets/js/interactions.js",
        "public/assets/js/partner-segment-map.js",
        "src/Domain/Training/PartnerSegment.php",
        "src/Domain/Training/SessionEditTabBadges.php",
        "src/Http/Sessions/PartnerSegmentsStore.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/PartnerSegmentTest.php",
        "tests/Domain/SessionEditTabBadgesTest.php",
        "tests/Http/Sessions/PartnerSegmentsStoreTest.php",
        "tests/Http/Sessions/PartnerSegmentsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 129,
          "deletions": 0,
          "lines_changed": 129
        },
        {
          "path": "db/migrations/190_add_session_partner_segments.sql",
          "insertions": 46,
          "deletions": 0,
          "lines_changed": 46
        },
        {
          "path": "docs/sessions.md",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 120,
          "deletions": 3,
          "lines_changed": 123
        },
        {
          "path": "public/assets/js/partner-segment-map.js",
          "insertions": 193,
          "deletions": 0,
          "lines_changed": 193
        },
        {
          "path": "src/Domain/Training/PartnerSegment.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "src/Domain/Training/SessionEditTabBadges.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Sessions/PartnerSegmentsStore.php",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 77,
          "deletions": 2,
          "lines_changed": 79
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 116,
          "deletions": 4,
          "lines_changed": 120
        },
        {
          "path": "tests/Domain/PartnerSegmentTest.php",
          "insertions": 143,
          "deletions": 0,
          "lines_changed": 143
        },
        {
          "path": "tests/Domain/SessionEditTabBadgesTest.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "tests/Http/Sessions/PartnerSegmentsStoreTest.php",
          "insertions": 130,
          "deletions": 0,
          "lines_changed": 130
        },
        {
          "path": "tests/Http/Sessions/PartnerSegmentsWiringTest.php",
          "insertions": 154,
          "deletions": 0,
          "lines_changed": 154
        }
      ],
      "files_changed": 17,
      "insertions": 1357,
      "deletions": 9
    },
    {
      "hash": "47a0892701af3f53cd6717e2eda2e111b0698a35",
      "short_hash": "47a0892",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789228520,
      "message": "feat: SPOR-874 - Sessions - Course à pied : foulée, rapport vertical, équilibre TCS G/D\n\nSuite de SPOR-783 : ajoute les 3 champs restants de la dynamique de course\nGarmin Running Dynamics disponibles dans le FIT (record 83 vertical_ratio,\n84 stance_time_balance, 85 step_length) — nouvelles tuiles Metrics sur le\ndétail de séance, FIT uniquement, jamais persisté (recalculé à la volée\ncomme le reste de la dynamique de course).\n\nLa métrique Garmin Connect « Perte de vitesse moyenne d'impact au sol »\n(cm/s et %) reste hors périmètre : aucun champ FIT documenté ne lui\ncorrespond, vraisemblablement calculée côté serveur Garmin Connect comme\nStress pour SPOR-871.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-874 - Sessions - Course à pied : foulée, rapport vertical, équilibre TCS G/D",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Import/TrackpointData.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/FitImporterTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 55,
          "deletions": 0,
          "lines_changed": 55
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 10,
          "deletions": 2,
          "lines_changed": 12
        },
        {
          "path": "src/Domain/Import/TrackpointData.php",
          "insertions": 12,
          "deletions": 1,
          "lines_changed": 13
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 31,
          "deletions": 1,
          "lines_changed": 32
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 129,
          "deletions": 0,
          "lines_changed": 129
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 64,
          "deletions": 2,
          "lines_changed": 66
        }
      ],
      "files_changed": 12,
      "insertions": 413,
      "deletions": 6
    },
    {
      "hash": "67b9083a1f10997f26fd54c18f050c2392c3d71f",
      "short_hash": "67b9083",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789227523,
      "message": "feat: SPOR-873 - Entrainement - Cohérence cardiaque : sources MIT du suivi FC par caméra\n\nLa modale « Méthodologie & sources » citait des références par protocole de\nrespiration mais rien pour la technique de mesure du pouls par webcam\nelle-même. Ajout d'une section « Suivi par la caméra » citant les travaux du\nMIT à l'origine de la technique (Poh/McDuff/Picard 2010-2011 sur le rPPG,\nWu et al. 2012 sur l'amplification vidéo) — bibliographie neutre de langue\ndans PulseSignalAnalyzer::SOURCES, réutilisée par la modale comme\nBreathingProtocol::$sources.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-873 - Entrainement - Cohérence cardiaque : sources MIT du suivi FC par caméra",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/entrainement.md",
        "src/Domain/Health/PulseSignalAnalyzer.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/breathing_method_modal.php",
        "tests/Http/Training/BreathingMethodModalTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Health/PulseSignalAnalyzer.php",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/partials/breathing_method_modal.php",
          "insertions": 16,
          "deletions": 3,
          "lines_changed": 19
        },
        {
          "path": "tests/Http/Training/BreathingMethodModalTest.php",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        }
      ],
      "files_changed": 8,
      "insertions": 82,
      "deletions": 3
    },
    {
      "hash": "753ca807472c72ab9f5f1d84f559e46c1a41980f",
      "short_hash": "753ca80",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789227032,
      "message": "fix: SPOR-872 - Import - Fusion doublon depuis la fiche de séance restait bloquée sur le spinner\n\nrunManualMerge() (import par lot, SPOR-834) injectait le comparateur dans\n[data-merge-modal-body], attribut absent de la modale #merge-session-modal\npropre à session_detail.php (dropzone SPOR-486) : TypeError non rattrapé,\nfile d'import bloquée indéfiniment. Ajout du wrapper manquant, aligné sur\nla coquille générique de session_modals.php.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "fix: SPOR-872 - Import - Fusion doublon depuis la fiche de séance restait bloquée sur le spinner",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "templates/session_detail.php",
        "tests/Http/Sessions/BatchMergeWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "tests/Http/Sessions/BatchMergeWiringTest.php",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        }
      ],
      "files_changed": 4,
      "insertions": 41,
      "deletions": 1
    },
    {
      "hash": "641cfc5f2ed6ba49075b0984a975dc09b3d2c088",
      "short_hash": "641cfc5",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789224320,
      "message": "feat: SPOR-871 - Sessions - Yoga : fréquence respiratoire + postures nommées (FIT)\n\nSur le détail d'une séance FIT, ajoute la fréquence respiratoire\n(nouveau champ/graphe, même schéma que Température) et un tableau de\npostures pour les séances guidées sans tours GPS classiques (Yoga...) :\ndurée exacte de chaque étape d'entraînement structuré (workout_step),\navec le texte libre du fichier en guise de nom quand il existe.\n\nRecherche menée sur un fichier FIT réel avant développement : FC et\ntempérature étaient déjà à parité ; les noms de postures affichés par\nGarmin Connect viennent d'une base d'exercices propriétaire non\nprésente dans le fichier (parité totale non atteignable, partiel mais\nhonnête) ; le stress est une donnée de monitoring continu absente de\nl'export d'activité, non implémenté (nécessiterait l'API Garmin\nConnect, hors architecture import-de-fichier du projet).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-871 - Sessions - Yoga : fréquence respiratoire + postures nommées (FIT)",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/189_add_lap_name.sql",
        "docs/sessions.md",
        "public/assets/js/session-charts.js",
        "src/Domain/Import/FitImporter.php",
        "src/Domain/Import/LapData.php",
        "src/Domain/Import/TrackpointData.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionPresenter.php",
        "src/Http/Sessions/SessionsController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/FitImporterTest.php",
        "tests/Http/Sessions/SessionPresenterTest.php",
        "tests/Http/Sessions/SwimStrokeWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 120,
          "deletions": 0,
          "lines_changed": 120
        },
        {
          "path": "db/migrations/189_add_lap_name.sql",
          "insertions": 13,
          "deletions": 0,
          "lines_changed": 13
        },
        {
          "path": "docs/sessions.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "public/assets/js/session-charts.js",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "src/Domain/Import/FitImporter.php",
          "insertions": 144,
          "deletions": 8,
          "lines_changed": 152
        },
        {
          "path": "src/Domain/Import/LapData.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "src/Domain/Import/TrackpointData.php",
          "insertions": 7,
          "deletions": 1,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 27,
          "deletions": 5,
          "lines_changed": 32
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        },
        {
          "path": "src/Http/Sessions/SessionsController.php",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 60,
          "deletions": 2,
          "lines_changed": 62
        },
        {
          "path": "tests/Domain/FitImporterTest.php",
          "insertions": 142,
          "deletions": 0,
          "lines_changed": 142
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 58,
          "deletions": 2,
          "lines_changed": 60
        },
        {
          "path": "tests/Http/Sessions/SwimStrokeWiringTest.php",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        }
      ],
      "files_changed": 17,
      "insertions": 691,
      "deletions": 25
    },
    {
      "hash": "ae175f41ea460942edcd31dfc7ef7f646ed3fd1e",
      "short_hash": "ae175f4",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789217425,
      "message": "style: SPOR-870 - Courses - Calendrier étiré sur 100% de la largeur de la carte\n\nColonnes flex à largeur fixe -> grille CSS (repeat(12, minmax(34px,\n1fr))) partagée entre la rangée des mois et celle des années, pour que\nle calendrier occupe toute la largeur disponible au lieu de laisser un\nvide à droite sur un écran large. Le trait d'année (grid-column: span\nN) reste toujours aligné sur ses colonnes, sans calcul de largeur en\npixels à synchroniser entre les deux rangées.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "style: SPOR-870 - Courses - Calendrier étiré sur 100% de la largeur de la carte",
      "type": "style",
      "modified_files": [
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "templates/races.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 28,
          "deletions": 15,
          "lines_changed": 43
        },
        {
          "path": "templates/races.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        }
      ],
      "files_changed": 3,
      "insertions": 53,
      "deletions": 17
    },
    {
      "hash": "597e3e8773beb0caba38cbc923c181cb4db61997",
      "short_hash": "597e3e8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789217040,
      "message": "style: SPOR-870 - Courses - Calendrier : colonnes réduites, trait d'année, infobulle stylisée\n\nColonnes plus compactes (hauteur 280→200px). Les mois restent en\nen-tête de colonne mais l'année n'est plus répétée sur chacune : un\ntrait par année, avec l'année centrée dessus, regroupe désormais les\ncolonnes consécutives d'une même année. La bulle native du navigateur\nau survol d'une course est remplacée par un panneau stylisé (nom,\nstatut coloré, date), même famille que le tooltip du calendrier\nexistant.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "style: SPOR-870 - Courses - Calendrier : colonnes réduites, trait d'année, infobulle stylisée",
      "type": "style",
      "modified_files": [
        "TECHNICAL.md",
        "docs/objectifs.md",
        "public/assets/css/app.css",
        "public/assets/js/interactions.js",
        "src/Domain/Training/RaceForecastColumns.php",
        "src/Http/Goals/RacesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/races.php",
        "tests/Domain/RaceForecastColumnsTest.php",
        "tests/Http/Goals/RaceForecastWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 54,
          "deletions": 5,
          "lines_changed": 59
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 5,
          "deletions": 2,
          "lines_changed": 7
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 53,
          "deletions": 11,
          "lines_changed": 64
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "src/Domain/Training/RaceForecastColumns.php",
          "insertions": 40,
          "deletions": 6,
          "lines_changed": 46
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 23,
          "deletions": 5,
          "lines_changed": 28
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 0,
          "deletions": 1,
          "lines_changed": 1
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 0,
          "deletions": 1,
          "lines_changed": 1
        },
        {
          "path": "templates/races.php",
          "insertions": 41,
          "deletions": 13,
          "lines_changed": 54
        },
        {
          "path": "tests/Domain/RaceForecastColumnsTest.php",
          "insertions": 19,
          "deletions": 1,
          "lines_changed": 20
        },
        {
          "path": "tests/Http/Goals/RaceForecastWiringTest.php",
          "insertions": 38,
          "deletions": 1,
          "lines_changed": 39
        }
      ],
      "files_changed": 11,
      "insertions": 352,
      "deletions": 46
    },
    {
      "hash": "ab3b614b3dbaf7138faf0e67d31001b36cbcd20d",
      "short_hash": "ab3b614",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789215998,
      "message": "feat: SPOR-870 - Courses - Calendrier des courses en colonnes mensuelles sur /races\n\nSur /races, un calendrier en colonnes des 12 prochains mois (même\nhauteur) visualise les prochaines courses et les courses en réflexion :\nchaque course active est positionnée dans sa colonne selon sa date,\nsans précision au jour près (positionnement proportionnel continu, pas\nde grille de jours). Réutilise le statut d'inscription déjà résolu\n(RaceRegistrationStatus) et son code couleur existant (ruban de coin\ndes cards) — aucune nouvelle notion de statut ni de couleur introduite.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-870 - Courses - Calendrier des courses en colonnes mensuelles sur /races",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/objectifs.md",
        "public/assets/css/app.css",
        "src/Domain/Training/RaceForecastColumns.php",
        "src/Http/Goals/RacesController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/races.php",
        "tests/Domain/RaceForecastColumnsTest.php",
        "tests/Http/Goals/RaceForecastWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        },
        {
          "path": "docs/objectifs.md",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "src/Domain/Training/RaceForecastColumns.php",
          "insertions": 115,
          "deletions": 0,
          "lines_changed": 115
        },
        {
          "path": "src/Http/Goals/RacesController.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/races.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "tests/Domain/RaceForecastColumnsTest.php",
          "insertions": 126,
          "deletions": 0,
          "lines_changed": 126
        },
        {
          "path": "tests/Http/Goals/RaceForecastWiringTest.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        }
      ],
      "files_changed": 11,
      "insertions": 488,
      "deletions": 0
    },
    {
      "hash": "456861599e2c57c994a67bc9e398073871ecf36e",
      "short_hash": "4568615",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789207632,
      "message": "feat: SPOR-869 - Sessions - Incidents ancrés GPS affichés sur la carte du résumé\n\nDemande explicite de l'utilisateur après livraison : les incidents\npositionnés sur la mini-carte de l'onglet Incidents apparaissent aussi\nen marqueurs cliquables sur #route-map (carte de lecture seule du\nrésumé de séance), pas seulement dans la modale d'édition. Seuls les\nincidents ancrés GPS sont concernés — un incident en position\ndistance/durée pure reste sans marqueur.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-869 - Sessions - Incidents ancrés GPS affichés sur la carte du résumé",
      "type": "feat",
      "modified_files": [
        "TECHNICAL.md",
        "docs/sessions.md",
        "src/Http/Sessions/SessionDetailController.php",
        "templates/session_detail.php",
        "tests/Http/Sessions/SessionIncidentsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "docs/sessions.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 23,
          "deletions": 2,
          "lines_changed": 25
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 18,
          "deletions": 2,
          "lines_changed": 20
        },
        {
          "path": "tests/Http/Sessions/SessionIncidentsWiringTest.php",
          "insertions": 17,
          "deletions": 0,
          "lines_changed": 17
        }
      ],
      "files_changed": 5,
      "insertions": 97,
      "deletions": 5,
      "tags": [
        "v1.7.47"
      ]
    },
    {
      "hash": "fe85810971d997b65efca48ab89e74249a9447e3",
      "short_hash": "fe85810",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789174377,
      "message": "feat: SPOR-869 - Sessions - Incidents de séance : ancrage GPS + mini-carte Leaflet dédiée\n\nSur une séance avec trace GPS, une mini-carte Leaflet dans l'onglet\nIncidents permet de cliquer sur la trace pour ancrer précisément un\nincident (point le plus proche, distance pré-remplie automatiquement) ;\nles incidents déjà positionnés s'affichent en marqueurs cliquables.\nRéutilise la trace déjà exposée à #route-map (aucune nouvelle table de\ntrackpoints), premier marqueur Leaflet réellement interactif de l'app.\nCouplage avec le widget d'ajout (SPOR-868) par événement DOM, script\nJS séparé (incident-map.js) chargé seulement si la séance a une trace\nGPS. Toujours privé. Clôture l'epic SPOR-867.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-869 - Sessions - Incidents de séance : ancrage GPS + mini-carte Leaflet dédiée",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/188_add_session_incidents_gps.sql",
        "docs/sessions.md",
        "public/assets/js/incident-map.js",
        "public/assets/js/interactions.js",
        "src/Domain/Training/SessionIncident.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionIncidentsStore.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/SessionIncidentTest.php",
        "tests/Http/Sessions/SessionIncidentsStoreTest.php",
        "tests/Http/Sessions/SessionIncidentsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 2,
          "lines_changed": 4
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 78,
          "deletions": 7,
          "lines_changed": 85
        },
        {
          "path": "db/migrations/188_add_session_incidents_gps.sql",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "docs/sessions.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "public/assets/js/incident-map.js",
          "insertions": 195,
          "deletions": 0,
          "lines_changed": 195
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 25,
          "deletions": 3,
          "lines_changed": 28
        },
        {
          "path": "src/Domain/Training/SessionIncident.php",
          "insertions": 31,
          "deletions": 5,
          "lines_changed": 36
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 5,
          "deletions": 1,
          "lines_changed": 6
        },
        {
          "path": "src/Http/Sessions/SessionIncidentsStore.php",
          "insertions": 9,
          "deletions": 4,
          "lines_changed": 13
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 31,
          "deletions": 13,
          "lines_changed": 44
        },
        {
          "path": "tests/Domain/SessionIncidentTest.php",
          "insertions": 34,
          "deletions": 1,
          "lines_changed": 35
        },
        {
          "path": "tests/Http/Sessions/SessionIncidentsStoreTest.php",
          "insertions": 19,
          "deletions": 6,
          "lines_changed": 25
        },
        {
          "path": "tests/Http/Sessions/SessionIncidentsWiringTest.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        }
      ],
      "files_changed": 15,
      "insertions": 505,
      "deletions": 42
    },
    {
      "hash": "be8682ae220f73358a19910e59afa679ab5f34bf",
      "short_hash": "be8682a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789168011,
      "message": "feat: SPOR-868 - Sessions - Incidents de séance : modèle + onglet dédié + saisie manuelle\n\nNouvel onglet \"Incidents\" dans l'édition de séance : signaler un ou\nplusieurs incidents (mécanique/chute/blessure/malaise/météo/autre),\ndescription libre optionnelle, position optionnelle en distance et/ou\ndurée écoulée depuis le départ (saisie manuelle). Toujours privé,\njamais sur une vue publique/partagée. Ancrage GPS précis + mini-carte\nLeaflet dédiée prévus en story séparée (SPOR-869, epic SPOR-867).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "feat: SPOR-868 - Sessions - Incidents de séance : modèle + onglet dédié + saisie manuelle",
      "type": "feat",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/187_add_session_incidents.sql",
        "docs/sessions.md",
        "public/assets/js/interactions.js",
        "src/Domain/Training/SessionEditTabBadges.php",
        "src/Domain/Training/SessionIncident.php",
        "src/Http/Sessions/EditModalTabs.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Sessions/SessionIncidentsStore.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/session_detail.php",
        "tests/Domain/SessionEditTabBadgesTest.php",
        "tests/Domain/SessionIncidentTest.php",
        "tests/Http/Sessions/EditModalTabsTest.php",
        "tests/Http/Sessions/SessionIncidentsStoreTest.php",
        "tests/Http/Sessions/SessionIncidentsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 101,
          "deletions": 0,
          "lines_changed": 101
        },
        {
          "path": "db/migrations/187_add_session_incidents.sql",
          "insertions": 26,
          "deletions": 0,
          "lines_changed": 26
        },
        {
          "path": "docs/sessions.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 138,
          "deletions": 3,
          "lines_changed": 141
        },
        {
          "path": "src/Domain/Training/SessionEditTabBadges.php",
          "insertions": 21,
          "deletions": 4,
          "lines_changed": 25
        },
        {
          "path": "src/Domain/Training/SessionIncident.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "src/Http/Sessions/EditModalTabs.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 83,
          "deletions": 2,
          "lines_changed": 85
        },
        {
          "path": "src/Http/Sessions/SessionIncidentsStore.php",
          "insertions": 77,
          "deletions": 0,
          "lines_changed": 77
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 82,
          "deletions": 0,
          "lines_changed": 82
        },
        {
          "path": "tests/Domain/SessionEditTabBadgesTest.php",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "tests/Domain/SessionIncidentTest.php",
          "insertions": 105,
          "deletions": 0,
          "lines_changed": 105
        },
        {
          "path": "tests/Http/Sessions/EditModalTabsTest.php",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "tests/Http/Sessions/SessionIncidentsStoreTest.php",
          "insertions": 90,
          "deletions": 0,
          "lines_changed": 90
        },
        {
          "path": "tests/Http/Sessions/SessionIncidentsWiringTest.php",
          "insertions": 75,
          "deletions": 0,
          "lines_changed": 75
        }
      ],
      "files_changed": 18,
      "insertions": 971,
      "deletions": 11
    },
    {
      "hash": "2ede908809051b660612e3bc353c963ac7d1d6e0",
      "short_hash": "2ede908",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789122081,
      "message": "docs: SPOR-866 - Dev - CLAUDE.md : convention de commit, préfixe de catégorie obligatoire\n\nTout commit doit désormais commencer par un préfixe type: (feat/fix/docs/\nstyle/refactor/test/perf/ops/chore/trad) — tout premier mot de la 1ère\nligne, devant la référence SPOR-XXX. Format : `type: SPOR-XXX - Domaine -\nRésumé court`.\n\nContrainte technique vérifiée dans le code de zRenard/WebReleaseNotes\n(classify_commit()) : la classification des notes de version publiques\n(release_notes.html, SPOR-644/746) lit tout ce qui précède le PREMIER\n« : » de la 1ère ligne et compare EXACTEMENT à un des mots-clés — sans ce\npréfixe en tête, ça retombe sur une heuristique par mot-clé anglais peu\nfiable sur des messages en français.\n\nDécisions : CSS/visuel -> toujours style (même une nouveauté, écart\nassumé au sens Conventional Commits standard) ; type dominant = nature du\nchangement livré (feat pour une story même si tests/doc inclus dans le\nmême commit, jamais de découpage des commits) ; trad ajouté pour les\ntraductions i18n seules (extension propre à ce projet, pas encore\nreconnue par WebReleaseNotes — adoptée dès maintenant quand même).\n\nCe commit applique lui-même la nouvelle convention (docs:, puisqu'il ne\ntouche que CLAUDE.md + la mindmap).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "docs: SPOR-866 - Dev - CLAUDE.md : convention de commit, préfixe de catégorie obligatoire",
      "type": "docs",
      "modified_files": [
        "CLAUDE.md",
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "CLAUDE.md",
          "insertions": 46,
          "deletions": 3,
          "lines_changed": 49
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        }
      ],
      "files_changed": 2,
      "insertions": 47,
      "deletions": 3,
      "tags": [
        "v1.7.46"
      ]
    },
    {
      "hash": "6053911e6d361491aa06f2ce987bccd3cde3a61f",
      "short_hash": "6053911",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789107114,
      "message": "SPOR-865 - Sessions - Graphes FC/Allure : résolution insuffisante pour un intervalle ~10 s\n\nRetour utilisateur : sur une séance de fractionné (sprints ~10 s), les\ncourbes FC et Allure du résumé étaient trop lissées pour évaluer la\nvitesse d'un intervalle ou la récupération FC juste après.\n\nCause : SessionPresenter::sample{HeartRates,Pace,Elevation,Power,Cadence,\nTemperature,VerticalOscillation,GroundContactTime}() décimaient TOUJOURS\nvers un plafond fixe de 80 points, quelle que soit la durée de la séance\n(hérité de SPOR-312). Pour un fractionné de 40 min à 1 Hz, le pas de\ndécimation dépassait largement 10 s : un sprint pouvait tomber entre deux\npoints échantillonnés et disparaître complètement du graphe.\n\nCorrectif : DEFAULT_MAX_SAMPLES 80 -> 1500 (constante privée partagée par\nles 8 fonctions). Quasiment aucune décimation en pratique pour une séance\ncourte/moyenne ; seules les séances très longues (ultra) restent\ndécimées, à une résolution bien plus fine qu'avant. PACE_WINDOW_SECONDS\n(fenêtre anti-bruit GPS, 10 s) volontairement inchangé — hors périmètre,\ndocumenté dans le ticket.\n\nVérifié sur la vraie séance référencée (/sessions/10277, 2026-09-10) :\n1242 points FC (contre 80) et 365 fenêtres d'allure (contre 80) — chaque\nintervalle et sa récupération sont maintenant visibles.\n\nTests : nouveaux cas SessionPresenterTest (FC/allure sous le plafond =\naucune décimation, très longue séance = décimation toujours active).\nDoc utilisateur docs/sessions.md, TECHNICAL.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-865 - Sessions - Graphes FC/Allure : résolution insuffisante pour un intervalle ~10 s",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/sessions.md",
        "src/Http/Sessions/SessionPresenter.php",
        "tests/Http/Sessions/SessionPresenterTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "docs/sessions.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Http/Sessions/SessionPresenter.php",
          "insertions": 23,
          "deletions": 8,
          "lines_changed": 31
        },
        {
          "path": "tests/Http/Sessions/SessionPresenterTest.php",
          "insertions": 70,
          "deletions": 0,
          "lines_changed": 70
        }
      ],
      "files_changed": 5,
      "insertions": 143,
      "deletions": 8,
      "tags": [
        "v1.7.45"
      ]
    },
    {
      "hash": "d03970c40e8cc568a30acd02fc7e3965a650a2a0",
      "short_hash": "d03970c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789079797,
      "message": "SPOR-864 - Entrainement - Séance personnalisée : blocs libres (libellé + durée)\n\nL'éditeur de séance personnalisée propose désormais deux natures de bloc :\n« respiration » (un rythme répété, inchangé) et « libre » — un simple\npalier de temps avec le libellé de votre choix (« Préparation » 30 s,\n« Retour au calme » 1 min). Pendant un bloc libre : le libellé + le\ndécompte à l'écran, visualiseur au repos, aucun son.\n\n- Domaine BreathingBlock (pur) : deux fabriques pattern()/free(), bornes\n  10 s-30 min, libellé libre 40 car. max ; timeline() d'un bloc libre =\n  un seul pas segment/phase = « free ».\n- BreathingWorkout::blocks -> list<BreathingBlock> ; sérialisation avec\n  une clé « type » (absente => pattern : rétrocompat des séances SPOR-861).\n  firstPatternCycleSeconds() pour la période du score de cohérence.\n- BreathingWorkoutInput : chaque bloc porte « type » (pattern|free).\n- Éditeur : 2e <template> + bouton « Bloc libre » ; breathing-workout-editor.js\n  addFreeBlock(), total (durée libre comptée telle quelle), fill par type.\n- breathing-guide.js : rendu d'un bloc libre. BreathingCoherencePresenter :\n  bande neutre « free » dans la courbe de cohérence.\n- i18n FR/EN, docs, TECHNICAL.md, mindmap. Tests : BreathingBlockTest +\n  maj Workout/Input/Store/Editor/Coherence.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-864 - Entrainement - Séance personnalisée : blocs libres (libellé + durée)",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "docs/entrainement.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "public/assets/js/breathing-workout-editor.js",
        "src/Domain/Training/Breathing/BreathingBlock.php",
        "src/Domain/Training/Breathing/BreathingWorkout.php",
        "src/Domain/Training/Breathing/BreathingWorkoutInput.php",
        "src/Http/Training/BreathingCoherencePresenter.php",
        "src/Http/Training/BreathingController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/breathing_workout_editor.php",
        "tests/Domain/Training/Breathing/BreathingBlockTest.php",
        "tests/Domain/Training/Breathing/BreathingWorkoutInputTest.php",
        "tests/Domain/Training/Breathing/BreathingWorkoutTest.php",
        "tests/Http/Training/BreathingCoherenceWiringTest.php",
        "tests/Http/Training/BreathingWorkoutEditorWiringTest.php",
        "tests/Http/Training/BreathingWorkoutStoreTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 41,
          "deletions": 20,
          "lines_changed": 61
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 15,
          "deletions": 10,
          "lines_changed": 25
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        },
        {
          "path": "public/assets/js/breathing-workout-editor.js",
          "insertions": 27,
          "deletions": 5,
          "lines_changed": 32
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingBlock.php",
          "insertions": 180,
          "deletions": 0,
          "lines_changed": 180
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingWorkout.php",
          "insertions": 35,
          "deletions": 77,
          "lines_changed": 112
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingWorkoutInput.php",
          "insertions": 32,
          "deletions": 27,
          "lines_changed": 59
        },
        {
          "path": "src/Http/Training/BreathingCoherencePresenter.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 37,
          "deletions": 27,
          "lines_changed": 64
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 10,
          "deletions": 3,
          "lines_changed": 13
        },
        {
          "path": "templates/partials/breathing_workout_editor.php",
          "insertions": 44,
          "deletions": 6,
          "lines_changed": 50
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingBlockTest.php",
          "insertions": 86,
          "deletions": 0,
          "lines_changed": 86
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingWorkoutInputTest.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingWorkoutTest.php",
          "insertions": 71,
          "deletions": 8,
          "lines_changed": 79
        },
        {
          "path": "tests/Http/Training/BreathingCoherenceWiringTest.php",
          "insertions": 24,
          "deletions": 0,
          "lines_changed": 24
        },
        {
          "path": "tests/Http/Training/BreathingWorkoutEditorWiringTest.php",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "tests/Http/Training/BreathingWorkoutStoreTest.php",
          "insertions": 7,
          "deletions": 3,
          "lines_changed": 10
        }
      ],
      "files_changed": 21,
      "insertions": 712,
      "deletions": 192,
      "tags": [
        "v1.7.44"
      ]
    },
    {
      "hash": "46ce34747f413447673da0645afb6dc5bb88b0b2",
      "short_hash": "46ce347",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789075717,
      "message": "SPOR-863 - Entrainement - Cohérence cardiaque : FC webcam au cours du temps + courbe dédiée sur la séance\n\nProlonge SPOR-862 : on n'historise plus seulement la FC moyenne, mais la\nsérie [bpm, t] complète + la chronologie des phases (mise en place /\ninspiration / rétention / expiration / retour au calme). Le détail de la\nséance affiche une carte « Cohérence cardiaque » : courbe de la FC estimée\nà la caméra dans le temps, sur fond de bandes de phase, avec le score.\n\n- Domaine BreathingPulseTrace (pur) : bornage 30-240, instants croissants,\n  cap 4000 pts, min 10 ; score délégué à CoherenceScore.\n- Migration 186 : table dédiée session_breathing_pulse (1 ligne/séance,\n  session_id PK + FK ON DELETE CASCADE → purge SPOR-694 transitive).\n- BreathingPulseStore (PDO, SQLite-testable) ; BreathingCoherencePresenter.\n- complete() : construit la timeline + la période, persiste la trace ;\n  FC moyenne dérivée de la série quand elle existe.\n- SessionDetailController : trace chargée uniquement si sport = breathing.\n- breathing-coherence-chart.js : canvas brut, ES5, vendorisé, 0 réseau.\n- Traitement toujours 100 % client pendant la séance ; la série part dans\n  le champ caché hr_samples du formulaire d'historisation, comme avg_hr.\n  Donnée santé privée, jamais publique (comme une FC importée).\n- i18n breathing.coherence.* FR/EN, docs, TECHNICAL.md, mindmap.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-863 - Entrainement - Cohérence cardiaque : FC webcam au cours du temps + courbe dédiée sur la s",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/186_add_session_breathing_pulse.sql",
        "docs/entrainement.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-coherence-chart.js",
        "public/assets/js/breathing-pulse.js",
        "src/Domain/Training/Breathing/BreathingPulseTrace.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Http/Training/BreathingCoherencePresenter.php",
        "src/Http/Training/BreathingController.php",
        "src/Http/Training/BreathingPulseStore.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/breathing.php",
        "templates/session_detail.php",
        "tests/Domain/Training/Breathing/BreathingPulseTraceTest.php",
        "tests/Http/Training/BreathingCoherenceWiringTest.php",
        "tests/Http/Training/BreathingCompleteWiringTest.php",
        "tests/Http/Training/BreathingPulseStoreTest.php",
        "tests/Http/Training/BreathingPulseWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 48,
          "deletions": 0,
          "lines_changed": 48
        },
        {
          "path": "db/migrations/186_add_session_breathing_pulse.sql",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 11,
          "deletions": 6,
          "lines_changed": 17
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "public/assets/js/breathing-coherence-chart.js",
          "insertions": 148,
          "deletions": 0,
          "lines_changed": 148
        },
        {
          "path": "public/assets/js/breathing-pulse.js",
          "insertions": 27,
          "deletions": 8,
          "lines_changed": 35
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingPulseTrace.php",
          "insertions": 235,
          "deletions": 0,
          "lines_changed": 235
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "src/Http/Training/BreathingCoherencePresenter.php",
          "insertions": 116,
          "deletions": 0,
          "lines_changed": 116
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 59,
          "deletions": 10,
          "lines_changed": 69
        },
        {
          "path": "src/Http/Training/BreathingPulseStore.php",
          "insertions": 40,
          "deletions": 0,
          "lines_changed": 40
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 11,
          "deletions": 1,
          "lines_changed": 12
        },
        {
          "path": "templates/breathing.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 30,
          "deletions": 0,
          "lines_changed": 30
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingPulseTraceTest.php",
          "insertions": 148,
          "deletions": 0,
          "lines_changed": 148
        },
        {
          "path": "tests/Http/Training/BreathingCoherenceWiringTest.php",
          "insertions": 123,
          "deletions": 0,
          "lines_changed": 123
        },
        {
          "path": "tests/Http/Training/BreathingCompleteWiringTest.php",
          "insertions": 8,
          "deletions": 1,
          "lines_changed": 9
        },
        {
          "path": "tests/Http/Training/BreathingPulseStoreTest.php",
          "insertions": 85,
          "deletions": 0,
          "lines_changed": 85
        },
        {
          "path": "tests/Http/Training/BreathingPulseWiringTest.php",
          "insertions": 10,
          "deletions": 5,
          "lines_changed": 15
        }
      ],
      "files_changed": 22,
      "insertions": 1181,
      "deletions": 32,
      "tags": [
        "v1.7.43"
      ]
    },
    {
      "hash": "a4f7c74be5a7ec5778d81d7be0476982b3dbb146",
      "short_hash": "a4f7c74",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789074318,
      "message": "SPOR-862 - Entrainement - Cohérence cardiaque : enregistrer la FC moyenne webcam dans la séance\n\nLe suivi rPPG restait purement client : la FC estimée à la caméra\nn'apparaissait pas sur la séance créée. breathing-pulse.js écrit désormais\nla FC MOYENNE de la série dans un champ caché `avg_hr` du formulaire\nd'historisation ; complete() la borne 30-240 (filtre capteur CLAUDE.md),\nla passe à SessionLoadCalculator et l'insère dans sessions.avg_hr.\n\nAucun signal instantané n'est transmis (pas de fetch/XHR/beacon dans le\nJS) — seule la valeur agrégée, donnée privée comme la FC d'un import.\n\nConsentement, doc utilisateur, TECHNICAL.md et le commentaire de la\nmigration 185 mis à jour en conséquence.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-862 - Entrainement - Cohérence cardiaque : enregistrer la FC moyenne webcam dans la séance",
      "type": "other",
      "modified_files": [
        "TECHNICAL.md",
        "db/migrations/185_add_user_breathing_camera_pref.sql",
        "docs/entrainement.md",
        "public/assets/js/breathing-pulse.js",
        "src/Http/Training/BreathingController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/breathing.php",
        "tests/Http/Training/BreathingCompleteWiringTest.php",
        "tests/Http/Training/BreathingPulseWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "TECHNICAL.md",
          "insertions": 16,
          "deletions": 7,
          "lines_changed": 23
        },
        {
          "path": "db/migrations/185_add_user_breathing_camera_pref.sql",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "public/assets/js/breathing-pulse.js",
          "insertions": 12,
          "deletions": 0,
          "lines_changed": 12
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 15,
          "deletions": 2,
          "lines_changed": 17
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/breathing.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "tests/Http/Training/BreathingCompleteWiringTest.php",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        },
        {
          "path": "tests/Http/Training/BreathingPulseWiringTest.php",
          "insertions": 11,
          "deletions": 3,
          "lines_changed": 14
        }
      ],
      "files_changed": 10,
      "insertions": 75,
      "deletions": 24
    },
    {
      "hash": "88d0ff91f688fe671d287f250a9d3a54de5d3043",
      "short_hash": "88d0ff9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789072828,
      "message": "SPOR-862 - Entrainement - Cohérence cardiaque : captation cardiaque webcam pendant la séance\n\nÉpic SPOR-853, phase 2. Vie privée (choix utilisateur) : 100 % client, rien\nn'est jamais transmis au serveur.\n\n- Domaine Domain\\Health\\CoherenceScore (pur, testable) : série [bpm, t] +\n  période respiratoire → détrend + ajustement d'une sinusoïde à la fréquence\n  respiratoire, score = amplitude / (amplitude + RMS résiduel) ×100\n  (amplitude de l'arythmie sinusale respiratoire, Lehrer & Gevirtz 2014).\n- Migration 185 : users.breathing_camera TINYINT (pré-cochage de la case\n  UNIQUEMENT). Purge SPOR-694.\n- Controller : savePrefs/loadPrefs/buildSeed portent la préférence + le bloc\n  pulse de textes i18n. complete() inchangé (RPE 2, la FC webcam n'y entre\n  pas).\n- Gabarit : case opt-in + consentement, #breathe-pulse (<video> + FC live),\n  #breathe-pulse-result (score après la séance).\n- JS breathing-pulse.js (ES5, vendorisé) : écoute breathe:start/finish/stop,\n  getUserMedia, même algo signal→BPM que PulseSignalAnalyzer, même calcul de\n  score que CoherenceScore, coupe la caméra à la fin / pagehide. Aucun\n  fetch/XHR/beacon/submit. breathing-guide.js finish() émet breathe:finish\n  avant l'historisation et retarde la redirection de 7 s si un score est\n  affiché.\n- i18n breathing.pulse.* (FR/EN). docs/entrainement.md. TECHNICAL.md,\n  mindmap 🟢.\n- Tests : CoherenceScoreTest, BreathingPulseWiringTest (contrat + garde-fous\n  vie privée).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-862 - Entrainement - Cohérence cardiaque : captation cardiaque webcam pendant la séance",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/185_add_user_breathing_camera_pref.sql",
        "docs/entrainement.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "public/assets/js/breathing-pulse.js",
        "src/Domain/Health/CoherenceScore.php",
        "src/Http/Training/BreathingController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/breathing.php",
        "tests/Domain/Health/CoherenceScoreTest.php",
        "tests/Http/Training/BreathingPulseWiringTest.php",
        "tests/Http/Training/BreathingWorkoutEditorWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 41,
          "deletions": 0,
          "lines_changed": 41
        },
        {
          "path": "db/migrations/185_add_user_breathing_camera_pref.sql",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 16,
          "deletions": 0,
          "lines_changed": 16
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 16,
          "deletions": 4,
          "lines_changed": 20
        },
        {
          "path": "public/assets/js/breathing-pulse.js",
          "insertions": 248,
          "deletions": 0,
          "lines_changed": 248
        },
        {
          "path": "src/Domain/Health/CoherenceScore.php",
          "insertions": 138,
          "deletions": 0,
          "lines_changed": 138
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 18,
          "deletions": 5,
          "lines_changed": 23
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "templates/breathing.php",
          "insertions": 25,
          "deletions": 1,
          "lines_changed": 26
        },
        {
          "path": "tests/Domain/Health/CoherenceScoreTest.php",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        },
        {
          "path": "tests/Http/Training/BreathingPulseWiringTest.php",
          "insertions": 103,
          "deletions": 0,
          "lines_changed": 103
        },
        {
          "path": "tests/Http/Training/BreathingWorkoutEditorWiringTest.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        }
      ],
      "files_changed": 16,
      "insertions": 773,
      "deletions": 12
    },
    {
      "hash": "175fd3cc5803953e27f4dfc0723281fd1f298f4f",
      "short_hash": "175fd3c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789072172,
      "message": "SPOR-854 - Entrainement - Sidebar : groupe « Entraînement » déplacé après « Mensurations »\n\nAjustement de placement : le groupe « Entraînement » (Cohérence cardiaque)\npasse de juste-après-Sessions à juste-après-Mensurations, dans la zone\nbien-être / santé de la barre latérale.",
      "message_short": "SPOR-854 - Entrainement - Sidebar : groupe « Entraînement » déplacé après « Mensurations »",
      "type": "other",
      "modified_files": [
        "templates/partials/app_sidebar.php"
      ],
      "modified_files_details": [
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 6,
          "deletions": 4,
          "lines_changed": 10
        }
      ],
      "files_changed": 1,
      "insertions": 6,
      "deletions": 4
    },
    {
      "hash": "8cfaf3a12b5992731b7cac2309bc58dec3283fbd",
      "short_hash": "8cfaf3a",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789072166,
      "message": "SPOR-861 - Entrainement - Cohérence cardiaque : éditeur de séance personnalisée multi-phases\n\nÉpic SPOR-853, phase 2.\n\nDécision utilisateur : la séance perso est ENTIÈREMENT construite par\nl'utilisateur — aucun 30 s de mise en place / retour au calme auto.\n\n- Domaine : BreathingPattern::fromBox() (+ secondsOf), BreathingWorkout\n  (blocs = pattern + durée ; 1..12 blocs ; timeline, toArray/fromArray),\n  BreathingWorkoutInput (validation formulaire).\n- Migration 184 : user_breathing_workouts (spec_json JSON, FK ON DELETE\n  CASCADE pour la purge SPOR-694). BreathingWorkoutStore (SQLite-testable :\n  forUser/find/count/save[plafond 20]/delete scopé propriétaire).\n- Routes POST /training/breathing/workouts[/{id}/delete] → saveWorkout /\n  deleteWorkout (CSRF, flash). show() injecte les séances perso dans le seed\n  comme des « protocoles » workout-<id> — breathing-guide.js les rejoue sans\n  distinction. complete() accepte workout-<id>.\n- buildSeed() refactoré : serializeTimeline() partagé (cycleOffset par cycle\n  = couple blockIndex/cycleIndex). BreathingSession::timeline() porte\n  cycleSeconds.\n- Gabarit : partials/breathing_workout_editor.php (section « Mes séances » +\n  éditeur <form> + <template> de bloc + modale de suppression par séance,\n  jamais window.confirm). Chips perso rv-chip--custom.\n- JS : breathing-workout-editor.js (ES5, vendorisé) — clone du template,\n  renumérotation blocks[i][…], monter/descendre/retirer, durée totale live.\n- i18n breathing.workout.* + breathing.block_of (FR/EN). docs/entrainement.md.\n  TECHNICAL.md, mindmap 🟢.\n- Tests : BreathingWorkout{,Input}Test, BreathingWorkoutStoreTest (SQLite),\n  BreathingWorkoutEditorWiringTest ; assertions existantes recalées.\n  Flux create/play/delete vérifié end-to-end + éditeur au navigateur headless.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-861 - Entrainement - Cohérence cardiaque : éditeur de séance personnalisée multi-phases",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/184_add_user_breathing_workouts.sql",
        "docs/entrainement.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "public/assets/js/breathing-workout-editor.js",
        "src/Domain/Training/Breathing/BreathingPattern.php",
        "src/Domain/Training/Breathing/BreathingSession.php",
        "src/Domain/Training/Breathing/BreathingWorkout.php",
        "src/Domain/Training/Breathing/BreathingWorkoutInput.php",
        "src/Http/Training/BreathingController.php",
        "src/Http/Training/BreathingWorkoutStore.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/breathing.php",
        "templates/partials/breathing_workout_editor.php",
        "tests/Domain/Training/Breathing/BreathingPatternTest.php",
        "tests/Domain/Training/Breathing/BreathingWorkoutInputTest.php",
        "tests/Domain/Training/Breathing/BreathingWorkoutTest.php",
        "tests/Http/Training/BreathingCompleteWiringTest.php",
        "tests/Http/Training/BreathingGuideWiringTest.php",
        "tests/Http/Training/BreathingMethodModalTest.php",
        "tests/Http/Training/BreathingPageWiringTest.php",
        "tests/Http/Training/BreathingPrefsWiringTest.php",
        "tests/Http/Training/BreathingWorkoutEditorWiringTest.php",
        "tests/Http/Training/BreathingWorkoutStoreTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 52,
          "deletions": 0,
          "lines_changed": 52
        },
        {
          "path": "config/routes.php",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "db/migrations/184_add_user_breathing_workouts.sql",
          "insertions": 21,
          "deletions": 0,
          "lines_changed": 21
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 51,
          "deletions": 0,
          "lines_changed": 51
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 12,
          "deletions": 4,
          "lines_changed": 16
        },
        {
          "path": "public/assets/js/breathing-workout-editor.js",
          "insertions": 145,
          "deletions": 0,
          "lines_changed": 145
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingPattern.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingSession.php",
          "insertions": 21,
          "deletions": 16,
          "lines_changed": 37
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingWorkout.php",
          "insertions": 175,
          "deletions": 0,
          "lines_changed": 175
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingWorkoutInput.php",
          "insertions": 91,
          "deletions": 0,
          "lines_changed": 91
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 210,
          "deletions": 46,
          "lines_changed": 256
        },
        {
          "path": "src/Http/Training/BreathingWorkoutStore.php",
          "insertions": 103,
          "deletions": 0,
          "lines_changed": 103
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 39,
          "deletions": 0,
          "lines_changed": 39
        },
        {
          "path": "templates/breathing.php",
          "insertions": 5,
          "deletions": 3,
          "lines_changed": 8
        },
        {
          "path": "templates/partials/breathing_workout_editor.php",
          "insertions": 123,
          "deletions": 0,
          "lines_changed": 123
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingPatternTest.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingWorkoutInputTest.php",
          "insertions": 87,
          "deletions": 0,
          "lines_changed": 87
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingWorkoutTest.php",
          "insertions": 89,
          "deletions": 0,
          "lines_changed": 89
        },
        {
          "path": "tests/Http/Training/BreathingCompleteWiringTest.php",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        },
        {
          "path": "tests/Http/Training/BreathingMethodModalTest.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "tests/Http/Training/BreathingPageWiringTest.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "tests/Http/Training/BreathingPrefsWiringTest.php",
          "insertions": 3,
          "deletions": 2,
          "lines_changed": 5
        },
        {
          "path": "tests/Http/Training/BreathingWorkoutEditorWiringTest.php",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        },
        {
          "path": "tests/Http/Training/BreathingWorkoutStoreTest.php",
          "insertions": 120,
          "deletions": 0,
          "lines_changed": 120
        }
      ],
      "files_changed": 29,
      "insertions": 1639,
      "deletions": 79
    },
    {
      "hash": "92c0ffad71391c7991c9895f3038fa9e06f766a1",
      "short_hash": "92c0ffa",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789068078,
      "message": "SPOR-857 - Entrainement - Fleur : pétales un par un + consigne au-dessus de l'animation\n\nRetours utilisateur :\n- « place le \"retenez\" etc. au-dessus de l'animation » : .rv-breathe__readout\n  sort du stage, devient un bloc centré au-dessus du visualiseur (min-height\n  fixe pour éviter le saut de mise en page). Plus de texte en surimpression.\n- « les pétales ne s'affichent pas l'un après l'autre, idem à la fermeture » :\n  chaque pétale a un --petal-order (séquence équilibrée [0,4,2,6,1,5,3,7] :\n  cardinaux d'abord, puis diagonaux). --rv-petal = clamp() sur --phase-progress\n  décalé de son rang → les pétales apparaissent en séquence à l'inspiration,\n  disparaissent en séquence inverse à l'expiration, sans que la fleur penche.\n\nCSS × 2, gabarit, TECHNICAL.md, tests. Séquence vérifiée au navigateur\nheadless (croix cardinale puis diagonales).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-857 - Entrainement - Fleur : pétales un par un + consigne au-dessus de l'animation",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "templates/breathing.php",
        "tests/Http/Training/BreathingGuideWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 22,
          "deletions": 10,
          "lines_changed": 32
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 10,
          "deletions": 6,
          "lines_changed": 16
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 22,
          "deletions": 10,
          "lines_changed": 32
        },
        {
          "path": "templates/breathing.php",
          "insertions": 13,
          "deletions": 7,
          "lines_changed": 20
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 9,
          "deletions": 7,
          "lines_changed": 16
        }
      ],
      "files_changed": 5,
      "insertions": 76,
      "deletions": 40
    },
    {
      "hash": "2c6892e495a1c9f2cbe3299476b8e4d9e95e0d0e",
      "short_hash": "2c6892e",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789067625,
      "message": "SPOR-857 SPOR-858 - Entrainement - Fleur/carré : centrage + ouverture symétrique des pétales\n\nRetour utilisateur : « rien n'est centré sur l'animation fleur ».\n\n- CENTRAGE : les transformations SVG (halo, bloom, square-breath, pétales)\n  passent de transform-box: view-box à fill-box — rapportées au centre de la\n  forme. Avec view-box, la fleur se décalait dès que --bloom < 1 (elle\n  rétrécit hors-centre).\n- PÉTALES : suppression de l'échelonnement rotatif par --petal-index (fleur\n  lopsided pendant l'ouverture/fermeture). Les 8 pétales bougent maintenant\n  ENSEMBLE, --rv-petal suivant --phase-progress → ouverture/fermeture\n  symétrique et progressive pendant la phase, fleur toujours centrée.\n- Pulsation des pétales à la rétention pleine (rv-breathe-petal-pulse).\n- --petal-index retiré du gabarit (mort). CSS × 2, TECHNICAL.md, tests.\n- Centrage + symétrie vérifiés au navigateur headless (fleur et carré, à\n  plusieurs valeurs de --bloom, point de centre superposé).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-857 SPOR-858 - Entrainement - Fleur/carré : centrage + ouverture symétrique des pétales",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "templates/breathing.php",
        "tests/Http/Training/BreathingGuideWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 22,
          "deletions": 19,
          "lines_changed": 41
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 14,
          "deletions": 8,
          "lines_changed": 22
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 22,
          "deletions": 19,
          "lines_changed": 41
        },
        {
          "path": "templates/breathing.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 13,
          "deletions": 9,
          "lines_changed": 22
        }
      ],
      "files_changed": 5,
      "insertions": 72,
      "deletions": 56
    },
    {
      "hash": "759c3f9c17c8becddf461c6df27047a4988e9109",
      "short_hash": "759c3f9",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789066781,
      "message": "SPOR-858 - Entrainement - Carré : tracé depuis le coin haut-gauche, retrait du carré de fond\n\nRetour utilisateur : le tracé doit partir du coin haut-gauche ; le carré de\nfond en bas à droite n'est pas utile.\n\n- Retrait de .rv-breathe__square-fill (le carré de fond intérieur).\n- Le tracé .rv-breathe__square-sweep se dessine maintenant depuis le début\n  du path du <rect> = coin haut-gauche, sens horaire (stroke-dashoffset: 0),\n  sa longueur = --cycle-progress (part du cycle courant écoulée, 0→1, repart\n  du coin à chaque cycle). Remplace la logique --sweep-start/--sweep-len.\n- Le contour respire via .rv-breathe__square-breath (scale léger sur --bloom).\n- Pulsation du glow du tracé aux rétentions (@keyframes rv-breathe-glow).\n- breathing-guide.js : --cycle-progress au lieu de --sweep-*. CSS × 2,\n  TECHNICAL.md, tests recalés.\n- Rendu vérifié au navigateur headless (tracé qui part du coin, 1 tour/cycle).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-858 - Entrainement - Carré : tracé depuis le coin haut-gauche, retrait du carré de fond",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "templates/breathing.php",
        "tests/Http/Training/BreathingPrefsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 19,
          "deletions": 14,
          "lines_changed": 33
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 13,
          "deletions": 7,
          "lines_changed": 20
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 19,
          "deletions": 14,
          "lines_changed": 33
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 7,
          "deletions": 6,
          "lines_changed": 13
        },
        {
          "path": "templates/breathing.php",
          "insertions": 8,
          "deletions": 4,
          "lines_changed": 12
        },
        {
          "path": "tests/Http/Training/BreathingPrefsWiringTest.php",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        }
      ],
      "files_changed": 6,
      "insertions": 70,
      "deletions": 49
    },
    {
      "hash": "d42b2f3e04b03254cf4a2af03eeb311d82664228",
      "short_hash": "d42b2f3",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789065228,
      "message": "SPOR-857 SPOR-858 - Entrainement - Fleur + carré : corriger l'absence d'animation\n\nRetour utilisateur (captures) : aucune animation ni sur la fleur (un seul\npétale visible), ni sur le carré (contour statique seul).\n\nDeux bugs :\n1. Fleur — le CSS posait `transform: scale()` directement sur `.rv-breathe__petal`,\n   ce qui ÉCRASE l'attribut `transform=\"rotate(N*45)\"` du pétale → les 8\n   pétales se superposaient (un seul visible). Fix : chaque pétale est\n   maintenant `<g transform=\"rotate()\"><path></g>` ; le CSS n'anime que le\n   `<path>` (scale depuis sa base + opacité via --rv-petal).\n2. Carré — `fill/stroke: url(#rv-breathe-grad)` référençait un dégradé défini\n   dans un AUTRE <svg> (la fleur) → invisible (un url(#id) ne traverse pas\n   deux documents SVG). Fix : fleur et carré dans un seul <svg\n   class=\"rv-breathe__viz\"> avec <defs> partagés.\n\n- Toutes les transformations SVG explicitement `transform-box: view-box;\n  transform-origin: center`. Ouverture/fermeture des pétales échelonnée,\n  balayage du carré, respiration du fond : inchangés côté logique.\n- CSS répercuté dans les deux app.css. breathing-guide.js : visualFor()\n  inhale_top. TECHNICAL.md + tests recalés.\n- Rendu vérifié au navigateur headless (fleur 8 pétales fannées, carré avec\n  balayage dégradé + fond respirant).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-857 SPOR-858 - Entrainement - Fleur + carré : corriger l'absence d'animation",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "templates/breathing.php",
        "tests/Http/Training/BreathingGuideWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 47,
          "deletions": 39,
          "lines_changed": 86
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 10,
          "deletions": 5,
          "lines_changed": 15
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 47,
          "deletions": 39,
          "lines_changed": 86
        },
        {
          "path": "templates/breathing.php",
          "insertions": 23,
          "deletions": 16,
          "lines_changed": 39
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 8,
          "deletions": 6,
          "lines_changed": 14
        }
      ],
      "files_changed": 5,
      "insertions": 135,
      "deletions": 105
    },
    {
      "hash": "a124a7483a82782c3759f3a62f4d438cd8ce0209",
      "short_hash": "a124a74",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789063881,
      "message": "SPOR-857 - Entrainement - Fleur : ouverture/fermeture progressive des pétales pendant la phase\n\nRetour utilisateur : les pétales doivent s'ouvrir AU FIL de l'inspiration\n(et se fermer au fil de l'expiration), pas basculer en fin de phase.\n\n- app.css (× 2) : chaque pétale a un seuil échelonné (--petal-index) ;\n  l'ouverture est calculée en calc()/clamp() à partir de --phase-progress\n  (posé à chaque frame par breathing-guide.js). À l'expiration, fermeture\n  dans l'ordre inverse (flétrissement). prefers-reduced-motion : fondu\n  d'opacité seul, pas de transform.\n- breathing-guide.js : visualFor() — 2e inspiration du soupir physiologique\n  (inhale_top) reste au maximum, pas de ré-ouverture.\n- BreathingGuideWiringTest : assertions sur l'échelonnement. TECHNICAL.md.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-857 - Entrainement - Fleur : ouverture/fermeture progressive des pétales pendant la phase",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "tests/Http/Training/BreathingGuideWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 21,
          "deletions": 4,
          "lines_changed": 25
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 8,
          "deletions": 5,
          "lines_changed": 13
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 21,
          "deletions": 4,
          "lines_changed": 25
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        }
      ],
      "files_changed": 5,
      "insertions": 62,
      "deletions": 13
    },
    {
      "hash": "0a1fb5c7d88ad1c008e5eb425f2cd62a151b24ed",
      "short_hash": "0a1fb5c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789062935,
      "message": "SPOR-860 - Entrainement - Cohérence cardiaque : modale « Méthodologie & sources scientifiques »\n\nÉpic SPOR-853, phase 1.\n\n- templates/partials/breathing_method_modal.php : .rv-modal-overlay\n  #breathing-method (vraie modale, patron #logout-modal). Principe (RSA,\n  0,1 Hz, box breathing), paragraphe guidage audio (Noble & Hochman /\n  Bernardi / Alvarsson ; voix-musique absentes), une entrée par protocole\n  avec résumé + mise en garde + sources (BreathingProtocol::sources, liens\n  DOI rel=noopener), précautions générales.\n- templates/breathing.php : bouton « Méthodologie & sources » +\n  $scripts = ['interactions.js', 'breathing-guide.js'] (la modale a besoin\n  d'interactions.js).\n- CSS .rv-breathe-method* dans les deux app.css. TECHNICAL.md, mindmap 🟢.\n- Tests : BreathingMethodModalTest ; assertion SPOR-857 recalée.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-860 - Entrainement - Cohérence cardiaque : modale « Méthodologie & sources scientifiques »",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "templates/breathing.php",
        "templates/partials/breathing_method_modal.php",
        "tests/Http/Training/BreathingGuideWiringTest.php",
        "tests/Http/Training/BreathingMethodModalTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 21,
          "deletions": 2,
          "lines_changed": 23
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 11,
          "deletions": 0,
          "lines_changed": 11
        },
        {
          "path": "templates/breathing.php",
          "insertions": 9,
          "deletions": 1,
          "lines_changed": 10
        },
        {
          "path": "templates/partials/breathing_method_modal.php",
          "insertions": 66,
          "deletions": 0,
          "lines_changed": 66
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 4,
          "deletions": 4,
          "lines_changed": 8
        },
        {
          "path": "tests/Http/Training/BreathingMethodModalTest.php",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        }
      ],
      "files_changed": 8,
      "insertions": 207,
      "deletions": 8
    },
    {
      "hash": "7dca684fb3253c8498c86ccc73cfca1d2e5f78a2",
      "short_hash": "7dca684",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789062552,
      "message": "SPOR-859 - Entrainement - Cohérence cardiaque : historisation en séance « Respiration »\n\nÉpic SPOR-853, phase 1.\n\n- Migration 183 : discipline `breathing` dans le catalogue `sports`\n  (is_builtin 1, coeff 0.30, ordre 9). Sport::BUILTIN_CODES RESTE à 8 : la\n  discipline n'entre pas dans onboarding/gear/badges/objectifs, seulement\n  dans séances/calendrier/tendances/charge.\n- BreathingController::complete() (POST /training/breathing/complete, CSRF) :\n  protocole revalidé, durée bornée 60–3600 s (repli, jamais de rejet dur),\n  RPE fixe 2/10, charge via SessionLoadCalculator (partagé avec la saisie\n  manuelle), started_at = now − durée dans le fuseau utilisateur. INSERT\n  minimal (distance_m NOT NULL → 0) → redirection /sessions/{id} + flash.\n  Dégrade en silence si la migration 183 n'est pas appliquée.\n- templates/breathing.php : <form id=\"breathe-complete-form\" hidden> soumis\n  par breathing-guide.js en fin de séance.\n- i18n : titre de séance « Respiration — <protocole> » / « Breathing — … ».\n- TECHNICAL.md, CLAUDE.md (Points d'attention : 9e discipline), mindmap 🟢.\n- Tests : BreathingCompleteWiringTest. Vérifié end-to-end (séance visible sur\n  /sessions, /trends, /calendar ; charge ≈ 0.1).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-859 - Entrainement - Cohérence cardiaque : historisation en séance « Respiration »",
      "type": "other",
      "modified_files": [
        "CLAUDE.md",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/183_add_breathing_sport.sql",
        "public/assets/js/breathing-guide.js",
        "src/Http/Training/BreathingController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/breathing.php",
        "tests/Http/Training/BreathingCompleteWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "CLAUDE.md",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 29,
          "deletions": 0,
          "lines_changed": 29
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "db/migrations/183_add_breathing_sport.sql",
          "insertions": 18,
          "deletions": 0,
          "lines_changed": 18
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 2,
          "deletions": 3,
          "lines_changed": 5
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 83,
          "deletions": 2,
          "lines_changed": 85
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "templates/breathing.php",
          "insertions": 8,
          "deletions": 0,
          "lines_changed": 8
        },
        {
          "path": "tests/Http/Training/BreathingCompleteWiringTest.php",
          "insertions": 78,
          "deletions": 0,
          "lines_changed": 78
        }
      ],
      "files_changed": 11,
      "insertions": 230,
      "deletions": 8
    },
    {
      "hash": "024c82cfe0a7f9375e1cf63d5f6aaf65a19faa7e",
      "short_hash": "024c82c",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789062028,
      "message": "SPOR-858 - Entrainement - Cohérence cardiaque : visualiseur « carré sobre » + préférences + guidage audio\n\nÉpic SPOR-853, phase 1.\n\n- Migration 182 : users.breathing_visual / breathing_audio_cues /\n  breathing_audio_ambient (défauts fleur / silence).\n- BreathingController::savePrefs() (POST /training/breathing/prefs, CSRF,\n  Flash) ; show() charge et applique les préférences + les passe au seed ;\n  buildSeed() expose cycleOffset/cycleSeconds par phase (balayage carré).\n- templates/breathing.php : SVG carré (fond respirant + balayage du\n  périmètre stroke-dasharray), formulaire de préférences (radios\n  fleur/carré, cases audio), bandeau flash.\n- breathing-guide.js : module audio 100 % WebAudio, aucun fichier — repères\n  sonores (OscillatorNode par phase, glissando inspiration/expiration) +\n  ambiance bruit rose (AudioBufferSourceNode bouclé). AudioContext créé sur\n  le geste « Démarrer », coupé à l'arrêt. Visualiseur + audio appliqués en\n  direct sans attendre l'enregistrement. Voix/musique écartées.\n- CSS .rv-breathe__square* / .rv-breathe__prefs* dans les deux app.css,\n  prefers-reduced-motion.\n- Tests : BreathingPrefsWiringTest ; assertions SPOR-857 recalées.\n  TECHNICAL.md, mindmap 🟢.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-858 - Entrainement - Cohérence cardiaque : visualiseur « carré sobre » + préférences + guidage ",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "db/migrations/182_add_user_breathing_prefs.sql",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "src/Http/Training/BreathingController.php",
        "templates/breathing.php",
        "tests/Http/Training/BreathingGuideWiringTest.php",
        "tests/Http/Training/BreathingPrefsWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "config/routes.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "db/migrations/182_add_user_breathing_prefs.sql",
          "insertions": 15,
          "deletions": 0,
          "lines_changed": 15
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 126,
          "deletions": 0,
          "lines_changed": 126
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 94,
          "deletions": 12,
          "lines_changed": 106
        },
        {
          "path": "templates/breathing.php",
          "insertions": 48,
          "deletions": 1,
          "lines_changed": 49
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 3,
          "deletions": 3,
          "lines_changed": 6
        },
        {
          "path": "tests/Http/Training/BreathingPrefsWiringTest.php",
          "insertions": 106,
          "deletions": 0,
          "lines_changed": 106
        }
      ],
      "files_changed": 11,
      "insertions": 503,
      "deletions": 17
    },
    {
      "hash": "c05e819081d9432c7bbc56500e4c8dde7116005d",
      "short_hash": "c05e819",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789061144,
      "message": "SPOR-857 - Entrainement - Cohérence cardiaque : moteur de séance + visualiseur « fleur bergfrue »\n\nÉpic SPOR-853, phase 1. Dépend de SPOR-854..856.\n\n- BreathingController::buildSeed() : timeline complète de chaque protocole\n  (BreathingSession) précalculée côté serveur + labels/textes i18n,\n  sérialisée dans #breathing-app[data-breathing-seed].\n- templates/breathing.php : sélecteur de protocole, fleur SVG inline (8\n  pétales, dégradé aurore), lecture phase/compte-à-rebours, boutons\n  Démarrer/Arrêter.\n- public/assets/js/breathing-guide.js (nouveau, vendorisé, zéro dépendance,\n  hors interactions.js figé) : rejoue la timeline via requestAnimationFrame\n  (dt borné), pilote le visualiseur par data-* + custom properties, aucune\n  logique de déroulé côté client. Accroche #breathe-complete-form pour\n  SPOR-859.\n- CSS .rv-breathe* dans public/assets/css/app.css ET\n  References/design/assets/css/app.css : pousse échelonnée des pétales,\n  pulsation du halo aux rétentions, chute à l'expiration,\n  prefers-reduced-motion.\n- i18n breathing.cycle_of ajouté au seed. TECHNICAL.md.\n- Tests : BreathingGuideWiringTest (contrat). Moteur vérifié hors CI via\n  scratchpad/breathe-sim.js (DOM stubbé).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-857 - Entrainement - Cohérence cardiaque : moteur de séance + visualiseur « fleur bergfrue »",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/app.css",
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "public/assets/css/app.css",
        "public/assets/js/breathing-guide.js",
        "src/Http/Training/BreathingController.php",
        "templates/breathing.php",
        "tests/Http/Training/BreathingGuideWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/app.css",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        },
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 1,
          "lines_changed": 2
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        },
        {
          "path": "public/assets/css/app.css",
          "insertions": 80,
          "deletions": 0,
          "lines_changed": 80
        },
        {
          "path": "public/assets/js/breathing-guide.js",
          "insertions": 223,
          "deletions": 0,
          "lines_changed": 223
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 69,
          "deletions": 3,
          "lines_changed": 72
        },
        {
          "path": "templates/breathing.php",
          "insertions": 84,
          "deletions": 7,
          "lines_changed": 91
        },
        {
          "path": "tests/Http/Training/BreathingGuideWiringTest.php",
          "insertions": 102,
          "deletions": 0,
          "lines_changed": 102
        }
      ],
      "files_changed": 8,
      "insertions": 671,
      "deletions": 11
    },
    {
      "hash": "5a7333f40caa707425d01bf98ae6615ebb8aaac8",
      "short_hash": "5a7333f",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789060385,
      "message": "SPOR-854 - Entrainement - Menu « Entraînement » + page /training/breathing (scaffold)\n\nÉpic SPOR-853, phase 1.\n\n- Sidebar : nouveau groupe « Entraînement » (patron « Tendances » :\n  rv-sidebar__group-label + sous-entrée --sub) avec le sous-menu « Cohérence\n  cardiaque » (activeNav training-breathing).\n- Route GET /training/breathing -> BreathingController::show (nommée\n  training_breathing, instanciée paresseusement).\n- src/Http/Training/BreathingController : AuthGuard::requireLogin(), rend\n  templates/breathing.php + pastilles de sidebar. Coquille de la page ; le\n  moteur, les visualiseurs, les préférences et la modale arrivent avec\n  SPOR-857..860.\n- i18n : nav.training_group / nav.training_breathing + bloc breathing.*\n  complet (FR/EN).\n- Doc utilisateur : docs/entrainement.md. TECHNICAL.md : section épic.\n  Mindmap : épic SPOR-853 + stories SPOR-854..862 sous « Cohérence cardiaque ».\n- Test : tests/Http/Training/BreathingPageWiringTest.php.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-854 - Entrainement - Menu « Entraînement » + page /training/breathing (scaffold)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/README.md",
        "docs/entrainement.md",
        "src/Http/Training/BreathingController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/breathing.php",
        "templates/partials/app_sidebar.php",
        "tests/Http/Training/BreathingPageWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 14,
          "deletions": 0,
          "lines_changed": 14
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 63,
          "deletions": 0,
          "lines_changed": 63
        },
        {
          "path": "config/routes.php",
          "insertions": 6,
          "deletions": 0,
          "lines_changed": 6
        },
        {
          "path": "docs/README.md",
          "insertions": 2,
          "deletions": 0,
          "lines_changed": 2
        },
        {
          "path": "docs/entrainement.md",
          "insertions": 37,
          "deletions": 0,
          "lines_changed": 37
        },
        {
          "path": "src/Http/Training/BreathingController.php",
          "insertions": 83,
          "deletions": 0,
          "lines_changed": 83
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 81,
          "deletions": 0,
          "lines_changed": 81
        },
        {
          "path": "templates/breathing.php",
          "insertions": 50,
          "deletions": 0,
          "lines_changed": 50
        },
        {
          "path": "templates/partials/app_sidebar.php",
          "insertions": 4,
          "deletions": 0,
          "lines_changed": 4
        },
        {
          "path": "tests/Http/Training/BreathingPageWiringTest.php",
          "insertions": 76,
          "deletions": 0,
          "lines_changed": 76
        }
      ],
      "files_changed": 11,
      "insertions": 492,
      "deletions": 0
    },
    {
      "hash": "69b2d386010056dd95a90b33c67f650478ab869f",
      "short_hash": "69b2d38",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789060375,
      "message": "SPOR-855 SPOR-856 - Entrainement - Domaine respiration : phases, patterns, protocoles, timeline\n\nÉpic Entraînement — Respiration guidée SPOR-853, phase 1.\n\nsrc/Domain/Training/Breathing/ (pur, aucune dépendance HTTP/BDD) :\n- BreathingPhase : enum inhale/inhale_top/hold_full/exhale/hold_empty,\n  labelKey() (i18n) + gesture() grow|hold|shrink (pilote les visualiseurs).\n- BreathingPattern : (phase, secondes) ordonnés, cycleSeconds(), toArray()\n  pour le seed JSON ; validation 1..8 phases, 1..60 s.\n- BreathingSession : 30 s intro + N cycles entiers (max 1) + 30 s outro ;\n  timeline() = source de vérité unique sérialisée dans le seed du moteur.\n- BreathingProtocol : catalogue des 5 protocoles phase 1 (carré 4-4-4-4 par\n  défaut, cohérence 5-5, expiration 4-6, 4-7-8, soupir physiologique) avec\n  leurs références scientifiques (DOI) reprises par la modale SPOR-860.\n\nTests : tests/Domain/Training/Breathing/ (25 cas).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-855 SPOR-856 - Entrainement - Domaine respiration : phases, patterns, protocoles, timeline",
      "type": "other",
      "modified_files": [
        "src/Domain/Training/Breathing/BreathingPattern.php",
        "src/Domain/Training/Breathing/BreathingPhase.php",
        "src/Domain/Training/Breathing/BreathingProtocol.php",
        "src/Domain/Training/Breathing/BreathingSession.php",
        "tests/Domain/Training/Breathing/BreathingPatternTest.php",
        "tests/Domain/Training/Breathing/BreathingProtocolTest.php",
        "tests/Domain/Training/Breathing/BreathingSessionTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Domain/Training/Breathing/BreathingPattern.php",
          "insertions": 74,
          "deletions": 0,
          "lines_changed": 74
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingPhase.php",
          "insertions": 42,
          "deletions": 0,
          "lines_changed": 42
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingProtocol.php",
          "insertions": 174,
          "deletions": 0,
          "lines_changed": 174
        },
        {
          "path": "src/Domain/Training/Breathing/BreathingSession.php",
          "insertions": 95,
          "deletions": 0,
          "lines_changed": 95
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingPatternTest.php",
          "insertions": 68,
          "deletions": 0,
          "lines_changed": 68
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingProtocolTest.php",
          "insertions": 72,
          "deletions": 0,
          "lines_changed": 72
        },
        {
          "path": "tests/Domain/Training/Breathing/BreathingSessionTest.php",
          "insertions": 84,
          "deletions": 0,
          "lines_changed": 84
        }
      ],
      "files_changed": 7,
      "insertions": 609,
      "deletions": 0
    },
    {
      "hash": "88e3afd0f95dc3a4eb0d5b4664295596cfa4f47c",
      "short_hash": "88e3afd",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789049115,
      "message": "SPOR-852 - Fix - Sessions MySQL : poule/œuf au 1er déploiement\n\nRégression SPOR-850 bloquant le déploiement v1.7.41 (prod restée en\nmaintenance). session_start() tourne dans public/index.php AVANT le routage,\ndonc avant /_ops/migrate : DatabaseSessionHandler faisait un SELECT sur\nphp_sessions (migration 181) sans rattraper la PDOException → 500 corps vide\nsur toutes les pages, /_ops/migrate compris → migration impossible.\n\nSessionBootstrap::databaseHandler() : tout échec de mise en place du handler\nMySQL (connexion, table absente/illisible via sessionTableUsable(), classe\nnon chargée) → return null → mode dégradé handler fichier, même repli que\npour un échec de connexion. Le déploiement redevient auto-réparant.\n\nTest : SessionBootstrapTest. Doc : TECHNICAL.md, mindmap. Aucune migration.\n\n[SPOR-852]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-852 - Fix - Sessions MySQL : poule/œuf au 1er déploiement",
      "type": "fix",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "src/Http/Auth/SessionBootstrap.php",
        "tests/Http/Auth/SessionBootstrapTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Auth/SessionBootstrap.php",
          "insertions": 29,
          "deletions": 1,
          "lines_changed": 30
        },
        {
          "path": "tests/Http/Auth/SessionBootstrapTest.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        }
      ],
      "files_changed": 4,
      "insertions": 85,
      "deletions": 1,
      "tags": [
        "v1.7.42"
      ]
    },
    {
      "hash": "7cac7cacdc727511bf7db27ad71b7163e2580ba5",
      "short_hash": "7cac7ca",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789042308,
      "message": "SPOR-834 - Import - Fusion en import par lot : auto / manuel par fichier\n\nRetour utilisateur (bug) : en import multiple, « Fusionner » échouait\n(0 imported, 2 failed) — le statut merge-ready n'était pas géré par le JS de\nlot (et le bouton était visible à tort, cf. [SPOR-851]).\n\nSuite de SPOR-833 (fusion unitaire). Un choix « Manuelle / Automatique » au\nlancement du lot décide du sort des fichiers rapprochés d'une séance existante\n(doublon exact OU même jour/distance ±1%/départ ±2min) :\n\n- Manuelle (défaut) : la file se met en pause et affiche le comparateur\n  merge-session-modal (splice inclus) rejoué dans le contexte de l'import,\n  puis reprend. Fermer = ignorer ce fichier.\n- Automatique : la trace/tours/zones/dénivelé du fichier complètent la séance\n  sans prompt ; sport/date/durée/distance/FC conservés. Récap « N fusionnées ».\n\nmergeConfirm()/mergeCancel() gardent leur corps métier : seule la sortie\ndevient JSON en mode X-Import-Batch. Nouveau mergePreviewFragment() rend le\nMÊME partial que la page détail (merge_session_modal_body.php, source unique\ndu markup). Picker de fenêtre extrait en merge-splice.js (Leaflet chargé\nparesseusement). SessionMergeSelection::AUTO_CHOICES (domaine pur).\n\nRoute : GET /sessions/{id}/merge/preview. i18n FR/EN. Doc : docs/sessions.md,\nTECHNICAL.md (+ section [SPOR-851]), mindmap.\n\nTests : BatchMergeWiringTest, SessionMergeSelectionTest, ImportBatchSummaryLink\nadapté. Suite verte (4256). Vérifié bout-en-bout en local (dispatch Slim,\nX-Import-Batch) : duplicate -> merge-ready -> confirm auto/manuel = merged,\ncancel = skipped ; scalaires conservés en fusion auto.\n\n[SPOR-834] [SPOR-851]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-834 - Import - Fusion en import par lot : auto / manuel par fichier",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "config/routes.php",
        "docs/sessions.md",
        "public/assets/js/interactions.js",
        "public/assets/js/merge-splice.js",
        "src/Domain/Training/SessionMergeSelection.php",
        "src/Http/Sessions/SessionDetailController.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "templates/partials/merge_session_modal_body.php",
        "templates/partials/session_modals.php",
        "templates/session_detail.php",
        "templates/sessions.php",
        "tests/Domain/SessionMergeSelectionTest.php",
        "tests/Http/Sessions/BatchMergeWiringTest.php",
        "tests/Http/Sessions/ImportBatchSummaryLinkWiringTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 2,
          "deletions": 1,
          "lines_changed": 3
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 98,
          "deletions": 2,
          "lines_changed": 100
        },
        {
          "path": "config/routes.php",
          "insertions": 3,
          "deletions": 0,
          "lines_changed": 3
        },
        {
          "path": "docs/sessions.md",
          "insertions": 6,
          "deletions": 3,
          "lines_changed": 9
        },
        {
          "path": "public/assets/js/interactions.js",
          "insertions": 176,
          "deletions": 43,
          "lines_changed": 219
        },
        {
          "path": "public/assets/js/merge-splice.js",
          "insertions": 145,
          "deletions": 0,
          "lines_changed": 145
        },
        {
          "path": "src/Domain/Training/SessionMergeSelection.php",
          "insertions": 19,
          "deletions": 0,
          "lines_changed": 19
        },
        {
          "path": "src/Http/Sessions/SessionDetailController.php",
          "insertions": 77,
          "deletions": 2,
          "lines_changed": 79
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "templates/partials/merge_session_modal_body.php",
          "insertions": 79,
          "deletions": 0,
          "lines_changed": 79
        },
        {
          "path": "templates/partials/session_modals.php",
          "insertions": 36,
          "deletions": 0,
          "lines_changed": 36
        },
        {
          "path": "templates/session_detail.php",
          "insertions": 17,
          "deletions": 186,
          "lines_changed": 203
        },
        {
          "path": "templates/sessions.php",
          "insertions": 5,
          "deletions": 0,
          "lines_changed": 5
        },
        {
          "path": "tests/Domain/SessionMergeSelectionTest.php",
          "insertions": 10,
          "deletions": 0,
          "lines_changed": 10
        },
        {
          "path": "tests/Http/Sessions/BatchMergeWiringTest.php",
          "insertions": 144,
          "deletions": 0,
          "lines_changed": 144
        },
        {
          "path": "tests/Http/Sessions/ImportBatchSummaryLinkWiringTest.php",
          "insertions": 9,
          "deletions": 2,
          "lines_changed": 11
        }
      ],
      "files_changed": 17,
      "insertions": 836,
      "deletions": 239,
      "tags": [
        "v1.7.41"
      ]
    },
    {
      "hash": "edfe6755b2de5fcfebd6010125bc943cb3653f0d",
      "short_hash": "edfe675",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1789042294,
      "message": "SPOR-851 - CSS - [hidden] sans effet sur les éléments à display auteur\n\nAucune règle [hidden]{display:none} dans public/assets/css/ : la règle UA\nest battue dans la cascade dès qu'une classe auteur pose un display\n(.rv-btn{display:inline-flex}, .rv-modal__metrics{display:grid}...). Poser\nhidden sur un tel élément ne le masquait pas — déjà contourné au cas par cas\nen JS pour SPOR-760, révélé à nouveau par SPOR-834 (bouton « Fusionner »\nvisible en import par lot).\n\nFix : [hidden]{display:none!important} dans base.css (+ maquette, CSS synchro\ndes deux côtés). Sans risque pour les <input hidden> des chips/dropzones.\n\nTest : HiddenAttributeCssTest.\n\n[SPOR-851]\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-851 - CSS - [hidden] sans effet sur les éléments à display auteur",
      "type": "other",
      "modified_files": [
        "References/design/assets/css/base.css",
        "public/assets/css/base.css",
        "tests/Infra/HiddenAttributeCssTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/design/assets/css/base.css",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "public/assets/css/base.css",
          "insertions": 9,
          "deletions": 0,
          "lines_changed": 9
        },
        {
          "path": "tests/Infra/HiddenAttributeCssTest.php",
          "insertions": 32,
          "deletions": 0,
          "lines_changed": 32
        }
      ],
      "files_changed": 3,
      "insertions": 50,
      "deletions": 0
    },
    {
      "hash": "d544d8421dfa46053e22e8407fb6f7b2580a3f76",
      "short_hash": "d544d84",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788973664,
      "message": "SPOR-850 - Auth - Sessions persistantes : stockage MySQL + « Rester connecté »\n\nLe « reset quotidien » des sessions en prod venait du cron de purge de\nl'hébergeur mutualisé sur les fichiers de session, hors de notre contrôle,\net du cookie de session pur (sans Max-Age) que le navigateur oublie à sa\nfermeture.\n\n- `Infra\\Session\\DatabaseSessionHandler` (\\SessionHandlerInterface +\n  \\SessionUpdateTimestampHandlerInterface) : payload en table `php_sessions`\n  (migration 181). L'API `$_SESSION` reste 100 % native, seul le backend\n  change. gc() aligné sur la plus large des fenêtres SessionLifetime.\n- `SessionBootstrap` : installe le handler MySQL (PDO court dédié, mode\n  dégradé → handler fichier si la connexion échoue), relève\n  gc_probability (Debian/OVH le mettent à 0). Bloc `var/sessions/` retiré.\n- Case « Rester connecté sur cet appareil » sur /login (décochée par\n  défaut) : l'intention passe par la phase pending 2FA\n  (AuthGuard::beginPending) puis est consommée par AuthGuard::login()\n  → `$_SESSION['persistent']`. Quand persistent, SessionBootstrap ré-émet\n  le cookie à chaque requête avec une échéance glissante de 36 h, plafonnée\n  par l'absolu 10 j (SessionLifetime).\n\nConséquence déploiement : reconnexion unique pour tout le monde au premier\ndéploiement (sessions fichier existantes non lues).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-850 - Auth - Sessions persistantes : stockage MySQL + « Rester connecté »",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md",
        "TECHNICAL.md",
        "db/migrations/181_add_php_sessions.sql",
        "docs/auth.md",
        "src/Http/Auth/AuthGuard.php",
        "src/Http/Auth/LoginController.php",
        "src/Http/Auth/SessionBootstrap.php",
        "src/Infra/I18n/lang/en.php",
        "src/Infra/I18n/lang/fr.php",
        "src/Infra/Session/DatabaseSessionHandler.php",
        "templates/login.php",
        "tests/Infra/Session/DatabaseSessionHandlerTest.php"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "TECHNICAL.md",
          "insertions": 61,
          "deletions": 0,
          "lines_changed": 61
        },
        {
          "path": "db/migrations/181_add_php_sessions.sql",
          "insertions": 20,
          "deletions": 0,
          "lines_changed": 20
        },
        {
          "path": "docs/auth.md",
          "insertions": 3,
          "deletions": 1,
          "lines_changed": 4
        },
        {
          "path": "src/Http/Auth/AuthGuard.php",
          "insertions": 14,
          "deletions": 1,
          "lines_changed": 15
        },
        {
          "path": "src/Http/Auth/LoginController.php",
          "insertions": 11,
          "deletions": 6,
          "lines_changed": 17
        },
        {
          "path": "src/Http/Auth/SessionBootstrap.php",
          "insertions": 68,
          "deletions": 26,
          "lines_changed": 94
        },
        {
          "path": "src/Infra/I18n/lang/en.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Infra/I18n/lang/fr.php",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        },
        {
          "path": "src/Infra/Session/DatabaseSessionHandler.php",
          "insertions": 104,
          "deletions": 0,
          "lines_changed": 104
        },
        {
          "path": "templates/login.php",
          "insertions": 4,
          "deletions": 1,
          "lines_changed": 5
        },
        {
          "path": "tests/Infra/Session/DatabaseSessionHandlerTest.php",
          "insertions": 93,
          "deletions": 0,
          "lines_changed": 93
        }
      ],
      "files_changed": 12,
      "insertions": 381,
      "deletions": 35
    },
    {
      "hash": "3ae7bc8226dc72b8189472f7a3b855311d75c279",
      "short_hash": "3ae7bc8",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788972319,
      "message": "SPOR-849 - Fix - GearController : erreur de syntaxe (sed mal appliqué) → 500 sur /gear\n\nLe remplacement i18n de la passe précédente (81e0c39) avait mangé le\nFlash::add() de delete() sur deux lignes : « $stmtTranslator::t(->rowCount() ».\n\nGarde-fou : tests/Infra/PhpSyntaxTest.php — php -l sur tout src/ public/ bin/\ntemplates/ (692 fichiers). Un contrôleur sans test de câblage ne peut plus\npartir en prod avec une erreur de syntaxe sans faire rougir la suite.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-849 - Fix - GearController : erreur de syntaxe (sed mal appliqué) → 500 sur /gear",
      "type": "fix",
      "modified_files": [
        "src/Http/Gear/GearController.php",
        "tests/Infra/PhpSyntaxTest.php"
      ],
      "modified_files_details": [
        {
          "path": "src/Http/Gear/GearController.php",
          "insertions": 4,
          "deletions": 2,
          "lines_changed": 6
        },
        {
          "path": "tests/Infra/PhpSyntaxTest.php",
          "insertions": 44,
          "deletions": 0,
          "lines_changed": 44
        }
      ],
      "files_changed": 2,
      "insertions": 48,
      "deletions": 2,
      "tags": [
        "v1.7.40"
      ]
    },
    {
      "hash": "0bb8414618f229f21e776c0bcc34aee1d06febf9",
      "short_hash": "0bb8414",
      "author": "zRenard",
      "email": "zrenard@gmail.com",
      "timestamp": 1788965976,
      "message": "SPOR-849 - i18n - Mindmap : nœud validateurs domaine (couche HTTP faite, domaine restant)\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>",
      "message_short": "SPOR-849 - i18n - Mindmap : nœud validateurs domaine (couche HTTP faite, domaine restant)",
      "type": "other",
      "modified_files": [
        "References/mindmap/Sport Log.md"
      ],
      "modified_files_details": [
        {
          "path": "References/mindmap/Sport Log.md",
          "insertions": 1,
          "deletions": 0,
          "lines_changed": 1
        }
      ],
      "files_changed": 1,
      "insertions": 1,
      "deletions": 0,
      "tags": [
        "v1.7.39"
      ]
    }
  ]
}